Compare commits

..

No commits in common. "16fd5b0b958016e0a3f49d31c839477cf1afa3df" and "cb4a4aed9ea3e5bb0bc3c7df0fce194fd312dccc" have entirely different histories.

20 changed files with 141 additions and 215 deletions

14
flake.lock generated
View file

@ -54,16 +54,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1750259320, "lastModified": 1749995256,
"narHash": "sha256-H8J4H2XCIMEJ5g6fZ179QfQvsc2dUqhqfBjC8RAHNRY=", "narHash": "sha256-LEGfcombb0otUf23oAmYCXR4+lMQKa49XmU0G5HItGI=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9ba04bda9249d5d5e5238303c9755de5a49a79c5", "rev": "daa45f10955cc2207ac9c5f0206774d2f757c162",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "nixos",
"ref": "nixos-25.05", "ref": "nixos-24.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@ -128,11 +128,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1750379715, "lastModified": 1750293335,
"narHash": "sha256-R7yDQlTHvARic5adjg7e/rT22m0mDME90mEgGM6thic=", "narHash": "sha256-qAX+hIcUqVl8BzLHTRBrceF2vGeht+gfhvvUZgKe/98=",
"owner": "mitchellh", "owner": "mitchellh",
"repo": "zig-overlay", "repo": "zig-overlay",
"rev": "f69e9790793d81a8f32af8426008cb2a3871bad5", "rev": "3453b39a83069811d077a0bc696849c6907112d5",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,6 +1,6 @@
{ {
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
flake-utils.url = "github:/numtide/flake-utils"; flake-utils.url = "github:/numtide/flake-utils";
zig.url = "github:mitchellh/zig-overlay"; zig.url = "github:mitchellh/zig-overlay";
}; };

View file

@ -1,90 +0,0 @@
pub const TransportationProductClass = enum(u32) {
zug = 0,
s_bahn = 1,
u_bahn = 2,
stadtbahn = 3,
strassen_trambahn = 4,
stadtbus = 5,
regionalbus = 6,
schnellbus = 7,
seil_zahnradbahn = 8,
schiff = 9,
anruf_sammel_taxi = 10,
sonstige = 11,
flugzeug = 12,
zug_nv = 13,
zug_fv = 14,
zug_fv_m_zuschlag = 15,
zug_fv_m_spez_fpr = 16,
sev = 17,
zug_shuttle = 18,
buergerbus = 19,
rufbus_liniengebunden = 20,
rufbus = 21,
_,
pub fn toString(self: TransportationProductClass) ?[]const u8 {
return switch (self) {
.zug => "Zug",
.s_bahn => "S-Bahn",
.u_bahn => "U-Bahn",
.stadtbahn => "Stadtbahn",
.strassen_trambahn => "Straßen-/Trambahn",
.stadtbus => "Stadtbus",
.regionalbus => "Regionalbus",
.schnellbus => "Schnellbus",
.seil_zahnradbahn => "Seil-/Zahnradbahn",
.schiff => "Schiff",
.anruf_sammel_taxi => "Anruf-Sammel-Taxi",
.sonstige => "Sonstige",
.flugzeug => "Flugzeug",
.zug_nv => "Zug (Nahverkehr)",
.zug_fv => "Zug (Fernverkehr)",
.zug_fv_m_zuschlag => "Zug (Fernverkehr) mit Zuschlag",
.zug_fv_m_spez_fpr => "Zug (Fernverkehr) mit spezifischer Fpr",
.sev => "SEV Schnienenersatzverkehr",
.zug_shuttle => "Zug Shuttle",
.buergerbus => "Bürgerbus",
.rufbus_liniengebunden => "Rufbus (liniengebunden)",
.rufbus => "Rufbus",
_ => null,
};
}
};
pub const Location = struct {
id: []const u8,
name: []const u8,
properties: struct {
platform: ?[]const u8 = null,
},
};
pub const StopEvent = struct {
location: Location,
departureTimePlanned: []const u8,
departureTimeBaseTimetable: []const u8,
departureTimeEstimated: ?[]const u8 = null,
transportation: struct {
name: []const u8,
product: struct {
class: TransportationProductClass,
},
operator: ?struct {
name: []const u8,
} = null,
destination: struct {
name: []const u8,
},
},
hints: ?[]const struct {
content: []const u8,
type: []const u8,
} = null,
isCancelled: bool = false,
};
pub const DmResponse = struct {
locations: []const Location,
stopEvents: []const StopEvent,
};

View file

@ -1 +0,0 @@
pub const efa = @import("efa/root.zig");

View file

@ -2,10 +2,99 @@ const std = @import("std");
const escaper = @import("../../escaper.zig"); const escaper = @import("../../escaper.zig");
pub const models = @import("models.zig");
const log = std.log.scoped(.efa); const log = std.log.scoped(.efa);
pub const ApiTransportationProductClass = enum(u32) {
zug = 0,
s_bahn = 1,
u_bahn = 2,
stadtbahn = 3,
strassen_trambahn = 4,
stadtbus = 5,
regionalbus = 6,
schnellbus = 7,
seil_zahnradbahn = 8,
schiff = 9,
anruf_sammel_taxi = 10,
sonstige = 11,
flugzeug = 12,
zug_nv = 13,
zug_fv = 14,
zug_fv_m_zuschlag = 15,
zug_fv_m_spez_fpr = 16,
sev = 17,
zug_shuttle = 18,
buergerbus = 19,
rufbus_liniengebunden = 20,
rufbus = 21,
_,
pub fn toString(self: ApiTransportationProductClass) ?[]const u8 {
return switch (self) {
.zug => "Zug",
.s_bahn => "S-Bahn",
.u_bahn => "U-Bahn",
.stadtbahn => "Stadtbahn",
.strassen_trambahn => "Straßen-/Trambahn",
.stadtbus => "Stadtbus",
.regionalbus => "Regionalbus",
.schnellbus => "Schnellbus",
.seil_zahnradbahn => "Seil-/Zahnradbahn",
.schiff => "Schiff",
.anruf_sammel_taxi => "Anruf-Sammel-Taxi",
.sonstige => "Sonstige",
.flugzeug => "Flugzeug",
.zug_nv => "Zug (Nahverkehr)",
.zug_fv => "Zug (Fernverkehr)",
.zug_fv_m_zuschlag => "Zug (Fernverkehr) mit Zuschlag",
.zug_fv_m_spez_fpr => "Zug (Fernverkehr) mit spezifischer Fpr",
.sev => "SEV Schnienenersatzverkehr",
.zug_shuttle => "Zug Shuttle",
.buergerbus => "Bürgerbus",
.rufbus_liniengebunden => "Rufbus (liniengebunden)",
.rufbus => "Rufbus",
_ => null,
};
}
};
pub const ApiLocation = struct {
id: []const u8,
name: []const u8,
properties: struct {
platform: ?[]const u8 = null,
},
};
pub const ApiStopEvent = struct {
location: ApiLocation,
departureTimePlanned: []const u8,
departureTimeBaseTimetable: []const u8,
departureTimeEstimated: ?[]const u8 = null,
transportation: struct {
name: []const u8,
product: struct {
class: ApiTransportationProductClass,
},
operator: ?struct {
name: []const u8,
} = null,
destination: struct {
name: []const u8,
},
},
hints: ?[]const struct {
content: []const u8,
type: []const u8,
} = null,
isCancelled: bool = false,
};
pub const DmResponse = struct {
locations: []const ApiLocation,
stopEvents: []const ApiStopEvent,
};
pub const Client = struct { pub const Client = struct {
pub const Error = error{ pub const Error = error{
InvalidResponse, InvalidResponse,
@ -81,7 +170,7 @@ pub const Client = struct {
try std.io.getStdOut().writeAll(body); try std.io.getStdOut().writeAll(body);
} }
pub fn getDepartures(self: *const Self, stop_id: []const u8) !std.json.Parsed(models.DmResponse) { pub fn getDepartures(self: *const Self, stop_id: []const u8) !std.json.Parsed(DmResponse) {
const escaped_stop_id = try escaper.escapeUriComponent(self.allocator, stop_id); const escaped_stop_id = try escaper.escapeUriComponent(self.allocator, stop_id);
defer if (escaped_stop_id) |s| self.allocator.free(s); defer if (escaped_stop_id) |s| self.allocator.free(s);
const url_stop_id = escaped_stop_id orelse stop_id; const url_stop_id = escaped_stop_id orelse stop_id;
@ -115,7 +204,7 @@ pub const Client = struct {
defer self.allocator.free(body); defer self.allocator.free(body);
const response = try std.json.parseFromSlice( const response = try std.json.parseFromSlice(
models.DmResponse, DmResponse,
self.allocator, self.allocator,
body, body,
.{ .allocate = .alloc_always, .ignore_unknown_fields = true }, .{ .allocate = .alloc_always, .ignore_unknown_fields = true },

View file

@ -5,7 +5,7 @@ const zdt = @import("zdt");
const Dimensions = @import("../../Dimensions.zig"); const Dimensions = @import("../../Dimensions.zig");
const renderer = @import("../../renderer.zig"); const renderer = @import("../../renderer.zig");
pub const api = @import("../../api/root.zig"); pub const efa = @import("efa.zig");
const template = @import("template/root.zig"); const template = @import("template/root.zig");
@ -28,7 +28,7 @@ pub const RenderOptions = struct {
show_operator: bool, show_operator: bool,
}; };
pub fn render(self: *const Self, efa_dm_resp: *const api.efa.models.DmResponse, options: RenderOptions) ![]const u8 { pub fn render(self: *const Self, efa_dm_resp: *const efa.DmResponse, options: RenderOptions) ![]const u8 {
const now_local = try options.now.tzConvert(.{ .tz = options.tz }); const now_local = try options.now.tzConvert(.{ .tz = options.tz });
var arena = std.heap.ArenaAllocator.init(self.allocator); var arena = std.heap.ArenaAllocator.init(self.allocator);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,31 +1,8 @@
const std = @import("std"); const std = @import("std");
const api = @import("../../../../api/root.zig"); const efa = @import("../../efa.zig");
const embed = @import("../../../../embed.zig");
pub const data = embed.embedBase64Files( pub fn iconFromTransportationProductClass(class: efa.ApiTransportationProductClass) ?[]const u8 {
(struct {
fn inner(comptime path: []const u8) []const u8 {
return @embedFile(path);
}
}).inner,
"image/svg+xml",
"svg",
"svg",
&.{
"train",
"subway",
"tram",
"bus",
"shuttle",
"gondola",
"flight",
"boat",
"no_transfer",
},
){};
pub fn iconFromTransportationProductClass(class: api.efa.models.TransportationProductClass) ?embed.EncodedFile {
return switch (class) { return switch (class) {
.zug, .zug,
.s_bahn, .s_bahn,
@ -34,11 +11,11 @@ pub fn iconFromTransportationProductClass(class: api.efa.models.TransportationPr
.zug_fv_m_zuschlag, .zug_fv_m_zuschlag,
.zug_fv_m_spez_fpr, .zug_fv_m_spez_fpr,
.zug_shuttle, .zug_shuttle,
=> data.train, => train,
.u_bahn => data.subway, .u_bahn => subway,
.stadtbahn, .strassen_trambahn => data.tram, .stadtbahn, .strassen_trambahn => tram,
.stadtbus, .stadtbus,
.regionalbus, .regionalbus,
@ -47,18 +24,40 @@ pub fn iconFromTransportationProductClass(class: api.efa.models.TransportationPr
.rufbus_liniengebunden, .rufbus_liniengebunden,
.rufbus, .rufbus,
.anruf_sammel_taxi, .anruf_sammel_taxi,
=> data.bus, => bus,
.seil_zahnradbahn => data.gondola, .seil_zahnradbahn => gondola,
.schiff => data.boat, .schiff => boat,
.sonstige => null, .sonstige => null,
.flugzeug => data.flight, .flugzeug => flight,
.sev => data.no_transfer, .sev => no_transfer,
_ => null, _ => null,
}; };
} }
fn encodePng(comptime path: []const u8) []const u8 {
const raw = @embedFile(path);
const len = std.base64.standard.Encoder.calcSize(raw.len);
var buf: [len]u8 = undefined;
{
@setEvalBranchQuota(1_000_000);
_ = std.base64.standard.Encoder.encode(&buf, raw);
}
const final = buf;
return &final;
}
pub const train = encodePng("img/train.png");
pub const subway = encodePng("img/subway.png");
pub const tram = encodePng("img/tram.png");
pub const bus = encodePng("img/bus.png");
pub const shuttle = encodePng("img/shuttle.png");
pub const gondola = encodePng("img/gondola.png");
pub const flight = encodePng("img/flight.png");
pub const boat = encodePng("img/boat.png");
pub const no_transfer = encodePng("img/no_transfer.png");

View file

@ -1,7 +1,7 @@
const std = @import("std"); const std = @import("std");
const escaper = @import("../../../escaper.zig"); const escaper = @import("../../../escaper.zig");
const api = @import("../../../api/root.zig"); const efa = @import("../efa.zig");
const icons = @import("icons/root.zig"); const icons = @import("icons/root.zig");
@ -9,7 +9,7 @@ pub const Departure = struct {
time: []const u8, time: []const u8,
time_unix: i128, time_unix: i128,
line: []const u8, line: []const u8,
transportation_product_class: api.efa.models.TransportationProductClass, transportation_product_class: efa.ApiTransportationProductClass,
operator: ?[]const u8, operator: ?[]const u8,
destination: []const u8, destination: []const u8,
platform: ?[]const u8, platform: ?[]const u8,
@ -188,16 +188,13 @@ pub fn render(context: *const Context, writer: anytype) !void {
); );
} }
if (icons.iconFromTransportationProductClass(departure.transportation_product_class)) |file| { if (icons.iconFromTransportationProductClass(departure.transportation_product_class)) |img| {
try writer.print( try writer.print(
\\ <td> \\ <td>
\\ <img class="icon" src="data:{[content_type]s};charset=utf-8;base64,{[data]s}" /> \\ <img class="icon" src="data:image/png;charset=utf-8;base64,{[img]s}" />
\\ </td> \\ </td>
, ,
.{ .{ .img = img },
.content_type = file.content_type,
.data = file.data,
},
); );
} else { } else {
try writer.writeAll( try writer.writeAll(

View file

@ -1,67 +0,0 @@
const std = @import("std");
pub const EncodedFile = struct {
content_type: []const u8,
data: []const u8,
};
pub const EmbedFn = *const fn (comptime path: []const u8) []const u8;
pub fn encodeFileBase64(
comptime embed_fn: EmbedFn,
comptime content_type: []const u8,
comptime path: []const u8,
) EncodedFile {
const raw = embed_fn(path);
const len = std.base64.standard.Encoder.calcSize(raw.len);
var buf: [len]u8 = undefined;
{
@setEvalBranchQuota(1_000_000);
_ = std.base64.standard.Encoder.encode(&buf, raw);
}
const final = buf;
return .{
.content_type = content_type,
.data = &final,
};
}
pub fn embedBase64Files(
comptime embed_fn: EmbedFn,
comptime content_type: []const u8,
comptime extension: []const u8,
comptime path: []const u8,
comptime names: []const []const u8,
) type {
var fields: [names.len]std.builtin.Type.StructField = undefined;
for (names, 0..) |name, i| {
const encoded = encodeFileBase64(
embed_fn,
content_type,
std.fmt.comptimePrint(
"{s}/{s}.{s}",
.{ path, name, extension },
),
);
var name_z: [name.len:0]u8 = undefined;
@memcpy(&name_z, name);
fields[i] = std.builtin.Type.StructField{
.name = &name_z,
.type = EncodedFile,
.is_comptime = true,
.alignment = 0,
.default_value_ptr = &encoded,
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.is_tuple = false,
.fields = &fields,
.decls = &[_]std.builtin.Type.Declaration{},
} });
}

View file

@ -52,7 +52,7 @@ pub fn main() !void {
return clap.help(writer, clap.Help, &params, .{}); return clap.help(writer, clap.Help, &params, .{});
} }
const efa = try render.api.efa.Client.init(allocator, res.args.efa.?); const efa = try render.apps.Departures.efa.Client.init(allocator, res.args.efa.?);
defer efa.deinit(); defer efa.deinit();
if (res.args.stopfinder) |query| { if (res.args.stopfinder) |query| {

View file

@ -1,4 +1,3 @@
pub const api = @import("api/root.zig");
pub const apps = @import("apps/root.zig"); pub const apps = @import("apps/root.zig");
pub const renderer = @import("renderer.zig"); pub const renderer = @import("renderer.zig");
pub const FeedClient = @import("FeedClient.zig"); pub const FeedClient = @import("FeedClient.zig");