diff --git a/src/http/handler.zig b/src/http/handler.zig index 3d51b26..e251b5f 100644 --- a/src/http/handler.zig +++ b/src/http/handler.zig @@ -168,27 +168,64 @@ fn handleWeatherInternal( break :blk false; }; - const output = if (params.format) |fmt| blk: { - if (std.mem.eql(u8, fmt, "j1")) { - res.content_type = .JSON; - break :blk try json.render(req_alloc, weather); - } - if (std.mem.eql(u8, fmt, "v2")) - break :blk try v2.render(req_alloc, weather, use_imperial); - if (std.mem.startsWith(u8, fmt, "%")) - break :blk try custom.render(req_alloc, weather, fmt, use_imperial); - // fall back to line if we don't understant the format parameter - break :blk try line.render(req_alloc, weather, fmt, use_imperial); - } else try formatted.render(req_alloc, weather, .{ .use_imperial = use_imperial }); - // Add coordinates header using response allocator 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); - if (res.content_type != .JSON) - res.content_type = .TEXT; + res.body = blk: { + if (params.format) |fmt| { + // Anything except the json will be plain text + res.content_type = .TEXT; + if (std.mem.eql(u8, fmt, "j1")) { + res.content_type = .JSON; // reset to json + break :blk try json.render(req_alloc, weather); + } + if (std.mem.eql(u8, fmt, "v2")) + break :blk try v2.render(req_alloc, weather, use_imperial); + if (std.mem.startsWith(u8, fmt, "%")) + break :blk try custom.render(req_alloc, weather, fmt, use_imperial); + // fall back to line if we don't understand the format parameter + break :blk try line.render(req_alloc, weather, fmt, use_imperial); + } else { + const format: formatted.Format = determineFormat(params, req.headers.get("user-agent")); + log.debug( + "Format: {}. params.ansi {}, params.text {}, user agent: {?s}", + .{ format, params.ansi, params.text_only, req.headers.get("user-agent") }, + ); + if (format != .html) res.content_type = .TEXT else res.content_type = .HTML; + break :blk try formatted.render(req_alloc, weather, .{ .use_imperial = use_imperial, .format = format }); + } + }; +} - res.body = output; +fn determineFormat(params: QueryParams, user_agent: ?[]const u8) formatted.Format { + if (params.ansi or params.text_only) { + // user explicitly requested something. If both are set, text will win + if (params.text_only) return .plain_text; + return .ansi; + } + const ua = user_agent orelse ""; + // https://github.com/chubin/wttr.in/blob/master/lib/globals.py#L82C1-L97C2 + const plain_text_agents = &[_][]const u8{ + "curl", + "httpie", + "lwp-request", + "wget", + "python-requests", + "python-httpx", + "openbsd ftp", + "powershell", + "fetch", + "aiohttp", + "http_get", + "xh", + "nushell", + "zig", + }; + for (plain_text_agents) |agent| + if (std.mem.indexOf(u8, ua, agent)) |_| + return .ansi; + return .html; } test "parseXForwardedFor extracts first IP" { diff --git a/src/http/query.zig b/src/http/query.zig index ca52c3a..c942ac9 100644 --- a/src/http/query.zig +++ b/src/http/query.zig @@ -1,11 +1,33 @@ const std = @import("std"); +///Units: +/// +/// m # metric (SI) (used by default everywhere except US) +/// u # USCS (used by default in US) +/// M # show wind speed in m/s +/// +///View options: +/// +/// 0 # only current weather +/// 1 # current weather + today's forecast +/// 2 # current weather + today's + tomorrow's forecast +/// A # ignore User-Agent and force ANSI output format (terminal) +/// d # restrict output to standard console font glyphs +/// F # do not show the "Follow" line +/// n # narrow version (only day and night) +/// q # quiet version (no "Weather report" text) +/// Q # superquiet version (no "Weather report", no city name) +/// T # switch terminal sequences off (no colors) pub const QueryParams = struct { format: ?[]const u8 = null, lang: ?[]const u8 = null, location: ?[]const u8 = null, units: ?Units = null, transparency: ?u8 = null, + /// A: Ignore user agent and force ansi mode + ansi: bool = false, + /// T: Avoid terminal sequences and just output plain text + text_only: bool = false, pub const Units = enum { metric, @@ -23,16 +45,22 @@ pub const QueryParams = struct { const key = kv.next() orelse continue; const value = kv.next(); + if (key.len == 1) { + switch (key[0]) { + 'u' => params.units = .uscs, + 'm' => params.units = .metric, + 'A' => params.ansi = true, + 'T' => params.text_only = true, + 't' => params.transparency = 150, + else => continue, + } + } if (std.mem.eql(u8, key, "format")) { params.format = if (value) |v| try allocator.dupe(u8, v) else null; } else if (std.mem.eql(u8, key, "lang")) { params.lang = if (value) |v| try allocator.dupe(u8, v) else null; } else if (std.mem.eql(u8, key, "location")) { params.location = if (value) |v| try allocator.dupe(u8, v) else null; - } else if (std.mem.eql(u8, key, "u")) { - params.units = .uscs; - } else if (std.mem.eql(u8, key, "m")) { - params.units = .metric; } else if (std.mem.eql(u8, key, "use_imperial")) { params.units = .uscs; } else if (std.mem.eql(u8, key, "use_metric")) { @@ -41,8 +69,6 @@ pub const QueryParams = struct { if (value) |v| { params.transparency = try std.fmt.parseInt(u8, v, 10); } - } else if (std.mem.eql(u8, key, "t")) { - params.transparency = 150; } } @@ -108,6 +134,7 @@ test "parse multiple parameters" { test "parse transparency" { const allocator = std.testing.allocator; const params_t = try QueryParams.parse(allocator, "t"); + try std.testing.expect(params_t.transparency != null); try std.testing.expectEqual(@as(u8, 150), params_t.transparency.?); const params_custom = try QueryParams.parse(allocator, "transparency=200");