Browse Source

First commit

master
Kenneth Bruen 9 months ago
commit
e0a61196ce
Signed by: kbruen
GPG Key ID: C1980A470C3EE5B1
  1. 18
      .gitignore
  2. 90
      build.zig
  3. 73
      src/curl.zig
  4. 22
      src/departure.zig
  5. 192
      src/home.zig
  6. 46
      src/main.zig
  7. 52
      src/raylib.zig
  8. 36
      src/state.zig

18
.gitignore vendored

@ -0,0 +1,18 @@
# This file is for zig-specific build artifacts.
# If you have OS-specific or editor-specific files to ignore,
# such as *.swp or .DS_Store, put those in your global
# ~/.gitignore and put this in your ~/.gitconfig:
#
# [core]
# excludesfile = ~/.gitignore
#
# Cheers!
# -andrewrk
zig-cache/
zig-out/
/release/
/debug/
/build/
/build-*/
/docgen_tmp/

90
build.zig

@ -0,0 +1,90 @@
const std = @import("std");
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "raylib-test",
// In this case the main source file is merely a path, however, in more
// complicated build scripts, this could be a generated file.
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// Link Raylib
exe.addLibraryPath(.{
.cwd_relative = "/opt/homebrew/Cellar/raylib/5.0/lib",
});
exe.linkSystemLibrary("raylib");
exe.linkSystemLibrary("curl");
// exe.addObjectFile(.{
// .cwd_relative = "/opt/homebrew/Cellar/raylib/5.0/lib/libraylib.a",
// });
exe.addIncludePath(.{
.cwd_relative = "/opt/homebrew/Cellar/raylib/5.0/include",
});
// Raylib dependencies
exe.linkFramework("Foundation");
exe.linkFramework("CoreVideo");
exe.linkFramework("IOKit");
exe.linkFramework("Cocoa");
exe.linkFramework("GLUT");
exe.linkFramework("OpenGL");
// This declares intent for the executable to be installed into the
// standard location when the user invokes the "install" step (the default
// step when running `zig build`).
b.installArtifact(exe);
// This *creates* a Run step in the build graph, to be executed when another
// step is evaluated that depends on it. The next line below will establish
// such a dependency.
const run_cmd = b.addRunArtifact(exe);
// By making the run step depend on the install step, it will be run from the
// installation directory rather than directly from within the cache directory.
// This is not necessary, however, if the application depends on other installed
// files, this ensures they will be present and in the expected location.
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
// This creates a build step. It will be visible in the `zig build --help` menu,
// and can be selected like this: `zig build run`
// This will evaluate the `run` step rather than the default, which is "install".
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
// Similar to creating the run step earlier, this exposes a `test` step to
// the `zig build --help` menu, providing a way for the user to request
// running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}

73
src/curl.zig

@ -0,0 +1,73 @@
pub const c_api = @cImport({
@cInclude("curl/curl.h");
});
pub const Option = enum(c_api.CURLoption) {
write_data = c_api.CURLOPT_WRITEDATA,
url = c_api.CURLOPT_URL,
port = c_api.CURLOPT_PORT,
proxy = c_api.CURLOPT_PROXY,
userpwd = c_api.CURLOPT_USERPWD,
proxy_userpwd = c_api.CURLOPT_PROXYUSERPWD,
range = c_api.CURLOPT_RANGE,
read_data = c_api.CURLOPT_READDATA,
error_buffer = c_api.CURLOPT_ERRORBUFFER,
write_function = c_api.CURLOPT_WRITEFUNCTION,
read_function = c_api.CURLOPT_READFUNCTION,
timeout = c_api.CURLOPT_TIMEOUT,
in_file_size = c_api.CURLOPT_INFILESIZE,
post_fields = c_api.CURLOPT_POSTFIELDS,
referer = c_api.CURLOPT_REFERER,
ftp_port = c_api.CURLOPT_FTPPORT,
user_agent = c_api.CURLOPT_USERAGENT,
low_speed_limit = c_api.CURLOPT_LOW_SPEED_LIMIT,
low_speed_time = c_api.CURLOPT_LOW_SPEED_TIME,
resume_from = c_api.CURLOPT_RESUME_FROM,
cookie = c_api.CURLOPT_COOKIE,
http_header = c_api.CURLOPT_HTTPHEADER,
// ...
};
handle: *c_api.CURL,
pub fn init() ?@This() {
if (c_api.curl_easy_init()) |handle| {
return .{
.handle = handle,
};
} else {
return null;
}
}
pub fn deinit(self: *@This()) void {
c_api.curl_easy_cleanup(self.handle);
}
pub fn reset(self: *@This()) void {
c_api.curl_easy_reset(self.handle);
}
pub fn perform(self: *@This()) c_api.CURLcode {
return c_api.curl_easy_perform(self.handle);
}
pub fn setopt_raw(
self: *@This(),
option: c_api.CURLoption,
args: anytype,
) c_api.CURLcode {
return @call(
.auto,
c_api.curl_easy_setopt,
.{ self.handle, option } ++ args,
);
}
pub fn setopt(
self: *@This(),
option: Option,
args: anytype,
) c_api.CURLcode {
return self.setopt_raw(@intFromEnum(option), args);
}

22
src/departure.zig

@ -0,0 +1,22 @@
const raylib = @import("raylib.zig");
const rl = raylib.rl;
const stateMod = @import("state.zig");
pub fn render(state: *stateMod.AppState) !void {
while (raylib.GetKeyPressed()) |key| {
switch (key) {
rl.KEY_LEFT => {
state.screen = .home;
},
else => {},
}
}
rl.BeginDrawing();
defer rl.EndDrawing();
rl.ClearBackground(raylib.ColorInt(0x18226f));
rl.DrawText(state.departure_screen_state.station_id.items.ptr, 16, 16, 32, rl.WHITE);
state.close_app = rl.WindowShouldClose();
}

192
src/home.zig

@ -0,0 +1,192 @@
const std = @import("std");
const raylib = @import("raylib.zig");
const rl = raylib.rl;
const state_mod = @import("state.zig");
const curl_mod = @import("curl.zig");
fn curlWriteHandler(ptr: [*]u8, size: usize, nmemb: usize, userdata: *std.ArrayList(u8)) callconv(.C) usize {
_ = size;
userdata.appendSlice(ptr[0..nmemb]) catch return 0;
return nmemb;
}
fn fetchThread(state: *state_mod.AppState) !void {
std.debug.print("Started fetchThread\n", .{});
defer std.debug.print("Ended fetchThread\n", .{});
defer state.home_screen_state.fetch_thread = null;
const allocator = state.allocator;
var station_name_buf = std.BoundedArray(u8, 200){};
var curl = curl_mod.init() orelse return;
defer curl.deinit();
const locations_base = "https://v6.db.transport.rest/locations";
var locations_uri = std.Uri.parse(locations_base) catch unreachable;
while (state.home_screen_state.fetch_thread != null) {
if (std.mem.eql(u8, station_name_buf.slice(), state.home_screen_state.station_name.items)) {
std.time.sleep(100 * 1000);
continue;
}
station_name_buf.resize(state.home_screen_state.station_name.items.len) catch continue;
std.mem.copyForwards(u8, station_name_buf.slice(), state.home_screen_state.station_name.items);
std.debug.print("[fetchThread] Detected update: {s}\n", .{station_name_buf.slice()});
curl.reset();
const query = try std.fmt.allocPrint(allocator, "query={s}&results=10&addresses=false&poi=false&pretty=false", .{station_name_buf.slice()});
defer allocator.free(query);
locations_uri.query = query;
defer locations_uri.query = null;
std.debug.print("[fetchThread] Making request to: {}\n", .{locations_uri});
const url = try std.fmt.allocPrintZ(allocator, "{}", .{locations_uri});
defer allocator.free(url);
_ = curl.setopt(.url, .{url.ptr});
var result = std.ArrayList(u8).init(allocator);
defer result.deinit();
_ = curl.setopt(.write_function, .{curlWriteHandler});
_ = curl.setopt(.write_data, .{&result});
const code = curl.perform();
std.debug.print("[fetchThread] cURL Code: {}\n", .{code});
if (code != 0) continue;
std.debug.print("[fetchThread] Fetched data: <redacted>(len: {})\n", .{result.items.len});
const parsed = std.json.parseFromSlice([]const std.json.Value, allocator, result.items, .{}) catch |err| {
std.debug.print("[fetchThread] JSON parse error: {}\n", .{err});
continue;
};
defer parsed.deinit();
var results = std.ArrayList(state_mod.HSSuggestion).init(allocator);
for (parsed.value) |station| {
if (station.object.get("name")) |nameValue| {
const name = nameValue.string;
if (station.object.get("id")) |idValue| {
const id = idValue.string;
results.append(.{
.id = std.fmt.allocPrintZ(allocator, "{s}", .{id}) catch continue,
.name = std.fmt.allocPrintZ(allocator, "{s}", .{name}) catch continue,
}) catch continue;
}
}
}
if (state.home_screen_state.suggestions.len > 0) {
for (state.home_screen_state.suggestions) |suggestion| {
allocator.free(suggestion.id);
allocator.free(suggestion.name);
}
allocator.free(state.home_screen_state.suggestions);
}
state.home_screen_state.suggestions = results.toOwnedSlice() catch continue;
}
}
pub fn render(state: *state_mod.AppState) !void {
var hs = &state.home_screen_state;
if (hs.fetch_thread == null) {
hs.fetch_thread = std.Thread.spawn(.{}, fetchThread, .{state}) catch null;
}
if (hs.suggestions.len > 0 and hs.selection_idx > hs.suggestions.len - 1) {
hs.selection_idx = @intCast(hs.suggestions.len - 1);
}
while (raylib.GetCharPressed()) |char| {
hs.station_name.appendAssumeCapacity(@intCast(char));
}
while (raylib.GetKeyPressed()) |key| {
switch (key) {
rl.KEY_BACKSPACE => {
if (hs.station_name.items.len > 0) {
hs.station_name.items[hs.station_name.items.len - 1] = 0;
_ = hs.station_name.pop();
}
},
rl.KEY_UP => {
hs.selection_idx -= 1;
if (hs.suggestions.len > 0 and hs.selection_idx < 0) {
hs.selection_idx = @intCast(hs.suggestions.len - 1);
}
},
rl.KEY_DOWN => {
hs.selection_idx += 1;
if (hs.suggestions.len > 0 and hs.selection_idx > hs.suggestions.len - 1) {
hs.selection_idx = 0;
}
},
rl.KEY_ENTER => {
if (hs.suggestions.len > 0 and hs.selection_idx < hs.suggestions.len) {
state.departure_screen_state.station_id.clearRetainingCapacity();
state.departure_screen_state.station_id.appendSliceAssumeCapacity(hs.suggestions[@intCast(hs.selection_idx)].id);
state.screen = .departure;
hs.fetch_thread = null;
}
},
else => {},
}
}
rl.BeginDrawing();
defer rl.EndDrawing();
var x: c_int = 16;
var y: c_int = 16;
const title_size: c_int = 32;
const body_size: c_int = 28;
rl.ClearBackground(rl.BLACK);
x += raylib.DrawAndMeasureText("Station: ", x, y, title_size, rl.WHITE).width + 8;
rl.DrawLine(x, y + title_size + 2, rl.GetScreenWidth() - 16, y + title_size + 2, rl.WHITE);
if (state.db_font) |db_font| {
rl.DrawTextEx(db_font, hs.station_name.items.ptr, rl.Vector2{ .x = @floatFromInt(x), .y = @floatFromInt(y) }, title_size, 0.9, rl.WHITE);
} else {
rl.DrawText(hs.station_name.items.ptr, x, y, title_size, rl.WHITE);
}
y += title_size + 2 + 16;
for (hs.suggestions, 0..) |suggestion, idx| {
var color = if (hs.selection_idx == idx) rl.YELLOW else rl.WHITE;
// Draw arrow for selection
if (hs.selection_idx == idx) {
const arrow_margin: c_int = 16;
rl.DrawLine(x - 10 - arrow_margin, y + body_size / 4, x - arrow_margin, y + body_size / 2, color);
rl.DrawLine(x - arrow_margin, y + body_size / 2, x - 10 - arrow_margin, y + body_size * 3 / 4, color);
}
// Check if mouse is hovering
if (rl.CheckCollisionPointRec(rl.GetMousePosition(), rl.Rectangle{
.x = @floatFromInt(x),
.y = @floatFromInt(y),
.width = @floatFromInt(rl.GetScreenWidth() - 16 - x),
.height = @floatFromInt(body_size),
})) {
color = rl.BLUE;
if (rl.IsMouseButtonPressed(rl.MOUSE_BUTTON_LEFT)) {
// Select
state.departure_screen_state.station_id.clearRetainingCapacity();
state.departure_screen_state.station_id.appendSliceAssumeCapacity(suggestion.id);
state.screen = .departure;
hs.fetch_thread = null;
return;
}
}
if (state.db_font) |db_font| {
rl.DrawTextEx(db_font, suggestion.name.ptr, rl.Vector2{ .x = @floatFromInt(x), .y = @floatFromInt(y) }, body_size, 0.9, color);
} else {
rl.DrawText(suggestion.name.ptr, x, y, body_size, color);
}
y += body_size + 2;
}
state.close_app = rl.WindowShouldClose();
}

46
src/main.zig

@ -0,0 +1,46 @@
const std = @import("std");
const raylib = @import("raylib.zig");
const rl = raylib.rl;
const stateMod = @import("state.zig");
const home = @import("home.zig");
const departure = @import("departure.zig");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
rl.SetConfigFlags(rl.FLAG_WINDOW_RESIZABLE | rl.FLAG_VSYNC_HINT);
rl.SetTargetFPS(60);
rl.InitWindow(800, 600, "Testing Raylib");
defer rl.CloseWindow();
// const font = blk: {
// const maybeFont = rl.LoadFontEx("./db.ttf", 64, null, 0);
// if (std.meta.eql(maybeFont, rl.GetFontDefault())) {
// break :blk null;
// }
// break :blk maybeFont;
// };
var station_name_buffer: [100]u8 = .{0} ** 100;
var platform_buffer: [20]u8 = .{0} ** 20;
var station_id_buffer: [10]u8 = .{0} ** 10;
var appState = stateMod.AppState{
.allocator = allocator,
// .db_font = font,
.home_screen_state = .{
.station_name = std.ArrayListUnmanaged(u8).initBuffer(&station_name_buffer),
},
.departure_screen_state = .{
.platform = std.ArrayListUnmanaged(u8).initBuffer(&platform_buffer),
.station_id = std.ArrayListUnmanaged(u8).initBuffer(&station_id_buffer), // 7 digit id
.departure_date = std.time.Instant.now() catch @panic("Idk buddy, hook a wall clock to your CPU ig"),
},
};
while (!appState.close_app) {
switch (appState.screen) {
.home => try home.render(&appState),
.departure => try departure.render(&appState),
}
}
}

52
src/raylib.zig

@ -0,0 +1,52 @@
pub const rl = @cImport({
@cInclude("raylib.h");
});
pub fn Color(r: u8, g: u8, b: u8, a: u8) rl.Color {
return .{
.r = r,
.g = g,
.b = b,
.a = a,
};
}
pub fn ColorInt(whole: u24) rl.Color {
return ColorIntA(@as(u32, whole) << 8 | 0xFF);
}
pub fn ColorIntA(whole: u32) rl.Color {
return .{
// zig fmt: off
.r = @truncate(whole >> 24),
.g = @truncate(whole >> 16),
.b = @truncate(whole >> 8),
.a = @truncate(whole >> 0),
// zig fmt: on
};
}
pub fn DrawAndMeasureText(
text: [*c]const u8,
pos_x: c_int,
pos_y: c_int,
font_size: c_int,
color: rl.Color,
) struct { width: c_int, height: c_int } {
rl.DrawText(text, pos_x, pos_y, font_size, color);
return .{
.width = rl.MeasureText(text, font_size),
.height = 10,
};
}
pub fn GetKeyPressed() ?c_int {
const result = rl.GetKeyPressed();
return if (result == 0)
null
else
result;
}
pub fn GetCharPressed() ?c_int {
const result = rl.GetCharPressed();
return if (result == 0)
null
else
result;
}

36
src/state.zig

@ -0,0 +1,36 @@
const std = @import("std");
const raylib = @import("raylib.zig");
const rl = raylib.rl;
pub const Screen = enum {
home,
departure,
};
pub const HSSuggestion = struct {
id: [:0]u8,
name: [:0]u8,
};
pub const HomeScreenState = struct {
station_name: std.ArrayListUnmanaged(u8),
fetch_thread: ?std.Thread = null,
suggestions: []HSSuggestion = &.{},
selection_idx: i8 = 0,
};
pub const DepartureScreenState = struct {
station_id: std.ArrayListUnmanaged(u8),
platform: std.ArrayListUnmanaged(u8),
departure_date: std.time.Instant,
loading: bool = false,
};
pub const AppState = struct {
allocator: std.mem.Allocator,
close_app: bool = false,
db_font: ?rl.Font = null,
screen: Screen = .home,
home_screen_state: HomeScreenState,
departure_screen_state: DepartureScreenState,
};
Loading…
Cancel
Save