partial changes for png generation

This commit is contained in:
Emil Lerch 2026-01-12 11:16:59 -08:00
parent 148bd862b5
commit d0f08aacfa
Signed by: lobo
GPG key ID: A7B62D657EF764F8
4 changed files with 114 additions and 39 deletions

View file

@ -16,6 +16,22 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
}); });
const zigimg = b.dependency("zigimg", .{
.target = target,
.optimize = optimize,
});
const freetype = b.dependency("ghostty", .{
.target = target,
.optimize = optimize,
}).builder.dependency("freetype", .{
.target = target,
.optimize = optimize,
});
const jetbrains_mono = b.dependency("jetbrains_mono", .{});
const nerd_fonts = b.dependency("nerd_fonts_symbols_only", .{});
const openflights = b.dependency("openflights", .{}); const openflights = b.dependency("openflights", .{});
const maxminddb_upstream = b.dependency("maxminddb", .{}); const maxminddb_upstream = b.dependency("maxminddb", .{});
@ -113,9 +129,17 @@ pub fn build(b: *std.Build) void {
}); });
root_module.addImport("httpz", httpz.module("httpz")); root_module.addImport("httpz", httpz.module("httpz"));
root_module.addImport("zeit", zeit.module("zeit")); root_module.addImport("zeit", zeit.module("zeit"));
root_module.addImport("zigimg", zigimg.module("zigimg"));
root_module.addImport("freetype", freetype.module("freetype"));
root_module.addAnonymousImport("airports.dat", .{ root_module.addAnonymousImport("airports.dat", .{
.root_source_file = openflights.path("data/airports.dat"), .root_source_file = openflights.path("data/airports.dat"),
}); });
root_module.addAnonymousImport("JetBrainsMono-Regular.ttf", .{
.root_source_file = jetbrains_mono.path("fonts/ttf/JetBrainsMono-Regular.ttf"),
});
root_module.addAnonymousImport("SymbolsNerdFont-Regular.ttf", .{
.root_source_file = nerd_fonts.path("SymbolsNerdFont-Regular.ttf"),
});
root_module.addOptions("build_options", build_options); root_module.addOptions("build_options", build_options);
root_module.addIncludePath(maxminddb_upstream.path("include")); root_module.addIncludePath(maxminddb_upstream.path("include"));
root_module.addIncludePath(b.path("libs/phoon_14Aug2014")); root_module.addIncludePath(b.path("libs/phoon_14Aug2014"));
@ -125,6 +149,7 @@ pub fn build(b: *std.Build) void {
maxminddb, maxminddb,
phoon, phoon,
sunriset, sunriset,
freetype.artifact("freetype"),
}; };
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "wttr", .name = "wttr",

View file

@ -20,6 +20,22 @@
}, },
.phoon = .{ .path = "libs/phoon_14Aug2014" }, .phoon = .{ .path = "libs/phoon_14Aug2014" },
.sunriset = .{ .path = "libs/sunriset" }, .sunriset = .{ .path = "libs/sunriset" },
.ghostty = .{
.url = "git+https://github.com/ghostty-org/ghostty#ec2912dbafe50cc32b786d2327dcd0213c83ecc6",
.hash = "ghostty-1.3.0-dev-5UdBC_y2RASwYWn5fjn71WsP-arlg8wSICLc0rYiozdf",
},
.zigimg = .{
.url = "git+https://github.com/zigimg/zigimg#9714df09f76891323c7fdbbbf23a17b79024fffb",
.hash = "zigimg-0.1.0-8_eo2j4mFwCU7tWnqvkYtzqe-OPRn_bxEql_IJhW85LT",
},
.jetbrains_mono = .{
.url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz",
.hash = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x",
},
.nerd_fonts_symbols_only = .{
.url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz",
.hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s",
},
}, },
.fingerprint = 0x710c2b57e81aa678, .fingerprint = 0x710c2b57e81aa678,
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",

View file

@ -26,6 +26,8 @@ format: ?[]const u8 = null,
lang: ?[]const u8 = null, lang: ?[]const u8 = null,
location: ?[]const u8 = null, location: ?[]const u8 = null,
transparency: ?u8 = null, transparency: ?u8 = null,
background: ?[]const u8 = null,
add_frame: bool = false,
/// A: Ignore user agent and force ansi mode /// A: Ignore user agent and force ansi mode
ansi: bool = false, ansi: bool = false,
/// T: Avoid terminal sequences and just output plain text /// T: Avoid terminal sequences and just output plain text
@ -60,6 +62,7 @@ pub fn parse(allocator: std.mem.Allocator, query_string: []const u8) !QueryParam
'A' => params.ansi = true, 'A' => params.ansi = true,
'T' => params.text_only = true, 'T' => params.text_only = true,
't' => params.transparency = 150, 't' => params.transparency = 150,
'p' => params.add_frame = true,
else => continue, else => continue,
} }
} }
@ -77,6 +80,8 @@ pub fn parse(allocator: std.mem.Allocator, query_string: []const u8) !QueryParam
if (value) |v| { if (value) |v| {
params.transparency = try std.fmt.parseInt(u8, v, 10); params.transparency = try std.fmt.parseInt(u8, v, 10);
} }
} else if (std.mem.eql(u8, key, "background")) {
params.background = if (value) |v| try allocator.dupe(u8, v) else null;
} }
} }

View file

@ -9,7 +9,9 @@ const Json = @import("../render/Json.zig");
const V2 = @import("../render/V2.zig"); const V2 = @import("../render/V2.zig");
const Custom = @import("../render/Custom.zig"); const Custom = @import("../render/Custom.zig");
const Prometheus = @import("../render/Prometheus.zig"); const Prometheus = @import("../render/Prometheus.zig");
const Png = @import("../render/Png.zig");
const help = @import("help.zig"); const help = @import("help.zig");
const types = @import("../weather/types.zig");
const log = std.log.scoped(.handler); const log = std.log.scoped(.handler);
@ -33,12 +35,14 @@ pub fn handleWeather(
defer { defer {
if (params.format) |f| req.arena.free(f); if (params.format) |f| req.arena.free(f);
if (params.lang) |l| req.arena.free(l); if (params.lang) |l| req.arena.free(l);
if (params.background) |b| req.arena.free(b);
} }
if (params.location) |loc| { if (params.location) |loc| {
break :blk loc; break :blk loc;
} else break :blk client_ip; // no location, just use client ip instead } else break :blk client_ip; // no location, just use client ip instead
}; };
if (std.mem.eql(u8, "favicon.ico", location)) { if (std.mem.eql(u8, "favicon.ico", location)) {
res.header("Content-Type", "image/x-icon"); res.header("Content-Type", "image/x-icon");
res.body = @embedFile("favicon.ico"); res.body = @embedFile("favicon.ico");
@ -75,16 +79,20 @@ fn handleWeatherInternal(
) !void { ) !void {
const req_alloc = req.arena; const req_alloc = req.arena;
// Check for PNG request
const is_png = std.mem.endsWith(u8, location_query, ".png");
const location_str = if (is_png) location_query[0 .. location_query.len - 4] else location_query;
// Resolve location. By the time we get here, we really // Resolve location. By the time we get here, we really
// should have a location from the path, query string, or // should have a location from the path, query string, or
// client IP lookup. So if we have an empty location parameter, it // client IP lookup. So if we have an empty location parameter, it
// is better to 404 than to fake it with a London response // is better to 404 than to fake it with a London response
if (location_query.len == 0) { if (location_str.len == 0) {
res.status = 404; res.status = 404;
res.body = "Location not found\n"; res.body = "Location not found\n";
return; return;
} }
const location = opts.resolver.resolve(location_query) catch |err| { const location = opts.resolver.resolve(location_str) catch |err| {
switch (err) { switch (err) {
error.LocationNotFound => { error.LocationNotFound => {
log.debug("Location not found for query {s}", .{location_query}); log.debug("Location not found for query {s}", .{location_query});
@ -118,6 +126,7 @@ fn handleWeatherInternal(
defer { defer {
if (params.format) |f| req_alloc.free(f); if (params.format) |f| req_alloc.free(f);
if (params.lang) |l| req_alloc.free(l); if (params.lang) |l| req_alloc.free(l);
if (params.background) |b| req_alloc.free(b);
} }
var render_options = params.render_options; var render_options = params.render_options;
@ -140,50 +149,70 @@ fn handleWeatherInternal(
const coords_header = try std.fmt.allocPrint(res.arena, "{d:.4},{d:.4}", .{ location.coords.latitude, location.coords.longitude }); const coords_header = try std.fmt.allocPrint(res.arena, "{d:.4},{d:.4}", .{ location.coords.latitude, location.coords.longitude });
res.headers.add("X-Location-Coordinates", coords_header); res.headers.add("X-Location-Coordinates", coords_header);
// Render weather data
if (is_png) {
res.content_type = .PNG;
var png_renderer = Png.init(req_alloc);
defer png_renderer.deinit();
var png_buffer: [1024 * 1024]u8 = undefined;
var png_writer_impl = std.Io.Writer.fixed(&png_buffer);
const png_writer = &png_writer_impl;
render_options.format = .ansi; // Force ANSI for PNG
try renderWeatherData(png_writer, weather, params, render_options);
const text_output = png_buffer[0..png_writer_impl.end];
try png_renderer.buffer.appendSlice(req_alloc, text_output);
const png_options = Png.PngOptions{
.transparency = params.transparency orelse 150,
.background = params.background,
.add_frame = params.add_frame,
};
try png_renderer.render(res.writer(), png_options);
return;
}
// Set content type based on format
if (params.format) |fmt| { if (params.format) |fmt| {
// Anything except the json will be plain text res.content_type = if (std.mem.eql(u8, fmt, "j1")) .JSON else .TEXT;
res.content_type = .TEXT;
if (std.mem.eql(u8, fmt, "1")) {
try Line.render(res.writer(), weather, .@"1", render_options.use_imperial);
return;
}
if (std.mem.eql(u8, fmt, "2")) {
try Line.render(res.writer(), weather, .@"2", render_options.use_imperial);
return;
}
if (std.mem.eql(u8, fmt, "3")) {
try Line.render(res.writer(), weather, .@"3", render_options.use_imperial);
return;
}
if (std.mem.eql(u8, fmt, "4")) {
try Line.render(res.writer(), weather, .@"4", render_options.use_imperial);
return;
}
if (std.mem.eql(u8, fmt, "j1")) {
res.content_type = .JSON; // reset to json
try Json.render(res.writer(), weather);
return;
}
if (std.mem.eql(u8, fmt, "p1")) {
try Prometheus.render(res.writer(), weather);
return;
}
if (std.mem.eql(u8, fmt, "v2")) {
try V2.render(res.writer(), weather, render_options.use_imperial);
return;
}
// Everything else goes to Custom renderer
try Custom.render(res.writer(), weather, fmt, render_options.use_imperial);
} else { } else {
// No specific format selected, we'll provide Formatted output in either
// text (ansi/plain) or html
render_options.format = determineFormat(params, req.headers.get("user-agent")); render_options.format = determineFormat(params, req.headers.get("user-agent"));
log.debug( log.debug(
"Format: {}. params.ansi {}, params.text {}, user agent: {?s}", "Format: {}. params.ansi {}, params.text {}, user agent: {?s}",
.{ render_options.format, params.ansi, params.text_only, req.headers.get("user-agent") }, .{ render_options.format, params.ansi, params.text_only, req.headers.get("user-agent") },
); );
if (render_options.format != .html) res.content_type = .TEXT else res.content_type = .HTML; res.content_type = if (render_options.format == .html) .HTML else .TEXT;
try Formatted.render(res.writer(), weather, render_options); }
try renderWeatherData(res.writer(), weather, params, render_options);
}
fn renderWeatherData(
writer: *std.Io.Writer,
weather: types.WeatherData,
params: QueryParams,
render_options: Formatted.RenderOptions,
) !void {
if (params.format) |fmt| {
if (std.mem.eql(u8, fmt, "1")) {
try Line.render(writer, weather, .@"1", render_options.use_imperial);
} else if (std.mem.eql(u8, fmt, "2")) {
try Line.render(writer, weather, .@"2", render_options.use_imperial);
} else if (std.mem.eql(u8, fmt, "3")) {
try Line.render(writer, weather, .@"3", render_options.use_imperial);
} else if (std.mem.eql(u8, fmt, "4")) {
try Line.render(writer, weather, .@"4", render_options.use_imperial);
} else if (std.mem.eql(u8, fmt, "j1")) {
try Json.render(writer, weather);
} else if (std.mem.eql(u8, fmt, "p1")) {
try Prometheus.render(writer, weather);
} else if (std.mem.eql(u8, fmt, "v2")) {
try V2.render(writer, weather, render_options.use_imperial);
} else {
try Custom.render(writer, weather, fmt, render_options.use_imperial);
}
} else {
try Formatted.render(writer, weather, render_options);
} }
} }