A Bluesky Playdate client

Playdate.

+3
.gitignore
··· 1 + .zig-cache/ 2 + zig-out/ 3 + .DS_Store
+6
.prettierrc
··· 1 + { 2 + "semi": false, 3 + "singleQuote": true, 4 + "tabWidth": 4, 5 + "trailingComma": "none" 6 + }
+55
.vscode/settings.json
··· 1 + { 2 + "zig.zls.enabled": "on", 3 + // You can add more Zig and ZLS options here 4 + 5 + // Whether to enable build-on-save diagnostics 6 + // 7 + // Further information about build-on save: 8 + // https://zigtools.org/zls/guides/build-on-save/ 9 + // "zig.zls.enableBuildOnSave": true, 10 + 11 + // All nested settings will only affect Zig files. 12 + "[zig]": { 13 + // The Zig FAQ answers some common questions about Zig's formatter (`zig fmt`) 14 + // https://github.com/ziglang/zig/wiki/FAQ 15 + // 16 + "editor.formatOnSave": true, 17 + 18 + "editor.inlayHints.enabled": "on", 19 + 20 + // overwrite words when accepting completions 21 + 22 + "editor.suggest.insertMode": "replace", 23 + "editor.codeActionsOnSave": { 24 + // Run code actions that currently supports adding and removing discards. 25 + // "source.fixAll": "explicit", 26 + // Run code actions that sorts @import declarations. 27 + // "source.organizeImports": "explicit", 28 + } 29 + }, 30 + "editor.semanticTokenColorCustomizations": { 31 + "rules": { 32 + "*.deprecated": { 33 + // highlight semantic tokens that are marked as "deprecated" 34 + "strikethrough": true 35 + } 36 + } 37 + }, 38 + "cSpell.words": [ 39 + "anyopaque", 40 + "callconv", 41 + "comptime", 42 + "linenum", 43 + "memcpy", 44 + "orelse", 45 + "pdapi", 46 + "pdtools", 47 + "playdate", 48 + "repost", 49 + "reposted", 50 + "stringval", 51 + "Sublist", 52 + "Trunc", 53 + "usize" 54 + ] 55 + }
+7 -1
README.md
··· 1 - # playdate-test 1 + # Playdate Bluesky client 2 + 3 + An experiment to learn Zig and the Playdate SDK. 4 + 5 + # Resources 6 + 7 + Thank you to https://github.com/DanB91/Zig-Playdate-Template for the starting point.
assets/fonts/Roobert-10-Bold.pft

This is a binary file and will not be displayed.

assets/fonts/Roobert-9-Mono-Condensed-table-8-14.png

This is a binary file and will not be displayed.

+95
assets/fonts/Roobert-9-Mono-Condensed.fnt
··· 1 + space 8 2 + ! 8 3 + " 8 4 + # 8 5 + $ 8 6 + % 8 7 + & 8 8 + ' 8 9 + ( 8 10 + ) 8 11 + * 8 12 + + 8 13 + , 8 14 + - 8 15 + . 8 16 + / 8 17 + 0 8 18 + 1 8 19 + 2 8 20 + 3 8 21 + 4 8 22 + 5 8 23 + 6 8 24 + 7 8 25 + 8 8 26 + 9 8 27 + : 8 28 + ; 8 29 + < 8 30 + = 8 31 + > 8 32 + ? 8 33 + @ 8 34 + A 8 35 + B 8 36 + C 8 37 + D 8 38 + E 8 39 + F 8 40 + G 8 41 + H 8 42 + I 8 43 + J 8 44 + K 8 45 + L 8 46 + M 8 47 + N 8 48 + O 8 49 + P 8 50 + Q 8 51 + R 8 52 + S 8 53 + T 8 54 + U 8 55 + V 8 56 + W 8 57 + X 8 58 + Y 8 59 + Z 8 60 + [ 8 61 + \ 8 62 + ] 8 63 + ^ 8 64 + _ 8 65 + ` 8 66 + a 8 67 + b 8 68 + c 8 69 + d 8 70 + e 8 71 + f 8 72 + g 8 73 + h 8 74 + i 8 75 + j 8 76 + k 8 77 + l 8 78 + m 8 79 + n 8 80 + o 8 81 + p 8 82 + q 8 83 + r 8 84 + s 8 85 + t 8 86 + u 8 87 + v 8 88 + w 8 89 + x 8 90 + y 8 91 + z 8 92 + { 8 93 + | 8 94 + } 8 95 + ~ 8
+217
build.zig
··· 1 + const std = @import("std"); 2 + const builtin = @import("builtin"); 3 + 4 + const name = "bluesky"; 5 + pub fn build(b: *std.Build) !void { 6 + const pdx_file_name = name ++ ".pdx"; 7 + const optimize = b.standardOptimizeOption(.{}); 8 + 9 + const writer = b.addWriteFiles(); 10 + const source_dir = writer.getDirectory(); 11 + writer.step.name = "write source directory"; 12 + 13 + const FORCE_COMPILE_M1_MAC = false; 14 + const supported_targets = [_]std.Build.ResolvedTarget{ 15 + host_or_cross_target( 16 + b, 17 + .{ 18 + .abi = .msvc, 19 + .os_tag = .windows, 20 + .cpu_arch = .x86_64, 21 + }, 22 + false, 23 + ), 24 + host_or_cross_target( 25 + b, 26 + .{ 27 + .abi = .none, 28 + .os_tag = .macos, 29 + .cpu_arch = .aarch64, 30 + }, 31 + FORCE_COMPILE_M1_MAC, 32 + ), 33 + host_or_cross_target( 34 + b, 35 + .{ 36 + .abi = .gnu, 37 + .os_tag = .linux, 38 + .cpu_arch = .x86_64, 39 + }, 40 + false, 41 + ), 42 + }; 43 + for (supported_targets) |target| { 44 + try compile_simulator_binary(b, optimize, target, writer); 45 + } 46 + 47 + const playdate_target = b.resolveTargetQuery(try std.Target.Query.parse(.{ 48 + .arch_os_abi = "thumb-freestanding-eabihf", 49 + .cpu_features = "cortex_m7+vfp4d16sp", 50 + })); 51 + const elf = b.addExecutable(.{ 52 + .name = "pdex.elf", 53 + .root_source_file = b.path("src/main.zig"), 54 + .target = playdate_target, 55 + .optimize = optimize, 56 + .pic = true, 57 + .single_threaded = true, 58 + }); 59 + elf.link_emit_relocs = true; 60 + elf.entry = .{ .symbol_name = "eventHandler" }; 61 + 62 + elf.setLinkerScript(b.path("link_map.ld")); 63 + if (optimize == .ReleaseFast) { 64 + elf.root_module.omit_frame_pointer = true; 65 + } 66 + _ = writer.addCopyFile(elf.getEmittedBin(), "pdex.elf"); 67 + _ = writer.addCopyFile(b.path("pdxinfo"), "pdxinfo"); 68 + 69 + try addCopyDirectory(writer, "assets", "./assets"); 70 + 71 + const playdate_sdk_path = try std.process.getEnvVarOwned(b.allocator, "PLAYDATE_SDK_PATH"); 72 + const pdc_path = b.pathJoin(&.{ playdate_sdk_path, "bin", if (builtin.os.tag == .windows) "pdc.exe" else "pdc" }); 73 + const pd_simulator_path = switch (builtin.os.tag) { 74 + .linux => b.pathJoin(&.{ playdate_sdk_path, "bin", "PlaydateSimulator" }), 75 + .macos => "open", // `open` focuses the window, while running the simulator directry doesn't. 76 + .windows => b.pathJoin(&.{ playdate_sdk_path, "bin", "PlaydateSimulator.exe" }), 77 + else => @panic("Unsupported OS"), 78 + }; 79 + 80 + const pdc = b.addSystemCommand(&.{pdc_path}); 81 + pdc.addDirectoryArg(source_dir); 82 + pdc.setName("pdc"); 83 + const pdx = pdc.addOutputFileArg(pdx_file_name); 84 + 85 + b.installDirectory(.{ 86 + .source_dir = pdx, 87 + .install_dir = .prefix, 88 + .install_subdir = pdx_file_name, 89 + }); 90 + b.installDirectory(.{ 91 + .source_dir = source_dir, 92 + .install_dir = .prefix, 93 + .install_subdir = "pdx_source_dir", 94 + }); 95 + 96 + const run_cmd = b.addSystemCommand(&.{pd_simulator_path}); 97 + run_cmd.addDirectoryArg(pdx); 98 + run_cmd.setName("PlaydateSimulator"); 99 + const run_step = b.step("run", "Run the app"); 100 + run_step.dependOn(&run_cmd.step); 101 + run_step.dependOn(b.getInstallStep()); 102 + 103 + const clean_step = b.step("clean", "Clean all artifacts"); 104 + clean_step.dependOn(&b.addRemoveDirTree(b.path("zig-out")).step); 105 + if (builtin.os.tag != .windows) { 106 + //Removing zig-cache from the Zig build script does not work on Windows: https://github.com/ziglang/zig/issues/9216 107 + clean_step.dependOn(&b.addRemoveDirTree(b.path("zig-cache")).step); 108 + clean_step.dependOn(&b.addRemoveDirTree(b.path(".zig-cache")).step); 109 + } 110 + 111 + // Add test step 112 + const test_step = b.step("test", "Run unit tests"); 113 + 114 + // Create test executables for each test file 115 + const test_files = [_][]const u8{ 116 + "src/test_memory.zig", 117 + "src/test_json_parser.zig", 118 + "src/test_network.zig", 119 + }; 120 + 121 + for (test_files) |test_file| { 122 + const test_exe = b.addTest(.{ 123 + .root_source_file = b.path(test_file), 124 + .optimize = optimize, 125 + }); 126 + 127 + const run_test = b.addRunArtifact(test_exe); 128 + test_step.dependOn(&run_test.step); 129 + } 130 + } 131 + 132 + //The purpose of this function is a result of: 133 + // 1) This script supports cross-compiling PDX's that work on Mac, Windows or Linux without having 134 + // to compile on those OS's. 135 + // 136 + // 2) Inside of a PDX, there can only be 1 pdex executable per OS regardless of the CPU architecture. 137 + // This has unexpected consequences where, say, a given PDX file can only work on M1 Macs, 138 + // but not Intel ones. Or, vice versa. 139 + // 140 + // So, in the build() function above, I hardcoded ".cpu_arch = .aarch64", which is for M1 Macs. 141 + // What this means is that if you compiling your game on, say, Windows, it will generate a .pdx 142 + // that will only work on M1 Macs, but not Intel Macs. 143 + // BUT, cruicially, if you compiling your game on an Intel Mac, the resulting PDX will work 144 + // on Intel Macs, but not M1 Macs. Without this function, the game would fail 145 + // to run on the machine your compiling the code on (Intel Mac), which I'd like to avoid. 146 + fn host_or_cross_target( 147 + b: *std.Build, 148 + cross_target: std.Target.Query, 149 + force_use_cross_target: bool, 150 + ) std.Build.ResolvedTarget { 151 + const result = 152 + if (!force_use_cross_target and b.graph.host.result.os.tag == cross_target.os_tag.?) 153 + b.graph.host 154 + else 155 + b.resolveTargetQuery(cross_target); 156 + return result; 157 + } 158 + 159 + fn compile_simulator_binary( 160 + b: *std.Build, 161 + optimize: std.builtin.OptimizeMode, 162 + target: std.Build.ResolvedTarget, 163 + writer: *std.Build.Step.WriteFile, 164 + ) !void { 165 + const os_tag = target.result.os.tag; 166 + const lib = b.addSharedLibrary(.{ 167 + .name = "pdex", 168 + .root_source_file = b.path("src/main.zig"), 169 + .optimize = optimize, 170 + .target = target, 171 + }); 172 + const pdex_extension = switch (os_tag) { 173 + .windows => "dll", 174 + .macos => "dylib", 175 + .linux => "so", 176 + else => @panic("Unsupported OS"), 177 + }; 178 + const pdex_filename = try std.fmt.allocPrint(b.allocator, "pdex.{s}", .{pdex_extension}); 179 + _ = writer.addCopyFile(lib.getEmittedBin(), pdex_filename); 180 + 181 + if (os_tag == .windows) { 182 + _ = writer.addCopyFile(lib.getEmittedPdb(), "pdex.pdb"); 183 + } 184 + } 185 + 186 + fn addCopyDirectory( 187 + wf: *std.Build.Step.WriteFile, 188 + src_path: []const u8, 189 + dest_path: []const u8, 190 + ) !void { 191 + const b = wf.step.owner; 192 + var dir = try b.build_root.handle.openDir( 193 + src_path, 194 + .{ .iterate = true }, 195 + ); 196 + defer dir.close(); 197 + var it = dir.iterate(); 198 + while (try it.next()) |entry| { 199 + const new_src_path = b.pathJoin(&.{ src_path, entry.name }); 200 + const new_dest_path = b.pathJoin(&.{ dest_path, entry.name }); 201 + const new_src = b.path(new_src_path); 202 + switch (entry.kind) { 203 + .file => { 204 + _ = wf.addCopyFile(new_src, new_dest_path); 205 + }, 206 + .directory => { 207 + try addCopyDirectory( 208 + wf, 209 + new_src_path, 210 + new_dest_path, 211 + ); 212 + }, 213 + //TODO: possible support for sym links? 214 + else => {}, 215 + } 216 + } 217 + }
+5
pdxinfo
··· 1 + name=Bluesky 2 + author=Russley Shaw 3 + description=A bluesky client for the Playdate 4 + bundleID=dev.russley.zig-bluesky 5 + imagePath=assets/images
+626
src/bsky_post.zig
··· 1 + const pdapi = @import("playdate_api_definitions.zig"); 2 + const std = @import("std"); 3 + const pdtools = @import("pdtools/index.zig"); 4 + const DrawableText = @import("pdtools/DrawableText.zig").DrawableText; 5 + const fonts = @import("fonts.zig"); 6 + 7 + // Memory size constants 8 + pub const AUTHOR_BUFFER_SIZE = 32; 9 + pub const CONTENT_BUFFER_SIZE = 512; 10 + pub const ALT_TEXT_BUFFER_SIZE = 256; 11 + pub const MAX_POSTS = 50; 12 + 13 + pub const CURSOR_BUFFER_SIZE = 256; 14 + 15 + pub const PostType = enum { 16 + normal, 17 + repost, 18 + reply, 19 + }; 20 + 21 + pub const BskyPost = struct { 22 + author: [AUTHOR_BUFFER_SIZE]u8, 23 + author_len: usize, 24 + content: [CONTENT_BUFFER_SIZE]u8, 25 + content_len: usize, 26 + alt_text: [ALT_TEXT_BUFFER_SIZE]u8, 27 + alt_text_len: usize, 28 + 29 + // Repost/Reply metadata 30 + post_type: PostType, 31 + repost_author: [AUTHOR_BUFFER_SIZE]u8, 32 + repost_author_len: usize, 33 + reply_author: [AUTHOR_BUFFER_SIZE]u8, 34 + reply_author_len: usize, 35 + }; 36 + 37 + /// Clean up text content by removing multiple consecutive newlines 38 + fn cleanupTextContent(buffer: []u8, content_len: usize) usize { 39 + if (content_len == 0) return 0; 40 + 41 + var write_pos: usize = 0; 42 + var read_pos: usize = 0; 43 + var consecutive_newlines: usize = 0; 44 + 45 + while (read_pos < content_len) { 46 + const char = buffer[read_pos]; 47 + 48 + if (char == '\n' or char == '\r') { 49 + consecutive_newlines += 1; 50 + // Allow max 2 consecutive newlines (one blank line) 51 + if (consecutive_newlines <= 2) { 52 + buffer[write_pos] = '\n'; // Normalize to \n 53 + write_pos += 1; 54 + } 55 + } else { 56 + consecutive_newlines = 0; 57 + buffer[write_pos] = char; 58 + write_pos += 1; 59 + } 60 + 61 + read_pos += 1; 62 + } 63 + 64 + return write_pos; 65 + } 66 + 67 + pub var g_playdate: *pdapi.PlaydateAPI = undefined; 68 + 69 + var g_posts_out: []BskyPost = undefined; 70 + var g_post_count: *usize = undefined; 71 + var g_post_max: usize = undefined; 72 + var g_parsing_feed: bool = false; 73 + var g_current_feed_index: i32 = -1; 74 + 75 + fn onDecodeError(decoder: ?*pdapi.JSONDecoder, err: ?[*:0]const u8, linenum: c_int) callconv(.C) void { 76 + _ = decoder; // Unused in this context 77 + 78 + const pd = g_playdate; 79 + pd.system.logToConsole("JSON decoding error at line %d: %s", linenum, err); 80 + 81 + // Log parsing context 82 + pd.system.logToConsole("Parsing context: posts found so far = %d", @as(c_int, @intCast(g_post_count.*))); 83 + pd.system.logToConsole("Current feed index: %d", g_current_feed_index); 84 + } 85 + 86 + fn debugDidDecodeSublist(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque { 87 + _ = decoder; 88 + _ = name; 89 + _ = json_type; 90 + return null; 91 + } 92 + 93 + fn author_didDecodeSublist(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque { 94 + _ = name; 95 + _ = json_type; 96 + 97 + // Reset to post-level parsing 98 + decoder.?.didDecodeTableValue = post_didDecodeTableValue; 99 + decoder.?.willDecodeSublist = null; 100 + decoder.?.didDecodeSublist = null; 101 + 102 + return null; 103 + } 104 + 105 + fn record_didDecodeSublist(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque { 106 + _ = name; 107 + _ = json_type; 108 + 109 + // Reset to post-level parsing 110 + decoder.?.didDecodeTableValue = post_didDecodeTableValue; 111 + decoder.?.willDecodeSublist = null; 112 + decoder.?.didDecodeSublist = null; 113 + 114 + return null; 115 + } 116 + 117 + fn author_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void { 118 + if (key == null) return; 119 + 120 + const key_str = std.mem.span(key.?); 121 + const json_type: pdapi.JSONValueType = @enumFromInt(value.type); 122 + 123 + // Extract handle from author object 124 + if (std.mem.eql(u8, key_str, "handle") and json_type == pdapi.JSONValueType.JSONString) { 125 + if (g_post_count.* < g_post_max) { 126 + const author_str = std.mem.span(value.data.stringval); 127 + const copy_len = @min(author_str.len, g_posts_out[g_post_count.*].author.len - 1); 128 + @memcpy(g_posts_out[g_post_count.*].author[0..copy_len], author_str[0..copy_len]); 129 + g_posts_out[g_post_count.*].author_len = copy_len; 130 + } 131 + } 132 + 133 + if (false) { 134 + debugDidDecodeTableValue(decoder, key, value); 135 + } 136 + } 137 + 138 + fn record_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void { 139 + if (key == null) return; 140 + 141 + const key_str = std.mem.span(key.?); 142 + const json_type: pdapi.JSONValueType = @enumFromInt(value.type); 143 + 144 + // Extract text from record object 145 + if (std.mem.eql(u8, key_str, "text") and json_type == pdapi.JSONValueType.JSONString) { 146 + if (g_post_count.* < g_post_max) { 147 + const content_str = std.mem.span(value.data.stringval); 148 + const copy_len = @min(content_str.len, g_posts_out[g_post_count.*].content.len - 1); 149 + @memcpy(g_posts_out[g_post_count.*].content[0..copy_len], content_str[0..copy_len]); 150 + 151 + // Clean up multiple consecutive newlines 152 + const cleaned_len = cleanupTextContent(g_posts_out[g_post_count.*].content[0..copy_len], copy_len); 153 + g_posts_out[g_post_count.*].content_len = cleaned_len; 154 + } 155 + } 156 + 157 + if (false) { 158 + debugDidDecodeTableValue(decoder, key, value); 159 + } 160 + } 161 + 162 + fn post_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void { 163 + if (key == null) return; 164 + 165 + const key_str = std.mem.span(key.?); 166 + const json_type: pdapi.JSONValueType = @enumFromInt(value.type); 167 + 168 + // Set up nested object parsing 169 + if (std.mem.eql(u8, key_str, "author") and json_type == pdapi.JSONValueType.JSONTable) { 170 + decoder.?.willDecodeSublist = null; 171 + decoder.?.didDecodeSublist = author_didDecodeSublist; 172 + decoder.?.didDecodeTableValue = author_didDecodeTableValue; 173 + return; 174 + } 175 + 176 + if (std.mem.eql(u8, key_str, "record") and json_type == pdapi.JSONValueType.JSONTable) { 177 + decoder.?.willDecodeSublist = null; 178 + decoder.?.didDecodeSublist = record_didDecodeSublist; 179 + decoder.?.didDecodeTableValue = record_didDecodeTableValue; 180 + return; 181 + } 182 + 183 + debugDidDecodeTableValue(decoder, key, value); 184 + } 185 + 186 + fn feed_item_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void { 187 + if (key == null) return; 188 + 189 + const key_str = std.mem.span(key.?); 190 + const json_type: pdapi.JSONValueType = @enumFromInt(value.type); 191 + 192 + // Set up post object parsing 193 + if (std.mem.eql(u8, key_str, "post") and json_type == pdapi.JSONValueType.JSONTable) { 194 + decoder.?.willDecodeSublist = null; 195 + decoder.?.didDecodeSublist = post_didDecodeSublist; 196 + decoder.?.didDecodeTableValue = post_didDecodeTableValue; 197 + return; 198 + } 199 + 200 + if (false) { 201 + debugDidDecodeTableValue(decoder, key, value); 202 + } 203 + } 204 + 205 + fn post_didDecodeSublist(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque { 206 + _ = name; 207 + _ = json_type; 208 + 209 + // Reset to feed item level parsing 210 + decoder.?.didDecodeTableValue = feed_item_didDecodeTableValue; 211 + decoder.?.willDecodeSublist = null; 212 + decoder.?.didDecodeSublist = null; 213 + 214 + return null; 215 + } 216 + 217 + fn willDecodeFeedList(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) void { 218 + _ = name; 219 + 220 + // This is called when entering each element in the feed array 221 + if (json_type == pdapi.JSONValueType.JSONTable) { 222 + // Initialize new post with empty data at current index 223 + if (g_post_count.* < g_post_max) { 224 + g_posts_out[g_post_count.*] = BskyPost{ 225 + .author = [_]u8{0} ** AUTHOR_BUFFER_SIZE, 226 + .author_len = 0, 227 + .content = [_]u8{0} ** CONTENT_BUFFER_SIZE, 228 + .content_len = 0, 229 + .alt_text = [_]u8{0} ** ALT_TEXT_BUFFER_SIZE, 230 + .alt_text_len = 0, 231 + .post_type = .normal, 232 + .repost_author = [_]u8{0} ** AUTHOR_BUFFER_SIZE, 233 + .repost_author_len = 0, 234 + .reply_author = [_]u8{0} ** AUTHOR_BUFFER_SIZE, 235 + .reply_author_len = 0, 236 + }; 237 + } 238 + 239 + // Set up feed item parsing (looking for "post" object) 240 + decoder.?.didDecodeTableValue = feed_item_didDecodeTableValue; 241 + } 242 + } 243 + 244 + fn didDecodeFeedList(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque { 245 + _ = name; 246 + 247 + if (json_type != pdapi.JSONValueType.JSONTable) { 248 + return null; 249 + } 250 + 251 + // Post parsing complete - increment count if we have valid data 252 + if (g_post_count.* < g_post_max) { 253 + const has_author = g_posts_out[g_post_count.*].author_len > 0; 254 + const has_content = g_posts_out[g_post_count.*].content_len > 0; 255 + 256 + if (has_author or has_content) { 257 + g_post_count.* += 1; 258 + } 259 + } 260 + 261 + // Reset decoder for root-level parsing (back to root after this feed item) 262 + decoder.?.didDecodeTableValue = root_didDecodeTableValue; 263 + decoder.?.willDecodeSublist = willDecodeFeedList; 264 + decoder.?.didDecodeSublist = didDecodeFeedList; 265 + 266 + return null; 267 + } 268 + 269 + fn root_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void { 270 + const json_type: pdapi.JSONValueType = @enumFromInt(value.type); 271 + const path_ptr = decoder.?.path orelse "_root"; 272 + const path = std.mem.span(path_ptr); 273 + 274 + // Check if we're inside a feed item by looking at the path 275 + if (key != null) { 276 + const key_str = std.mem.span(key.?); 277 + 278 + // Extract author handle 279 + if (std.mem.eql(u8, key_str, "handle") and json_type == pdapi.JSONValueType.JSONString) { 280 + // Check if we're in an author context (path should contain something like feed[n].post.author) 281 + if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".post.author") != null) { 282 + if (g_post_count.* < g_post_max) { 283 + const author_str = std.mem.span(value.data.stringval); 284 + const copy_len = @min(author_str.len, g_posts_out[g_post_count.*].author.len - 1); 285 + @memcpy(g_posts_out[g_post_count.*].author[0..copy_len], author_str[0..copy_len]); 286 + g_posts_out[g_post_count.*].author_len = copy_len; 287 + g_playdate.system.logToConsole("Found author: %.*s", @as(c_int, @intCast(copy_len)), @as([*:0]const u8, @ptrCast(&g_posts_out[g_post_count.*].author))); 288 + } 289 + return; 290 + } 291 + } 292 + 293 + // Extract post text content 294 + if (std.mem.eql(u8, key_str, "text") and json_type == pdapi.JSONValueType.JSONString) { 295 + // Check if we're in a record context (path should contain something like feed[n].post.record) 296 + if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".post.record") != null) { 297 + if (g_post_count.* < g_post_max) { 298 + const content_str = std.mem.span(value.data.stringval); 299 + const copy_len = @min(content_str.len, g_posts_out[g_post_count.*].content.len - 1); 300 + @memcpy(g_posts_out[g_post_count.*].content[0..copy_len], content_str[0..copy_len]); 301 + g_posts_out[g_post_count.*].content_len = copy_len; 302 + const preview_len = @min(50, copy_len); 303 + g_playdate.system.logToConsole("Found text content (%d chars): %.*s...", @as(c_int, @intCast(copy_len)), @as(c_int, @intCast(preview_len)), @as([*:0]const u8, @ptrCast(&g_posts_out[g_post_count.*].content))); 304 + } 305 + return; 306 + } 307 + } 308 + 309 + // Extract alt text from image embeds 310 + if (std.mem.eql(u8, key_str, "alt") and json_type == pdapi.JSONValueType.JSONString) { 311 + // Check if we're in an image embed context (path should contain something like feed[n].post.embed.images[]) 312 + if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".post.embed.images[") != null) { 313 + if (g_post_count.* < g_post_max) { 314 + const alt_str = std.mem.span(value.data.stringval); 315 + 316 + // Create alt text with [IMG]: prefix 317 + const prefix = "[IMG]: "; 318 + const prefix_len = prefix.len; 319 + const max_alt_len = g_posts_out[g_post_count.*].alt_text.len - prefix_len - 1; 320 + const alt_copy_len = @min(alt_str.len, max_alt_len); 321 + 322 + // Copy prefix first 323 + @memcpy(g_posts_out[g_post_count.*].alt_text[0..prefix_len], prefix); 324 + 325 + // Copy alt text after prefix 326 + @memcpy(g_posts_out[g_post_count.*].alt_text[prefix_len .. prefix_len + alt_copy_len], alt_str[0..alt_copy_len]); 327 + 328 + g_posts_out[g_post_count.*].alt_text_len = prefix_len + alt_copy_len; 329 + } 330 + return; 331 + } 332 + } 333 + 334 + // Handle repost detection 335 + if (std.mem.eql(u8, key_str, "$type") and json_type == pdapi.JSONValueType.JSONString) { 336 + const type_str = std.mem.span(value.data.stringval); 337 + // Check if we're in a reason context (indicating a repost) 338 + if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".reason") != null) { 339 + if (std.mem.eql(u8, type_str, "app.bsky.feed.defs#reasonRepost")) { 340 + if (g_post_count.* < g_post_max) { 341 + g_posts_out[g_post_count.*].post_type = .repost; 342 + g_playdate.system.logToConsole("Detected repost"); 343 + } 344 + } 345 + return; 346 + } 347 + } 348 + 349 + // Handle repost author (the person who reposted) 350 + if (std.mem.eql(u8, key_str, "handle") and json_type == pdapi.JSONValueType.JSONString) { 351 + // Check if we're in a reason.by context (repost author) 352 + if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".reason.by") != null) { 353 + if (g_post_count.* < g_post_max) { 354 + const repost_author_str = std.mem.span(value.data.stringval); 355 + const copy_len = @min(repost_author_str.len, g_posts_out[g_post_count.*].repost_author.len - 1); 356 + @memcpy(g_posts_out[g_post_count.*].repost_author[0..copy_len], repost_author_str[0..copy_len]); 357 + g_posts_out[g_post_count.*].repost_author_len = copy_len; 358 + g_playdate.system.logToConsole("Found repost author: %.*s", @as(c_int, @intCast(copy_len)), @as([*:0]const u8, @ptrCast(&g_posts_out[g_post_count.*].repost_author))); 359 + } 360 + return; 361 + } 362 + } 363 + 364 + // Handle reply detection - look for reply field in record 365 + if (std.mem.eql(u8, key_str, "reply") and json_type == pdapi.JSONValueType.JSONTable) { 366 + // Check if we're in a post.record context 367 + if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".post.record") != null) { 368 + if (g_post_count.* < g_post_max) { 369 + g_posts_out[g_post_count.*].post_type = .reply; 370 + g_playdate.system.logToConsole("Detected reply"); 371 + } 372 + return; 373 + } 374 + } 375 + 376 + // Handle feed item initialization and completion 377 + if (std.mem.eql(u8, key_str, "post") and json_type == pdapi.JSONValueType.JSONTable) { 378 + // Check if we're at feed[n].post level (not deeper) 379 + if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.count(u8, path, ".") == 0) { 380 + // Extract the feed index from the path like "feed[5]" 381 + const feed_start = std.mem.indexOf(u8, path, "feed[").? + 5; 382 + const feed_end = std.mem.indexOf(u8, path[feed_start..], "]").? + feed_start; 383 + const index_str = path[feed_start..feed_end]; 384 + const feed_index = std.fmt.parseInt(i32, index_str, 10) catch -1; 385 + 386 + // If this is a new feed item, complete the previous post 387 + if (g_current_feed_index != feed_index) { 388 + if (g_current_feed_index >= 0 and g_post_count.* < g_post_max) { 389 + // Complete previous post if it has data 390 + const has_author = g_posts_out[g_post_count.*].author_len > 0; 391 + const has_content = g_posts_out[g_post_count.*].content_len > 0; 392 + const has_alt_text = g_posts_out[g_post_count.*].alt_text_len > 0; 393 + if (has_author or has_content or has_alt_text) { 394 + g_post_count.* += 1; 395 + } 396 + } 397 + 398 + // Initialize new post 399 + g_current_feed_index = feed_index; 400 + if (g_post_count.* < g_post_max) { 401 + g_posts_out[g_post_count.*] = BskyPost{ 402 + .author = [_]u8{0} ** AUTHOR_BUFFER_SIZE, 403 + .author_len = 0, 404 + .content = [_]u8{0} ** CONTENT_BUFFER_SIZE, 405 + .content_len = 0, 406 + .alt_text = [_]u8{0} ** ALT_TEXT_BUFFER_SIZE, 407 + .alt_text_len = 0, 408 + .post_type = .normal, 409 + .repost_author = [_]u8{0} ** AUTHOR_BUFFER_SIZE, 410 + .repost_author_len = 0, 411 + .reply_author = [_]u8{0} ** AUTHOR_BUFFER_SIZE, 412 + .reply_author_len = 0, 413 + }; 414 + g_playdate.system.logToConsole("Initialized new post at feed index %d", feed_index); 415 + } 416 + } 417 + return; 418 + } 419 + } 420 + } 421 + 422 + if (false) { 423 + debugDidDecodeTableValue(decoder, key, value); 424 + } 425 + } 426 + 427 + fn debugDidDecodeArrayValue(decoder: ?*pdapi.JSONDecoder, pos: c_int, value: pdapi.JSONValue) callconv(.C) void { 428 + _ = decoder; 429 + _ = pos; 430 + _ = value; 431 + } 432 + 433 + fn debugDidDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void { 434 + _ = decoder; 435 + _ = key; 436 + _ = value; 437 + } 438 + 439 + pub fn decodePostsJson( 440 + playdate: *pdapi.PlaydateAPI, 441 + buffer: []const u8, 442 + buffer_size: usize, 443 + post_max: usize, 444 + posts: []BskyPost, 445 + post_count: *usize, 446 + cursor_buffer: []u8, 447 + cursor_len: *usize, 448 + ) void { 449 + // Initialize globals for parsing 450 + g_playdate = playdate; 451 + g_posts_out = posts; 452 + g_post_count = post_count; 453 + g_post_max = post_max; 454 + _ = cursor_buffer; 455 + _ = cursor_len; 456 + 457 + const pd = playdate; 458 + 459 + if (buffer_size == 0) { 460 + pd.system.logToConsole("JSON decode called with empty buffer"); 461 + return; 462 + } 463 + 464 + // Reset parsing state 465 + post_count.* = 0; 466 + g_parsing_feed = false; 467 + g_current_feed_index = -1; 468 + 469 + // Log the first and last few characters of the JSON for debugging 470 + pd.system.logToConsole("JSON buffer size: %d bytes", @as(c_int, @intCast(buffer_size))); 471 + 472 + // Log first 100 characters 473 + const preview_len = @min(100, buffer_size); 474 + var preview_buffer: [101]u8 = undefined; 475 + @memcpy(preview_buffer[0..preview_len], buffer[0..preview_len]); 476 + preview_buffer[preview_len] = 0; 477 + pd.system.logToConsole("JSON start: %s", @as([*:0]const u8, @ptrCast(&preview_buffer))); 478 + 479 + // Log last 50 characters if buffer is large enough 480 + if (buffer_size > 50) { 481 + const tail_start = buffer_size - 50; 482 + var tail_buffer: [51]u8 = undefined; 483 + @memcpy(tail_buffer[0..50], buffer[tail_start..buffer_size]); 484 + tail_buffer[50] = 0; 485 + pd.system.logToConsole("JSON end: %s", @as([*:0]const u8, @ptrCast(&tail_buffer))); 486 + } 487 + 488 + if (false) { 489 + pdtools.logLargeMessage(pd, buffer, buffer_size); 490 + } 491 + 492 + var decoder = pdapi.JSONDecoder{ 493 + .willDecodeSublist = null, 494 + .shouldDecodeTableValueForKey = null, 495 + .didDecodeTableValue = root_didDecodeTableValue, 496 + .shouldDecodeArrayValueAtIndex = null, 497 + .didDecodeArrayValue = null, 498 + .didDecodeSublist = null, 499 + .decodeError = onDecodeError, 500 + .userdata = null, 501 + .returnString = 0, 502 + .path = null, 503 + }; 504 + 505 + var jsonValue: pdapi.JSONValue = undefined; 506 + 507 + pd.system.logToConsole("Starting JSON decoding..."); 508 + const decode_result = pd.json.decodeString(&decoder, @as(?[*:0]const u8, @ptrCast(buffer)), &jsonValue); 509 + pd.system.logToConsole("JSON decoding finished with result: %d", decode_result); 510 + 511 + // Complete the final post if we have one 512 + if (g_current_feed_index >= 0 and g_post_count.* < g_post_max) { 513 + const has_author = g_posts_out[g_post_count.*].author_len > 0; 514 + const has_content = g_posts_out[g_post_count.*].content_len > 0; 515 + const has_alt_text = g_posts_out[g_post_count.*].alt_text_len > 0; 516 + pd.system.logToConsole("Final post check: author=%d content=%d alt=%d", @as(c_int, if (has_author) 1 else 0), @as(c_int, if (has_content) 1 else 0), @as(c_int, if (has_alt_text) 1 else 0)); 517 + if (has_author or has_content or has_alt_text) { 518 + g_post_count.* += 1; 519 + pd.system.logToConsole("Added final post, total count: %d", @as(c_int, @intCast(g_post_count.*))); 520 + } 521 + } 522 + 523 + pd.system.logToConsole("JSON parsing complete - final post count: %d", @as(c_int, @intCast(g_post_count.*))); 524 + } 525 + 526 + pub fn renderPost( 527 + pd: *pdapi.PlaydateAPI, 528 + post: *const BskyPost, 529 + x: c_int, 530 + y: c_int, 531 + width: c_int, 532 + dry_run: bool, 533 + ) usize { 534 + const font = fonts.g_font; 535 + const padding = 4; 536 + const corner_radius = 4; 537 + 538 + // Account for post padding 539 + const content_width = @as(?c_int, @intCast(width - (padding * 2))); 540 + 541 + // Author height 542 + const author_len = std.mem.len(@as([*:0]const u8, @ptrCast(&post.author))); 543 + const author_drawable = DrawableText{ 544 + .playdate = pd, 545 + .text = post.author[0..author_len], 546 + .max_width = content_width, 547 + .font = font, 548 + .wrapping_mode = .WrapWord, 549 + .alignment = .AlignTextLeft, 550 + }; 551 + 552 + // Content height 553 + const content_len = std.mem.len(@as([*:0]const u8, @ptrCast(&post.content))); 554 + const content_drawable = DrawableText{ 555 + .playdate = pd, 556 + .text = post.content[0..content_len], 557 + .max_width = content_width, 558 + .font = font, 559 + .wrapping_mode = .WrapWord, 560 + .alignment = .AlignTextLeft, 561 + }; 562 + 563 + // Alt text height 564 + const alt_text_len = std.mem.len(@as([*:0]const u8, @ptrCast(&post.alt_text))); 565 + var alt_drawable = DrawableText{ 566 + .playdate = pd, 567 + .text = post.alt_text[0..alt_text_len], 568 + .max_width = content_width, 569 + .font = font, 570 + .wrapping_mode = .WrapWord, 571 + .alignment = .AlignTextLeft, 572 + }; 573 + 574 + // Render the post (it's at least partially visible) 575 + 576 + // Create DrawableText for @ symbol and author text 577 + const at_symbol = "@"; 578 + 579 + const at_drawable = DrawableText{ 580 + .playdate = pd, 581 + .text = at_symbol, 582 + .max_width = content_width, 583 + .font = font, 584 + .wrapping_mode = .WrapWord, 585 + .alignment = .AlignTextLeft, 586 + }; 587 + 588 + var current_y = y + padding; 589 + const current_x = x + padding; 590 + 591 + // Draw @ symbol and author text 592 + if (author_len > 0) { 593 + at_drawable.render(current_x, current_y, dry_run); 594 + const at_width = at_drawable.getWidth(); 595 + author_drawable.render(current_x + at_width, current_y, dry_run); 596 + current_y += author_drawable.getHeight(); 597 + } 598 + 599 + // Draw post content with text wrapping 600 + content_drawable.render(current_x, current_y, dry_run); 601 + current_y += content_drawable.getHeight(); 602 + current_y += padding; 603 + 604 + // Draw alt text if available 605 + if (alt_text_len > 0) { 606 + alt_drawable.render(current_x, current_y, dry_run); 607 + current_y += alt_drawable.getHeight(); 608 + current_y += padding; 609 + } 610 + 611 + const height = current_y - y; 612 + 613 + if (!dry_run) { 614 + pd.graphics.drawRoundRect( 615 + x, 616 + y, 617 + width, 618 + height, 619 + corner_radius, 620 + 1, 621 + @intFromEnum(pdapi.LCDSolidColor.ColorBlack), 622 + ); 623 + } 624 + 625 + return @as(usize, @intCast(height)); // Return total height of the rendered post 626 + }
+14
src/definitions.zig
··· 1 + pub const BSKY_USERNAME_SIZE = 64; 2 + pub const BSKY_PASSWORD_SIZE = 64; 3 + 4 + pub const EDITOR_BUFFER_SIZE = 256; 5 + pub const RESPONSE_BUFFER_SIZE = 1024 * 1024; 6 + 7 + pub const MARGIN = 4; 8 + 9 + pub const DEBUG_DONT_POST = true; 10 + 11 + // Credential storage constants 12 + pub const USERNAME_FILENAME = "bsky-username.txt"; 13 + pub const PASSWORD_FILENAME = "bsky-password.txt"; 14 + pub const MAX_CREDENTIAL_LENGTH = 128;
+19
src/fonts.zig
··· 1 + const pdapi = @import("playdate_api_definitions.zig"); 2 + 3 + const g_font_path = "assets/fonts/Roobert-10-Bold.pft"; 4 + 5 + pub var g_font: *pdapi.LCDFont = undefined; 6 + 7 + pub fn initializeFonts(pd: *pdapi.PlaydateAPI) void { 8 + 9 + // Load the font 10 + const font = pd.graphics.loadFont(g_font_path, null); 11 + 12 + if (font) |f| { 13 + pd.graphics.setFont(f); 14 + g_font = f; 15 + pd.system.logToConsole("Custom Roobert font loaded successfully"); 16 + } else { 17 + pd.system.logToConsole("Failed to load custom font, using system default"); 18 + } 19 + }
+1243
src/main.zig
··· 1 + const std = @import("std"); 2 + const pdapi = @import("playdate_api_definitions.zig"); 3 + const network = @import("network.zig"); 4 + const bsky_post = @import("bsky_post.zig"); 5 + const pdtools = @import("pdtools/index.zig"); 6 + const ScrollingValue = @import("pdtools/ScrollingValue.zig").SlidingValue; 7 + const panic_handler = @import("panic_handler.zig"); 8 + const defs = @import("definitions.zig"); 9 + const keyboard_mod = @import("pdtools/keyboard.zig"); 10 + const Keyboard = keyboard_mod.Keyboard(256); 11 + 12 + const fonts = @import("fonts.zig"); 13 + 14 + // Global buffers to avoid large stack allocations 15 + var g_headers_buffer: [512]u8 = undefined; 16 + 17 + // Login input state 18 + const LoginInputState = enum { 19 + username, 20 + password, 21 + ready, 22 + }; 23 + 24 + // Login field selection 25 + const LoginFieldSelection = enum { 26 + username, 27 + password, 28 + login_button, 29 + }; 30 + 31 + // Post field selection 32 + const PostFieldSelection = enum { 33 + edit_text, 34 + confirm_post, 35 + }; 36 + 37 + // Simple app state for timeline fetching 38 + const AppState = struct { 39 + playdate: *pdapi.PlaydateAPI, 40 + message: []const u8, 41 + response_buffer: [defs.RESPONSE_BUFFER_SIZE]u8 = undefined, 42 + response_length: usize, 43 + 44 + posts: [bsky_post.MAX_POSTS]bsky_post.BskyPost, 45 + post_count: usize, 46 + cursor_buffer: [bsky_post.CURSOR_BUFFER_SIZE]u8, 47 + cursor_len: usize, 48 + 49 + network_access_requested: bool, 50 + body_scroll: ScrollingValue, 51 + 52 + // Login state 53 + is_logged_in: bool, 54 + access_token: [512]u8, 55 + access_token_len: usize, 56 + user_did: [256]u8, 57 + user_did_len: usize, 58 + 59 + keyboard: keyboard_mod.Keyboard(256), 60 + 61 + // Login input state 62 + login_input_state: LoginInputState, 63 + login_field_selection: LoginFieldSelection, 64 + username: [defs.MAX_CREDENTIAL_LENGTH]u8, 65 + password: [defs.MAX_CREDENTIAL_LENGTH]u8, 66 + 67 + previous_buttons: pdapi.PDButtons, 68 + 69 + // Post composition state 70 + post_field_selection: PostFieldSelection, 71 + post_text: [defs.EDITOR_BUFFER_SIZE]u8, 72 + post_text_len: usize, 73 + }; 74 + 75 + var g_app: AppState = undefined; 76 + 77 + const Page = enum { 78 + login, 79 + home, 80 + post, 81 + }; 82 + 83 + var g_selected_page: Page = .login; 84 + 85 + const LCD_WIDTH = pdapi.LCD_COLUMNS; 86 + const LCD_HEIGHT = pdapi.LCD_ROWS; 87 + const MARGIN = 4; 88 + const CHUNK_SIZE = 512; // Read 512 bytes at a time 89 + 90 + // Credential storage functions 91 + fn saveCredentials(pd: *pdapi.PlaydateAPI) void { 92 + pdtools.saveStringFile(pd, defs.USERNAME_FILENAME, &g_app.username); 93 + pdtools.saveStringFile(pd, defs.PASSWORD_FILENAME, &g_app.password); 94 + } 95 + 96 + fn loadCredentials(pd: *pdapi.PlaydateAPI) void { 97 + pd.system.logToConsole("Loading username from: %s", defs.USERNAME_FILENAME.ptr); 98 + pdtools.loadStringFile(pd, defs.USERNAME_FILENAME, &g_app.username); 99 + pd.system.logToConsole("Loading password from: %s", defs.PASSWORD_FILENAME.ptr); 100 + pdtools.loadStringFile(pd, defs.PASSWORD_FILENAME, &g_app.password); 101 + } 102 + 103 + fn clearCredentials(pd: *pdapi.PlaydateAPI) void { 104 + // Clear in-memory credentials 105 + @memset(&g_app.username, 0); 106 + @memset(&g_app.password, 0); 107 + g_app.login_input_state = .username; 108 + g_app.login_field_selection = .username; 109 + 110 + pdtools.delStringFile(pd, defs.USERNAME_FILENAME); 111 + pdtools.delStringFile(pd, defs.PASSWORD_FILENAME); 112 + 113 + pd.system.logToConsole("Credentials cleared"); 114 + g_app.message = "Credentials cleared - enter new ones"; 115 + } 116 + 117 + // Network callback functions 118 + fn onLoginSuccess(status_code: network.HttpStatusCode, response_data: []const u8) void { 119 + const pd = g_app.playdate; 120 + pd.system.logToConsole("Login successful with status: %d", status_code); 121 + 122 + // Parse the JSON to extract access token 123 + if (std.mem.indexOf(u8, response_data, "\"accessJwt\":\"")) |start_pos| { 124 + const token_start = start_pos + 13; // Length of "accessJwt":" 125 + if (std.mem.indexOfPos(u8, response_data, token_start, "\"")) |end_pos| { 126 + const token_len = end_pos - token_start; 127 + if (token_len < g_app.access_token.len) { 128 + @memcpy(g_app.access_token[0..token_len], response_data[token_start..end_pos]); 129 + g_app.access_token_len = token_len; 130 + 131 + // Also parse the DID from the response 132 + if (std.mem.indexOf(u8, response_data, "\"did\":\"")) |did_start_pos| { 133 + const did_token_start = did_start_pos + 7; // Length of "did":" 134 + if (std.mem.indexOfPos(u8, response_data, did_token_start, "\"")) |did_end_pos| { 135 + const did_len = did_end_pos - did_token_start; 136 + if (did_len < g_app.user_did.len) { 137 + @memcpy(g_app.user_did[0..did_len], response_data[did_token_start..did_end_pos]); 138 + g_app.user_did[did_len] = 0; 139 + g_app.user_did_len = did_len; 140 + pd.system.logToConsole("User DID extracted: %.*s", @as(c_int, @intCast(did_len)), @as([*c]const u8, @ptrCast(&g_app.user_did))); 141 + } 142 + } 143 + } 144 + 145 + g_app.is_logged_in = true; 146 + g_app.message = "Login successful - fetching posts..."; 147 + g_selected_page = .home; 148 + pd.system.logToConsole("Access token extracted successfully (length: %d)", @as(c_int, @intCast(token_len))); 149 + 150 + // Automatically fetch posts after successful login 151 + fetchBlueskyFeed(); 152 + } else { 153 + g_app.message = "Access token too long"; 154 + pd.system.logToConsole("Access token too long: %d", @as(c_int, @intCast(token_len))); 155 + } 156 + } else { 157 + g_app.message = "Failed to parse access token"; 158 + pd.system.logToConsole("Failed to find end of access token"); 159 + } 160 + } else { 161 + g_app.message = "Access token not found"; 162 + pd.system.logToConsole("Access token not found in response"); 163 + } 164 + } 165 + 166 + fn onLoginFailure(status_code: network.HttpStatusCode, error_message: []const u8) void { 167 + const pd = g_app.playdate; 168 + pd.system.logToConsole("Login failed with status: %d", status_code); 169 + pd.system.logToConsole("Error: %.*s", @as(c_int, @intCast(error_message.len)), @as([*c]const u8, @ptrCast(error_message.ptr))); 170 + g_app.message = if (status_code == 0) "Login connection error" else "Login failed"; 171 + } 172 + 173 + fn onFeedSuccess(status_code: network.HttpStatusCode, response_data: []const u8) void { 174 + const pd = g_app.playdate; 175 + pd.system.logToConsole("Feed fetch successful with status: %d, %d bytes", status_code, response_data.len); 176 + 177 + // The response_data is already pointing to our g_app.response_buffer 178 + // So we just need to set the length - no copying needed 179 + g_app.response_length = response_data.len; 180 + 181 + // Ensure null termination 182 + if (g_app.response_length < g_app.response_buffer.len) { 183 + g_app.response_buffer[g_app.response_length] = 0; 184 + } 185 + 186 + g_app.message = "Feed data received"; 187 + 188 + // Parse the JSON response into posts 189 + parseFeedResponseIntoPosts(); 190 + } 191 + 192 + fn onFeedFailure(status_code: network.HttpStatusCode, error_message: []const u8) void { 193 + const pd = g_app.playdate; 194 + pd.system.logToConsole("Feed fetch failed with status: %d", status_code); 195 + 196 + if (error_message.len > 0) { 197 + pd.system.logToConsole("Error message: %.*s", @as(c_int, @intCast(error_message.len)), @as([*c]const u8, @ptrCast(error_message.ptr))); 198 + } else { 199 + pd.system.logToConsole("No error message provided"); 200 + } 201 + 202 + // Clear any previous response data 203 + g_app.response_length = 0; 204 + g_app.post_count = 0; 205 + 206 + if (status_code == 0) { 207 + g_app.message = "Network connection failed"; 208 + } else if (status_code == 401) { 209 + g_app.message = "Login expired - please re-login"; 210 + } else if (status_code >= 400 and status_code < 500) { 211 + g_app.message = "Request error - check credentials"; 212 + } else if (status_code >= 500) { 213 + g_app.message = "Server error - try again later"; 214 + } else { 215 + g_app.message = "Feed fetch failed"; 216 + } 217 + } 218 + 219 + fn onPostSuccess(status_code: network.HttpStatusCode, response_data: []const u8) void { 220 + const pd = g_app.playdate; 221 + pd.system.logToConsole("Post successful with status: %d", status_code); 222 + 223 + // Clear the post text since it was successfully posted 224 + @memset(&g_app.post_text, 0); 225 + g_app.post_text_len = 0; 226 + 227 + // Reset to default text for next post 228 + const default_post_text = "Hello World!\n\nPosted from my #playdate"; 229 + const default_len = @min(default_post_text.len, g_app.post_text.len - 1); 230 + @memcpy(g_app.post_text[0..default_len], default_post_text[0..default_len]); 231 + g_app.post_text[default_len] = 0; 232 + g_app.post_text_len = default_len; 233 + 234 + g_app.message = "Post published successfully!"; 235 + 236 + // Log response for debugging 237 + if (response_data.len > 0) { 238 + const preview_len = @min(100, response_data.len); 239 + pd.system.logToConsole("Post response: %.*s", @as(c_int, @intCast(preview_len)), @as([*c]const u8, @ptrCast(response_data.ptr))); 240 + } 241 + } 242 + 243 + fn onPostFailure(status_code: network.HttpStatusCode, error_message: []const u8) void { 244 + const pd = g_app.playdate; 245 + pd.system.logToConsole("Post failed with status: %d", status_code); 246 + 247 + if (error_message.len > 0) { 248 + pd.system.logToConsole("Post error: %.*s", @as(c_int, @intCast(error_message.len)), @as([*c]const u8, @ptrCast(error_message.ptr))); 249 + } 250 + 251 + if (status_code == 0) { 252 + g_app.message = "Post failed - network error"; 253 + } else if (status_code == 401) { 254 + g_app.message = "Post failed - please re-login"; 255 + } else if (status_code == 413) { 256 + g_app.message = "Post too long - please shorten"; 257 + } else if (status_code >= 400 and status_code < 500) { 258 + g_app.message = "Post failed - invalid request"; 259 + } else if (status_code >= 500) { 260 + g_app.message = "Post failed - server error"; 261 + } else { 262 + g_app.message = "Post failed - unknown error"; 263 + } 264 + } 265 + 266 + fn parseFeedResponseIntoPosts() void { 267 + // Check if we have a valid response 268 + if (g_app.response_length == 0) { 269 + g_app.playdate.system.logToConsole("No response data to parse"); 270 + g_app.message = "No response data received"; 271 + return; 272 + } 273 + 274 + // Validate response length isn't too large 275 + if (g_app.response_length >= g_app.response_buffer.len) { 276 + g_app.playdate.system.logToConsole("WARNING: Response length %d exceeds buffer size %d", @as(c_int, @intCast(g_app.response_length)), @as(c_int, @intCast(g_app.response_buffer.len))); 277 + g_app.response_length = g_app.response_buffer.len - 1; 278 + } 279 + 280 + g_app.playdate.system.logToConsole("Starting JSON parsing of %d bytes", @as(c_int, @intCast(g_app.response_length))); 281 + 282 + // Log first few characters for debugging 283 + const preview_len = @min(100, g_app.response_length); 284 + g_app.playdate.system.logToConsole("Response preview (first %d chars): %.*s", @as(c_int, @intCast(preview_len)), @as(c_int, @intCast(preview_len)), @as([*]const u8, @ptrCast(&g_app.response_buffer))); 285 + 286 + // Reset post count before parsing 287 + g_app.post_count = 0; 288 + 289 + // Add error handling around JSON parsing 290 + bsky_post.decodePostsJson( 291 + g_app.playdate, 292 + &g_app.response_buffer, 293 + g_app.response_length, 294 + g_app.posts.len, 295 + &g_app.posts, 296 + &g_app.post_count, 297 + &g_app.cursor_buffer, 298 + &g_app.cursor_len, 299 + ); 300 + 301 + g_app.playdate.system.logToConsole("JSON parsing completed, found %d posts", @as(c_int, @intCast(g_app.post_count))); 302 + 303 + if (g_app.post_count == 0) { 304 + g_app.message = "No posts found in response"; 305 + } else { 306 + g_app.message = "Feed loaded successfully"; 307 + } 308 + } 309 + 310 + fn onScrollMaxThreshold() void { 311 + // Handle scroll max threshold being reached 312 + g_app.playdate.system.logToConsole("Scroll max threshold reached"); 313 + } 314 + 315 + fn onScrollMinThreshold() void { 316 + // Handle scroll min threshold being reached 317 + g_app.playdate.system.logToConsole("Scroll min threshold reached"); 318 + 319 + // Refresh posts. 320 + g_app.playdate.system.logToConsole("Refreshing posts..."); 321 + fetchBlueskyFeed(); 322 + } 323 + 324 + pub export fn eventHandler(playdate: *pdapi.PlaydateAPI, event: pdapi.PDSystemEvent, arg: u32) callconv(.C) c_int { 325 + _ = arg; 326 + switch (event) { 327 + .EventInit => { 328 + playdate.system.logToConsole("Initializing timeline fetch app..."); 329 + 330 + // Initialize panic handler for better error reporting 331 + panic_handler.init(playdate); 332 + 333 + // Test basic memory allocation to catch early issues 334 + playdate.system.logToConsole("Testing basic memory allocation..."); 335 + 336 + playdate.system.logToConsole("Memory test passed - AppState size: %d bytes", @as(c_int, @intCast(@sizeOf(AppState)))); 337 + 338 + playdate.system.logToConsole("Initializing full application"); 339 + 340 + // Initialize full app state 341 + g_app.playdate = playdate; 342 + 343 + fonts.initializeFonts(playdate); 344 + 345 + // Initialize keyboard 346 + g_app.keyboard = keyboard_mod.Keyboard(256){ 347 + .playdate = playdate, 348 + .font = fonts.g_font, 349 + .title = undefined, 350 + .editor_buffer = undefined, 351 + .output_buffer = undefined, 352 + }; 353 + 354 + g_app.message = "Use ← → to navigate, B to interact"; 355 + g_app.response_length = 0; 356 + g_app.post_count = 0; 357 + g_app.cursor_len = 0; 358 + g_app.network_access_requested = false; 359 + 360 + // Initialize scrolling value for posts with reasonable bounds 361 + g_app.body_scroll = ScrollingValue{ 362 + .playdate = playdate, 363 + .soft_min_runout = 100, // Static runout distance from min_value 364 + .soft_max_runout = 100, // Static runout distance from max_value 365 + .runout_margin = 5.0, // Margin before callback detection 366 + .min_value = -200, // Allow scrolling above top with hard limit 367 + .max_value = 2000, // Hard limit - will be updated based on content 368 + .current_value = 0, 369 + .current_height = 0, 370 + .crank_position_offset = 0, 371 + .onScrollMaxThreshold = &onScrollMaxThreshold, 372 + .onScrollMinThreshold = &onScrollMinThreshold, 373 + }; 374 + g_app.is_logged_in = false; 375 + g_app.access_token_len = 0; 376 + g_app.user_did_len = 0; 377 + // Initialize login state with default credentials for testing 378 + g_app.login_input_state = .ready; // Start ready since we have default credentials 379 + g_app.login_field_selection = .username; // Start with username selected 380 + 381 + // Initialize with empty credentials first 382 + @memset(&g_app.username, 0); 383 + @memset(&g_app.password, 0); 384 + 385 + g_app.previous_buttons = 0; 386 + 387 + // Initialize post composition state with default text 388 + g_app.post_field_selection = .edit_text; 389 + const default_post_text = "Hello World!\n\nPosted from my #playdate"; 390 + const default_len = @min(default_post_text.len, g_app.post_text.len - 1); 391 + @memcpy(g_app.post_text[0..default_len], default_post_text[0..default_len]); 392 + g_app.post_text[default_len] = 0; 393 + g_app.post_text_len = default_len; 394 + 395 + // Set up real update callback with full functionality 396 + playdate.system.setUpdateCallback(update_and_render, null); 397 + playdate.system.logToConsole("Update callback set"); 398 + 399 + playdate.system.logToConsole("App initialized successfully"); 400 + 401 + playdate.graphics.clear(@intFromEnum(pdapi.LCDSolidColor.ColorWhite)); 402 + 403 + // Try to load saved credentials 404 + playdate.system.logToConsole("Attempting to load saved credentials..."); 405 + loadCredentials(playdate); 406 + g_app.login_input_state = .ready; 407 + playdate.system.logToConsole("Credentials loaded - Username: %s", &g_app.username); 408 + g_app.message = "Saved credentials loaded"; 409 + }, 410 + 411 + else => {}, 412 + } 413 + return 0; 414 + } 415 + 416 + fn loginToBluesky() void { 417 + const pd = g_app.playdate; 418 + 419 + // Check if we're already busy with a network operation 420 + if (!network.isNetworkIdle()) { 421 + return; 422 + } 423 + 424 + // Request network access 425 + if (!g_app.network_access_requested) { 426 + pd.system.logToConsole("Requesting network access for https://bsky.social..."); 427 + const response = pd.network.playdate_http.requestAccess("https://bsky.social", 443, true, "login", null, null); 428 + g_app.network_access_requested = true; 429 + 430 + switch (response) { 431 + .AccessAllow => { 432 + pd.system.logToConsole("Network access GRANTED"); 433 + }, 434 + .AccessDeny => { 435 + pd.system.logToConsole("Network access DENIED by user"); 436 + g_app.message = "Network access denied"; 437 + return; 438 + }, 439 + else => { 440 + pd.system.logToConsole("Network access request FAILED"); 441 + g_app.message = "Access request failed"; 442 + return; 443 + }, 444 + } 445 + } else { 446 + pd.system.logToConsole("Network access already requested"); 447 + } 448 + 449 + // Check if we have credentials 450 + if (g_app.username[0] == 0 or g_app.password[0] == 0) { 451 + g_app.message = "Please enter username and password"; 452 + g_app.login_input_state = if (g_app.username[0] == 0) .username else .password; 453 + return; 454 + } 455 + 456 + // Prepare login request body using entered credentials 457 + var login_body: [512]u8 = undefined; 458 + const username = std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.username))); 459 + const password = std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.password))); 460 + 461 + // Debug logging for credentials 462 + pd.system.logToConsole("Login credentials - Username: %s", @as([*:0]const u8, @ptrCast(&g_app.username))); 463 + pd.system.logToConsole("Login credentials - Password: %s", @as([*:0]const u8, @ptrCast(&g_app.password))); 464 + const login_json = std.fmt.bufPrint(&login_body, "{{\"identifier\":\"{s}\",\"password\":\"{s}\"}}", .{ username, password }) catch { 465 + g_app.message = "Failed to format login request"; 466 + return; 467 + }; 468 + 469 + // Create HTTP request 470 + const request = network.HttpRequest{ 471 + .method = .POST, 472 + .url = "https://bsky.social/xrpc/com.atproto.server.createSession", 473 + .server = "bsky.social", 474 + .port = 443, 475 + .path = "/xrpc/com.atproto.server.createSession", 476 + .use_https = true, 477 + .bearer_token = null, 478 + .body = login_json, 479 + .response_buffer = &g_app.response_buffer, 480 + .success_callback = onLoginSuccess, 481 + .failure_callback = onLoginFailure, 482 + }; 483 + 484 + if (network.makeHttpRequest(pd, request)) { 485 + g_app.message = "Logging in..."; 486 + } else { 487 + g_app.message = "Network connection failed - check internet"; 488 + pd.system.logToConsole("Troubleshooting suggestions:"); 489 + pd.system.logToConsole("- Check internet connection"); 490 + pd.system.logToConsole("- Try in Playdate simulator vs device"); 491 + pd.system.logToConsole("- Check firewall settings"); 492 + pd.system.logToConsole("- Verify DNS resolution works"); 493 + } 494 + } 495 + 496 + fn fetchBlueskyFeed() void { 497 + const pd = g_app.playdate; 498 + 499 + // Check if logged in 500 + if (!g_app.is_logged_in) { 501 + g_app.message = "Please log in first"; 502 + return; 503 + } 504 + 505 + // Check if we're already busy with a network operation 506 + if (!network.isNetworkIdle()) { 507 + return; 508 + } 509 + 510 + // Reset state for refetch 511 + g_app.post_count = 0; 512 + g_app.body_scroll.setValue(0); 513 + 514 + // Request network access only once 515 + if (!g_app.network_access_requested) { 516 + pd.system.logToConsole("Requesting network access..."); 517 + const response = pd.network.playdate_http.requestAccess("https://bsky.social", 443, true, "timeline", null, null); 518 + g_app.network_access_requested = true; 519 + 520 + switch (response) { 521 + .AccessAllow => {}, 522 + .AccessDeny => { 523 + g_app.message = "Network access denied"; 524 + return; 525 + }, 526 + else => { 527 + g_app.message = "Access request failed"; 528 + return; 529 + }, 530 + } 531 + } 532 + 533 + // Create HTTP request with bearer token 534 + const bearer_token = g_app.access_token[0..g_app.access_token_len]; 535 + const request = network.HttpRequest{ 536 + .method = .GET, 537 + .url = "https://bsky.social/xrpc/app.bsky.feed.getTimeline?limit=50", 538 + .server = "bsky.social", 539 + .port = 443, 540 + .path = "/xrpc/app.bsky.feed.getTimeline?limit=50", 541 + .use_https = true, 542 + .bearer_token = bearer_token, 543 + .body = null, 544 + .response_buffer = &g_app.response_buffer, 545 + .success_callback = onFeedSuccess, 546 + .failure_callback = onFeedFailure, 547 + }; 548 + 549 + if (network.makeHttpRequest(pd, request)) { 550 + g_app.message = "Fetching feed data..."; 551 + } else { 552 + g_app.message = "Failed to start feed request"; 553 + } 554 + } 555 + 556 + // Login keyboard callbacks 557 + fn usernameKeyboardCancelled() void { 558 + g_app.playdate.system.logToConsole("Username input cancelled"); 559 + g_app.login_input_state = .username; // Stay on username input 560 + } 561 + 562 + fn usernameKeyboardConfirmed(text: []const u8) void { 563 + g_app.playdate.system.logToConsole("Username entered: %.*s", @as(c_int, @intCast(text.len)), @as([*c]const u8, @ptrCast(text.ptr))); 564 + 565 + // Copy username 566 + const copy_len = @min(text.len, g_app.username.len - 1); 567 + @memcpy(g_app.username[0..copy_len], text[0..copy_len]); 568 + g_app.username[copy_len] = 0; 569 + 570 + saveCredentials(g_app.playdate); 571 + 572 + // Move to password input 573 + g_app.login_input_state = .password; 574 + } 575 + 576 + fn passwordKeyboardCancelled() void { 577 + g_app.playdate.system.logToConsole("Password input cancelled"); 578 + g_app.login_input_state = .username; // Go back to username input 579 + } 580 + 581 + fn passwordKeyboardConfirmed(text: []const u8) void { 582 + g_app.playdate.system.logToConsole("Password entered (length: %d)", @as(c_int, @intCast(text.len))); 583 + 584 + // Copy password 585 + const copy_len = @min(text.len, g_app.password.len - 1); 586 + @memcpy(g_app.password[0..copy_len], text[0..copy_len]); 587 + g_app.password[copy_len] = 0; 588 + 589 + saveCredentials(g_app.playdate); 590 + 591 + g_app.login_input_state = .ready; 592 + } 593 + 594 + // JSON writer context for building post JSON 595 + const JsonWriteContext = struct { 596 + buffer: []u8, 597 + pos: usize, 598 + }; 599 + 600 + // JSON write callback for Playdate JSON encoder 601 + fn jsonWriteCallback(userdata: ?*anyopaque, str: [*c]const u8, len: c_int) callconv(.C) void { 602 + if (userdata == null) return; 603 + const context: *JsonWriteContext = @ptrCast(@alignCast(userdata.?)); 604 + const string_len = @as(usize, @intCast(len)); 605 + 606 + if (context.pos + string_len > context.buffer.len) return; // Buffer overflow protection 607 + 608 + const src_slice = @as([*]const u8, @ptrCast(str))[0..string_len]; 609 + @memcpy(context.buffer[context.pos .. context.pos + string_len], src_slice); 610 + context.pos += string_len; 611 + } 612 + 613 + fn submitBlueskyPost() void { 614 + const pd = g_app.playdate; 615 + 616 + // Check if logged in 617 + if (!g_app.is_logged_in) { 618 + g_app.message = "Please log in first"; 619 + return; 620 + } 621 + 622 + // Check if we have post content 623 + if (g_app.post_text_len == 0) { 624 + g_app.message = "Please enter post content first"; 625 + return; 626 + } 627 + 628 + // Check if we're already busy with a network operation 629 + if (!network.isNetworkIdle()) { 630 + return; 631 + } 632 + 633 + // Request network access if not already done 634 + if (!g_app.network_access_requested) { 635 + const response = pd.network.playdate_http.requestAccess("https://bsky.social", 443, true, "post", null, null); 636 + g_app.network_access_requested = true; 637 + 638 + switch (response) { 639 + .AccessAllow => {}, 640 + .AccessDeny => { 641 + g_app.message = "Network access denied"; 642 + return; 643 + }, 644 + else => { 645 + g_app.message = "Access request failed"; 646 + return; 647 + }, 648 + } 649 + } 650 + 651 + // Get current timestamp in ISO 8601 format using accurate Playdate API 652 + const seconds_since_2000 = pd.system.getSecondsSinceEpoch(null); 653 + 654 + // Convert to PDDateTime for formatting 655 + var datetime: pdapi.PDDateTime = undefined; 656 + pd.system.convertEpochToDateTime(seconds_since_2000, &datetime); 657 + 658 + // Format as ISO 8601 using the utility function 659 + const timestamp = pdtools.datetimeToISO(datetime); 660 + 661 + // Check if we have user DID 662 + if (g_app.user_did_len == 0) { 663 + g_app.message = "Missing user DID - please re-login"; 664 + return; 665 + } 666 + 667 + // Prepare post request body using Playdate JSON encoder 668 + var post_body: [1024]u8 = undefined; 669 + var json_context = JsonWriteContext{ .buffer = &post_body, .pos = 0 }; 670 + 671 + var encoder: pdapi.JSONEncoder = undefined; 672 + pd.json.initEncoder(&encoder, jsonWriteCallback, &json_context, 0); 673 + 674 + const post_text = g_app.post_text[0..g_app.post_text_len]; 675 + const user_did = g_app.user_did[0..g_app.user_did_len]; 676 + 677 + // Build JSON: {"repo":"...", "collection":"...", "record":{"text":"...", "createdAt":"..."}} 678 + encoder.startTable(&encoder); 679 + 680 + // Add repo field 681 + encoder.addTableMember(&encoder, "repo", 4); 682 + encoder.writeString(&encoder, @as([*c]const u8, @ptrCast(user_did.ptr)), @as(c_int, @intCast(user_did.len))); 683 + 684 + // Add collection field 685 + encoder.addTableMember(&encoder, "collection", 10); 686 + encoder.writeString(&encoder, "app.bsky.feed.post", 18); 687 + 688 + // Add record field 689 + encoder.addTableMember(&encoder, "record", 6); 690 + encoder.startTable(&encoder); 691 + 692 + // Add text field to record 693 + encoder.addTableMember(&encoder, "text", 4); 694 + encoder.writeString(&encoder, @as([*c]const u8, @ptrCast(post_text.ptr)), @as(c_int, @intCast(post_text.len))); 695 + 696 + // Add createdAt field to record 697 + encoder.addTableMember(&encoder, "createdAt", 9); 698 + encoder.writeString(&encoder, @as([*c]const u8, @ptrCast(timestamp.ptr)), @as(c_int, @intCast(timestamp.len))); 699 + 700 + encoder.endTable(&encoder); // End record 701 + encoder.endTable(&encoder); // End root 702 + 703 + const post_json = post_body[0..json_context.pos]; 704 + 705 + // Log json 706 + pdtools.logLargeMessage(pd, post_json, post_json.len); 707 + 708 + // Create HTTP request 709 + if (defs.DEBUG_DONT_POST) { 710 + g_app.message = "Debug mode - not posting"; 711 + return; 712 + } 713 + 714 + const bearer_token = g_app.access_token[0..g_app.access_token_len]; 715 + const request = network.HttpRequest{ 716 + .method = .POST, 717 + .url = "https://bsky.social/xrpc/com.atproto.repo.createRecord", 718 + .server = "bsky.social", 719 + .port = 443, 720 + .path = "/xrpc/com.atproto.repo.createRecord", 721 + .use_https = true, 722 + .bearer_token = bearer_token, 723 + .body = post_json, 724 + .response_buffer = &g_app.response_buffer, 725 + .success_callback = onPostSuccess, 726 + .failure_callback = onPostFailure, 727 + }; 728 + 729 + if (network.makeHttpRequest(pd, request)) { 730 + g_app.message = "Posting..."; 731 + } else { 732 + g_app.message = "Failed to start post request"; 733 + } 734 + } 735 + 736 + // Post keyboard callbacks 737 + fn postKeyboardCancelled() void { 738 + g_app.playdate.system.logToConsole("Post input cancelled"); 739 + // Stay on post page, don't change anything 740 + } 741 + 742 + // New callback for text editing that doesn't auto-submit 743 + fn postTextEditConfirmed(text: []const u8) void { 744 + g_app.playdate.system.logToConsole("Post text edited (length: %d)", @as(c_int, @intCast(text.len))); 745 + 746 + // Copy post text but don't submit automatically 747 + const copy_len = @min(text.len, g_app.post_text.len - 1); 748 + @memcpy(g_app.post_text[0..copy_len], text[0..copy_len]); 749 + g_app.post_text[copy_len] = 0; 750 + g_app.post_text_len = copy_len; 751 + 752 + g_app.message = "Post text updated - use confirm button to post"; 753 + } 754 + 755 + fn update_and_render(userdata: ?*anyopaque) callconv(.C) c_int { 756 + _ = userdata; // Ignore userdata, use global state 757 + 758 + // Safety check - ensure app is initialized 759 + const pd = g_app.playdate; 760 + 761 + // Update network processing 762 + network.updateNetwork(pd); 763 + 764 + if (g_app.keyboard.updateAndRender()) { 765 + return 1; 766 + } 767 + 768 + // Handle button input 769 + var current: pdapi.PDButtons = undefined; 770 + var pushed: pdapi.PDButtons = undefined; 771 + var released: pdapi.PDButtons = undefined; 772 + pd.system.getButtonState(&current, &pushed, &released); 773 + 774 + // Handle keyboard input first if keyboard is active 775 + 776 + // Handle up/down navigation for login field selection 777 + if (g_selected_page == .login and !g_app.is_logged_in) { 778 + if (pushed & pdapi.BUTTON_UP != 0) { 779 + g_app.login_field_selection = switch (g_app.login_field_selection) { 780 + .username => .login_button, 781 + .password => .username, 782 + .login_button => .password, 783 + }; 784 + } else if (pushed & pdapi.BUTTON_DOWN != 0) { 785 + g_app.login_field_selection = switch (g_app.login_field_selection) { 786 + .username => .password, 787 + .password => .login_button, 788 + .login_button => .username, 789 + }; 790 + } 791 + } 792 + 793 + // Handle up/down navigation for post field selection 794 + if (g_selected_page == .post and g_app.is_logged_in) { 795 + if (pushed & pdapi.BUTTON_UP != 0) { 796 + g_app.post_field_selection = switch (g_app.post_field_selection) { 797 + .edit_text => .confirm_post, 798 + .confirm_post => .edit_text, 799 + }; 800 + } else if (pushed & pdapi.BUTTON_DOWN != 0) { 801 + g_app.post_field_selection = switch (g_app.post_field_selection) { 802 + .edit_text => .confirm_post, 803 + .confirm_post => .edit_text, 804 + }; 805 + } 806 + } 807 + 808 + // Check if left/right was pressed to change page selection 809 + if (pushed & pdapi.BUTTON_LEFT != 0) { 810 + g_selected_page = switch (g_selected_page) { 811 + .login => if (g_app.is_logged_in) .post else .login, 812 + .home => .login, 813 + .post => .home, 814 + }; 815 + } else if (pushed & pdapi.BUTTON_RIGHT != 0) { 816 + g_selected_page = switch (g_selected_page) { 817 + .login => if (g_app.is_logged_in) .home else .login, 818 + .home => if (g_app.is_logged_in) .post else .login, 819 + .post => .login, 820 + }; 821 + } 822 + 823 + // Check if B button was pushed (only if keyboard not active) 824 + if (pushed & pdapi.BUTTON_B != 0) { 825 + if (g_selected_page == .login and !g_app.is_logged_in) { 826 + // Handle login input based on selected field 827 + switch (g_app.login_field_selection) { 828 + .username => { 829 + // Start username input 830 + const initial_text = if (g_app.username[0] != 0) std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.username))) else null; 831 + g_app.keyboard.start( 832 + "Enter username:", 833 + g_app.username.len, 834 + initial_text, 835 + false, 836 + 1, 837 + usernameKeyboardCancelled, 838 + usernameKeyboardConfirmed, 839 + ); 840 + }, 841 + .password => { 842 + // Start password input 843 + const initial_text = if (g_app.password[0] != 0) std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.password))) else null; 844 + g_app.keyboard.start( 845 + "Enter password:", 846 + g_app.password.len, 847 + initial_text, 848 + false, 849 + 1, 850 + passwordKeyboardCancelled, 851 + passwordKeyboardConfirmed, 852 + ); 853 + }, 854 + .login_button => { 855 + // Check if we can login 856 + if (g_app.username[0] > 0 and g_app.password[0] > 0) { 857 + loginToBluesky(); 858 + } else { 859 + g_app.message = "Please enter username and password"; 860 + } 861 + }, 862 + } 863 + } else if (g_selected_page == .home) { 864 + fetchBlueskyFeed(); 865 + } else if (g_selected_page == .post) { 866 + // Check if user is logged in before allowing post creation 867 + if (!g_app.is_logged_in) { 868 + g_app.message = "Please log in first to create posts"; 869 + g_selected_page = .login; 870 + } else { 871 + // Handle post field selection 872 + switch (g_app.post_field_selection) { 873 + .edit_text => { 874 + // Start post composition with keyboard (multiline enabled) 875 + const initial_text = if (g_app.post_text_len > 0) g_app.post_text[0..g_app.post_text_len] else null; 876 + g_app.keyboard.start("Enter your post:", 300, initial_text, true, 5, postKeyboardCancelled, postTextEditConfirmed); 877 + }, 878 + .confirm_post => { 879 + // Submit the post directly 880 + if (g_app.post_text_len > 0) { 881 + submitBlueskyPost(); 882 + } else { 883 + g_app.message = "Please enter some text first"; 884 + } 885 + }, 886 + } 887 + } 888 + } 889 + } 890 + 891 + // Render keyboard if active, otherwise render the selected page 892 + switch (g_selected_page) { 893 + .login => render_login(0, 20, LCD_WIDTH, LCD_HEIGHT - 20), 894 + .home => render_home(0, 20, LCD_WIDTH, LCD_HEIGHT - 20), 895 + .post => render_create_post(0, 20, LCD_WIDTH, LCD_HEIGHT - 20), 896 + } 897 + render_header(0, 0, LCD_WIDTH); 898 + 899 + return 1; 900 + } 901 + 902 + fn render_header(x: c_int, y: c_int, w: c_int) void { 903 + const pd = g_app.playdate; 904 + 905 + const header_height = pd.graphics.getFontHeight(fonts.g_font) + MARGIN * 2; 906 + 907 + // Draw header background 908 + pd.graphics.fillRect( 909 + x, 910 + y, 911 + w + MARGIN * 2, 912 + header_height, 913 + @intFromEnum(pdapi.LCDSolidColor.ColorBlack), 914 + ); 915 + 916 + // Set draw mode for white text on black background 917 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillWhite); 918 + 919 + // Draw header text 920 + const header_text = "Bluesky"; 921 + _ = pd.graphics.drawText( 922 + header_text.ptr, 923 + header_text.len, 924 + pdapi.PDStringEncoding.UTF8Encoding, 925 + x + MARGIN, 926 + y + MARGIN, 927 + ); 928 + 929 + const text_width = pd.graphics.getTextWidth( 930 + fonts.g_font, 931 + header_text.ptr, 932 + header_text.len, 933 + pdapi.PDStringEncoding.UTF8Encoding, 934 + 0, 935 + ); 936 + 937 + // spinning loading indicator. 938 + // Spinning loading indicator based on network state 939 + const current_network_state = network.getNetworkState(); 940 + if (current_network_state == .connecting or current_network_state == .requesting) { 941 + pdtools.renderSpinner(pd, x + w - 20, y + 2, pdapi.LCDSolidColor.ColorWhite); 942 + } 943 + 944 + // Reset draw mode to normal 945 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 946 + 947 + // Show all available pages 948 + var temp_w: c_int = 0; 949 + var curr_x = text_width + x + MARGIN * 2; 950 + 951 + // Login button (always available) 952 + pdtools.renderButton( 953 + pd, 954 + curr_x, 955 + y, 956 + "Login", 957 + g_selected_page == .login, 958 + &temp_w, 959 + null, 960 + ); 961 + 962 + if (g_app.is_logged_in) { 963 + curr_x += temp_w; 964 + pdtools.renderButton( 965 + pd, 966 + curr_x, 967 + y, 968 + "Home", 969 + g_selected_page == .home, 970 + &temp_w, 971 + null, 972 + ); 973 + 974 + curr_x += temp_w; 975 + pdtools.renderButton( 976 + pd, 977 + curr_x, 978 + y, 979 + "Post", 980 + g_selected_page == .post, 981 + null, 982 + null, 983 + ); 984 + } 985 + } 986 + 987 + fn render_post_list(x: c_int, y: c_int, w: c_int, h: c_int) void { 988 + const scrollbar_width = 10; 989 + const post_gap = 4; 990 + const padding = 4; 991 + const pd = g_app.playdate; 992 + 993 + // Clip the whole thing so we don't go out of bounds 994 + pd.graphics.setClipRect(x, y, w, h); 995 + 996 + const posts_width = w - scrollbar_width - (padding * 2); 997 + 998 + const start_y = @as(c_int, @intFromFloat(@as(f32, @floatFromInt(y)) - g_app.body_scroll.getValue())); 999 + var current_y = start_y + padding; 1000 + const current_x = x + padding; 1001 + 1002 + // Now render each post with actual positions 1003 + for (0..g_app.post_count) |i| { 1004 + const post = &g_app.posts[i]; 1005 + const post_height = bsky_post.renderPost( 1006 + pd, 1007 + post, 1008 + current_x, 1009 + current_y, 1010 + posts_width, 1011 + false, 1012 + ); 1013 + current_y += @as(c_int, @intCast(post_height)); 1014 + current_y += post_gap; 1015 + } 1016 + current_y += padding; 1017 + 1018 + // current_y is the scrolled position, so we need to add back the scroll offset to get true content height 1019 + const content_height = current_y - start_y; 1020 + 1021 + g_app.body_scroll.current_height = @as(f32, @floatFromInt(h)); 1022 + g_app.body_scroll.min_value = 0; 1023 + g_app.body_scroll.max_value = @floatFromInt(content_height); 1024 + 1025 + // Render scrollbar on the right side (below header, taking remaining height) 1026 + const scrollbar_x = pdapi.LCD_COLUMNS - scrollbar_width; 1027 + pdtools.renderScrollbar( 1028 + pd, 1029 + scrollbar_x, 1030 + y, 1031 + scrollbar_width, 1032 + h, 1033 + g_app.body_scroll.getValue(), 1034 + @as(f32, @floatFromInt(content_height)), 1035 + @as(f32, @floatFromInt(h)), 1036 + ); 1037 + 1038 + pd.graphics.clearClipRect(); 1039 + } 1040 + 1041 + fn render_home(x: i32, y: i32, w: usize, h: usize) void { 1042 + const pd = g_app.playdate; 1043 + 1044 + // #region Body 1045 + 1046 + // Update scrolling value with crank input 1047 + g_app.body_scroll.update(); 1048 + 1049 + pd.graphics.fillRect( 1050 + @intCast(x), 1051 + @intCast(y), 1052 + @intCast(w), 1053 + @intCast(h), 1054 + @intFromEnum(pdapi.LCDSolidColor.ColorWhite), 1055 + ); 1056 + 1057 + // Set draw mode for black text on white background 1058 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillBlack); 1059 + 1060 + // Show login prompt if not logged in 1061 + if (!g_app.is_logged_in) { 1062 + _ = pd.graphics.drawText("Please log in first", 19, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, y + MARGIN); 1063 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 1064 + return; 1065 + } 1066 + 1067 + // Get current network state 1068 + const current_network_state = network.getNetworkState(); 1069 + 1070 + // Show loading progress if network is active 1071 + if (current_network_state == .connecting or current_network_state == .requesting) { 1072 + const progress_msg = "Loading feed..."; 1073 + _ = pd.graphics.drawText(progress_msg.ptr, progress_msg.len, pdapi.PDStringEncoding.UTF8Encoding, x, y); 1074 + 1075 + // Reset draw mode to normal 1076 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 1077 + return; 1078 + } 1079 + 1080 + // If we have parsed posts, display them formatted 1081 + if (g_app.post_count > 0) { 1082 + render_post_list(@intCast(x), @intCast(y), @intCast(w), @intCast(h)); 1083 + } else if (current_network_state == .success or current_network_state == .idle) { 1084 + // Show message when no posts are available 1085 + const no_posts_msg = "No posts found - Press B to refresh"; 1086 + _ = pd.graphics.drawText(no_posts_msg.ptr, no_posts_msg.len, pdapi.PDStringEncoding.UTF8Encoding, x, y); 1087 + } else { 1088 + // Show current status message 1089 + _ = pd.graphics.drawText(g_app.message.ptr, g_app.message.len, pdapi.PDStringEncoding.UTF8Encoding, x, y); 1090 + } 1091 + 1092 + // Reset draw mode to normal 1093 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 1094 + } 1095 + 1096 + fn render_login(x: c_int, y: c_int, w: c_int, h: c_int) void { 1097 + const pd = g_app.playdate; 1098 + 1099 + pd.graphics.fillRect(x, y, w, h, @intFromEnum(pdapi.LCDSolidColor.ColorWhite)); 1100 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillBlack); 1101 + 1102 + const line_height = pd.graphics.getFontHeight(fonts.g_font) + 4; 1103 + var current_y = y + MARGIN; 1104 + 1105 + const current_network_state = network.getNetworkState(); 1106 + 1107 + if (current_network_state == .requesting or current_network_state == .connecting) { 1108 + _ = pd.graphics.drawText("Logging in...", 12, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1109 + } else if (current_network_state == .network_error) { 1110 + _ = pd.graphics.drawText("Login failed!", 13, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1111 + current_y += line_height; 1112 + _ = pd.graphics.drawText("Press B to retry", 16, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1113 + } else { 1114 + _ = pd.graphics.drawText("Welcome to Bluesky!", 20, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1115 + current_y += line_height * 2; 1116 + 1117 + // Show current username with selection indicator 1118 + var username_text: [200]u8 = undefined; 1119 + const username_prefix = if (g_app.login_field_selection == .username) pdtools.SELECTION_ARROW ++ " Username: " else " Username: "; 1120 + const username_value = if (g_app.username[0] != 0) std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.username))) else "(not entered)"; 1121 + const username_display = std.fmt.bufPrint(&username_text, "{s}{s}", .{ username_prefix, username_value }) catch "Username: (error)"; 1122 + _ = pd.graphics.drawText(username_display.ptr, username_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1123 + current_y += line_height; 1124 + 1125 + // Show password status with selection indicator 1126 + const password_prefix = if (g_app.login_field_selection == .password) pdtools.SELECTION_ARROW ++ " Password: " else " Password: "; 1127 + var password_text: [200]u8 = undefined; 1128 + const password_display = if (g_app.password[0] != 0) blk: { 1129 + // Generate dots based on actual password length using safe ASCII dots 1130 + var dots_buffer: [64]u8 = undefined; 1131 + const password_span = std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.password))); 1132 + const dots_len = @min(password_span.len, dots_buffer.len); 1133 + @memset(dots_buffer[0..dots_len], '*'); 1134 + const dots_str = dots_buffer[0..dots_len]; 1135 + break :blk std.fmt.bufPrint(&password_text, "{s}{s}", .{ password_prefix, dots_str }) catch "Password: (error)"; 1136 + } else std.fmt.bufPrint(&password_text, "{s}(not entered)", .{password_prefix}) catch "Password: (error)"; 1137 + _ = pd.graphics.drawText(password_display.ptr, password_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1138 + current_y += line_height; 1139 + 1140 + // Show login button with selection indicator 1141 + const login_prefix = if (g_app.login_field_selection == .login_button) pdtools.SELECTION_ARROW ++ " " else " "; 1142 + const login_text = if (g_app.username[0] != 0 and g_app.password[0] != 0) "Login" else "Login (enter credentials first)"; 1143 + var login_button_text: [200]u8 = undefined; 1144 + const login_display = std.fmt.bufPrint(&login_button_text, "{s}{s}", .{ login_prefix, login_text }) catch "Login"; 1145 + _ = pd.graphics.drawText(login_display.ptr, login_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1146 + current_y += line_height * 2; 1147 + 1148 + // Show navigation instructions 1149 + const instruction = pdtools.UP_ARROW_EMOJI ++ pdtools.DOWN_ARROW_EMOJI ++ ": Select field B: Edit/Login"; 1150 + _ = pd.graphics.drawText(instruction.ptr, instruction.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1151 + current_y += line_height; 1152 + } 1153 + 1154 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 1155 + } 1156 + 1157 + fn render_create_post(x: c_int, y: c_int, w: c_int, h: c_int) void { 1158 + const pd = g_app.playdate; 1159 + 1160 + pd.graphics.fillRect(x, y, w, h, @intFromEnum(pdapi.LCDSolidColor.ColorWhite)); 1161 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillBlack); 1162 + 1163 + const line_height = pd.graphics.getFontHeight(fonts.g_font) + 4; 1164 + var current_y = y + MARGIN; 1165 + 1166 + // Check if user is logged in 1167 + if (!g_app.is_logged_in) { 1168 + _ = pd.graphics.drawText("Please log in first to create posts", 34, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1169 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 1170 + return; 1171 + } 1172 + 1173 + // Post composition page 1174 + _ = pd.graphics.drawText("Create a Post", 13, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1175 + current_y += line_height; 1176 + 1177 + // Show character count 1178 + var char_count_text: [50]u8 = undefined; 1179 + const char_count_display = std.fmt.bufPrint(&char_count_text, "Characters: {d}/300", .{g_app.post_text_len}) catch "Characters: 0/300"; 1180 + _ = pd.graphics.drawText(char_count_display.ptr, char_count_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1181 + current_y += line_height; 1182 + 1183 + // Show current post text if any 1184 + if (g_app.post_text_len > 0) { 1185 + _ = pd.graphics.drawText("Current post:", 13, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1186 + current_y += line_height; 1187 + 1188 + // Draw a border around the post text area 1189 + const text_area_height = line_height * 6; // Space for multiple lines 1190 + pd.graphics.drawRect(x + MARGIN, current_y, w - MARGIN * 2, text_area_height, @intFromEnum(pdapi.LCDSolidColor.ColorBlack)); 1191 + 1192 + // Render multiline post text 1193 + const post_text = g_app.post_text[0..g_app.post_text_len]; 1194 + var text_y = current_y + 4; 1195 + var line_start: usize = 0; 1196 + var displayed_lines: usize = 0; 1197 + const max_lines = 5; 1198 + 1199 + for (post_text, 0..) |char, i| { 1200 + if (char == '\n' or i == post_text.len - 1) { 1201 + if (displayed_lines >= max_lines) break; 1202 + 1203 + const line_end = if (char == '\n') i else i + 1; 1204 + const line_text = post_text[line_start..line_end]; 1205 + 1206 + _ = pd.graphics.drawText(line_text.ptr, line_text.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN + 4, text_y); 1207 + text_y += line_height; 1208 + displayed_lines += 1; 1209 + line_start = i + 1; 1210 + } 1211 + } 1212 + 1213 + // If no newlines, display as single line 1214 + if (displayed_lines == 0) { 1215 + _ = pd.graphics.drawText(post_text.ptr, post_text.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN + 4, current_y + 4); 1216 + } 1217 + 1218 + current_y += text_area_height + line_height; 1219 + } else { 1220 + current_y += line_height * 2; 1221 + _ = pd.graphics.drawText("No post drafted yet", 19, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1222 + current_y += line_height * 2; 1223 + } 1224 + 1225 + // Show field selection options 1226 + const edit_prefix = if (g_app.post_field_selection == .edit_text) pdtools.SELECTION_ARROW ++ " " else " "; 1227 + var edit_text: [100]u8 = undefined; 1228 + const edit_display = std.fmt.bufPrint(&edit_text, "{s}Edit Post Text", .{edit_prefix}) catch "Edit Post Text"; 1229 + _ = pd.graphics.drawText(edit_display.ptr, edit_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1230 + current_y += line_height; 1231 + 1232 + const confirm_prefix = if (g_app.post_field_selection == .confirm_post) pdtools.SELECTION_ARROW ++ " " else " "; 1233 + var confirm_text: [100]u8 = undefined; 1234 + const confirm_display = std.fmt.bufPrint(&confirm_text, "{s}Confirm & Post", .{confirm_prefix}) catch "Confirm & Post"; 1235 + _ = pd.graphics.drawText(confirm_display.ptr, confirm_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1236 + current_y += line_height * 2; 1237 + 1238 + // Show navigation instructions 1239 + const instruction = pdtools.UP_ARROW_EMOJI ++ pdtools.DOWN_ARROW_EMOJI ++ ": Select action B: Execute " ++ pdtools.LEFT_ARROW_EMOJI ++ pdtools.RIGHT_ARROW_EMOJI ++ ": Navigate tabs"; 1240 + _ = pd.graphics.drawText(instruction.ptr, instruction.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y); 1241 + 1242 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 1243 + }
+316
src/network.zig
··· 1 + const std = @import("std"); 2 + const pdapi = @import("playdate_api_definitions.zig"); 3 + 4 + pub const NetworkState = enum { 5 + idle, 6 + connecting, 7 + authenticating, 8 + requesting, 9 + fetching_posts, 10 + success, 11 + network_error, 12 + }; 13 + 14 + // HTTP method types 15 + pub const HttpMethod = enum { 16 + GET, 17 + POST, 18 + PUT, 19 + DELETE, 20 + PATCH, 21 + }; 22 + 23 + // HTTP status code ranges 24 + pub const HttpStatusCode = u16; 25 + 26 + // Callback function types 27 + pub const SuccessCallback = *const fn (status_code: HttpStatusCode, response_data: []const u8) void; 28 + pub const FailureCallback = *const fn (status_code: HttpStatusCode, error_message: []const u8) void; 29 + 30 + // Request configuration 31 + pub const HttpRequest = struct { 32 + method: HttpMethod, 33 + url: []const u8, 34 + server: []const u8, 35 + port: u16, 36 + path: []const u8, 37 + use_https: bool, 38 + bearer_token: ?[]const u8, 39 + body: ?[]const u8, 40 + response_buffer: []u8, 41 + success_callback: ?SuccessCallback, 42 + failure_callback: ?FailureCallback, 43 + }; 44 + 45 + // Network request state 46 + pub const NetworkRequestState = struct { 47 + state: NetworkState, 48 + connection: ?*pdapi.HTTPConnection, 49 + request: ?HttpRequest, 50 + bytes_read: usize, 51 + is_reading_chunks: bool, 52 + total_bytes_read: usize, 53 + expected_content_length: usize, 54 + network_access_requested: bool, 55 + }; 56 + 57 + // Global network state 58 + var g_network_state = NetworkRequestState{ 59 + .state = .idle, 60 + .connection = null, 61 + .request = null, 62 + .bytes_read = 0, 63 + .is_reading_chunks = false, 64 + .total_bytes_read = 0, 65 + .expected_content_length = 0, 66 + .network_access_requested = false, 67 + }; 68 + 69 + // Global buffers to avoid large stack allocations 70 + var g_headers_buffer: [512]u8 = undefined; 71 + 72 + /// Convert HttpMethod enum to string 73 + fn httpMethodToString(method: HttpMethod) []const u8 { 74 + return switch (method) { 75 + .GET => "GET", 76 + .POST => "POST", 77 + .PUT => "PUT", 78 + .DELETE => "DELETE", 79 + .PATCH => "PATCH", 80 + }; 81 + } 82 + 83 + /// Create HTTP connection with proper configuration 84 + pub fn createConnection(playdate: *pdapi.PlaydateAPI, server: []const u8, port: u16, use_https: bool) ?*pdapi.HTTPConnection { 85 + // Create a null-terminated buffer for the server name 86 + var server_buffer: [256]u8 = undefined; 87 + if (server.len >= server_buffer.len) { 88 + playdate.system.logToConsole("ERROR: Server name too long: %d", @as(c_int, @intCast(server.len))); 89 + return null; 90 + } 91 + @memcpy(server_buffer[0..server.len], server); 92 + server_buffer[server.len] = 0; 93 + 94 + playdate.system.logToConsole("Attempting to create connection to %s:%d", @as([*:0]const u8, @ptrCast(&server_buffer)), port); 95 + if (use_https) { 96 + playdate.system.logToConsole("Using HTTPS connection"); 97 + } else { 98 + playdate.system.logToConsole("Using HTTP connection"); 99 + } 100 + const connection = playdate.network.playdate_http.newConnection(@as([*:0]const u8, @ptrCast(&server_buffer)), port, use_https); 101 + 102 + // Set connection timeout - increase to 30 seconds for more reliability 103 + playdate.network.playdate_http.setConnectTimeout(connection, 30000); // 30 seconds 104 + playdate.network.playdate_http.setReadTimeout(connection, 30000); // 30 seconds 105 + playdate.system.logToConsole("HTTP connection created with 30s timeouts"); 106 + 107 + return connection; 108 + } 109 + 110 + /// Start an HTTP request with optional bearer token 111 + pub fn makeHttpRequest(playdate: *pdapi.PlaydateAPI, request: HttpRequest) bool { 112 + // Check if already processing a request 113 + if (g_network_state.state != .idle) { 114 + playdate.system.logToConsole("ERROR: Network request already in progress"); 115 + return false; 116 + } 117 + 118 + // Create connection 119 + const connection = createConnection(playdate, request.server, request.port, request.use_https); 120 + if (connection == null) { 121 + return false; 122 + } 123 + 124 + // Store request info 125 + g_network_state.connection = connection; 126 + g_network_state.request = request; 127 + g_network_state.state = .connecting; 128 + g_network_state.bytes_read = 0; 129 + g_network_state.total_bytes_read = 0; 130 + g_network_state.is_reading_chunks = false; 131 + 132 + // Build headers 133 + var headers_len: usize = 0; 134 + 135 + // Add Authorization header if bearer token provided 136 + if (request.bearer_token) |token| { 137 + const auth_header = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Authorization: Bearer {s}\r\n", .{token}) catch { 138 + playdate.system.logToConsole("ERROR: Failed to format authorization header"); 139 + return false; 140 + }; 141 + headers_len += auth_header.len; 142 + } 143 + 144 + // Add Content-Type header for POST/PUT/PATCH requests 145 + if (request.method == .POST or request.method == .PUT or request.method == .PATCH) { 146 + const content_type = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Content-Type: application/json\r\n", .{}) catch { 147 + playdate.system.logToConsole("ERROR: Failed to format content-type header"); 148 + return false; 149 + }; 150 + headers_len += content_type.len; 151 + } 152 + 153 + // Null terminate headers 154 + if (headers_len < g_headers_buffer.len) { 155 + g_headers_buffer[headers_len] = 0; 156 + } 157 + 158 + // Send request using query method 159 + const method_str = httpMethodToString(request.method); 160 + const headers_ptr = if (headers_len > 0) @as([*:0]const u8, @ptrCast(&g_headers_buffer)) else null; 161 + const body_ptr = if (request.body) |body| @as([*:0]const u8, @ptrCast(body.ptr)) else null; 162 + const body_len = if (request.body) |body| body.len else 0; 163 + 164 + const result = playdate.network.playdate_http.query(connection, @as([*:0]const u8, @ptrCast(method_str.ptr)), @as([*:0]const u8, @ptrCast(request.path.ptr)), headers_ptr, if (headers_len > 0) headers_len else 0, body_ptr, body_len); 165 + 166 + if (result != .NET_OK) { 167 + playdate.system.logToConsole("ERROR: Failed to send HTTP request"); 168 + g_network_state.state = .network_error; 169 + return false; 170 + } 171 + 172 + g_network_state.state = .requesting; 173 + playdate.system.logToConsole("HTTP %s request sent to %s%s", @as([*:0]const u8, @ptrCast(method_str.ptr)), @as([*:0]const u8, @ptrCast(request.server.ptr)), @as([*:0]const u8, @ptrCast(request.path.ptr))); 174 + 175 + return true; 176 + } 177 + 178 + /// Process network responses and handle callbacks 179 + pub fn updateNetwork(playdate: *pdapi.PlaydateAPI) void { 180 + if (g_network_state.state == .idle or g_network_state.connection == null) { 181 + return; 182 + } 183 + 184 + const connection = g_network_state.connection.?; 185 + _ = g_network_state.request.?; 186 + 187 + // Check for errors 188 + const net_error = playdate.network.playdate_http.getError(connection); 189 + if (net_error != .NET_OK) { 190 + playdate.system.logToConsole("ERROR: HTTP network error: %d", @intFromEnum(net_error)); 191 + handleNetworkError(playdate, "Network error"); 192 + return; 193 + } 194 + 195 + // Read available data if we're in requesting state 196 + if (g_network_state.state == .requesting) { 197 + const bytes_available = playdate.network.playdate_http.getBytesAvailable(connection); 198 + if (bytes_available > 0) { 199 + readResponseData(playdate); 200 + } else { 201 + // Check if request is complete by trying to get response status 202 + const status_code = playdate.network.playdate_http.getResponseStatus(connection); 203 + if (status_code > 0) { 204 + // We have a response, process it 205 + processResponse(playdate); 206 + } 207 + } 208 + } 209 + } 210 + 211 + /// Read response data into the provided buffer 212 + fn readResponseData(playdate: *pdapi.PlaydateAPI) void { 213 + const connection = g_network_state.connection.?; 214 + const request = g_network_state.request.?; 215 + const available_space = request.response_buffer.len - g_network_state.bytes_read; 216 + 217 + if (available_space == 0) { 218 + playdate.system.logToConsole("WARNING: Response buffer full, stopping read"); 219 + processResponse(playdate); 220 + return; 221 + } 222 + 223 + const bytes_to_read = @min(available_space, 1024); // Read in chunks of 1KB or less 224 + const buffer_ptr = request.response_buffer[g_network_state.bytes_read .. g_network_state.bytes_read + bytes_to_read]; 225 + 226 + const bytes_read = playdate.network.playdate_http.read(connection, @as([*]u8, @ptrCast(buffer_ptr.ptr)), @intCast(bytes_to_read)); 227 + 228 + if (bytes_read > 0) { 229 + g_network_state.bytes_read += @intCast(bytes_read); 230 + g_network_state.total_bytes_read += @intCast(bytes_read); 231 + playdate.system.logToConsole("Read %d bytes, total: %d", bytes_read, g_network_state.total_bytes_read); 232 + } 233 + } 234 + 235 + /// Process the completed response and call appropriate callback 236 + fn processResponse(playdate: *pdapi.PlaydateAPI) void { 237 + const connection = g_network_state.connection.?; 238 + const request = g_network_state.request.?; 239 + 240 + // Get HTTP status code 241 + const status_code = playdate.network.playdate_http.getResponseStatus(connection); 242 + playdate.system.logToConsole("HTTP response status: %d", status_code); 243 + 244 + // Null-terminate response data 245 + if (g_network_state.bytes_read < request.response_buffer.len) { 246 + request.response_buffer[g_network_state.bytes_read] = 0; 247 + } 248 + 249 + const response_data = request.response_buffer[0..g_network_state.bytes_read]; 250 + 251 + // Determine if this is a success (2xx) or failure (4xx/5xx) 252 + if (status_code >= 200 and status_code < 300) { 253 + // Success (2xx) 254 + g_network_state.state = .success; 255 + if (request.success_callback) |callback| { 256 + callback(@intCast(status_code), response_data); 257 + } 258 + } else if (status_code >= 400) { 259 + // Client/Server error (4xx/5xx) 260 + g_network_state.state = .network_error; 261 + if (request.failure_callback) |callback| { 262 + callback(@intCast(status_code), response_data); 263 + } 264 + } else { 265 + // Other status codes (1xx, 3xx) - treat as success for now 266 + g_network_state.state = .success; 267 + if (request.success_callback) |callback| { 268 + callback(@intCast(status_code), response_data); 269 + } 270 + } 271 + 272 + // Cleanup 273 + cleanup(playdate); 274 + } 275 + 276 + /// Handle network errors 277 + fn handleNetworkError(playdate: *pdapi.PlaydateAPI, error_message: []const u8) void { 278 + g_network_state.state = .network_error; 279 + 280 + if (g_network_state.request) |request| { 281 + if (request.failure_callback) |callback| { 282 + callback(0, error_message); // Status code 0 indicates network error 283 + } 284 + } 285 + 286 + cleanup(playdate); 287 + } 288 + 289 + /// Clean up network resources 290 + fn cleanup(playdate: *pdapi.PlaydateAPI) void { 291 + if (g_network_state.connection) |connection| { 292 + playdate.network.playdate_http.close(connection); 293 + g_network_state.connection = null; 294 + } 295 + 296 + g_network_state.request = null; 297 + g_network_state.bytes_read = 0; 298 + g_network_state.total_bytes_read = 0; 299 + g_network_state.is_reading_chunks = false; 300 + g_network_state.state = .idle; 301 + } 302 + 303 + /// Get current network state 304 + pub fn getNetworkState() NetworkState { 305 + return g_network_state.state; 306 + } 307 + 308 + /// Check if network is idle 309 + pub fn isNetworkIdle() bool { 310 + return g_network_state.state == .idle; 311 + } 312 + 313 + /// Force cleanup network resources 314 + pub fn forceCleanup(playdate: *pdapi.PlaydateAPI) void { 315 + cleanup(playdate); 316 + }
+74
src/panic_handler.zig
··· 1 + const std = @import("std"); 2 + const pdapi = @import("playdate_api_definitions.zig"); 3 + const builtin = @import("builtin"); 4 + 5 + var global_playdate: *pdapi.PlaydateAPI = undefined; 6 + pub fn init(playdate: *pdapi.PlaydateAPI) void { 7 + global_playdate = playdate; 8 + } 9 + 10 + pub fn panic( 11 + msg: []const u8, 12 + error_return_trace: ?*std.builtin.StackTrace, 13 + return_address: ?usize, 14 + ) noreturn { 15 + _ = error_return_trace; 16 + _ = return_address; 17 + 18 + switch (comptime builtin.os.tag) { 19 + .freestanding => { 20 + //Playdate hardware 21 + 22 + //TODO: The Zig std library does not yet support stacktraces on Playdate hardware. 23 + //We will need to do this manually. Some notes on trying to get it working: 24 + //Frame pointer is R7 25 + //Next Frame pointer is *R7 26 + //Return address is *(R7+4) 27 + //To print out the trace corrently, 28 + //We need to know the load address and it doesn't seem to be exactly 29 + //0x6000_0000 as originally thought 30 + 31 + global_playdate.system.logToConsole("PANIC: %s", msg.ptr); 32 + global_playdate.system.@"error"("PANIC: %s", msg.ptr); 33 + }, 34 + else => { 35 + //playdate simulator 36 + var stack_trace_buffer = [_]u8{0} ** 4096; 37 + var buffer = [_]u8{0} ** 4096; 38 + var stream = std.io.fixedBufferStream(&stack_trace_buffer); 39 + 40 + const stack_trace_string = b: { 41 + if (builtin.strip_debug_info) { 42 + break :b "Unable to dump stack trace: Debug info stripped"; 43 + } 44 + const debug_info = std.debug.getSelfDebugInfo() catch |err| { 45 + const to_print = std.fmt.bufPrintZ( 46 + &buffer, 47 + "Unable to dump stack trace: Unable to open debug info: {s}\n", 48 + .{@errorName(err)}, 49 + ) catch break :b "Unable to dump stack trace: Unable to open debug info due unknown error"; 50 + break :b to_print; 51 + }; 52 + std.debug.writeCurrentStackTrace( 53 + stream.writer(), 54 + debug_info, 55 + .no_color, 56 + null, 57 + ) catch break :b "Unable to dump stack trace: Unknown error writng stack trace"; 58 + 59 + //NOTE: playdate.system.error (and all Playdate APIs that deal with strings) require a null termination 60 + const null_char_index = @min(stream.pos, stack_trace_buffer.len - 1); 61 + stack_trace_buffer[null_char_index] = 0; 62 + 63 + break :b &stack_trace_buffer; 64 + }; 65 + global_playdate.system.@"error"( 66 + "PANIC: %s\n\n%s", 67 + msg.ptr, 68 + stack_trace_string.ptr, 69 + ); 70 + }, 71 + } 72 + 73 + while (true) {} 74 + }
+81
src/pdtools/DrawableText.zig
··· 1 + const pdapi = @import("../playdate_api_definitions.zig"); 2 + 3 + pub const DrawableText = struct { 4 + playdate: *pdapi.PlaydateAPI, 5 + text: []const u8, 6 + max_width: ?c_int = null, 7 + max_height: ?c_int = null, 8 + font: ?*pdapi.LCDFont = null, 9 + wrapping_mode: pdapi.PDTextWrappingMode = .WrapWord, 10 + alignment: pdapi.PDTextAlignment = .AlignTextLeft, 11 + 12 + pub fn getWidth(self: *const DrawableText) c_int { 13 + const natural_width = self.playdate.graphics.getTextWidth( 14 + self.font, 15 + @as(?[*:0]const u8, @ptrCast(self.text.ptr)), 16 + self.text.len, 17 + pdapi.PDStringEncoding.UTF8Encoding, 18 + 0, 19 + ); 20 + 21 + // If a max width is set, return the minimum of the two 22 + if (self.max_width) |max_w| { 23 + return @min(max_w, natural_width); 24 + } 25 + 26 + return natural_width; 27 + } 28 + 29 + pub fn getHeight(self: *const DrawableText) c_int { 30 + const width_constraint = self.max_width orelse 0; 31 + 32 + const natural_height = self.playdate.graphics.getTextHeightForMaxWidth( 33 + self.font, 34 + @as(?[*:0]const u8, @ptrCast(self.text.ptr)), 35 + self.text.len, 36 + width_constraint, 37 + pdapi.PDStringEncoding.UTF8Encoding, 38 + self.wrapping_mode, 39 + 0, 40 + 0, 41 + ); 42 + 43 + // If a max height is set, return the minimum of the two 44 + if (self.max_height) |max_h| { 45 + return @min(max_h, natural_height); 46 + } 47 + 48 + return natural_height; 49 + } 50 + 51 + pub fn render(self: *const DrawableText, x: c_int, y: c_int, dry_run: bool) void { 52 + if (dry_run) return; 53 + 54 + const width = self.getWidth(); 55 + const height = self.getHeight(); 56 + 57 + if (self.max_width != null) { 58 + // Use drawTextInRect for constrained rendering 59 + _ = self.playdate.graphics.drawTextInRect( 60 + @as(?[*:0]const u8, @ptrCast(self.text.ptr)), 61 + self.text.len, 62 + pdapi.PDStringEncoding.UTF8Encoding, 63 + x, 64 + y, 65 + width, 66 + height, 67 + self.wrapping_mode, 68 + self.alignment, 69 + ); 70 + } else { 71 + // Use simple drawText for unconstrained rendering 72 + _ = self.playdate.graphics.drawText( 73 + @as(?[*:0]const u8, @ptrCast(self.text.ptr)), 74 + self.text.len, 75 + pdapi.PDStringEncoding.UTF8Encoding, 76 + x, 77 + y, 78 + ); 79 + } 80 + } 81 + };
+160
src/pdtools/ScrollingValue.zig
··· 1 + const pdapi = @import("../playdate_api_definitions.zig"); 2 + 3 + // A representation of a scrolling or sliding value with soft limits and springback behavior 4 + pub const SlidingValue = struct { 5 + playdate: *pdapi.PlaydateAPI, 6 + 7 + // Soft runout distances for the scrolling value 8 + soft_min_runout: f32, 9 + soft_max_runout: f32, 10 + 11 + // Margin before runout callback detection happens 12 + runout_margin: f32 = 5.0, 13 + 14 + min_value: f32, 15 + max_value: f32, 16 + 17 + current_value: f32, 18 + current_height: f32, 19 + 20 + // Rate at which the value springs back to the min/max 21 + spring_rate: f32 = 0.5, 22 + 23 + // ms until lack of movement causes spring back to min/max or ticks 24 + spring_back_ms: u32 = 250, 25 + 26 + // Called when the scroll max threshold is reached 27 + scroll_max_threshold_ms: ?u32 = 1000, 28 + ms_since_scroll_max: u32 = 0, 29 + onScrollMaxThreshold: ?*const fn () void, 30 + called_scroll_max: bool = false, 31 + 32 + // Called when the scroll min threshold is reached 33 + scroll_min_threshold_ms: ?u32 = 1000, 34 + ms_since_scroll_min: u32 = 0, 35 + called_scroll_min: bool = false, 36 + onScrollMinThreshold: ?*const fn () void, 37 + 38 + crank_position_offset: f32, 39 + crank_multiplier: f32 = 1.0, 40 + 41 + last_update_time: u32 = 0, 42 + last_movement_time: u32 = 0, 43 + 44 + /// Updates the scrolling value based on crank input and applies constraints 45 + pub fn update(self: *SlidingValue) void { 46 + const current_time = self.playdate.system.getCurrentTimeMilliseconds(); 47 + 48 + const crank_change = self.playdate.system.getCrankChange(); 49 + const adjusted_change = crank_change * self.crank_multiplier; 50 + 51 + // Update current value 52 + self.current_value += adjusted_change; 53 + 54 + // Calculate hard limits based on soft limits + runout distances 55 + // For max limit, account for viewport height 56 + const hard_min_limit = self.min_value - self.soft_min_runout; 57 + const effective_max_soft_limit = self.max_value - self.current_height; 58 + const hard_max_limit = effective_max_soft_limit + self.soft_max_runout; 59 + 60 + // Hard clamp to absolute hard limits 61 + self.current_value = @max(hard_min_limit, @min(hard_max_limit, self.current_value)); 62 + 63 + // Track if we moved 64 + const moved = @abs(adjusted_change) > 0.01; 65 + if (moved) { 66 + self.last_movement_time = current_time; 67 + } 68 + 69 + // Handle spring back behavior when beyond soft limits (min_value and max_value) 70 + // For max value, account for viewport height 71 + const effective_max_for_spring = self.max_value - self.current_height; 72 + const time_since_last_movement = current_time - self.last_movement_time; 73 + if (time_since_last_movement >= self.spring_back_ms) { 74 + if (self.current_value > effective_max_for_spring) { 75 + // Spring back towards effective max (soft limit accounting for viewport) 76 + const spring_force = (self.current_value - effective_max_for_spring) * self.spring_rate; 77 + self.current_value -= spring_force; 78 + if (self.current_value < effective_max_for_spring) { 79 + self.current_value = effective_max_for_spring; 80 + } 81 + } else if (self.current_value < self.min_value) { 82 + // Spring back towards min_value (soft limit) 83 + const spring_force = (self.min_value - self.current_value) * self.spring_rate; 84 + self.current_value += spring_force; 85 + if (self.current_value > self.min_value) { 86 + self.current_value = self.min_value; 87 + } 88 + } 89 + } 90 + 91 + // Handle scroll thresholds with margin and debouncing 92 + // In the new paradigm, min_value and max_value ARE the soft limits 93 + // Thresholds trigger when beyond these soft limits 94 + // For max threshold, account for viewport height - the effective max is when content bottom is visible 95 + const effective_max_value = self.max_value - self.current_height; 96 + 97 + if (self.current_value >= effective_max_value + self.runout_margin) { 98 + self.ms_since_scroll_max += current_time - self.last_update_time; 99 + if (self.scroll_max_threshold_ms) |threshold| { 100 + if (self.ms_since_scroll_max >= threshold and self.onScrollMaxThreshold != null and !self.called_scroll_max) { 101 + self.onScrollMaxThreshold.?(); 102 + self.called_scroll_max = true; 103 + self.ms_since_scroll_max = 0; // Reset after triggering 104 + } 105 + } 106 + } else { 107 + self.ms_since_scroll_max = 0; 108 + // Reset debounce flag when we're back within normal range 109 + if (self.current_value <= effective_max_value - self.runout_margin) { 110 + self.called_scroll_max = false; 111 + } 112 + } 113 + 114 + if (self.current_value <= self.min_value - self.runout_margin) { 115 + self.ms_since_scroll_min += current_time - self.last_update_time; 116 + if (self.scroll_min_threshold_ms) |threshold| { 117 + if (self.ms_since_scroll_min >= threshold and self.onScrollMinThreshold != null and !self.called_scroll_min) { 118 + self.onScrollMinThreshold.?(); 119 + self.called_scroll_min = true; 120 + self.ms_since_scroll_min = 0; // Reset after triggering 121 + } 122 + } 123 + } else { 124 + self.ms_since_scroll_min = 0; 125 + // Reset debounce flag when we're back within normal range 126 + if (self.current_value >= self.min_value + self.runout_margin) { 127 + self.called_scroll_min = false; 128 + } 129 + } 130 + 131 + self.last_update_time = current_time; 132 + } 133 + 134 + /// Sets the current value and resets movement tracking 135 + pub fn setValue(self: *SlidingValue, value: f32) void { 136 + // Calculate hard limits and clamp to them, accounting for viewport height 137 + const hard_min_limit = self.min_value - self.soft_min_runout; 138 + const effective_max_soft_limit = self.max_value - self.current_height; 139 + const hard_max_limit = effective_max_soft_limit + self.soft_max_runout; 140 + self.current_value = @max(hard_min_limit, @min(hard_max_limit, value)); 141 + self.last_movement_time = self.playdate.system.getCurrentTimeMilliseconds(); 142 + 143 + // Reset debounce flags when value is explicitly set 144 + self.called_scroll_max = false; 145 + self.called_scroll_min = false; 146 + self.ms_since_scroll_max = 0; 147 + self.ms_since_scroll_min = 0; 148 + } 149 + 150 + /// Gets the current constrained value 151 + pub fn getValue(self: *SlidingValue) f32 { 152 + return self.current_value; 153 + } 154 + 155 + /// Returns true if the value is beyond soft limits (min_value and effective max_value) 156 + pub fn isBeyondSoftLimits(self: *SlidingValue) bool { 157 + const effective_max_value = self.max_value - self.current_height; 158 + return self.current_value < self.min_value or self.current_value > effective_max_value; 159 + } 160 + };
+198
src/pdtools/index.zig
··· 1 + const pdapi = @import("../playdate_api_definitions.zig"); 2 + const fonts = @import("../fonts.zig"); 3 + const std = @import("std"); 4 + 5 + pub const CRANK_EMOJI = "🎣"; 6 + pub const UP_ARROW_EMOJI = "⬆️"; 7 + pub const DOWN_ARROW_EMOJI = "⬇️"; 8 + pub const LEFT_ARROW_EMOJI = "⬅️"; 9 + pub const RIGHT_ARROW_EMOJI = "➡️"; 10 + pub const SELECTION_ARROW = ">"; 11 + 12 + pub fn drawText( 13 + pd: *pdapi.PlaydateAPI, 14 + font: ?*pdapi.LCDFont, 15 + text: []const u8, 16 + x: c_int, 17 + y: c_int, 18 + out_width: *c_int, 19 + out_height: *c_int, 20 + ) void { 21 + out_width.* = pd.graphics.getTextWidth(font, text.ptr, text.len, pdapi.PDStringEncoding.UTF8Encoding, 0); 22 + out_height.* = pd.graphics.getFontHeight(font); 23 + 24 + _ = pd.graphics.drawText( 25 + text.ptr, 26 + text.len, 27 + pdapi.PDStringEncoding.UTF8Encoding, 28 + x, 29 + y, 30 + ); 31 + } 32 + 33 + pub fn logLargeMessage(pd: *pdapi.PlaydateAPI, buffer: []const u8, buffer_size: usize) void { 34 + 35 + // Log the complete response for debugging 36 + pd.system.logToConsole("========== COMPLETE JSON RESPONSE START =========="); 37 + 38 + // Log in chunks to avoid truncation 39 + const chunk_size = 512; // Log 512 chars at a time 40 + var offset: usize = 0; 41 + var chunk_num: usize = 1; 42 + 43 + while (offset < buffer_size) { 44 + const remaining = buffer_size - offset; 45 + const current_chunk_size = @min(chunk_size, remaining); 46 + 47 + pd.system.logToConsole("%.*s", @as(c_int, @intCast(current_chunk_size)), &buffer[offset]); 48 + 49 + offset += current_chunk_size; 50 + chunk_num += 1; 51 + } 52 + 53 + pd.system.logToConsole("========== COMPLETE JSON RESPONSE END =========="); 54 + } 55 + 56 + pub fn renderScrollbar( 57 + pd: *pdapi.PlaydateAPI, 58 + x: c_int, 59 + y: c_int, 60 + width: c_int, 61 + height: c_int, 62 + scroll_offset: f32, 63 + content_height: f32, 64 + viewport_height: f32, 65 + ) void { 66 + // Calculate scrollbar thumb position and size relative to the given bounds 67 + const thumb_y = y + @as(c_int, @intFromFloat(scroll_offset / content_height * @as(f32, @floatFromInt(height)))); 68 + const thumb_height = @as(c_int, @intFromFloat(viewport_height / content_height * @as(f32, @floatFromInt(height)))); 69 + 70 + // Draw scrollbar background (white background) 71 + pd.graphics.fillRect(x, y, width, height, @as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorWhite)))); 72 + 73 + // Draw scrollbar thumb (black thumb) 74 + pd.graphics.fillRect(x, thumb_y, width, thumb_height, @as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorBlack)))); 75 + } 76 + 77 + pub fn renderButton( 78 + pd: *pdapi.PlaydateAPI, 79 + x: c_int, 80 + y: c_int, 81 + label: []const u8, 82 + active: bool, 83 + out_width: ?*c_int, 84 + out_height: ?*c_int, 85 + ) void { 86 + const MARGIN: c_int = 4; 87 + const BG_COLOR = if (active) pdapi.LCDSolidColor.ColorWhite else pdapi.LCDSolidColor.ColorBlack; 88 + 89 + const text_width = pd.graphics.getTextWidth( 90 + fonts.g_font, 91 + label.ptr, 92 + label.len, 93 + pdapi.PDStringEncoding.UTF8Encoding, 94 + 0, 95 + ); 96 + const text_height = pd.graphics.getFontHeight(fonts.g_font); 97 + const btn_width = text_width + MARGIN * 2; 98 + const btn_height = text_height + MARGIN * 2; 99 + 100 + if (out_width) |w| w.* = btn_width; 101 + if (out_height) |h| h.* = btn_height; 102 + 103 + // Draw button background 104 + pd.graphics.fillRect( 105 + x, 106 + y, 107 + text_width + MARGIN * 2, 108 + text_height + MARGIN * 2, 109 + @as(usize, @intCast(@intFromEnum(BG_COLOR))), 110 + ); 111 + 112 + pd.graphics.drawRect( 113 + x, 114 + y, 115 + text_width + MARGIN * 2, 116 + text_height + MARGIN * 2, 117 + @as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorBlack))), 118 + ); 119 + 120 + // Draw button label with appropriate draw mode for text color 121 + if (active) { 122 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillBlack); 123 + } else { 124 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillWhite); 125 + } 126 + _ = pd.graphics.drawText( 127 + label.ptr, 128 + label.len, 129 + pdapi.PDStringEncoding.UTF8Encoding, 130 + x + MARGIN, 131 + y + MARGIN, 132 + ); 133 + 134 + // Reset draw mode to normal 135 + pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 136 + } 137 + 138 + pub fn renderSpinner(pd: *pdapi.PlaydateAPI, x: c_int, y: c_int, color: ?pdapi.LCDSolidColor) void { 139 + const time = pd.system.getCurrentTimeMilliseconds(); 140 + const c = color orelse pdapi.LCDSolidColor.ColorBlack; 141 + const rotation = @as(f32, @floatFromInt(time % 2000)) / 2000.0 * 360.0; 142 + pd.graphics.drawEllipse( 143 + x, 144 + y, 145 + 16, 146 + 16, 147 + 2, 148 + rotation, 149 + rotation + 90, 150 + @as(usize, @intCast(@intFromEnum(c))), 151 + ); 152 + } 153 + 154 + pub fn datetimeToISO(datetime: pdapi.PDDateTime) []const u8 { 155 + var buffer: [32]u8 = undefined; 156 + const result = std.fmt.bufPrint( 157 + &buffer, 158 + "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.000Z", 159 + .{ datetime.year, datetime.month, datetime.day, datetime.hour, datetime.minute, datetime.second }, 160 + ) catch return "2025-08-27T00:00:00.000Z"; // Fallback 161 + 162 + return buffer[0..result.len]; 163 + } 164 + 165 + pub fn saveStringFile(playdate: *pdapi.PlaydateAPI, path: [:0]const u8, data: []const u8) void { 166 + const file = playdate.file.open(path.ptr, pdapi.FILE_WRITE) orelse { 167 + playdate.system.logToConsole("Failed to open file for writing: %s", path.ptr); 168 + return; 169 + }; 170 + defer _ = playdate.file.close(file); 171 + 172 + const bytes_written = playdate.file.write(file, data.ptr, @intCast(data.len)); 173 + if (bytes_written != @as(c_int, @intCast(data.len))) { 174 + playdate.system.logToConsole("Failed to write all data to file"); 175 + } 176 + } 177 + 178 + pub fn loadStringFile(playdate: *pdapi.PlaydateAPI, path: [:0]const u8, output: []u8) void { 179 + const file = playdate.file.open(path.ptr, pdapi.FILE_READ_DATA) orelse { 180 + const err = playdate.file.geterr(); 181 + playdate.system.logToConsole("Failed to open file for reading: %s, Error: %s", path.ptr, err); 182 + return; 183 + }; 184 + defer _ = playdate.file.close(file); 185 + 186 + const bytes_read = playdate.file.read(file, output.ptr, @intCast(output.len - 1)); 187 + if (bytes_read <= 0) { 188 + playdate.system.logToConsole("Failed to read from file"); 189 + return; 190 + } 191 + 192 + const read_len = @as(usize, @intCast(bytes_read)); 193 + output[read_len] = 0; // Null terminate 194 + } 195 + 196 + pub fn delStringFile(playdate: *pdapi.PlaydateAPI, path: [:0]const u8) void { 197 + _ = playdate.file.unlink(path.ptr, 0); 198 + }
+633
src/pdtools/keyboard.zig
··· 1 + const pdapi = @import("../playdate_api_definitions.zig"); 2 + const std = @import("std"); 3 + const pdtools = @import("index.zig"); 4 + const defs = @import("../definitions.zig"); 5 + 6 + // Keyboard configuration constants 7 + const VISIBLE_CHARS = 13; // More characters for better context 8 + const ROW_HEIGHT = 26; 9 + const CHAR_WIDTH = 25; 10 + const CHAR_SPACING = 2; 11 + const START_Y = 80; 12 + const SCREEN_WIDTH = 400; 13 + const CENTER_X = SCREEN_WIDTH / 2; 14 + const BUTTON_WIDTH = 80; 15 + const BUTTON_HEIGHT = 25; 16 + const BUTTON_Y_OFFSET = 160; 17 + 18 + const KeyboardState = enum { 19 + inactive, 20 + active, 21 + completed, 22 + cancelled, 23 + }; 24 + 25 + const Selected = enum { 26 + uppercase, 27 + lowercase, 28 + special, 29 + cancel, 30 + confirm, 31 + }; 32 + 33 + // Callback function types 34 + pub const KeyboardCancelCallback = *const fn () void; 35 + pub const KeyboardConfirmCallback = *const fn (text: []const u8) void; 36 + 37 + // Character sets - each row is a complete character set that can be scrolled through 38 + const UPPERCASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ "; 39 + const LOWERCASE_CHARS = "abcdefghijklmnopqrstuvwxyz "; 40 + const SPECIAL_CHARS = "0123456789!@#$%^&*()_+-=[]{}\\|;':\",./<>?~` "; 41 + const SPECIAL_CHARS_WITH_NEWLINE = "0123456789!@#$%^&*()_+-=[]{}\\|;':\",./<>?~` \n"; 42 + 43 + const SelectionMode = enum { 44 + characters, 45 + buttons, 46 + }; 47 + 48 + // Keyboard struct definition 49 + pub fn Keyboard(comptime buffer_size: usize) type { 50 + return struct { 51 + playdate: *pdapi.PlaydateAPI, 52 + font: *pdapi.LCDFont, 53 + 54 + state: KeyboardState = .inactive, 55 + selected: Selected = .uppercase, 56 + selection_mode: SelectionMode = .characters, 57 + title: [*:0]const u8, 58 + editor_buffer: [buffer_size]u8, 59 + output_buffer: [buffer_size]u8 = undefined, 60 + input_len: usize = 0, 61 + 62 + // Absolute crank position for smooth scrolling 63 + crank_position: f32 = 0.0, 64 + initial_crank_position: f32 = 0.0, 65 + 66 + // Text cursor position within input_buffer 67 + cursor_pos: usize = 0, 68 + 69 + // Callback functions 70 + cancel_callback: ?KeyboardCancelCallback = null, 71 + confirm_callback: ?KeyboardConfirmCallback = null, 72 + 73 + editor_lines: usize = 1, 74 + allow_newlines: bool = false, 75 + 76 + max_length: usize = 0, 77 + 78 + // Sound effects for UI feedback 79 + click_synth: ?*pdapi.PDSynth = null, 80 + previous_selected_char_index: usize = 0, 81 + 82 + pub fn setup(self: *Keyboard(buffer_size), pd: *pdapi.PlaydateAPI, font: *pdapi.LCDFont) void { 83 + self.playdate = pd; 84 + self.font = font; 85 + 86 + // Initialize click sound synth 87 + self.click_synth = pd.sound.synth.newSynth(); 88 + if (self.click_synth) |synth| { 89 + // Set up a short pulse wave for clicking 90 + pd.sound.synth.setWaveform(synth, pdapi.SoundWaveform.kWaveformSquare); 91 + pd.sound.synth.setVolume(synth, 0.1, 0.1); // Low volume 92 + } 93 + } 94 + 95 + // Initialize and show the keyboard 96 + pub fn start( 97 + self: *Keyboard(buffer_size), 98 + title: []const u8, 99 + max_length: usize, 100 + initial_value: ?[]const u8, 101 + allow_newlines: bool, 102 + editor_lines: usize, 103 + cancel_callback: ?KeyboardCancelCallback, 104 + confirm_callback: ?KeyboardConfirmCallback, 105 + ) void { 106 + // Clear the screen when entering keyboard mode 107 + self.playdate.graphics.clear(@as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorWhite)))); 108 + 109 + // Reset keyboard state 110 + self.state = .active; 111 + self.selected = .uppercase; 112 + self.selection_mode = .characters; 113 + self.input_len = 0; 114 + self.cursor_pos = 0; 115 + self.allow_newlines = allow_newlines; 116 + self.editor_lines = editor_lines; 117 + self.cancel_callback = cancel_callback; 118 + self.confirm_callback = confirm_callback; 119 + 120 + // Set title (assuming title is null-terminated string literal) 121 + self.title = @as([*:0]const u8, @ptrCast(title.ptr)); 122 + 123 + // Initialize with initial value 124 + if (initial_value) |value| { 125 + const copy_len = @min(value.len, self.editor_buffer.len - 1); 126 + @memcpy(self.editor_buffer[0..copy_len], value[0..copy_len]); 127 + self.input_len = copy_len; 128 + self.cursor_pos = copy_len; 129 + } else { 130 + self.input_len = 0; 131 + self.cursor_pos = 0; 132 + } 133 + 134 + // Record initial crank position for absolute positioning 135 + self.initial_crank_position = self.playdate.system.getCrankAngle(); 136 + self.crank_position = self.initial_crank_position; 137 + self.max_length = @min(max_length, self.editor_buffer.len - 1); // Leave room for null terminator 138 + 139 + // Reset character tracking for sound 140 + self.previous_selected_char_index = 0; 141 + } 142 + 143 + // Render the keyboard interface 144 + pub fn updateAndRender(self: *Keyboard(buffer_size)) bool { 145 + if (self.state != .active) return false; 146 + 147 + self.handleInput(); 148 + 149 + // Clear screen 150 + self.playdate.graphics.clear(@as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorWhite)))); 151 + 152 + const font_height = self.playdate.graphics.getFontHeight(self.font); 153 + 154 + // Draw title 155 + var temp_width: c_int = 0; 156 + var temp_height: c_int = 0; 157 + var curr_y: c_int = defs.MARGIN; 158 + const prompt_y = curr_y; 159 + const title_slice = std.mem.span(self.title); 160 + _ = pdtools.drawText( 161 + self.playdate, 162 + self.font, 163 + title_slice, 164 + defs.MARGIN, 165 + defs.MARGIN, 166 + &temp_width, 167 + &temp_height, 168 + ); 169 + 170 + curr_y += temp_height + defs.MARGIN; 171 + 172 + // Draw current input 173 + const line_count = if (self.allow_newlines) self.editor_lines else 1; 174 + const rectHeight = font_height * @as(c_int, @intCast(line_count)) + defs.MARGIN * 2; 175 + const rectWidth = pdapi.LCD_COLUMNS - defs.MARGIN * 2; 176 + var input_x: c_int = defs.MARGIN; 177 + var input_y: c_int = curr_y; 178 + self.playdate.graphics.drawRect(input_x, input_y, rectWidth, rectHeight, @as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorBlack)))); 179 + input_y += defs.MARGIN; 180 + input_x += defs.MARGIN; 181 + curr_y += rectHeight + defs.MARGIN; 182 + 183 + // Render multiline text if newlines are allowed 184 + if (self.allow_newlines) { 185 + const input_text = self.editor_buffer[0..self.input_len]; 186 + var line_y = input_y; 187 + var line_start: usize = 0; 188 + var current_line: usize = 0; 189 + 190 + // Split text by newlines and draw each line 191 + for (input_text, 0..) |char, i| { 192 + if (char == '\n' or i == input_text.len - 1) { 193 + const line_end = if (char == '\n') i else i + 1; 194 + const line_text = input_text[line_start..line_end]; 195 + 196 + if (current_line < line_count) { 197 + _ = pdtools.drawText( 198 + self.playdate, 199 + self.font, 200 + line_text, 201 + input_x, 202 + line_y, 203 + &temp_width, 204 + &temp_height, 205 + ); 206 + line_y += font_height; 207 + current_line += 1; 208 + } 209 + line_start = i + 1; 210 + } 211 + } 212 + 213 + // If no newlines in text, draw as single line 214 + if (current_line == 0) { 215 + _ = pdtools.drawText( 216 + self.playdate, 217 + self.font, 218 + input_text, 219 + input_x, 220 + input_y, 221 + &temp_width, 222 + &temp_height, 223 + ); 224 + } 225 + } else { 226 + const input_text = self.editor_buffer[0..self.input_len]; 227 + _ = pdtools.drawText( 228 + self.playdate, 229 + self.font, 230 + input_text, 231 + input_x, 232 + input_y, 233 + &temp_width, 234 + &temp_height, 235 + ); 236 + } 237 + 238 + curr_y += font_height * @as(c_int, @intCast(line_count)) + defs.MARGIN; 239 + 240 + // Draw character counter above input area on the right 241 + var counter_buffer: [32]u8 = undefined; 242 + const counter_text = std.fmt.bufPrint(counter_buffer[0..], "{d}/{d}", .{ self.input_len, self.max_length }) catch "0/0"; 243 + const counter_width = self.playdate.graphics.getTextWidth(self.font, counter_text.ptr, counter_text.len, pdapi.PDStringEncoding.UTF8Encoding, 0); 244 + const counter_x = rectWidth - counter_width; // Right-aligned with some margin 245 + const counter_y = prompt_y; // Above the input box 246 + _ = self.playdate.graphics.drawText(counter_text.ptr, counter_text.len, pdapi.PDStringEncoding.UTF8Encoding, counter_x, counter_y); 247 + 248 + // Draw cursor in input field at cursor position 249 + var cursor_x: c_int = 0; 250 + var cursor_y: c_int = input_y; 251 + 252 + if (self.allow_newlines) { 253 + // Calculate cursor position accounting for newlines 254 + const text_before_cursor = self.editor_buffer[0..self.cursor_pos]; 255 + var current_line: usize = 0; 256 + var line_start: usize = 0; 257 + 258 + // Find which line the cursor is on 259 + for (text_before_cursor, 0..) |char, i| { 260 + if (char == '\n') { 261 + current_line += 1; 262 + line_start = i + 1; 263 + } 264 + } 265 + 266 + // Calculate cursor position within the current line 267 + const line_text = text_before_cursor[line_start..]; 268 + cursor_x = self.playdate.graphics.getTextWidth(self.font, @ptrCast(line_text), line_text.len, pdapi.PDStringEncoding.UTF8Encoding, 0); 269 + cursor_y = input_y + @as(c_int, @intCast(current_line)) * font_height; 270 + } else { 271 + cursor_x = self.playdate.graphics.getTextWidth(self.font, @ptrCast(&self.editor_buffer), self.cursor_pos, pdapi.PDStringEncoding.UTF8Encoding, 0); 272 + } 273 + 274 + self.playdate.graphics.fillRect(cursor_x + defs.MARGIN + 2, cursor_y, 2, 14, @as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorBlack)))); 275 + 276 + // Draw the 3 character set rows 277 + const row_configs = [3]struct { 278 + row: Selected, 279 + y_pos: c_int, 280 + }{ 281 + .{ .row = .uppercase, .y_pos = START_Y }, 282 + .{ .row = .lowercase, .y_pos = START_Y + ROW_HEIGHT }, 283 + .{ .row = .special, .y_pos = START_Y + ROW_HEIGHT * 2 }, 284 + }; 285 + 286 + for (row_configs) |config| { 287 + const is_selected_row = (self.selected == config.row and self.selection_mode == .characters); 288 + const chars = self.getCharacterSet(config.row); 289 + 290 + // Draw visible characters centered around the current crank position 291 + const center_pos = VISIBLE_CHARS / 2; // Position 6 for 13 chars (0-12) 292 + 293 + // Calculate selected character index from crank position 294 + const relative_position = self.crank_position - self.initial_crank_position; 295 + const chars_per_360 = @as(f32, @floatFromInt(chars.len)); 296 + const position_factor = relative_position / 360.0; 297 + const selected_char_index = @as(usize, @intCast(@mod(@as(i32, @intFromFloat(position_factor * chars_per_360)), @as(i32, @intCast(chars.len))))); 298 + 299 + // Play click sound when character changes (only for the currently selected row) 300 + if (is_selected_row and selected_char_index != self.previous_selected_char_index) { 301 + self.playClickSound(); 302 + self.previous_selected_char_index = selected_char_index; 303 + } 304 + 305 + for (0..VISIBLE_CHARS) |i| { 306 + // Calculate character index relative to center 307 + const offset = @as(i32, @intCast(i)) - @as(i32, @intCast(center_pos)); 308 + var char_index = @as(i32, @intCast(selected_char_index)) + offset; 309 + 310 + // Wrap around character set bounds 311 + if (char_index < 0) { 312 + char_index += @as(i32, @intCast(chars.len)); 313 + } 314 + char_index = @rem(char_index, @as(i32, @intCast(chars.len))); 315 + 316 + const char = chars[@as(usize, @intCast(char_index))]; 317 + 318 + // Center the entire character row on screen 319 + const total_width = VISIBLE_CHARS * CHAR_WIDTH + (VISIBLE_CHARS - 1) * CHAR_SPACING; 320 + const start_x = CENTER_X - @divTrunc(total_width, 2); 321 + const x = start_x + @as(c_int, @intCast(i * (CHAR_WIDTH + CHAR_SPACING))); 322 + 323 + // The center character is selected when this row is active 324 + const is_selected_char = (is_selected_row and i == center_pos); 325 + 326 + // Draw character box with better sizing 327 + const box_height = 24; 328 + const char_color = if (is_selected_char) pdapi.LCDSolidColor.ColorBlack else pdapi.LCDSolidColor.ColorWhite; 329 + const border_color = if (is_selected_row) pdapi.LCDSolidColor.ColorBlack else pdapi.LCDSolidColor.ColorXOR; 330 + 331 + self.playdate.graphics.fillRect(x, config.y_pos, CHAR_WIDTH, box_height, @as(usize, @intCast(@intFromEnum(char_color)))); 332 + self.playdate.graphics.drawRect(x, config.y_pos, CHAR_WIDTH, box_height, @as(usize, @intCast(@intFromEnum(border_color)))); 333 + 334 + // Draw character with improved centering 335 + if (char == ' ') { 336 + const space_text = "SPC"; 337 + const text_width = self.playdate.graphics.getTextWidth(self.font, space_text.ptr, space_text.len, pdapi.PDStringEncoding.UTF8Encoding, 0); 338 + const text_height = 12; // Approximate font height 339 + const text_x = x + @divTrunc(CHAR_WIDTH - text_width, 2); 340 + const text_y = config.y_pos + @divTrunc(box_height - text_height, 2); 341 + 342 + if (is_selected_char) { 343 + self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeInverted); 344 + } 345 + _ = self.playdate.graphics.drawText(space_text.ptr, space_text.len, pdapi.PDStringEncoding.UTF8Encoding, text_x, text_y); 346 + if (is_selected_char) { 347 + self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 348 + } 349 + } else if (char == '\n') { 350 + const newline_text = "NL"; 351 + const text_width = self.playdate.graphics.getTextWidth(self.font, newline_text.ptr, newline_text.len, pdapi.PDStringEncoding.UTF8Encoding, 0); 352 + const text_height = 12; // Approximate font height 353 + const text_x = x + @divTrunc(CHAR_WIDTH - text_width, 2); 354 + const text_y = config.y_pos + @divTrunc(box_height - text_height, 2); 355 + 356 + if (is_selected_char) { 357 + self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeInverted); 358 + } 359 + _ = self.playdate.graphics.drawText(newline_text.ptr, newline_text.len, pdapi.PDStringEncoding.UTF8Encoding, text_x, text_y); 360 + if (is_selected_char) { 361 + self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 362 + } 363 + } else { 364 + var char_str = [2]u8{ char, 0 }; 365 + const text_width = self.playdate.graphics.getTextWidth(self.font, @ptrCast(&char_str), 1, pdapi.PDStringEncoding.UTF8Encoding, 0); 366 + const text_height = 12; // Approximate font height 367 + const text_x = x + @divTrunc(CHAR_WIDTH - text_width, 2); 368 + const text_y = config.y_pos + @divTrunc(box_height - text_height, 2); 369 + 370 + if (is_selected_char) { 371 + self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeInverted); 372 + } 373 + _ = self.playdate.graphics.drawText(@ptrCast(&char_str), 1, pdapi.PDStringEncoding.UTF8Encoding, text_x, text_y); 374 + if (is_selected_char) { 375 + self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 376 + } 377 + } 378 + } 379 + } 380 + 381 + // Draw Cancel and Confirm buttons 382 + const button_names = [2][]const u8{ "Cancel", "Confirm" }; 383 + const button_spacing = 20; 384 + const total_button_width = 2 * BUTTON_WIDTH + button_spacing; 385 + const buttons_start_x = CENTER_X - @divTrunc(total_button_width, 2); 386 + 387 + for (0..2) |i| { 388 + const x = buttons_start_x + @as(c_int, @intCast(i * (BUTTON_WIDTH + button_spacing))); 389 + const y = BUTTON_Y_OFFSET; 390 + 391 + const is_selected = (self.selection_mode == .buttons and 392 + ((i == 0 and self.selected == .cancel) or (i == 1 and self.selected == .confirm))); 393 + const button_color = if (is_selected) pdapi.LCDSolidColor.ColorBlack else pdapi.LCDSolidColor.ColorWhite; 394 + const border_color = pdapi.LCDSolidColor.ColorBlack; 395 + 396 + self.playdate.graphics.fillRect(x, y, BUTTON_WIDTH, BUTTON_HEIGHT, @as(usize, @intCast(@intFromEnum(button_color)))); 397 + self.playdate.graphics.drawRect(x, y, BUTTON_WIDTH, BUTTON_HEIGHT, @as(usize, @intCast(@intFromEnum(border_color)))); 398 + 399 + // Draw button text 400 + const text = button_names[i]; 401 + const text_width = self.playdate.graphics.getTextWidth(self.font, text.ptr, text.len, pdapi.PDStringEncoding.UTF8Encoding, 0); 402 + const text_x = x + @divTrunc(BUTTON_WIDTH - text_width, 2); 403 + const text_y = y + @divTrunc(BUTTON_HEIGHT - 12, 2); 404 + 405 + if (is_selected) { 406 + self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeInverted); 407 + } 408 + _ = self.playdate.graphics.drawText(text.ptr, text.len, pdapi.PDStringEncoding.UTF8Encoding, text_x, text_y); 409 + if (is_selected) { 410 + self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy); 411 + } 412 + } 413 + 414 + // Draw instructions below the buttons 415 + const instruction_y = BUTTON_Y_OFFSET + BUTTON_HEIGHT + 5; // 5 pixels below buttons 416 + const instr1 = "A: Select B: Backspace " ++ pdtools.LEFT_ARROW_EMOJI ++ pdtools.RIGHT_ARROW_EMOJI ++ ": Move Cursor"; 417 + const instr2 = pdtools.UP_ARROW_EMOJI ++ pdtools.DOWN_ARROW_EMOJI ++ ": Switch Rows " ++ pdtools.CRANK_EMOJI ++ ": Scroll Characters"; 418 + _ = self.playdate.graphics.drawText(instr1.ptr, instr1.len, pdapi.PDStringEncoding.UTF8Encoding, 10, instruction_y); 419 + _ = self.playdate.graphics.drawText(instr2.ptr, instr2.len, pdapi.PDStringEncoding.UTF8Encoding, 10, instruction_y + 15); 420 + 421 + return true; 422 + } 423 + 424 + // Check if keyboard is currently active 425 + pub fn isKeyboardActive(self: *Keyboard(buffer_size)) bool { 426 + return self.state == .active; 427 + } 428 + 429 + // Get the current keyboard state 430 + pub fn getKeyboardState(self: *Keyboard(buffer_size)) KeyboardState { 431 + return self.state; 432 + } 433 + 434 + // Get the entered text (call after keyboard is completed) 435 + pub fn getEnteredText(self: *Keyboard(buffer_size)) []const u8 { 436 + if (self.state == .completed) { 437 + return self.output_buffer[0..std.mem.len(@as([*:0]const u8, @ptrCast(self.output_buffer)))]; 438 + } 439 + return ""; 440 + } 441 + 442 + // Close the keyboard and clean up 443 + pub fn closeKeyboard(self: *Keyboard(buffer_size)) void { 444 + self.state = .inactive; 445 + self.input_len = 0; 446 + self.cursor_pos = 0; 447 + self.cancel_callback = null; 448 + self.confirm_callback = null; 449 + @memset(&self.editor_buffer, 0); 450 + 451 + // Clean up synth 452 + if (self.click_synth) |synth| { 453 + self.playdate.sound.synth.freeSynth(synth); 454 + self.click_synth = null; 455 + } 456 + } 457 + 458 + pub fn renderKeyboard(self: *Keyboard(buffer_size), pd: *pdapi.PlaydateAPI, font: ?*pdapi.LCDFont) void { 459 + _ = pd; // We use self.playdate 460 + self.font = font; // Update font 461 + self.render(); 462 + } 463 + 464 + // Helper functions for character set access 465 + fn getCharacterSet(self: *Keyboard(buffer_size), row: Selected) []const u8 { 466 + return switch (row) { 467 + .uppercase => UPPERCASE_CHARS, 468 + .lowercase => LOWERCASE_CHARS, 469 + .special => if (self.allow_newlines) SPECIAL_CHARS_WITH_NEWLINE else SPECIAL_CHARS, 470 + .cancel, .confirm => "", // These don't have character sets 471 + }; 472 + } 473 + 474 + // Play a short click sound 475 + fn playClickSound(self: *Keyboard(buffer_size)) void { 476 + if (self.click_synth) |synth| { 477 + // Play a short click at 800Hz for 50ms 478 + self.playdate.sound.synth.playNote(synth, 800.0, 1.0, 0.05, 0); 479 + } 480 + } 481 + 482 + fn getCurrentCharacter(self: *Keyboard(buffer_size)) u8 { 483 + const chars = self.getCharacterSet(self.selected); 484 + if (chars.len == 0) return ' '; // Default for non-character selections 485 + 486 + // Convert crank position to character index 487 + const relative_position = self.crank_position - self.initial_crank_position; 488 + const chars_per_360 = @as(f32, @floatFromInt(chars.len)); 489 + const position_factor = relative_position / 360.0; // Full rotation = full character set 490 + const char_index = @as(usize, @intCast(@mod(@as(i32, @intFromFloat(position_factor * chars_per_360)), @as(i32, @intCast(chars.len))))); 491 + return chars[char_index]; 492 + } 493 + 494 + // Handle button input for keyboard navigation and text entry 495 + pub fn handleInput(self: *Keyboard(buffer_size)) void { 496 + if (self.state != .active) return; 497 + 498 + // Handle button input 499 + var current: pdapi.PDButtons = undefined; 500 + var pushed: pdapi.PDButtons = undefined; 501 + var released: pdapi.PDButtons = undefined; 502 + self.playdate.system.getButtonState(&current, &pushed, &released); 503 + 504 + // Up/Down: Switch between character rows OR move between character/button selection 505 + if (pushed & pdapi.BUTTON_UP != 0) { 506 + if (self.selection_mode == .characters) { 507 + // Switch to previous character row 508 + self.selected = switch (self.selected) { 509 + .uppercase => .special, 510 + .lowercase => .uppercase, 511 + .special => .lowercase, 512 + .cancel, .confirm => .uppercase, // Reset to characters if on buttons 513 + }; 514 + // Reset character tracking when switching rows 515 + self.previous_selected_char_index = 0; 516 + } else { 517 + // Switch from buttons to characters 518 + self.selection_mode = .characters; 519 + self.selected = .uppercase; 520 + // Reset character tracking when switching to characters 521 + self.previous_selected_char_index = 0; 522 + } 523 + } 524 + 525 + if (pushed & pdapi.BUTTON_DOWN != 0) { 526 + if (self.selection_mode == .characters) { 527 + // Check if we should move to buttons (if we're at bottom row) 528 + self.selected = switch (self.selected) { 529 + .uppercase => .lowercase, 530 + .lowercase => .special, 531 + .special => { 532 + // Move to button selection instead 533 + self.selection_mode = .buttons; 534 + self.selected = .cancel; 535 + return; 536 + }, 537 + .cancel, .confirm => .cancel, // Stay on buttons 538 + }; 539 + // Reset character tracking when switching rows 540 + self.previous_selected_char_index = 0; 541 + } 542 + } 543 + 544 + // Left/Right: Move text cursor or navigate buttons 545 + if (pushed & pdapi.BUTTON_LEFT != 0) { 546 + if (self.selection_mode == .characters) { 547 + // Move cursor left in text 548 + if (self.cursor_pos > 0) { 549 + self.cursor_pos -= 1; 550 + } 551 + } else { 552 + // Navigate buttons 553 + self.selected = if (self.selected == .cancel) .confirm else .cancel; 554 + } 555 + } 556 + 557 + if (pushed & pdapi.BUTTON_RIGHT != 0) { 558 + if (self.selection_mode == .characters) { 559 + // Move cursor right in text 560 + if (self.cursor_pos < self.input_len) { 561 + self.cursor_pos += 1; 562 + } 563 + } else { 564 + // Navigate buttons 565 + self.selected = if (self.selected == .cancel) .confirm else .cancel; 566 + } 567 + } 568 + 569 + // A button: Select current character OR activate button 570 + if (pushed & pdapi.BUTTON_A != 0) { 571 + if (self.selection_mode == .characters) { 572 + const char = self.getCurrentCharacter(); 573 + 574 + // Insert character at cursor position if there's space 575 + if (self.input_len < self.max_length) { 576 + // Shift characters to the right to make room for new character 577 + var i = self.input_len; 578 + while (i > self.cursor_pos) { 579 + self.editor_buffer[i] = self.editor_buffer[i - 1]; 580 + i -= 1; 581 + } 582 + 583 + // Insert the new character at cursor position 584 + self.editor_buffer[self.cursor_pos] = char; 585 + self.input_len += 1; 586 + self.cursor_pos += 1; // Move cursor to after inserted character 587 + } 588 + } else { 589 + // Button selection 590 + if (self.selected == .cancel) { 591 + // Cancel 592 + self.state = .cancelled; 593 + if (self.cancel_callback) |callback| { 594 + callback(); 595 + } 596 + } else if (self.selected == .confirm) { 597 + // Confirm 598 + const copy_len = @min(self.input_len, self.output_buffer.len - 1); 599 + @memcpy(self.output_buffer[0..copy_len], self.editor_buffer[0..copy_len]); 600 + self.output_buffer[copy_len] = 0; // Null terminate 601 + self.state = .completed; 602 + 603 + if (self.confirm_callback) |callback| { 604 + // Pass the entered text to the callback 605 + const entered_text = self.editor_buffer[0..self.input_len]; 606 + callback(entered_text); 607 + } 608 + } 609 + } 610 + } 611 + 612 + // B button: Backspace (delete character before cursor) 613 + if (pushed & pdapi.BUTTON_B != 0) { 614 + if (self.cursor_pos > 0 and self.input_len > 0) { 615 + // Shift characters to the left to remove character before cursor 616 + var i = self.cursor_pos - 1; 617 + while (i < self.input_len - 1) { 618 + self.editor_buffer[i] = self.editor_buffer[i + 1]; 619 + i += 1; 620 + } 621 + 622 + // Clear the last character and update lengths 623 + self.input_len -= 1; 624 + self.cursor_pos -= 1; 625 + self.editor_buffer[self.input_len] = 0; 626 + } 627 + } 628 + 629 + // Crank: Update absolute position for smooth character scrolling 630 + self.crank_position = self.playdate.system.getCrankAngle(); 631 + } 632 + }; 633 + }
+1575
src/playdate_api_definitions.zig
··· 1 + const std = @import("std"); 2 + const builtin = @import("builtin"); 3 + 4 + pub const PlaydateAPI = extern struct { 5 + system: *const PlaydateSys, 6 + file: *const PlaydateFile, 7 + graphics: *const PlaydateGraphics, 8 + sprite: *const PlaydateSprite, 9 + display: *const PlaydateDisplay, 10 + sound: *const PlaydateSound, 11 + lua: *const PlaydateLua, 12 + json: *const PlaydateJSON, 13 + scoreboards: *const PlaydateScoreboards, 14 + network: *const PlaydateNetwork, 15 + }; 16 + 17 + /////////Zig Utility Functions/////////// 18 + pub fn is_compiling_for_playdate_hardware() bool { 19 + return builtin.os.tag == .freestanding and builtin.cpu.arch.isThumb(); 20 + } 21 + 22 + ////////Buttons////////////// 23 + pub const PDButtons = c_int; 24 + pub const BUTTON_LEFT = (1 << 0); 25 + pub const BUTTON_RIGHT = (1 << 1); 26 + pub const BUTTON_UP = (1 << 2); 27 + pub const BUTTON_DOWN = (1 << 3); 28 + pub const BUTTON_B = (1 << 4); 29 + pub const BUTTON_A = (1 << 5); 30 + 31 + ///////////////System///////////////////////// 32 + pub const PDMenuItem = opaque {}; 33 + pub const PDCallbackFunction = *const fn (userdata: ?*anyopaque) callconv(.C) c_int; 34 + pub const PDMenuItemCallbackFunction = *const fn (userdata: ?*anyopaque) callconv(.C) void; 35 + pub const PDButtonCallbackFunction = *const fn ( 36 + button: PDButtons, 37 + down: c_int, 38 + when: u32, 39 + userdata: ?*anyopaque, 40 + ) callconv(.C) c_int; 41 + pub const PDSystemEvent = enum(c_int) { 42 + EventInit, 43 + EventInitLua, 44 + EventLock, 45 + EventUnlock, 46 + EventPause, 47 + EventResume, 48 + EventTerminate, 49 + EventKeyPressed, // arg is keycode 50 + EventKeyReleased, 51 + EventLowPower, 52 + }; 53 + pub const PDLanguage = enum(c_int) { 54 + PDLanguageEnglish, 55 + PDLanguageJapanese, 56 + PDLanguageUnknown, 57 + }; 58 + 59 + pub const AccessRequestCallback = ?*const fn (allowed: bool, userdata: ?*anyopaque) callconv(.C) void; 60 + pub const AccessReply = enum(c_int) { 61 + AccessAsk = 0, 62 + AccessDeny, 63 + AccessAllow, 64 + }; 65 + 66 + pub const PDPeripherals = c_int; 67 + pub const PERIPHERAL_NONE = 0; 68 + pub const PERIPHERAL_ACCELEROMETER = (1 << 0); 69 + // ... 70 + pub const PERIPHERAL_ALL = 0xFFFF; 71 + 72 + pub const PDStringEncoding = enum(c_int) { 73 + ASCIIEncoding, 74 + UTF8Encoding, 75 + @"16BitLEEncoding", 76 + }; 77 + 78 + pub const PDDateTime = extern struct { 79 + year: u16, 80 + month: u8, // 1-12 81 + day: u8, // 1-31 82 + weekday: u8, // 1=monday-7=sunday 83 + hour: u8, // 0-23 84 + minute: u8, 85 + second: u8, 86 + }; 87 + 88 + pub const PlaydateSys = extern struct { 89 + realloc: *const fn (ptr: ?*anyopaque, size: usize) callconv(.C) ?*anyopaque, 90 + formatString: *const fn (ret: ?*[*c]u8, fmt: ?[*:0]const u8, ...) callconv(.C) c_int, 91 + logToConsole: *const fn (fmt: ?[*:0]const u8, ...) callconv(.C) void, 92 + @"error": *const fn (fmt: ?[*:0]const u8, ...) callconv(.C) void, 93 + getLanguage: *const fn () callconv(.C) PDLanguage, 94 + getCurrentTimeMilliseconds: *const fn () callconv(.C) c_uint, 95 + getSecondsSinceEpoch: *const fn (milliseconds: ?*c_uint) callconv(.C) c_uint, 96 + drawFPS: *const fn (x: c_int, y: c_int) callconv(.C) void, 97 + 98 + setUpdateCallback: *const fn (update: ?PDCallbackFunction, userdata: ?*anyopaque) callconv(.C) void, 99 + getButtonState: *const fn (current: ?*PDButtons, pushed: ?*PDButtons, released: ?*PDButtons) callconv(.C) void, 100 + setPeripheralsEnabled: *const fn (mask: PDPeripherals) callconv(.C) void, 101 + getAccelerometer: *const fn (outx: ?*f32, outy: ?*f32, outz: ?*f32) callconv(.C) void, 102 + getCrankChange: *const fn () callconv(.C) f32, 103 + getCrankAngle: *const fn () callconv(.C) f32, 104 + isCrankDocked: *const fn () callconv(.C) c_int, 105 + setCrankSoundsDisabled: *const fn (flag: c_int) callconv(.C) c_int, // returns previous setting 106 + 107 + getFlipped: *const fn () callconv(.C) c_int, 108 + setAutoLockDisabled: *const fn (disable: c_int) callconv(.C) void, 109 + 110 + setMenuImage: *const fn (bitmap: ?*LCDBitmap, xOffset: c_int) callconv(.C) void, 111 + addMenuItem: *const fn (title: ?[*:0]const u8, callback: ?PDMenuItemCallbackFunction, userdata: ?*anyopaque) callconv(.C) ?*PDMenuItem, 112 + addCheckmarkMenuItem: *const fn (title: ?[*:0]const u8, value: c_int, callback: ?PDMenuItemCallbackFunction, userdata: ?*anyopaque) callconv(.C) ?*PDMenuItem, 113 + addOptionsMenuItem: *const fn (title: ?[*:0]const u8, optionTitles: [*c]?[*:0]const u8, optionsCount: c_int, f: ?PDMenuItemCallbackFunction, userdata: ?*anyopaque) callconv(.C) ?*PDMenuItem, 114 + removeAllMenuItems: *const fn () callconv(.C) void, 115 + removeMenuItem: *const fn (menuItem: ?*PDMenuItem) callconv(.C) void, 116 + getMenuItemValue: *const fn (menuItem: ?*PDMenuItem) callconv(.C) c_int, 117 + setMenuItemValue: *const fn (menuItem: ?*PDMenuItem, value: c_int) callconv(.C) void, 118 + getMenuItemTitle: *const fn (menuItem: ?*PDMenuItem) callconv(.C) ?[*:0]const u8, 119 + setMenuItemTitle: *const fn (menuItem: ?*PDMenuItem, title: ?[*:0]const u8) callconv(.C) void, 120 + getMenuItemUserdata: *const fn (menuItem: ?*PDMenuItem) callconv(.C) ?*anyopaque, 121 + setMenuItemUserdata: *const fn (menuItem: ?*PDMenuItem, ud: ?*anyopaque) callconv(.C) void, 122 + 123 + getReduceFlashing: *const fn () callconv(.C) c_int, 124 + 125 + // 1.1 126 + getElapsedTime: *const fn () callconv(.C) f32, 127 + resetElapsedTime: *const fn () callconv(.C) void, 128 + 129 + // 1.4 130 + getBatteryPercentage: *const fn () callconv(.C) f32, 131 + getBatteryVoltage: *const fn () callconv(.C) f32, 132 + 133 + // 1.13 134 + getTimezoneOffset: *const fn () callconv(.C) i32, 135 + shouldDisplay24HourTime: *const fn () callconv(.C) c_int, 136 + convertEpochToDateTime: *const fn (epoch: u32, datetime: ?*PDDateTime) callconv(.C) void, 137 + convertDateTimeToEpoch: *const fn (datetime: ?*PDDateTime) callconv(.C) u32, 138 + 139 + //2.0 140 + clearICache: *const fn () callconv(.C) void, 141 + 142 + // 2.4 143 + setButtonCallback: *const fn ( 144 + cb: ?PDButtonCallbackFunction, 145 + buttonud: ?*anyopaque, 146 + queuesize: c_int, 147 + ) callconv(.C) void, 148 + setSerialMessageCallback: *const fn ( 149 + callback: *const fn (data: ?[*:0]const u8) callconv(.C) void, 150 + ) callconv(.C) void, 151 + vaFormatString: *const fn ( 152 + outstr: [*c][*c]u8, 153 + fmt: ?[*:0]const u8, 154 + args: VaList, 155 + ) callconv(.C) c_int, 156 + parseString: *const fn ( 157 + str: ?[*:0]const u8, 158 + format: ?[*:0]const u8, 159 + ..., 160 + ) callconv(.C) c_int, 161 + 162 + // ??? 163 + delay: *const fn (milliseconds: u32) callconv(.C) void, 164 + 165 + // 2.7 166 + getServerTime: *const fn (callback: *const fn (time: ?[*:0]const u8, err: ?[*:0]const u8) callconv(.C) void) callconv(.C) void, 167 + restartGame: *const fn (launchargs: ?[*:0]const u8) callconv(.C) void, 168 + getLaunchArgs: *const fn (outpath: [*c][*:0]const u8) callconv(.C) ?[*:0]const u8, 169 + sendMirrorData: *const fn (command: u8, data: [*c]u8, len: c_int) callconv(.C) bool, 170 + 171 + //NOTE(Daniel Bokser): std.builtin.VaList is not available when targeting Playdate hardware, 172 + // so we need to directly include it 173 + const VaList = if (is_compiling_for_playdate_hardware() or builtin.os.tag == .windows) 174 + @cImport({ 175 + @cInclude("stdarg.h"); 176 + }).va_list 177 + else 178 + //NOTE(Daniel Bokser): 179 + // We must use std.builtin.VaList when building for the Linux simulator. 180 + // Using stdarg.h results in a compiler error otherwise. 181 + std.builtin.VaList; 182 + }; 183 + 184 + ////////LCD and Graphics/////////////////////// 185 + pub const LCD_COLUMNS = 400; 186 + pub const LCD_ROWS = 240; 187 + pub const LCD_ROWSIZE = 52; 188 + pub const LCDBitmap = opaque {}; 189 + pub const LCDVideoPlayer = opaque {}; 190 + pub const LCDStreamPlayer = opaque {}; 191 + pub const PlaydateVideo = extern struct { 192 + loadVideo: *const fn (?[*:0]const u8) callconv(.C) ?*LCDVideoPlayer, 193 + freePlayer: *const fn (?*LCDVideoPlayer) callconv(.C) void, 194 + setContext: *const fn (?*LCDVideoPlayer, ?*LCDBitmap) callconv(.C) c_int, 195 + useScreenContext: *const fn (?*LCDVideoPlayer) callconv(.C) void, 196 + renderFrame: *const fn (?*LCDVideoPlayer, c_int) callconv(.C) c_int, 197 + getError: *const fn (?*LCDVideoPlayer) callconv(.C) ?[*:0]const u8, 198 + getInfo: *const fn (?*LCDVideoPlayer, [*c]c_int, [*c]c_int, [*c]f32, [*c]c_int, [*c]c_int) callconv(.C) void, 199 + getContext: *const fn (?*LCDVideoPlayer) callconv(.C) ?*LCDBitmap, 200 + }; 201 + 202 + pub const LCDPattern = [16]u8; 203 + pub const LCDColor = usize; //Pointer to LCDPattern or a LCDSolidColor value 204 + pub const LCDSolidColor = enum(c_int) { 205 + ColorBlack, 206 + ColorWhite, 207 + ColorClear, 208 + ColorXOR, 209 + }; 210 + pub const LCDBitmapDrawMode = enum(c_int) { 211 + DrawModeCopy, 212 + DrawModeWhiteTransparent, 213 + DrawModeBlackTransparent, 214 + DrawModeFillWhite, 215 + DrawModeFillBlack, 216 + DrawModeXOR, 217 + DrawModeNXOR, 218 + DrawModeInverted, 219 + }; 220 + pub const LCDLineCapStyle = enum(c_int) { 221 + LineCapStyleButt, 222 + LineCapStyleSquare, 223 + LineCapStyleRound, 224 + }; 225 + 226 + pub const LCDFontLanguage = enum(c_int) { 227 + LCDFontLanguageEnglish, 228 + LCDFontLanguageJapanese, 229 + LCDFontLanguageUnknown, 230 + }; 231 + 232 + pub const LCDBitmapFlip = enum(c_int) { 233 + BitmapUnflipped, 234 + BitmapFlippedX, 235 + BitmapFlippedY, 236 + BitmapFlippedXY, 237 + }; 238 + 239 + pub const LCDPolygonFillRule = enum(c_int) { 240 + PolygonFillNonZero, 241 + PolygonFillEvenOdd, 242 + }; 243 + 244 + pub const PDTextWrappingMode = enum(c_int) { 245 + WrapClip, 246 + WrapCharacter, 247 + WrapWord, 248 + }; 249 + 250 + pub const PDTextAlignment = enum(c_int) { 251 + AlignTextLeft, 252 + AlignTextCenter, 253 + AlignTextRight, 254 + }; 255 + 256 + pub const LCDTileMap = opaque {}; 257 + pub const LCDBitmapTable = opaque {}; 258 + pub const LCDFont = opaque {}; 259 + pub const LCDFontPage = opaque {}; 260 + pub const LCDFontGlyph = opaque {}; 261 + pub const LCDFontData = opaque {}; 262 + pub const LCDRect = extern struct { 263 + left: c_int, 264 + right: c_int, 265 + top: c_int, 266 + bottom: c_int, 267 + }; 268 + 269 + pub const PlaydateVideostream = extern struct { 270 + newPlayer: *const fn () callconv(.C) ?*LCDStreamPlayer, 271 + freePlayer: *const fn (p: ?*LCDStreamPlayer) callconv(.C) void, 272 + 273 + setBufferSize: *const fn (p: ?*LCDStreamPlayer, video: c_int, audio: c_int) callconv(.C) void, 274 + 275 + setFile: *const fn (p: ?*LCDStreamPlayer, file: ?*SDFile) callconv(.C) void, 276 + 277 + setHTTPConnection: *const fn (p: ?*LCDStreamPlayer, conn: ?*HTTPConnection) callconv(.C) void, 278 + 279 + getFilePlayer: *const fn (p: ?*LCDStreamPlayer) callconv(.C) ?*FilePlayer, 280 + 281 + getVideoPlayer: *const fn (p: ?*LCDStreamPlayer) callconv(.C) ?*LCDVideoPlayer, 282 + 283 + // returns true if it drew a frame, else false 284 + update: *const fn (p: ?*LCDStreamPlayer) callconv(.C) bool, 285 + 286 + getBufferedFrameCount: *const fn (p: ?*LCDStreamPlayer) callconv(.C) c_int, 287 + 288 + // uint32_t (*getBytesRead)(LCDStreamPlayer* p); 289 + getBytesRead: *const fn (p: ?*LCDStreamPlayer) callconv(.C) u32, 290 + 291 + // 3.0 292 + setTCPConnection: *const fn (p: ?*LCDStreamPlayer, conn: ?*TCPConnection) callconv(.C) void, 293 + }; 294 + 295 + pub const PlaydateTilemap = extern struct { 296 + newTilemap: *const fn () callconv(.C) ?*LCDTileMap, 297 + freeTilemap: *const fn (m: ?*LCDTileMap) callconv(.C) void, 298 + 299 + setImageTable: *const fn (m: ?*LCDTileMap, table: ?*LCDBitmapTable) callconv(.C) void, 300 + getImageTable: *const fn (m: ?*LCDTileMap) callconv(.C) ?*LCDBitmapTable, 301 + 302 + setSize: *const fn (m: ?*LCDTileMap, tilesWide: c_int, tilesHigh: c_int) callconv(.C) void, 303 + getSize: *const fn (m: ?*LCDTileMap, tilesWide: ?*c_int, tilesHigh: ?*c_int) callconv(.C) void, 304 + getPixelSize: *const fn (m: ?*LCDTileMap, outWidth: ?*u32, outHeight: ?*u32) callconv(.C) void, 305 + 306 + setTiles: *const fn (m: ?*LCDTileMap, indexes: [*c]u16, count: c_int, rowwidth: c_int) callconv(.C) void, 307 + 308 + setTileAtPosition: *const fn (m: ?*LCDTileMap, x: c_int, y: c_int, idx: u16) callconv(.C) void, 309 + getTileAtPosition: *const fn (m: ?*LCDTileMap, x: c_int, y: c_int) callconv(.C) c_int, 310 + 311 + drawAtPoint: *const fn (m: ?*LCDTileMap, x: f32, y: f32) callconv(.C) void, 312 + }; 313 + 314 + pub const PlaydateGraphics = extern struct { 315 + video: *const PlaydateVideo, 316 + // Drawing Functions 317 + clear: *const fn (color: LCDColor) callconv(.C) void, 318 + setBackgroundColor: *const fn (color: LCDSolidColor) callconv(.C) void, 319 + setStencil: *const fn (stencil: ?*LCDBitmap) callconv(.C) void, // deprecated in favor of setStencilImage, which adds a "tile" flag 320 + setDrawMode: *const fn (mode: LCDBitmapDrawMode) callconv(.C) void, 321 + setDrawOffset: *const fn (dx: c_int, dy: c_int) callconv(.C) void, 322 + setClipRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int) callconv(.C) void, 323 + clearClipRect: *const fn () callconv(.C) void, 324 + setLineCapStyle: *const fn (endCapStyle: LCDLineCapStyle) callconv(.C) void, 325 + setFont: *const fn (font: ?*LCDFont) callconv(.C) void, 326 + setTextTracking: *const fn (tracking: c_int) callconv(.C) void, 327 + pushContext: *const fn (target: ?*LCDBitmap) callconv(.C) void, 328 + popContext: *const fn () callconv(.C) void, 329 + 330 + drawBitmap: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int, flip: LCDBitmapFlip) callconv(.C) void, 331 + tileBitmap: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int, width: c_int, height: c_int, flip: LCDBitmapFlip) callconv(.C) void, 332 + drawLine: *const fn (x1: c_int, y1: c_int, x2: c_int, y2: c_int, width: c_int, color: LCDColor) callconv(.C) void, 333 + fillTriangle: *const fn (x1: c_int, y1: c_int, x2: c_int, y2: c_int, x3: c_int, y3: c_int, color: LCDColor) callconv(.C) void, 334 + drawRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, color: LCDColor) callconv(.C) void, 335 + fillRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, color: LCDColor) callconv(.C) void, 336 + drawEllipse: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, lineWidth: c_int, startAngle: f32, endAngle: f32, color: LCDColor) callconv(.C) void, 337 + fillEllipse: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, startAngle: f32, endAngle: f32, color: LCDColor) callconv(.C) void, 338 + drawScaledBitmap: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int, xscale: f32, yscale: f32) callconv(.C) void, 339 + drawText: *const fn (text: ?*const anyopaque, len: usize, encoding: PDStringEncoding, x: c_int, y: c_int) callconv(.C) c_int, 340 + 341 + // LCDBitmap 342 + newBitmap: *const fn (width: c_int, height: c_int, color: LCDColor) callconv(.C) ?*LCDBitmap, 343 + freeBitmap: *const fn (bitmap: ?*LCDBitmap) callconv(.C) void, 344 + loadBitmap: *const fn (path: ?[*:0]const u8, outerr: ?*?[*:0]const u8) callconv(.C) ?*LCDBitmap, 345 + copyBitmap: *const fn (bitmap: ?*LCDBitmap) callconv(.C) ?*LCDBitmap, 346 + loadIntoBitmap: *const fn (path: ?[*:0]const u8, bitmap: ?*LCDBitmap, outerr: ?*?[*:0]const u8) callconv(.C) void, 347 + getBitmapData: *const fn (bitmap: ?*LCDBitmap, width: ?*c_int, height: ?*c_int, rowbytes: ?*c_int, mask: ?*[*c]u8, data: ?*[*c]u8) callconv(.C) void, 348 + clearBitmap: *const fn (bitmap: ?*LCDBitmap, bgcolor: LCDColor) callconv(.C) void, 349 + rotatedBitmap: *const fn (bitmap: ?*LCDBitmap, rotation: f32, xscale: f32, yscale: f32, allocedSize: ?*c_int) callconv(.C) ?*LCDBitmap, 350 + 351 + // LCDBitmapTable 352 + newBitmapTable: *const fn (count: c_int, width: c_int, height: c_int) callconv(.C) ?*LCDBitmapTable, 353 + freeBitmapTable: *const fn (table: ?*LCDBitmapTable) callconv(.C) void, 354 + loadBitmapTable: *const fn (path: ?[*:0]const u8, outerr: ?*?[*:0]const u8) callconv(.C) ?*LCDBitmapTable, 355 + loadIntoBitmapTable: *const fn (path: ?[*:0]const u8, table: ?*LCDBitmapTable, outerr: ?*?[*:0]const u8) callconv(.C) void, 356 + getTableBitmap: *const fn (table: ?*LCDBitmapTable, idx: c_int) callconv(.C) ?*LCDBitmap, 357 + 358 + // LCDFont 359 + loadFont: *const fn (path: ?[*:0]const u8, outErr: ?*?[*:0]const u8) callconv(.C) ?*LCDFont, 360 + getFontPage: *const fn (font: ?*LCDFont, c: u32) callconv(.C) ?*LCDFontPage, 361 + getPageGlyph: *const fn (page: ?*LCDFontPage, c: u32, bitmap: ?**LCDBitmap, advance: ?*c_int) callconv(.C) ?*LCDFontGlyph, 362 + getGlyphKerning: *const fn (glyph: ?*LCDFontGlyph, glyphcode: u32, nextcode: u32) callconv(.C) c_int, 363 + getTextWidth: *const fn (font: ?*LCDFont, text: ?*const anyopaque, len: usize, encoding: PDStringEncoding, tracking: c_int) callconv(.C) c_int, 364 + 365 + // raw framebuffer access 366 + getFrame: *const fn () callconv(.C) [*c]u8, // row stride = LCD_ROWSIZE 367 + getDisplayFrame: *const fn () callconv(.C) [*c]u8, // row stride = LCD_ROWSIZE 368 + getDebugBitmap: *const fn () callconv(.C) ?*LCDBitmap, // valid in simulator only, function is null on device 369 + copyFrameBufferBitmap: *const fn () callconv(.C) ?*LCDBitmap, 370 + markUpdatedRows: *const fn (start: c_int, end: c_int) callconv(.C) void, 371 + display: *const fn () callconv(.C) void, 372 + 373 + // misc util. 374 + setColorToPattern: *const fn (color: ?*LCDColor, bitmap: ?*LCDBitmap, x: c_int, y: c_int) callconv(.C) void, 375 + checkMaskCollision: *const fn (bitmap1: ?*LCDBitmap, x1: c_int, y1: c_int, flip1: LCDBitmapFlip, bitmap2: ?*LCDBitmap, x2: c_int, y2: c_int, flip2: LCDBitmapFlip, rect: LCDRect) callconv(.C) c_int, 376 + 377 + // 1.1 378 + setScreenClipRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int) callconv(.C) void, 379 + 380 + // 1.1.1 381 + fillPolygon: *const fn (nPoints: c_int, coords: [*c]c_int, color: LCDColor, fillRule: LCDPolygonFillRule) callconv(.C) void, 382 + getFontHeight: *const fn (font: ?*LCDFont) callconv(.C) u8, 383 + 384 + // 1.7 385 + getDisplayBufferBitmap: *const fn () callconv(.C) ?*LCDBitmap, 386 + drawRotatedBitmap: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int, rotation: f32, centerx: f32, centery: f32, xscale: f32, yscale: f32) callconv(.C) void, 387 + setTextLeading: *const fn (lineHeightAdustment: c_int) callconv(.C) void, 388 + 389 + // 1.8 390 + setBitmapMask: *const fn (bitmap: ?*LCDBitmap, mask: ?*LCDBitmap) callconv(.C) c_int, 391 + getBitmapMask: *const fn (bitmap: ?*LCDBitmap) callconv(.C) ?*LCDBitmap, 392 + 393 + // 1.10 394 + setStencilImage: *const fn (stencil: ?*LCDBitmap, tile: c_int) callconv(.C) void, 395 + 396 + // 1.12 397 + makeFontFromData: *const fn (data: ?*LCDFontData, wide: c_int) callconv(.C) *LCDFont, 398 + 399 + // 2.1 400 + getTextTracking: *const fn () callconv(.C) c_int, 401 + 402 + // 2.5 403 + setPixel: *const fn (x: c_int, y: c_int, c: LCDColor) callconv(.C) void, 404 + getBitmapPixel: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int) callconv(.C) LCDSolidColor, 405 + getBitmapTableInfo: *const fn (table: ?*LCDBitmapTable, count: ?*c_int, width: ?*c_int) callconv(.C) void, 406 + 407 + // 2.6 408 + drawTextInRect: *const fn (text: ?*const anyopaque, len: usize, encoding: PDStringEncoding, x: c_int, y: c_int, width: c_int, height: c_int, wrap: PDTextWrappingMode, @"align": PDTextAlignment) callconv(.C) void, 409 + 410 + // 2.7 411 + getTextHeightForMaxWidth: *const fn (font: ?*LCDFont, text: ?[*:0]const u8, len: usize, maxwidth: c_int, encoding: PDStringEncoding, wrap: PDTextWrappingMode, tracking: c_int, extraLeading: c_int) callconv(.C) c_int, 412 + drawRoundRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, radius: c_int, lineWidth: c_int, color: LCDColor) callconv(.C) void, 413 + fillRoundRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, radius: c_int, color: LCDColor) callconv(.C) void, 414 + 415 + // 3.0 416 + tilemap: *const PlaydateTilemap, 417 + videostream: *const PlaydateVideostream, 418 + }; 419 + pub const PlaydateDisplay = struct { 420 + getWidth: *const fn () callconv(.C) c_int, 421 + getHeight: *const fn () callconv(.C) c_int, 422 + 423 + setRefreshRate: *const fn (rate: f32) callconv(.C) void, 424 + 425 + setInverted: *const fn (flag: c_int) callconv(.C) void, 426 + setScale: *const fn (s: c_uint) callconv(.C) void, 427 + setMosaic: *const fn (x: c_uint, y: c_uint) callconv(.C) void, 428 + setFlipped: *const fn (x: c_int, y: c_int) callconv(.C) void, 429 + setOffset: *const fn (x: c_int, y: c_int) callconv(.C) void, 430 + 431 + // 2.7 432 + getRefreshRate: *const fn () callconv(.C) f32, 433 + getFPS: *const fn () callconv(.C) f32, 434 + }; 435 + 436 + //////File System///// 437 + pub const SDFile = opaque {}; 438 + 439 + pub const FileOptions = c_int; 440 + pub const FILE_READ = (1 << 0); 441 + pub const FILE_READ_DATA = (1 << 1); 442 + pub const FILE_WRITE = (1 << 2); 443 + pub const FILE_APPEND = (2 << 2); 444 + 445 + pub const SEEK_SET = 0; 446 + pub const SEEK_CUR = 1; 447 + pub const SEEK_END = 2; 448 + 449 + pub const FileStat = extern struct { 450 + isdir: c_int, 451 + size: c_uint, 452 + m_year: c_int, 453 + m_month: c_int, 454 + m_day: c_int, 455 + m_hour: c_int, 456 + m_minute: c_int, 457 + m_second: c_int, 458 + }; 459 + 460 + pub const PlaydateFile = extern struct { 461 + geterr: *const fn () callconv(.C) ?[*:0]const u8, 462 + 463 + listfiles: *const fn ( 464 + path: ?[*:0]const u8, 465 + callback: *const fn (path: ?[*:0]const u8, userdata: ?*anyopaque) callconv(.C) void, 466 + userdata: ?*anyopaque, 467 + showhidden: c_int, 468 + ) callconv(.C) c_int, 469 + stat: *const fn (path: ?[*:0]const u8, stat: ?*FileStat) callconv(.C) c_int, 470 + mkdir: *const fn (path: ?[*:0]const u8) callconv(.C) c_int, 471 + unlink: *const fn (name: ?[*:0]const u8, recursive: c_int) callconv(.C) c_int, 472 + rename: *const fn (from: ?[*:0]const u8, to: ?[*:0]const u8) callconv(.C) c_int, 473 + 474 + open: *const fn (name: ?[*:0]const u8, mode: FileOptions) callconv(.C) ?*SDFile, 475 + close: *const fn (file: ?*SDFile) callconv(.C) c_int, 476 + read: *const fn (file: ?*SDFile, buf: ?*anyopaque, len: c_uint) callconv(.C) c_int, 477 + write: *const fn (file: ?*SDFile, buf: ?*const anyopaque, len: c_uint) callconv(.C) c_int, 478 + flush: *const fn (file: ?*SDFile) callconv(.C) c_int, 479 + tell: *const fn (file: ?*SDFile) callconv(.C) c_int, 480 + seek: *const fn (file: ?*SDFile, pos: c_int, whence: c_int) callconv(.C) c_int, 481 + }; 482 + 483 + /////////Audio////////////// 484 + pub const MicSource = enum(c_int) { 485 + kMicInputAutodetect = 0, 486 + kMicInputInternal = 1, 487 + kMicInputHeadset = 2, 488 + }; 489 + pub const PlaydateSound = extern struct { 490 + channel: *const PlaydateSoundChannel, 491 + fileplayer: *const PlaydateSoundFileplayer, 492 + sample: *const PlaydateSoundSample, 493 + sampleplayer: *const PlaydateSoundSampleplayer, 494 + synth: *const PlaydateSoundSynth, 495 + sequence: *const PlaydateSoundSequence, 496 + effect: *const PlaydateSoundEffect, 497 + lfo: *const PlaydateSoundLFO, 498 + envelope: *const PlaydateSoundEnvelope, 499 + source: *const PlaydateSoundSource, 500 + controlsignal: *const PlaydateControlSignal, 501 + track: *const PlaydateSoundTrack, 502 + instrument: *const PlaydateSoundInstrument, 503 + 504 + getCurrentTime: *const fn () callconv(.C) u32, 505 + addSource: *const fn (callback: AudioSourceFunction, context: ?*anyopaque, stereo: c_int) callconv(.C) ?*SoundSource, 506 + 507 + getDefaultChannel: *const fn () callconv(.C) ?*SoundChannel, 508 + 509 + addChannel: *const fn (channel: ?*SoundChannel) callconv(.C) void, 510 + removeChannel: *const fn (channel: ?*SoundChannel) callconv(.C) void, 511 + 512 + setMicCallback: *const fn (callback: RecordCallback, context: ?*anyopaque, source: MicSource) callconv(.C) void, 513 + getHeadphoneState: *const fn ( 514 + headphone: ?*c_int, 515 + headsetmic: ?*c_int, 516 + changeCallback: ?*const fn (headphone: c_int, mic: c_int) callconv(.C) void, 517 + ) callconv(.C) void, 518 + setOutputsActive: *const fn (headphone: c_int, mic: c_int) callconv(.C) void, 519 + 520 + // 1.5 521 + removeSource: *const fn (?*SoundSource) callconv(.C) void, 522 + 523 + // 1.12 524 + signal: *const PlaydateSoundSignal, 525 + 526 + // 2.2 527 + getError: *const fn () callconv(.C) ?[*:0]const u8, 528 + }; 529 + 530 + //data is mono 531 + pub const RecordCallback = *const fn (context: ?*anyopaque, buffer: [*c]i16, length: c_int) callconv(.C) c_int; 532 + // len is # of samples in each buffer, function should return 1 if it produced output 533 + pub const AudioSourceFunction = *const fn (context: ?*anyopaque, left: [*c]i16, right: [*c]i16, len: c_int) callconv(.C) c_int; 534 + pub const SndCallbackProc = *const fn (c: ?*SoundSource, userdata: ?*anyopaque) callconv(.C) void; 535 + 536 + pub const SoundChannel = opaque {}; 537 + pub const SoundSource = opaque {}; 538 + pub const SoundEffect = opaque {}; 539 + pub const PDSynthSignalValue = opaque {}; 540 + 541 + pub const PlaydateSoundChannel = extern struct { 542 + newChannel: *const fn () callconv(.C) ?*SoundChannel, 543 + freeChannel: *const fn (channel: ?*SoundChannel) callconv(.C) void, 544 + addSource: *const fn (channel: ?*SoundChannel, source: ?*SoundSource) callconv(.C) c_int, 545 + removeSource: *const fn (channel: ?*SoundChannel, source: ?*SoundSource) callconv(.C) c_int, 546 + addCallbackSource: *const fn (?*SoundChannel, AudioSourceFunction, ?*anyopaque, c_int) callconv(.C) ?*SoundSource, 547 + addEffect: *const fn (channel: ?*SoundChannel, effect: ?*SoundEffect) callconv(.C) void, 548 + removeEffect: *const fn (channel: ?*SoundChannel, effect: ?*SoundEffect) callconv(.C) void, 549 + setVolume: *const fn (channel: ?*SoundChannel, f32) callconv(.C) void, 550 + getVolume: *const fn (channel: ?*SoundChannel) callconv(.C) f32, 551 + setVolumeModulator: *const fn (channel: ?*SoundChannel, mod: ?*PDSynthSignalValue) callconv(.C) void, 552 + getVolumeModulator: *const fn (channel: ?*SoundChannel) callconv(.C) ?*PDSynthSignalValue, 553 + setPan: *const fn (channel: ?*SoundChannel, pan: f32) callconv(.C) void, 554 + setPanModulator: *const fn (channel: ?*SoundChannel, mod: ?*PDSynthSignalValue) callconv(.C) void, 555 + getPanModulator: *const fn (channel: ?*SoundChannel) callconv(.C) ?*PDSynthSignalValue, 556 + getDryLevelSignal: *const fn (channe: ?*SoundChannel) callconv(.C) ?*PDSynthSignalValue, 557 + getWetLevelSignal: *const fn (channel: ?*SoundChannel) callconv(.C) ?*PDSynthSignalValue, 558 + }; 559 + 560 + pub const FilePlayer = SoundSource; 561 + pub const PlaydateSoundFileplayer = extern struct { 562 + newPlayer: *const fn () callconv(.C) ?*FilePlayer, 563 + freePlayer: *const fn (player: ?*FilePlayer) callconv(.C) void, 564 + loadIntoPlayer: *const fn (player: ?*FilePlayer, path: ?[*:0]const u8) callconv(.C) c_int, 565 + setBufferLength: *const fn (player: ?*FilePlayer, bufferLen: f32) callconv(.C) void, 566 + play: *const fn (player: ?*FilePlayer, repeat: c_int) callconv(.C) c_int, 567 + isPlaying: *const fn (player: ?*FilePlayer) callconv(.C) c_int, 568 + pause: *const fn (player: ?*FilePlayer) callconv(.C) void, 569 + stop: *const fn (player: ?*FilePlayer) callconv(.C) void, 570 + setVolume: *const fn (player: ?*FilePlayer, left: f32, right: f32) callconv(.C) void, 571 + getVolume: *const fn (player: ?*FilePlayer, left: ?*f32, right: ?*f32) callconv(.C) void, 572 + getLength: *const fn (player: ?*FilePlayer) callconv(.C) f32, 573 + setOffset: *const fn (player: ?*FilePlayer, offset: f32) callconv(.C) void, 574 + setRate: *const fn (player: ?*FilePlayer, rate: f32) callconv(.C) void, 575 + setLoopRange: *const fn (player: ?*FilePlayer, start: f32, end: f32) callconv(.C) void, 576 + didUnderrun: *const fn (player: ?*FilePlayer) callconv(.C) c_int, 577 + setFinishCallback: *const fn ( 578 + player: ?*FilePlayer, 579 + callback: ?SndCallbackProc, 580 + userdata: ?*anyopaque, 581 + ) callconv(.C) void, 582 + setLoopCallback: *const fn ( 583 + player: ?*FilePlayer, 584 + callback: ?SndCallbackProc, 585 + userdata: ?*anyopaque, 586 + ) callconv(.C) void, 587 + getOffset: *const fn (player: ?*FilePlayer) callconv(.C) f32, 588 + getRate: *const fn (player: ?*FilePlayer) callconv(.C) f32, 589 + setStopOnUnderrun: *const fn (player: ?*FilePlayer, flag: c_int) callconv(.C) void, 590 + fadeVolume: *const fn ( 591 + player: ?*FilePlayer, 592 + left: f32, 593 + right: f32, 594 + len: i32, 595 + finishCallback: ?SndCallbackProc, 596 + userdata: ?*anyopaque, 597 + ) callconv(.C) void, 598 + setMP3StreamSource: *const fn ( 599 + player: ?*FilePlayer, 600 + dataSource: *const fn (data: [*c]u8, bytes: c_int, userdata: ?*anyopaque) callconv(.C) c_int, 601 + userdata: ?*anyopaque, 602 + bufferLen: f32, 603 + ) callconv(.C) void, 604 + }; 605 + 606 + pub const AudioSample = opaque {}; 607 + pub const SamplePlayer = SoundSource; 608 + 609 + pub const SoundFormat = enum(c_uint) { 610 + kSound8bitMono = 0, 611 + kSound8bitStereo = 1, 612 + kSound16bitMono = 2, 613 + kSound16bitStereo = 3, 614 + kSoundADPCMMono = 4, 615 + kSoundADPCMStereo = 5, 616 + }; 617 + pub inline fn SoundFormatIsStereo(f: SoundFormat) bool { 618 + return @intFromEnum(f) & 1; 619 + } 620 + pub inline fn SoundFormatIs16bit(f: SoundFormat) bool { 621 + return switch (f) { 622 + .kSound16bitMono, 623 + .kSound16bitStereo, 624 + .kSoundADPCMMono, 625 + .kSoundADPCMStereo, 626 + => true, 627 + else => false, 628 + }; 629 + } 630 + pub inline fn SoundFormat_bytesPerFrame(fmt: SoundFormat) u32 { 631 + return (if (SoundFormatIsStereo(fmt)) 2 else 1) * 632 + (if (SoundFormatIs16bit(fmt)) 2 else 1); 633 + } 634 + 635 + pub const PlaydateSoundSample = extern struct { 636 + newSampleBuffer: *const fn (byteCount: c_int) callconv(.C) ?*AudioSample, 637 + loadIntoSample: *const fn (sample: ?*AudioSample, path: ?[*:0]const u8) callconv(.C) c_int, 638 + load: *const fn (path: ?[*:0]const u8) callconv(.C) ?*AudioSample, 639 + newSampleFromData: *const fn (data: [*c]u8, format: SoundFormat, sampleRate: u32, byteCount: c_int, shouldFreeData: c_int) callconv(.C) ?*AudioSample, 640 + getData: *const fn (sample: ?*AudioSample, data: ?*[*c]u8, format: [*c]SoundFormat, sampleRate: ?*u32, byteLength: ?*u32) callconv(.C) void, 641 + freeSample: *const fn (sample: ?*AudioSample) callconv(.C) void, 642 + getLength: *const fn (sample: ?*AudioSample) callconv(.C) f32, 643 + 644 + // 2.4 645 + decompress: *const fn (sample: ?*AudioSample) callconv(.C) c_int, 646 + }; 647 + 648 + pub const PlaydateSoundSampleplayer = extern struct { 649 + newPlayer: *const fn () callconv(.C) ?*SamplePlayer, 650 + freePlayer: *const fn (?*SamplePlayer) callconv(.C) void, 651 + setSample: *const fn (player: ?*SamplePlayer, sample: ?*AudioSample) callconv(.C) void, 652 + play: *const fn (player: ?*SamplePlayer, repeat: c_int, rate: f32) callconv(.C) c_int, 653 + isPlaying: *const fn (player: ?*SamplePlayer) callconv(.C) c_int, 654 + stop: *const fn (player: ?*SamplePlayer) callconv(.C) void, 655 + setVolume: *const fn (player: ?*SamplePlayer, left: f32, right: f32) callconv(.C) void, 656 + getVolume: *const fn (player: ?*SamplePlayer, left: ?*f32, right: ?*f32) callconv(.C) void, 657 + getLength: *const fn (player: ?*SamplePlayer) callconv(.C) f32, 658 + setOffset: *const fn (player: ?*SamplePlayer, offset: f32) callconv(.C) void, 659 + setRate: *const fn (player: ?*SamplePlayer, rate: f32) callconv(.C) void, 660 + setPlayRange: *const fn (player: ?*SamplePlayer, start: c_int, end: c_int) callconv(.C) void, 661 + setFinishCallback: *const fn ( 662 + player: ?*SamplePlayer, 663 + callback: ?SndCallbackProc, 664 + userdata: ?*anyopaque, 665 + ) callconv(.C) void, 666 + setLoopCallback: *const fn ( 667 + player: ?*SamplePlayer, 668 + callback: ?SndCallbackProc, 669 + userdata: ?*anyopaque, 670 + ) callconv(.C) void, 671 + getOffset: *const fn (player: ?*SamplePlayer) callconv(.C) f32, 672 + getRate: *const fn (player: ?*SamplePlayer) callconv(.C) f32, 673 + setPaused: *const fn (player: ?*SamplePlayer, flag: c_int) callconv(.C) void, 674 + }; 675 + 676 + pub const PDSynth = SoundSource; 677 + pub const SoundWaveform = enum(c_uint) { 678 + kWaveformSquare = 0, 679 + kWaveformTriangle = 1, 680 + kWaveformSine = 2, 681 + kWaveformNoise = 3, 682 + kWaveformSawtooth = 4, 683 + kWaveformPOPhase = 5, 684 + kWaveformPODigital = 6, 685 + kWaveformPOVosim = 7, 686 + }; 687 + pub const NOTE_C4 = 60.0; 688 + pub const MIDINote = f32; 689 + pub inline fn pd_noteToFrequency(n: MIDINote) f32 { 690 + return 440 * std.math.pow(f32, 2, (n - 69) / 12.0); 691 + } 692 + pub inline fn pd_frequencyToNote(f: f32) MIDINote { 693 + return 12 * std.math.log(f32, 2, f) - 36.376316562; 694 + } 695 + 696 + // generator render callback 697 + // samples are in Q8.24 format. left is either the left channel or the single mono channel, 698 + // right is non-NULL only if the stereo flag was set in the setGenerator() call. 699 + // nsamples is at most 256 but may be shorter 700 + // rate is Q0.32 per-frame phase step, drate is per-frame rate step (i.e., do rate += drate every frame) 701 + // return value is the number of sample frames rendered 702 + pub const SynthRenderFunc = *const fn (userdata: ?*anyopaque, left: [*c]i32, right: [*c]i32, nsamples: c_int, rate: u32, drate: i32) callconv(.C) c_int; 703 + 704 + // generator event callbacks 705 + 706 + // len == -1 if indefinite 707 + pub const SynthNoteOnFunc = *const fn (userdata: ?*anyopaque, note: MIDINote, velocity: f32, len: f32) callconv(.C) void; 708 + 709 + pub const SynthReleaseFunc = *const fn (userdata: ?*anyopaque, stop: c_int) callconv(.C) void; 710 + pub const SynthSetParameterFunc = *const fn (userdata: ?*anyopaque, parameter: c_int, value: f32) callconv(.C) c_int; 711 + pub const SynthDeallocFunc = *const fn (userdata: ?*anyopaque) callconv(.C) void; 712 + pub const SynthCopyUserdata = *const fn (userdata: ?*anyopaque) callconv(.C) ?*anyopaque; 713 + 714 + pub const PlaydateSoundSynth = extern struct { 715 + newSynth: *const fn () callconv(.C) ?*PDSynth, 716 + freeSynth: *const fn (synth: ?*PDSynth) callconv(.C) void, 717 + 718 + setWaveform: *const fn (synth: ?*PDSynth, wave: SoundWaveform) callconv(.C) void, 719 + setGenerator_deprecated: *const fn ( 720 + synth: ?*PDSynth, 721 + stereo: c_int, 722 + render: SynthRenderFunc, 723 + note_on: SynthNoteOnFunc, 724 + release: SynthReleaseFunc, 725 + set_param: SynthSetParameterFunc, 726 + dealloc: SynthDeallocFunc, 727 + userdata: ?*anyopaque, 728 + ) callconv(.C) void, 729 + setSample: *const fn ( 730 + synth: ?*PDSynth, 731 + sample: ?*AudioSample, 732 + sustain_start: u32, 733 + sustain_end: u32, 734 + ) callconv(.C) void, 735 + 736 + setAttackTime: *const fn (synth: ?*PDSynth, attack: f32) callconv(.C) void, 737 + setDecayTime: *const fn (synth: ?*PDSynth, decay: f32) callconv(.C) void, 738 + setSustainLevel: *const fn (synth: ?*PDSynth, sustain: f32) callconv(.C) void, 739 + setReleaseTime: *const fn (synth: ?*PDSynth, release: f32) callconv(.C) void, 740 + 741 + setTranspose: *const fn (synth: ?*PDSynth, half_steps: f32) callconv(.C) void, 742 + 743 + setFrequencyModulator: *const fn (synth: ?*PDSynth, mod: ?*PDSynthSignalValue) callconv(.C) void, 744 + getFrequencyModulator: *const fn (synth: ?*PDSynth) callconv(.C) ?*PDSynthSignalValue, 745 + setAmplitudeModulator: *const fn (synth: ?*PDSynth, mod: ?*PDSynthSignalValue) callconv(.C) void, 746 + getAmplitudeModulator: *const fn (synth: ?*PDSynth) callconv(.C) ?*PDSynthSignalValue, 747 + 748 + getParameterCount: *const fn (synth: ?*PDSynth) callconv(.C) c_int, 749 + setParameter: *const fn (synth: ?*PDSynth, parameter: c_int, value: f32) callconv(.C) c_int, 750 + setParameterModulator: *const fn (synth: ?*PDSynth, parameter: c_int, mod: ?*PDSynthSignalValue) callconv(.C) void, 751 + getParameterModulator: *const fn (synth: ?*PDSynth, parameter: c_int) callconv(.C) ?*PDSynthSignalValue, 752 + 753 + playNote: *const fn (synth: ?*PDSynth, freq: f32, vel: f32, len: f32, when: u32) callconv(.C) void, 754 + playMIDINote: *const fn (synth: ?*PDSynth, note: MIDINote, vel: f32, len: f32, when: u32) callconv(.C) void, 755 + noteOff: *const fn (synth: ?*PDSynth, when: u32) callconv(.C) void, 756 + stop: *const fn (synth: ?*PDSynth) callconv(.C) void, 757 + 758 + setVolume: *const fn (synth: ?*PDSynth, left: f32, right: f32) callconv(.C) void, 759 + getVolume: *const fn (synth: ?*PDSynth, left: ?*f32, right: ?*f32) callconv(.C) void, 760 + 761 + isPlaying: *const fn (synth: ?*PDSynth) callconv(.C) c_int, 762 + 763 + // 1.13 764 + getEnvelope: *const fn (synth: ?*PDSynth) callconv(.C) ?*PDSynthEnvelope, // synth keeps ownership--don't free this! 765 + 766 + // 2.2 767 + setWavetable: *const fn (synth: ?*PDSynth, sample: ?*AudioSample, log2size: c_int, columns: c_int, rows: c_int) callconv(.C) c_int, 768 + 769 + // 2.4 770 + setGenerator: *const fn ( 771 + synth: ?*PDSynth, 772 + stereo: c_int, 773 + render: SynthRenderFunc, 774 + noteOn: SynthNoteOnFunc, 775 + release: SynthReleaseFunc, 776 + setparam: SynthSetParameterFunc, 777 + dealloc: SynthDeallocFunc, 778 + copyUserdata: SynthCopyUserdata, 779 + userdata: ?*anyopaque, 780 + ) callconv(.C) void, 781 + copy: *const fn (synth: ?*PDSynth) callconv(.C) ?*PDSynth, 782 + 783 + // 2.6 784 + clearEnvelope: *const fn (synth: ?*PDSynth) callconv(.C) void, 785 + }; 786 + 787 + pub const SequenceTrack = opaque {}; 788 + pub const SoundSequence = opaque {}; 789 + pub const SequenceFinishedCallback = *const fn (seq: ?*SoundSequence, userdata: ?*anyopaque) callconv(.C) void; 790 + 791 + pub const PlaydateSoundSequence = extern struct { 792 + newSequence: *const fn () callconv(.C) ?*SoundSequence, 793 + freeSequence: *const fn (sequence: ?*SoundSequence) callconv(.C) void, 794 + 795 + loadMidiFile: *const fn (seq: ?*SoundSequence, path: ?[*:0]const u8) callconv(.C) c_int, 796 + getTime: *const fn (seq: ?*SoundSequence) callconv(.C) u32, 797 + setTime: *const fn (seq: ?*SoundSequence, time: u32) callconv(.C) void, 798 + setLoops: *const fn (seq: ?*SoundSequence, loopstart: c_int, loopend: c_int, loops: c_int) callconv(.C) void, 799 + getTempo_deprecated: *const fn (seq: ?*SoundSequence) callconv(.C) c_int, 800 + setTempo: *const fn (seq: ?*SoundSequence, stepsPerSecond: c_int) callconv(.C) void, 801 + getTrackCount: *const fn (seq: ?*SoundSequence) callconv(.C) c_int, 802 + addTrack: *const fn (seq: ?*SoundSequence) callconv(.C) ?*SequenceTrack, 803 + getTrackAtIndex: *const fn (seq: ?*SoundSequence, track: c_uint) callconv(.C) ?*SequenceTrack, 804 + setTrackAtIndex: *const fn (seq: ?*SoundSequence, ?*SequenceTrack, idx: c_uint) callconv(.C) void, 805 + allNotesOff: *const fn (seq: ?*SoundSequence) callconv(.C) void, 806 + 807 + // 1.1 808 + isPlaying: *const fn (seq: ?*SoundSequence) callconv(.C) c_int, 809 + getLength: *const fn (seq: ?*SoundSequence) callconv(.C) u32, 810 + play: *const fn (seq: ?*SoundSequence, finishCallback: SequenceFinishedCallback, userdata: ?*anyopaque) callconv(.C) void, 811 + stop: *const fn (seq: ?*SoundSequence) callconv(.C) void, 812 + getCurrentStep: *const fn (seq: ?*SoundSequence, timeOffset: ?*c_int) callconv(.C) c_int, 813 + setCurrentStep: *const fn (seq: ?*SoundSequence, step: c_int, timeOffset: c_int, playNotes: c_int) callconv(.C) void, 814 + 815 + // 2.5 816 + getTempo: *const fn (seq: ?*SoundSequence) callconv(.C) f32, 817 + }; 818 + 819 + pub const EffectProc = *const fn (e: ?*SoundEffect, left: [*c]i32, right: [*c]i32, nsamples: c_int, bufactive: c_int) callconv(.C) c_int; 820 + 821 + pub const PlaydateSoundEffect = extern struct { 822 + newEffect: *const fn (proc: ?*const EffectProc, userdata: ?*anyopaque) callconv(.C) ?*SoundEffect, 823 + freeEffect: *const fn (effect: ?*SoundEffect) callconv(.C) void, 824 + 825 + setMix: *const fn (effect: ?*SoundEffect, level: f32) callconv(.C) void, 826 + setMixModulator: *const fn (effect: ?*SoundEffect, signal: ?*PDSynthSignalValue) callconv(.C) void, 827 + getMixModulator: *const fn (effect: ?*SoundEffect) callconv(.C) ?*PDSynthSignalValue, 828 + 829 + setUserdata: *const fn (effect: ?*SoundEffect, userdata: ?*anyopaque) callconv(.C) void, 830 + getUserdata: *const fn (effect: ?*SoundEffect) callconv(.C) ?*anyopaque, 831 + 832 + twopolefilter: *const PlaydateSoundEffectTwopolefilter, 833 + onepolefilter: *const PlaydateSoundEffectOnepolefilter, 834 + bitcrusher: *const PlaydateSoundEffectBitcrusher, 835 + ringmodulator: *const PlaydateSoundEffectRingmodulator, 836 + delayline: *const PlaydateSoundEffectDelayline, 837 + overdrive: *const PlaydateSoundEffectOverdrive, 838 + }; 839 + pub const LFOType = enum(c_uint) { 840 + kLFOTypeSquare = 0, 841 + kLFOTypeTriangle = 1, 842 + kLFOTypeSine = 2, 843 + kLFOTypeSampleAndHold = 3, 844 + kLFOTypeSawtoothUp = 4, 845 + kLFOTypeSawtoothDown = 5, 846 + kLFOTypeArpeggiator = 6, 847 + kLFOTypeFunction = 7, 848 + }; 849 + pub const PDSynthLFO = opaque {}; 850 + pub const PlaydateSoundLFO = extern struct { 851 + newLFO: *const fn (LFOType) callconv(.C) ?*PDSynthLFO, 852 + freeLFO: *const fn (lfo: ?*PDSynthLFO) callconv(.C) void, 853 + 854 + setType: *const fn (lfo: ?*PDSynthLFO, type: LFOType) callconv(.C) void, 855 + setRate: *const fn (lfo: ?*PDSynthLFO, rate: f32) callconv(.C) void, 856 + setPhase: *const fn (lfo: ?*PDSynthLFO, phase: f32) callconv(.C) void, 857 + setCenter: *const fn (lfo: ?*PDSynthLFO, center: f32) callconv(.C) void, 858 + setDepth: *const fn (lfo: ?*PDSynthLFO, depth: f32) callconv(.C) void, 859 + setArpeggiation: *const fn (lfo: ?*PDSynthLFO, nSteps: c_int, steps: [*c]f32) callconv(.C) void, 860 + setFunction: *const fn ( 861 + lfo: ?*PDSynthLFO, 862 + lfoFunc: *const fn (lfo: ?*PDSynthLFO, userdata: ?*anyopaque) callconv(.C) f32, 863 + userdata: ?*anyopaque, 864 + interpolate: c_int, 865 + ) callconv(.C) void, 866 + setDelay: *const fn (lfo: ?*PDSynthLFO, holdoff: f32, ramptime: f32) callconv(.C) void, 867 + setRetrigger: *const fn (lfo: ?*PDSynthLFO, flag: c_int) callconv(.C) void, 868 + 869 + getValue: *const fn (lfo: ?*PDSynthLFO) callconv(.C) f32, 870 + 871 + // 1.10 872 + setGlobal: *const fn (lfo: ?*PDSynthLFO, global: c_int) callconv(.C) void, 873 + }; 874 + 875 + pub const PDSynthEnvelope = opaque {}; 876 + pub const PlaydateSoundEnvelope = extern struct { 877 + newEnvelope: *const fn (attack: f32, decay: f32, sustain: f32, release: f32) callconv(.C) ?*PDSynthEnvelope, 878 + freeEnvelope: *const fn (env: ?*PDSynthEnvelope) callconv(.C) void, 879 + 880 + setAttack: *const fn (env: ?*PDSynthEnvelope, attack: f32) callconv(.C) void, 881 + setDecay: *const fn (env: ?*PDSynthEnvelope, decay: f32) callconv(.C) void, 882 + setSustain: *const fn (env: ?*PDSynthEnvelope, sustain: f32) callconv(.C) void, 883 + setRelease: *const fn (env: ?*PDSynthEnvelope, release: f32) callconv(.C) void, 884 + 885 + setLegato: *const fn (env: ?*PDSynthEnvelope, flag: c_int) callconv(.C) void, 886 + setRetrigger: *const fn (env: ?*PDSynthEnvelope, flag: c_int) callconv(.C) void, 887 + 888 + getValue: *const fn (env: ?*PDSynthEnvelope) callconv(.C) f32, 889 + 890 + // 1.13 891 + setCurvature: *const fn (env: ?*PDSynthEnvelope, amount: f32) callconv(.C) void, 892 + setVelocitySensitivity: *const fn (env: ?*PDSynthEnvelope, velsens: f32) callconv(.C) void, 893 + setRateScaling: *const fn (env: ?*PDSynthEnvelope, scaling: f32, start: MIDINote, end: MIDINote) callconv(.C) void, 894 + }; 895 + 896 + pub const PlaydateSoundSource = extern struct { 897 + setVolume: *const fn (c: ?*SoundSource, lvol: f32, rvol: f32) callconv(.C) void, 898 + getVolume: *const fn (c: ?*SoundSource, outl: ?*f32, outr: ?*f32) callconv(.C) void, 899 + isPlaying: *const fn (c: ?*SoundSource) callconv(.C) c_int, 900 + setFinishCallback: *const fn ( 901 + c: ?*SoundSource, 902 + callback: SndCallbackProc, 903 + userdata: ?*anyopaque, 904 + ) callconv(.C) void, 905 + }; 906 + 907 + pub const ControlSignal = opaque {}; 908 + pub const PlaydateControlSignal = extern struct { 909 + newSignal: *const fn () callconv(.C) ?*ControlSignal, 910 + freeSignal: *const fn (signal: ?*ControlSignal) callconv(.C) void, 911 + clearEvents: *const fn (control: ?*ControlSignal) callconv(.C) void, 912 + addEvent: *const fn (control: ?*ControlSignal, step: c_int, value: f32, c_int) callconv(.C) void, 913 + removeEvent: *const fn (control: ?*ControlSignal, step: c_int) callconv(.C) void, 914 + getMIDIControllerNumber: *const fn (control: ?*ControlSignal) callconv(.C) c_int, 915 + }; 916 + 917 + pub const PlaydateSoundTrack = extern struct { 918 + newTrack: *const fn () callconv(.C) ?*SequenceTrack, 919 + freeTrack: *const fn (track: ?*SequenceTrack) callconv(.C) void, 920 + 921 + setInstrument: *const fn (track: ?*SequenceTrack, inst: ?*PDSynthInstrument) callconv(.C) void, 922 + getInstrument: *const fn (track: ?*SequenceTrack) callconv(.C) ?*PDSynthInstrument, 923 + 924 + addNoteEvent: *const fn (track: ?*SequenceTrack, step: u32, len: u32, note: MIDINote, velocity: f32) callconv(.C) void, 925 + removeNoteEvent: *const fn (track: ?*SequenceTrack, step: u32, note: MIDINote) callconv(.C) void, 926 + clearNotes: *const fn (track: ?*SequenceTrack) callconv(.C) void, 927 + 928 + getControlSignalCount: *const fn (track: ?*SequenceTrack) callconv(.C) c_int, 929 + getControlSignal: *const fn (track: ?*SequenceTrack, idx: c_int) callconv(.C) ?*ControlSignal, 930 + clearControlEvents: *const fn (track: ?*SequenceTrack) callconv(.C) void, 931 + 932 + getPolyphony: *const fn (track: ?*SequenceTrack) callconv(.C) c_int, 933 + activeVoiceCount: *const fn (track: ?*SequenceTrack) callconv(.C) c_int, 934 + 935 + setMuted: *const fn (track: ?*SequenceTrack, mute: c_int) callconv(.C) void, 936 + 937 + // 1.1 938 + getLength: *const fn (track: ?*SequenceTrack) callconv(.C) u32, 939 + getIndexForStep: *const fn (track: ?*SequenceTrack, step: u32) callconv(.C) c_int, 940 + getNoteAtIndex: *const fn (track: ?*SequenceTrack, index: c_int, outSteo: ?*u32, outLen: ?*u32, outeNote: ?*MIDINote, outVelocity: ?*f32) callconv(.C) c_int, 941 + 942 + //1.10 943 + getSignalForController: *const fn (track: ?*SequenceTrack, controller: c_int, create: c_int) callconv(.C) ?*ControlSignal, 944 + }; 945 + 946 + pub const PDSynthInstrument = SoundSource; 947 + pub const PlaydateSoundInstrument = extern struct { 948 + newInstrument: *const fn () callconv(.C) ?*PDSynthInstrument, 949 + freeInstrument: *const fn (inst: ?*PDSynthInstrument) callconv(.C) void, 950 + addVoice: *const fn (inst: ?*PDSynthInstrument, synth: ?*PDSynth, rangeStart: MIDINote, rangeEnd: MIDINote, transpose: f32) callconv(.C) c_int, 951 + playNote: *const fn (inst: ?*PDSynthInstrument, frequency: f32, vel: f32, len: f32, when: u32) callconv(.C) ?*PDSynth, 952 + playMIDINote: *const fn (inst: ?*PDSynthInstrument, note: MIDINote, vel: f32, len: f32, when: u32) callconv(.C) ?*PDSynth, 953 + setPitchBend: *const fn (inst: ?*PDSynthInstrument, bend: f32) callconv(.C) void, 954 + setPitchBendRange: *const fn (inst: ?*PDSynthInstrument, halfSteps: f32) callconv(.C) void, 955 + setTranspose: *const fn (inst: ?*PDSynthInstrument, halfSteps: f32) callconv(.C) void, 956 + noteOff: *const fn (inst: ?*PDSynthInstrument, note: MIDINote, when: u32) callconv(.C) void, 957 + allNotesOff: *const fn (inst: ?*PDSynthInstrument, when: u32) callconv(.C) void, 958 + setVolume: *const fn (inst: ?*PDSynthInstrument, left: f32, right: f32) callconv(.C) void, 959 + getVolume: *const fn (inst: ?*PDSynthInstrument, left: ?*f32, right: ?*f32) callconv(.C) void, 960 + activeVoiceCount: *const fn (inst: ?*PDSynthInstrument) callconv(.C) c_int, 961 + }; 962 + 963 + pub const PDSynthSignal = opaque {}; 964 + pub const SignalStepFunc = *const fn (userdata: ?*anyopaque, ioframes: [*c]c_int, ifval: ?*f32) callconv(.C) f32; 965 + // len = -1 for indefinite 966 + pub const SignalNoteOnFunc = *const fn (userdata: ?*anyopaque, note: MIDINote, vel: f32, len: f32) callconv(.C) void; 967 + // ended = 0 for note release, = 1 when note stops playing 968 + pub const SignalNoteOffFunc = *const fn (userdata: ?*anyopaque, stopped: c_int, offset: c_int) callconv(.C) void; 969 + pub const SignalDeallocFunc = *const fn (userdata: ?*anyopaque) callconv(.C) void; 970 + pub const PlaydateSoundSignal = struct { 971 + newSignal: *const fn (step: SignalStepFunc, noteOn: SignalNoteOnFunc, noteOff: SignalNoteOffFunc, dealloc: SignalDeallocFunc, userdata: ?*anyopaque) callconv(.C) ?*PDSynthSignal, 972 + freeSignal: *const fn (signal: ?*PDSynthSignal) callconv(.C) void, 973 + getValue: *const fn (signal: ?*PDSynthSignal) callconv(.C) f32, 974 + setValueScale: *const fn (signal: ?*PDSynthSignal, scale: f32) callconv(.C) void, 975 + setValueOffset: *const fn (signal: ?*PDSynthSignal, offset: f32) callconv(.C) void, 976 + }; 977 + 978 + // EFFECTS 979 + 980 + // A SoundEffect processes the output of a channel's SoundSources 981 + 982 + pub const TwoPoleFilter = SoundEffect; 983 + pub const TwoPoleFilterType = enum(c_int) { 984 + FilterTypeLowPass, 985 + FilterTypeHighPass, 986 + FilterTypeBandPass, 987 + FilterTypeNotch, 988 + FilterTypePEQ, 989 + FilterTypeLowShelf, 990 + FilterTypeHighShelf, 991 + }; 992 + pub const PlaydateSoundEffectTwopolefilter = extern struct { 993 + newFilter: *const fn () callconv(.C) ?*TwoPoleFilter, 994 + freeFilter: *const fn (filter: ?*TwoPoleFilter) callconv(.C) void, 995 + setType: *const fn (filter: ?*TwoPoleFilter, type: TwoPoleFilterType) callconv(.C) void, 996 + setFrequency: *const fn (filter: ?*TwoPoleFilter, frequency: f32) callconv(.C) void, 997 + setFrequencyModulator: *const fn (filter: ?*TwoPoleFilter, signal: ?*PDSynthSignalValue) callconv(.C) void, 998 + getFrequencyModulator: *const fn (filter: ?*TwoPoleFilter) callconv(.C) ?*PDSynthSignalValue, 999 + setGain: *const fn (filter: ?*TwoPoleFilter, f32) callconv(.C) void, 1000 + setResonance: *const fn (filter: ?*TwoPoleFilter, f32) callconv(.C) void, 1001 + setResonanceModulator: *const fn (filter: ?*TwoPoleFilter, signal: ?*PDSynthSignalValue) callconv(.C) void, 1002 + getResonanceModulator: *const fn (filter: ?*TwoPoleFilter) callconv(.C) ?*PDSynthSignalValue, 1003 + }; 1004 + 1005 + pub const OnePoleFilter = SoundEffect; 1006 + pub const PlaydateSoundEffectOnepolefilter = extern struct { 1007 + newFilter: *const fn () callconv(.C) ?*OnePoleFilter, 1008 + freeFilter: *const fn (filter: ?*OnePoleFilter) callconv(.C) void, 1009 + setParameter: *const fn (filter: ?*OnePoleFilter, parameter: f32) callconv(.C) void, 1010 + setParameterModulator: *const fn (filter: ?*OnePoleFilter, signal: ?*PDSynthSignalValue) callconv(.C) void, 1011 + getParameterModulator: *const fn (filter: ?*OnePoleFilter) callconv(.C) ?*PDSynthSignalValue, 1012 + }; 1013 + 1014 + pub const BitCrusher = SoundEffect; 1015 + pub const PlaydateSoundEffectBitcrusher = extern struct { 1016 + newBitCrusher: *const fn () callconv(.C) ?*BitCrusher, 1017 + freeBitCrusher: *const fn (filter: ?*BitCrusher) callconv(.C) void, 1018 + setAmount: *const fn (filter: ?*BitCrusher, amount: f32) callconv(.C) void, 1019 + setAmountModulator: *const fn (filter: ?*BitCrusher, signal: ?*PDSynthSignalValue) callconv(.C) void, 1020 + getAmountModulator: *const fn (filter: ?*BitCrusher) callconv(.C) ?*PDSynthSignalValue, 1021 + setUndersampling: *const fn (filter: ?*BitCrusher, undersampling: f32) callconv(.C) void, 1022 + setUndersampleModulator: *const fn (filter: ?*BitCrusher, signal: ?*PDSynthSignalValue) callconv(.C) void, 1023 + getUndersampleModulator: *const fn (filter: ?*BitCrusher) callconv(.C) ?*PDSynthSignalValue, 1024 + }; 1025 + 1026 + pub const RingModulator = SoundEffect; 1027 + pub const PlaydateSoundEffectRingmodulator = extern struct { 1028 + newRingmod: *const fn () callconv(.C) ?*RingModulator, 1029 + freeRingmod: *const fn (filter: ?*RingModulator) callconv(.C) void, 1030 + setFrequency: *const fn (filter: ?*RingModulator, frequency: f32) callconv(.C) void, 1031 + setFrequencyModulator: *const fn (filter: ?*RingModulator, signal: ?*PDSynthSignalValue) callconv(.C) void, 1032 + getFrequencyModulator: *const fn (filter: ?*RingModulator) callconv(.C) ?*PDSynthSignalValue, 1033 + }; 1034 + 1035 + pub const DelayLine = SoundEffect; 1036 + pub const DelayLineTap = SoundSource; 1037 + pub const PlaydateSoundEffectDelayline = extern struct { 1038 + newDelayLine: *const fn (length: c_int, stereo: c_int) callconv(.C) ?*DelayLine, 1039 + freeDelayLine: *const fn (filter: ?*DelayLine) callconv(.C) void, 1040 + setLength: *const fn (filter: ?*DelayLine, frames: c_int) callconv(.C) void, 1041 + setFeedback: *const fn (filter: ?*DelayLine, fb: f32) callconv(.C) void, 1042 + addTap: *const fn (filter: ?*DelayLine, delay: c_int) callconv(.C) ?*DelayLineTap, 1043 + 1044 + // note that DelayLineTap is a SoundSource, not a SoundEffect 1045 + freeTap: *const fn (tap: ?*DelayLineTap) callconv(.C) void, 1046 + setTapDelay: *const fn (t: ?*DelayLineTap, frames: c_int) callconv(.C) void, 1047 + setTapDelayModulator: *const fn (t: ?*DelayLineTap, mod: ?*PDSynthSignalValue) callconv(.C) void, 1048 + getTapDelayModulator: *const fn (t: ?*DelayLineTap) callconv(.C) ?*PDSynthSignalValue, 1049 + setTapChannelsFlipped: *const fn (t: ?*DelayLineTap, flip: c_int) callconv(.C) void, 1050 + }; 1051 + 1052 + pub const Overdrive = SoundEffect; 1053 + pub const PlaydateSoundEffectOverdrive = extern struct { 1054 + newOverdrive: *const fn () callconv(.C) ?*Overdrive, 1055 + freeOverdrive: *const fn (filter: ?*Overdrive) callconv(.C) void, 1056 + setGain: *const fn (o: ?*Overdrive, gain: f32) callconv(.C) void, 1057 + setLimit: *const fn (o: ?*Overdrive, limit: f32) callconv(.C) void, 1058 + setLimitModulator: *const fn (o: ?*Overdrive, mod: ?*PDSynthSignalValue) callconv(.C) void, 1059 + getLimitModulator: *const fn (o: ?*Overdrive) callconv(.C) ?*PDSynthSignalValue, 1060 + setOffset: *const fn (o: ?*Overdrive, offset: f32) callconv(.C) void, 1061 + setOffsetModulator: *const fn (o: ?*Overdrive, mod: ?*PDSynthSignalValue) callconv(.C) void, 1062 + getOffsetModulator: *const fn (o: ?*Overdrive) callconv(.C) ?*PDSynthSignalValue, 1063 + }; 1064 + 1065 + //////Sprite///// 1066 + pub const SpriteCollisionResponseType = enum(c_int) { 1067 + CollisionTypeSlide, 1068 + CollisionTypeFreeze, 1069 + CollisionTypeOverlap, 1070 + CollisionTypeBounce, 1071 + }; 1072 + pub const PDRect = extern struct { 1073 + x: f32, 1074 + y: f32, 1075 + width: f32, 1076 + height: f32, 1077 + }; 1078 + 1079 + pub fn PDRectMake(x: f32, y: f32, width: f32, height: f32) callconv(.C) PDRect { 1080 + return .{ 1081 + .x = x, 1082 + .y = y, 1083 + .width = width, 1084 + .height = height, 1085 + }; 1086 + } 1087 + 1088 + pub const CollisionPoint = extern struct { 1089 + x: f32, 1090 + y: f32, 1091 + }; 1092 + pub const CollisionVector = extern struct { 1093 + x: c_int, 1094 + y: c_int, 1095 + }; 1096 + 1097 + pub const SpriteCollisionInfo = extern struct { 1098 + sprite: ?*LCDSprite, // The sprite being moved 1099 + other: ?*LCDSprite, // The sprite being moved 1100 + responseType: SpriteCollisionResponseType, // The result of collisionResponse 1101 + overlaps: u8, // True if the sprite was overlapping other when the collision started. False if it didn’t overlap but tunneled through other. 1102 + ti: f32, // A number between 0 and 1 indicating how far along the movement to the goal the collision occurred 1103 + move: CollisionPoint, // The difference between the original coordinates and the actual ones when the collision happened 1104 + normal: CollisionVector, // The collision normal; usually -1, 0, or 1 in x and y. Use this value to determine things like if your character is touching the ground. 1105 + touch: CollisionPoint, // The coordinates where the sprite started touching other 1106 + spriteRect: PDRect, // The rectangle the sprite occupied when the touch happened 1107 + otherRect: PDRect, // The rectangle the sprite being collided with occupied when the touch happened 1108 + }; 1109 + 1110 + pub const SpriteQueryInfo = extern struct { 1111 + sprite: ?*LCDSprite, // The sprite being intersected by the segment 1112 + // ti1 and ti2 are numbers between 0 and 1 which indicate how far from the starting point of the line segment the collision happened 1113 + ti1: f32, // entry point 1114 + ti2: f32, // exit point 1115 + entryPoint: CollisionPoint, // The coordinates of the first intersection between sprite and the line segment 1116 + exitPoint: CollisionPoint, // The coordinates of the second intersection between sprite and the line segment 1117 + }; 1118 + 1119 + pub const LCDSprite = opaque {}; 1120 + pub const CWCollisionInfo = opaque {}; 1121 + pub const CWItemInfo = opaque {}; 1122 + 1123 + pub const LCDSpriteDrawFunction = ?*const fn (sprite: ?*LCDSprite, bounds: PDRect, drawrect: PDRect) callconv(.C) void; 1124 + pub const LCDSpriteUpdateFunction = ?*const fn (sprite: ?*LCDSprite) callconv(.C) void; 1125 + pub const LCDSpriteCollisionFilterProc = ?*const fn (sprite: ?*LCDSprite, other: ?*LCDSprite) callconv(.C) SpriteCollisionResponseType; 1126 + 1127 + pub const PlaydateSprite = extern struct { 1128 + setAlwaysRedraw: *const fn (flag: c_int) callconv(.C) void, 1129 + addDirtyRect: *const fn (dirtyRect: LCDRect) callconv(.C) void, 1130 + drawSprites: *const fn () callconv(.C) void, 1131 + updateAndDrawSprites: *const fn () callconv(.C) void, 1132 + 1133 + newSprite: *const fn () callconv(.C) ?*LCDSprite, 1134 + freeSprite: *const fn (sprite: ?*LCDSprite) callconv(.C) void, 1135 + copy: *const fn (sprite: ?*LCDSprite) callconv(.C) ?*LCDSprite, 1136 + 1137 + addSprite: *const fn (sprite: ?*LCDSprite) callconv(.C) void, 1138 + removeSprite: *const fn (sprite: ?*LCDSprite) callconv(.C) void, 1139 + removeSprites: *const fn (sprite: [*c]?*LCDSprite, count: c_int) callconv(.C) void, 1140 + removeAllSprites: *const fn () callconv(.C) void, 1141 + getSpriteCount: *const fn () callconv(.C) c_int, 1142 + 1143 + setBounds: *const fn (sprite: ?*LCDSprite, bounds: PDRect) callconv(.C) void, 1144 + getBounds: *const fn (sprite: ?*LCDSprite) callconv(.C) PDRect, 1145 + moveTo: *const fn (sprite: ?*LCDSprite, x: f32, y: f32) callconv(.C) void, 1146 + moveBy: *const fn (sprite: ?*LCDSprite, dx: f32, dy: f32) callconv(.C) void, 1147 + 1148 + setImage: *const fn (sprite: ?*LCDSprite, image: ?*LCDBitmap, flip: LCDBitmapFlip) callconv(.C) void, 1149 + getImage: *const fn (sprite: ?*LCDSprite) callconv(.C) ?*LCDBitmap, 1150 + setSize: *const fn (s: ?*LCDSprite, width: f32, height: f32) callconv(.C) void, 1151 + setZIndex: *const fn (s: ?*LCDSprite, zIndex: i16) callconv(.C) void, 1152 + getZIndex: *const fn (sprite: ?*LCDSprite) callconv(.C) i16, 1153 + 1154 + setDrawMode: *const fn (sprite: ?*LCDSprite, mode: LCDBitmapDrawMode) callconv(.C) LCDBitmapDrawMode, 1155 + setImageFlip: *const fn (sprite: ?*LCDSprite, flip: LCDBitmapFlip) callconv(.C) void, 1156 + getImageFlip: *const fn (sprite: ?*LCDSprite) callconv(.C) LCDBitmapFlip, 1157 + setStencil: *const fn (sprite: ?*LCDSprite, mode: ?*LCDBitmap) callconv(.C) void, // deprecated in favor of setStencilImage() 1158 + 1159 + setClipRect: *const fn (sprite: ?*LCDSprite, clipRect: LCDRect) callconv(.C) void, 1160 + clearClipRect: *const fn (sprite: ?*LCDSprite) callconv(.C) void, 1161 + setClipRectsInRange: *const fn (clipRect: LCDRect, startZ: c_int, endZ: c_int) callconv(.C) void, 1162 + clearClipRectsInRange: *const fn (startZ: c_int, endZ: c_int) callconv(.C) void, 1163 + 1164 + setUpdatesEnabled: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void, 1165 + updatesEnabled: *const fn (sprite: ?*LCDSprite) callconv(.C) c_int, 1166 + setCollisionsEnabled: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void, 1167 + collisionsEnabled: *const fn (sprite: ?*LCDSprite) callconv(.C) c_int, 1168 + setVisible: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void, 1169 + isVisible: *const fn (sprite: ?*LCDSprite) callconv(.C) c_int, 1170 + setOpaque: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void, 1171 + markDirty: *const fn (sprite: ?*LCDSprite) callconv(.C) void, 1172 + 1173 + setTag: *const fn (sprite: ?*LCDSprite, tag: u8) callconv(.C) void, 1174 + getTag: *const fn (sprite: ?*LCDSprite) callconv(.C) u8, 1175 + 1176 + setIgnoresDrawOffset: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void, 1177 + 1178 + setUpdateFunction: *const fn (sprite: ?*LCDSprite, func: LCDSpriteUpdateFunction) callconv(.C) void, 1179 + setDrawFunction: *const fn (sprite: ?*LCDSprite, func: LCDSpriteDrawFunction) callconv(.C) void, 1180 + 1181 + getPosition: *const fn (s: ?*LCDSprite, x: ?*f32, y: ?*f32) callconv(.C) void, 1182 + 1183 + // Collisions 1184 + resetCollisionWorld: *const fn () callconv(.C) void, 1185 + 1186 + setCollideRect: *const fn (sprite: ?*LCDSprite, collideRect: PDRect) callconv(.C) void, 1187 + getCollideRect: *const fn (sprite: ?*LCDSprite) callconv(.C) PDRect, 1188 + clearCollideRect: *const fn (sprite: ?*LCDSprite) callconv(.C) void, 1189 + 1190 + // caller is responsible for freeing the returned array for all collision methods 1191 + setCollisionResponseFunction: *const fn (sprite: ?*LCDSprite, func: LCDSpriteCollisionFilterProc) callconv(.C) void, 1192 + checkCollisions: *const fn (sprite: ?*LCDSprite, goalX: f32, goalY: f32, actualX: ?*f32, actualY: ?*f32, len: ?*c_int) callconv(.C) [*c]SpriteCollisionInfo, // access results using const info = &results[i]; 1193 + moveWithCollisions: *const fn (sprite: ?*LCDSprite, goalX: f32, goalY: f32, actualX: ?*f32, actualY: ?*f32, len: ?*c_int) callconv(.C) [*c]SpriteCollisionInfo, 1194 + querySpritesAtPoint: *const fn (x: f32, y: f32, len: ?*c_int) callconv(.C) [*c]?*LCDSprite, 1195 + querySpritesInRect: *const fn (x: f32, y: f32, width: f32, height: f32, len: ?*c_int) callconv(.C) [*c]?*LCDSprite, 1196 + querySpritesAlongLine: *const fn (x1: f32, y1: f32, x2: f32, y2: f32, len: ?*c_int) callconv(.C) [*c]?*LCDSprite, 1197 + querySpriteInfoAlongLine: *const fn (x1: f32, y1: f32, x2: f32, y2: f32, len: ?*c_int) callconv(.C) [*c]SpriteQueryInfo, // access results using const info = &results[i]; 1198 + overlappingSprites: *const fn (sprite: ?*LCDSprite, len: ?*c_int) callconv(.C) [*c]?*LCDSprite, 1199 + allOverlappingSprites: *const fn (len: ?*c_int) callconv(.C) [*c]?*LCDSprite, 1200 + 1201 + // added in 1.7 1202 + setStencilPattern: *const fn (sprite: ?*LCDSprite, pattern: [*c]u8) callconv(.C) void, //pattern is 8 bytes 1203 + clearStencil: *const fn (sprite: ?*LCDSprite) callconv(.C) void, 1204 + 1205 + setUserdata: *const fn (sprite: ?*LCDSprite, userdata: ?*anyopaque) callconv(.C) void, 1206 + getUserdata: *const fn (sprite: ?*LCDSprite) callconv(.C) ?*anyopaque, 1207 + 1208 + // added in 1.10 1209 + setStencilImage: *const fn (sprite: ?*LCDSprite, stencil: ?*LCDBitmap, tile: c_int) callconv(.C) void, 1210 + 1211 + // 2.1 1212 + setCenter: *const fn (s: ?*LCDSprite, x: f32, y: f32) callconv(.C) void, 1213 + getCenter: *const fn (s: ?*LCDSprite, x: ?*f32, y: ?*f32) callconv(.C) void, 1214 + 1215 + // 2.7 1216 + setTilemap: *const fn (s: ?*LCDSprite, tilemap: ?*LCDTileMap) callconv(.C) void, 1217 + getTilemap: *const fn (s: ?*LCDSprite) callconv(.C) ?*LCDTileMap, 1218 + }; 1219 + 1220 + ////////Lua/////// 1221 + pub const LuaState = ?*anyopaque; 1222 + pub const LuaCFunction = ?*const fn (state: ?*LuaState) callconv(.C) c_int; 1223 + pub const LuaUDObject = opaque {}; 1224 + 1225 + //literal value 1226 + pub const LValType = enum(c_int) { 1227 + Int = 0, 1228 + Float = 1, 1229 + Str = 2, 1230 + }; 1231 + pub const LuaReg = extern struct { 1232 + name: ?[*:0]const u8, 1233 + func: LuaCFunction, 1234 + }; 1235 + pub const LuaType = enum(c_int) { 1236 + TypeNil = 0, 1237 + TypeBool = 1, 1238 + TypeInt = 2, 1239 + TypeFloat = 3, 1240 + TypeString = 4, 1241 + TypeTable = 5, 1242 + TypeFunction = 6, 1243 + TypeThread = 7, 1244 + TypeObject = 8, 1245 + }; 1246 + pub const LuaVal = extern struct { 1247 + name: ?[*:0]const u8, 1248 + type: LValType, 1249 + v: extern union { 1250 + intval: c_uint, 1251 + floatval: f32, 1252 + strval: ?[*:0]const u8, 1253 + }, 1254 + }; 1255 + pub const PlaydateLua = extern struct { 1256 + // these two return 1 on success, else 0 with an error message in outErr 1257 + addFunction: *const fn (f: LuaCFunction, name: ?[*:0]const u8, outErr: ?*?[*:0]const u8) callconv(.C) c_int, 1258 + registerClass: *const fn (name: ?[*:0]const u8, reg: ?*const LuaReg, vals: [*c]const LuaVal, isstatic: c_int, outErr: ?*?[*:0]const u8) callconv(.C) c_int, 1259 + 1260 + pushFunction: *const fn (f: LuaCFunction) callconv(.C) void, 1261 + indexMetatable: *const fn () callconv(.C) c_int, 1262 + 1263 + stop: *const fn () callconv(.C) void, 1264 + start: *const fn () callconv(.C) void, 1265 + 1266 + // stack operations 1267 + getArgCount: *const fn () callconv(.C) c_int, 1268 + getArgType: *const fn (pos: c_int, outClass: ?*?[*:0]const u8) callconv(.C) LuaType, 1269 + 1270 + argIsNil: *const fn (pos: c_int) callconv(.C) c_int, 1271 + getArgBool: *const fn (pos: c_int) callconv(.C) c_int, 1272 + getArgInt: *const fn (pos: c_int) callconv(.C) c_int, 1273 + getArgFloat: *const fn (pos: c_int) callconv(.C) f32, 1274 + getArgString: *const fn (pos: c_int) callconv(.C) ?[*:0]const u8, 1275 + getArgBytes: *const fn (pos: c_int, outlen: ?*usize) callconv(.C) [*c]const u8, 1276 + getArgObject: *const fn (pos: c_int, type: ?*i8, ?*?*LuaUDObject) callconv(.C) ?*anyopaque, 1277 + 1278 + getBitmap: *const fn (c_int) callconv(.C) ?*LCDBitmap, 1279 + getSprite: *const fn (c_int) callconv(.C) ?*LCDSprite, 1280 + 1281 + // for returning values back to Lua 1282 + pushNil: *const fn () callconv(.C) void, 1283 + pushBool: *const fn (val: c_int) callconv(.C) void, 1284 + pushInt: *const fn (val: c_int) callconv(.C) void, 1285 + pushFloat: *const fn (val: f32) callconv(.C) void, 1286 + pushString: *const fn (str: ?[*:0]const u8) callconv(.C) void, 1287 + pushBytes: *const fn (str: [*c]const u8, len: usize) callconv(.C) void, 1288 + pushBitmap: *const fn (bitmap: ?*LCDBitmap) callconv(.C) void, 1289 + pushSprite: *const fn (sprite: ?*LCDSprite) callconv(.C) void, 1290 + 1291 + pushObject: *const fn (obj: ?*anyopaque, type: ?*i8, nValues: c_int) callconv(.C) ?*LuaUDObject, 1292 + retainObject: *const fn (obj: ?*LuaUDObject) callconv(.C) ?*LuaUDObject, 1293 + releaseObject: *const fn (obj: ?*LuaUDObject) callconv(.C) void, 1294 + 1295 + setObjectValue: *const fn (obj: ?*LuaUDObject, slot: c_int) callconv(.C) void, 1296 + getObjectValue: *const fn (obj: ?*LuaUDObject, slot: c_int) callconv(.C) c_int, 1297 + 1298 + // calling lua from C has some overhead. use sparingly! 1299 + callFunction_deprecated: *const fn (name: ?[*:0]const u8, nargs: c_int) callconv(.C) void, 1300 + callFunction: *const fn (name: ?[*:0]const u8, nargs: c_int, outerr: ?*?[*:0]const u8) callconv(.C) c_int, 1301 + }; 1302 + 1303 + ///////JSON/////// 1304 + pub const JSONValueType = enum(c_int) { 1305 + JSONNull = 0, 1306 + JSONTrue = 1, 1307 + JSONFalse = 2, 1308 + JSONInteger = 3, 1309 + JSONFloat = 4, 1310 + JSONString = 5, 1311 + JSONArray = 6, 1312 + JSONTable = 7, 1313 + }; 1314 + pub const JSONValue = extern struct { 1315 + type: u8, 1316 + data: extern union { 1317 + intval: c_int, 1318 + floatval: f32, 1319 + stringval: [*c]u8, 1320 + arrayval: ?*anyopaque, 1321 + tableval: ?*anyopaque, 1322 + }, 1323 + }; 1324 + pub inline fn json_intValue(value: JSONValue) c_int { 1325 + switch (@intFromEnum(value.type)) { 1326 + .JSONInteger => return value.data.intval, 1327 + .JSONFloat => return @intFromFloat(value.data.floatval), 1328 + .JSONString => return std.fmt.parseInt(c_int, std.mem.span(value.data.stringval), 10) catch 0, 1329 + .JSONTrue => return 1, 1330 + else => return 0, 1331 + } 1332 + } 1333 + pub inline fn json_floatValue(value: JSONValue) f32 { 1334 + switch (@as(JSONValueType, @enumFromInt(value.type))) { 1335 + .JSONInteger => return @floatFromInt(value.data.intval), 1336 + .JSONFloat => return value.data.floatval, 1337 + .JSONString => return 0, 1338 + .JSONTrue => 1.0, 1339 + else => return 0.0, 1340 + } 1341 + } 1342 + pub inline fn json_boolValue(value: JSONValue) c_int { 1343 + return if (@as(JSONValueType, @enumFromInt(value.type)) == .JSONString) 1344 + @intFromBool(value.data.stringval[0] != 0) 1345 + else 1346 + json_intValue(value); 1347 + } 1348 + pub inline fn json_stringValue(value: JSONValue) [*c]u8 { 1349 + return if (@as(JSONValueType, @enumFromInt(value.type)) == .JSONString) 1350 + value.data.stringval 1351 + else 1352 + null; 1353 + } 1354 + 1355 + // decoder 1356 + 1357 + pub const JSONDecoder = extern struct { 1358 + decodeError: *const fn (decoder: ?*JSONDecoder, @"error": ?[*:0]const u8, linenum: c_int) callconv(.C) void, 1359 + 1360 + // the following functions are each optional 1361 + willDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) void, 1362 + shouldDecodeTableValueForKey: ?*const fn (decoder: ?*JSONDecoder, key: ?[*:0]const u8) callconv(.C) c_int, 1363 + didDecodeTableValue: ?*const fn (decoder: ?*JSONDecoder, key: ?[*:0]const u8, value: JSONValue) callconv(.C) void, 1364 + shouldDecodeArrayValueAtIndex: ?*const fn (decoder: ?*JSONDecoder, pos: c_int) callconv(.C) c_int, 1365 + didDecodeArrayValue: ?*const fn (decoder: ?*JSONDecoder, pos: c_int, value: JSONValue) callconv(.C) void, 1366 + didDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) ?*anyopaque, 1367 + 1368 + userdata: ?*anyopaque, 1369 + returnString: c_int, // when set, the decoder skips parsing and returns the current subtree as a string 1370 + path: ?[*:0]const u8, // updated during parsing, reflects current position in tree 1371 + }; 1372 + 1373 + // convenience functions for setting up a table-only or array-only decoder 1374 + 1375 + pub inline fn json_setTableDecode( 1376 + decoder: ?*JSONDecoder, 1377 + willDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) void, 1378 + didDecodeTableValue: ?*const fn (decoder: ?*JSONDecoder, key: ?[*:0]const u8, value: JSONValue) callconv(.C) void, 1379 + didDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, name: JSONValueType) callconv(.C) ?*anyopaque, 1380 + ) void { 1381 + decoder.?.didDecodeTableValue = didDecodeTableValue; 1382 + decoder.?.didDecodeArrayValue = null; 1383 + decoder.?.willDecodeSublist = willDecodeSublist; 1384 + decoder.?.didDecodeSublist = didDecodeSublist; 1385 + } 1386 + 1387 + pub inline fn json_setArrayDecode( 1388 + decoder: ?*JSONDecoder, 1389 + willDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) void, 1390 + didDecodeArrayValue: ?*const fn (decoder: ?*JSONDecoder, pos: c_int, value: JSONValue) callconv(.C) void, 1391 + didDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) ?*anyopaque, 1392 + ) void { 1393 + decoder.?.didDecodeTableValue = null; 1394 + decoder.?.didDecodeArrayValue = didDecodeArrayValue; 1395 + decoder.?.willDecodeSublist = willDecodeSublist; 1396 + decoder.?.didDecodeSublist = didDecodeSublist; 1397 + } 1398 + 1399 + pub const JSONReader = extern struct { 1400 + read: *const fn (userdata: ?*anyopaque, buf: [*c]u8, bufsize: c_int) callconv(.C) c_int, 1401 + userdata: ?*anyopaque, 1402 + }; 1403 + pub const writeFunc = *const fn (userdata: ?*anyopaque, str: [*c]const u8, len: c_int) callconv(.C) void; 1404 + 1405 + pub const JSONEncoder = extern struct { 1406 + writeStringFunc: writeFunc, 1407 + userdata: ?*anyopaque, 1408 + 1409 + state: u32, //this is pretty, startedTable, startedArray and depth bitfields combined 1410 + 1411 + startArray: *const fn (encoder: ?*JSONEncoder) callconv(.C) void, 1412 + addArrayMember: *const fn (encoder: ?*JSONEncoder) callconv(.C) void, 1413 + endArray: *const fn (encoder: ?*JSONEncoder) callconv(.C) void, 1414 + startTable: *const fn (encoder: ?*JSONEncoder) callconv(.C) void, 1415 + addTableMember: *const fn (encoder: ?*JSONEncoder, name: [*c]const u8, len: c_int) callconv(.C) void, 1416 + endTable: *const fn (encoder: ?*JSONEncoder) callconv(.C) void, 1417 + writeNull: *const fn (encoder: ?*JSONEncoder) callconv(.C) void, 1418 + writeFalse: *const fn (encoder: ?*JSONEncoder) callconv(.C) void, 1419 + writeTrue: *const fn (encoder: ?*JSONEncoder) callconv(.C) void, 1420 + writeInt: *const fn (encoder: ?*JSONEncoder, num: c_int) callconv(.C) void, 1421 + writeDouble: *const fn (encoder: ?*JSONEncoder, num: f64) callconv(.C) void, 1422 + writeString: *const fn (encoder: ?*JSONEncoder, str: [*c]const u8, len: c_int) callconv(.C) void, 1423 + }; 1424 + 1425 + pub const PlaydateJSON = extern struct { 1426 + initEncoder: *const fn (encoder: ?*JSONEncoder, write: writeFunc, userdata: ?*anyopaque, pretty: c_int) callconv(.C) void, 1427 + 1428 + decode: *const fn (functions: ?*JSONDecoder, reader: JSONReader, outval: ?*JSONValue) callconv(.C) c_int, 1429 + decodeString: *const fn (functions: ?*JSONDecoder, jsonString: ?[*:0]const u8, outval: ?*JSONValue) callconv(.C) c_int, 1430 + }; 1431 + 1432 + ///////Scoreboards/////////// 1433 + pub const PDScore = extern struct { 1434 + rank: u32, 1435 + value: u32, 1436 + player: [*c]u8, 1437 + }; 1438 + pub const PDScoresList = extern struct { 1439 + boardID: [*c]u8, 1440 + count: c_uint, 1441 + lastUpdated: u32, 1442 + playerIncluded: c_int, 1443 + limit: c_uint, 1444 + scores: [*c]PDScore, 1445 + }; 1446 + pub const PDBoard = extern struct { 1447 + boardID: [*c]u8, 1448 + name: [*c]u8, 1449 + }; 1450 + pub const PDBoardsList = extern struct { 1451 + count: c_uint, 1452 + lastUpdated: u32, 1453 + boards: [*c]PDBoard, 1454 + }; 1455 + pub const AddScoreCallback = ?*const fn (score: ?*PDScore, errorMessage: ?[*:0]const u8) callconv(.C) void; 1456 + pub const PersonalBestCallback = ?*const fn (score: ?*PDScore, errorMessage: ?[*:0]const u8) callconv(.C) void; 1457 + pub const BoardsListCallback = ?*const fn (boards: ?*PDBoardsList, errorMessage: ?[*:0]const u8) callconv(.C) void; 1458 + pub const ScoresCallback = ?*const fn (scores: ?*PDScoresList, errorMessage: ?[*:0]const u8) callconv(.C) void; 1459 + 1460 + pub const PlaydateScoreboards = extern struct { 1461 + addScore: *const fn (boardId: ?[*:0]const u8, value: u32, callback: AddScoreCallback) callconv(.C) c_int, 1462 + getPersonalBest: *const fn (boardId: ?[*:0]const u8, callback: PersonalBestCallback) callconv(.C) c_int, 1463 + freeScore: *const fn (score: ?*PDScore) callconv(.C) void, 1464 + 1465 + getScoreboards: *const fn (callback: BoardsListCallback) callconv(.C) c_int, 1466 + freeBoardsList: *const fn (boards: ?*PDBoardsList) callconv(.C) void, 1467 + 1468 + getScores: *const fn (boardId: ?[*:0]const u8, callback: ScoresCallback) callconv(.C) c_int, 1469 + freeScoresList: *const fn (scores: ?*PDScoresList) callconv(.C) void, 1470 + }; 1471 + 1472 + ///////Network/////////// 1473 + pub const HTTPConnection = opaque {}; 1474 + pub const TCPConnection = opaque {}; 1475 + 1476 + pub const PDNetErr = enum(c_int) { 1477 + NET_OK = 0, 1478 + NET_NO_DEVICE = -1, 1479 + NET_BUSY = -2, 1480 + NET_WRITE_ERROR = -3, 1481 + NET_WRITE_BUSY = -4, 1482 + NET_WRITE_TIMEOUT = -5, 1483 + NET_READ_ERROR = -6, 1484 + NET_READ_BUSY = -7, 1485 + NET_READ_TIMEOUT = -8, 1486 + NET_READ_OVERFLOW = -9, 1487 + NET_FRAME_ERROR = -10, 1488 + NET_BAD_RESPONSE = -11, 1489 + NET_ERROR_RESPONSE = -12, 1490 + NET_RESET_TIMEOUT = -13, 1491 + NET_BUFFER_TOO_SMALL = -14, 1492 + NET_UNEXPECTED_RESPONSE = -15, 1493 + NET_NOT_CONNECTED_TO_AP = -16, 1494 + NET_NOT_IMPLEMENTED = -17, 1495 + NET_CONNECTION_CLOSED = -18, 1496 + }; 1497 + 1498 + pub const WifiStatus = enum(c_int) { 1499 + WifiNotConnected = 0, //< Not connected to an AP 1500 + WifiConnected, //< Device is connected to an AP 1501 + WifiNotAvailable, //< A connection has been attempted and no configured AP was available 1502 + }; 1503 + 1504 + pub const HTTPConnectionCallback = ?*const fn (connection: ?*HTTPConnection) callconv(.C) void; 1505 + pub const HTTPHeaderCallback = ?*const fn (conn: ?*HTTPConnection, key: ?[*:0]const u8, value: ?[*:0]const u8) callconv(.C) void; 1506 + 1507 + pub const PlaydateHTTP = extern struct { 1508 + requestAccess: *const fn (server: ?[*:0]const u8, port: c_int, usessl: bool, purpose: ?[*:0]const u8, requestCallback: AccessRequestCallback, userdata: ?*anyopaque) callconv(.C) AccessReply, 1509 + 1510 + newConnection: *const fn (server: ?[*:0]const u8, port: c_int, usessl: bool) callconv(.C) ?*HTTPConnection, 1511 + retain: *const fn (http: ?*HTTPConnection) callconv(.C) ?*HTTPConnection, 1512 + release: *const fn (http: ?*HTTPConnection) callconv(.C) void, 1513 + 1514 + setConnectTimeout: *const fn (connection: ?*HTTPConnection, ms: c_int) callconv(.C) void, 1515 + setKeepAlive: *const fn (connection: ?*HTTPConnection, keepalive: bool) callconv(.C) void, 1516 + setByteRange: *const fn (connection: ?*HTTPConnection, start: c_int, end: c_int) callconv(.C) void, 1517 + setUserdata: *const fn (connection: ?*HTTPConnection, userdata: ?*anyopaque) callconv(.C) void, 1518 + getUserdata: *const fn (connection: ?*HTTPConnection) callconv(.C) ?*anyopaque, 1519 + 1520 + get: *const fn (connection: ?*HTTPConnection, path: ?[*:0]const u8, headers: ?[*:0]const u8, headerlen: usize) callconv(.C) PDNetErr, 1521 + post: *const fn (connection: ?*HTTPConnection, path: ?[*:0]const u8, headers: ?[*:0]const u8, headerlen: usize, body: ?[*:0]const u8, bodylen: usize) callconv(.C) PDNetErr, 1522 + query: *const fn (connection: ?*HTTPConnection, method: ?[*:0]const u8, path: ?[*:0]const u8, headers: ?[*:0]const u8, headerlen: usize, body: ?[*:0]const u8, bodylen: usize) callconv(.C) PDNetErr, 1523 + getError: *const fn (connection: ?*HTTPConnection) callconv(.C) PDNetErr, 1524 + getProgress: *const fn (connection: ?*HTTPConnection, read: ?*c_int, total: ?*c_int) callconv(.C) void, 1525 + getResponseStatus: *const fn (connection: ?*HTTPConnection) callconv(.C) c_int, 1526 + getBytesAvailable: *const fn (connection: ?*HTTPConnection) callconv(.C) usize, 1527 + setReadTimeout: *const fn (connection: ?*HTTPConnection, ms: c_int) callconv(.C) void, 1528 + setReadBufferSize: *const fn (connection: ?*HTTPConnection, bytes: c_int) callconv(.C) void, 1529 + read: *const fn (connection: ?*HTTPConnection, buf: [*c]u8, buflen: c_uint) callconv(.C) c_int, 1530 + close: *const fn (connection: ?*HTTPConnection) callconv(.C) void, 1531 + 1532 + setHeaderReceivedCallback: *const fn (connection: ?*HTTPConnection, headercb: HTTPHeaderCallback) callconv(.C) void, 1533 + setHeadersReadCallback: *const fn (connection: ?*HTTPConnection, callback: HTTPConnectionCallback) callconv(.C) void, 1534 + setResponseCallback: *const fn (connection: ?*HTTPConnection, callback: HTTPConnectionCallback) callconv(.C) void, 1535 + setRequestCompleteCallback: *const fn (connection: ?*HTTPConnection, callback: HTTPConnectionCallback) callconv(.C) void, 1536 + setConnectionClosedCallback: *const fn (connection: ?*HTTPConnection, callback: HTTPConnectionCallback) callconv(.C) void, 1537 + }; 1538 + 1539 + pub const TCPConnectionCallback = ?*const fn (connection: ?*TCPConnection, err: PDNetErr) callconv(.C) void; 1540 + pub const TCPOpenCallback = ?*const fn (connection: ?*TCPConnection, err: PDNetErr, ud: ?*anyopaque) callconv(.C) void; 1541 + 1542 + pub const PlaydateTCP = extern struct { 1543 + requestAccess: *const fn (server: ?[*:0]const u8, port: c_int, usessl: bool, purpose: ?[*:0]const u8, requestCallback: AccessRequestCallback, userdata: ?*anyopaque) callconv(.C) AccessReply, 1544 + 1545 + newConnection: *const fn (server: ?[*:0]const u8, port: c_int, usessl: bool) callconv(.C) ?*TCPConnection, 1546 + retain: *const fn (tcp: ?*TCPConnection) callconv(.C) ?*TCPConnection, 1547 + release: *const fn (tcp: ?*TCPConnection) callconv(.C) void, 1548 + getError: *const fn (connection: ?*TCPConnection) callconv(.C) PDNetErr, 1549 + 1550 + setConnectTimeout: *const fn (connection: ?*TCPConnection, ms: c_int) callconv(.C) void, 1551 + setUserdata: *const fn (connection: ?*TCPConnection, userdata: ?*anyopaque) callconv(.C) void, 1552 + getUserdata: *const fn (connection: ?*TCPConnection) callconv(.C) ?*anyopaque, 1553 + 1554 + open: *const fn (connection: ?*TCPConnection, cb: TCPOpenCallback, ud: ?*anyopaque) callconv(.C) PDNetErr, 1555 + close: *const fn (connection: ?*TCPConnection) callconv(.C) PDNetErr, 1556 + 1557 + setConnectionClosedCallback: *const fn (connection: ?*TCPConnection, callback: TCPConnectionCallback) callconv(.C) void, 1558 + 1559 + setReadTimeout: *const fn (connection: ?*TCPConnection, ms: c_int) callconv(.C) void, 1560 + setReadBufferSize: *const fn (connection: ?*TCPConnection, bytes: c_int) callconv(.C) void, 1561 + getBytesAvailable: *const fn (connection: ?*TCPConnection) callconv(.C) usize, 1562 + 1563 + read: *const fn (connection: ?*TCPConnection, buffer: [*c]u8, length: usize) callconv(.C) c_int, // returns # of bytes read, or PDNetErr on error 1564 + write: *const fn (connection: ?*TCPConnection, buffer: [*c]const u8, length: usize) callconv(.C) c_int, // returns # of bytes read, or PDNetErr on error 1565 + }; 1566 + 1567 + pub const PlaydateNetwork = extern struct { 1568 + playdate_http: *const PlaydateHTTP, 1569 + playdate_tcp: *const PlaydateTCP, 1570 + 1571 + getStatus: *const fn () callconv(.C) WifiStatus, 1572 + setEnabled: *const fn (flag: bool, callback: ?*const fn (err: PDNetErr) callconv(.C) void) callconv(.C) void, 1573 + 1574 + reserved: [3]usize, 1575 + };