diff --git a/src/http/handler.zig b/src/http/handler.zig index e251b5f..932c690 100644 --- a/src/http/handler.zig +++ b/src/http/handler.zig @@ -153,20 +153,21 @@ fn handleWeatherInternal( if (params.lang) |l| req_alloc.free(l); } + var render_options = params.render_options; // Determine if imperial units should be used // Priority: explicit ?u or ?m > lang=us > US IP > default metric - const use_imperial = blk: { - if (params.units) |u| - break :blk u == .uscs; + if (params.use_imperial == null) { + // User did not ask for anything explicitly - if (params.lang) |lang| + // Check if lang=us + if (params.lang) |lang| { if (std.mem.eql(u8, lang, "us")) - break :blk true; + render_options.use_imperial = true; + } - if (client_ip.len > 0 and opts.geoip.isUSIp(client_ip)) - break :blk true; - break :blk false; - }; + if (!render_options.use_imperial and client_ip.len > 0 and opts.geoip.isUSIp(client_ip)) + render_options.use_imperial = true; // this is a US IP + } // 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 }); @@ -181,19 +182,19 @@ fn handleWeatherInternal( 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); + break :blk try v2.render(req_alloc, weather, render_options.use_imperial); if (std.mem.startsWith(u8, fmt, "%")) - break :blk try custom.render(req_alloc, weather, fmt, use_imperial); + break :blk try custom.render(req_alloc, weather, fmt, render_options.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); + break :blk try line.render(req_alloc, weather, fmt, render_options.use_imperial); } else { - const format: formatted.Format = determineFormat(params, req.headers.get("user-agent")); + render_options.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") }, + .{ render_options.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 }); + if (render_options.format != .html) res.content_type = .TEXT else res.content_type = .HTML; + break :blk try formatted.render(req_alloc, weather, render_options); } }; } @@ -255,10 +256,10 @@ test "imperial units selection logic" { const allocator = std.testing.allocator; const params_u = try QueryParams.parse(allocator, "u"); - try std.testing.expectEqual(QueryParams.Units.uscs, params_u.units.?); + try std.testing.expect(params_u.use_imperial.?); const params_m = try QueryParams.parse(allocator, "m"); - try std.testing.expectEqual(QueryParams.Units.metric, params_m.units.?); + try std.testing.expect(!params_m.use_imperial.?); const params_lang = try QueryParams.parse(allocator, "lang=us"); defer allocator.free(params_lang.lang.?); diff --git a/src/http/help.zig b/src/http/help.zig index b0dbc89..db56061 100644 --- a/src/http/help.zig +++ b/src/http/help.zig @@ -38,10 +38,10 @@ pub const help_page = \\ 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 (not necessary - this version does not have a 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) + \\ F # do not show the "Follow" line (not necessary - this version does not have a 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) \\ \\PNG options: diff --git a/src/http/query.zig b/src/http/query.zig index c942ac9..d853672 100644 --- a/src/http/query.zig +++ b/src/http/query.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const RenderOptions = @import("../render/formatted.zig").RenderOptions; ///Units: /// @@ -22,22 +23,21 @@ 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, - uscs, - }; + /// This is necessary because it it imporant to know if the user explicitly + /// requested imperial/metric + use_imperial: ?bool = null, + render_options: RenderOptions, pub fn parse(allocator: std.mem.Allocator, query_string: []const u8) !QueryParams { - var params = QueryParams{}; + // SAFETY: function adds render_options at end of function before return + var params = QueryParams{ .render_options = undefined }; var iter = std.mem.splitScalar(u8, query_string, '&'); - + var render_options = RenderOptions{}; while (iter.next()) |pair| { if (pair.len == 0) continue; @@ -47,8 +47,14 @@ pub const QueryParams = struct { if (key.len == 1) { switch (key[0]) { - 'u' => params.units = .uscs, - 'm' => params.units = .metric, + '0' => render_options.days = 0, + '1' => render_options.days = 1, + '2' => render_options.days = 2, + 'u' => params.use_imperial = true, + 'm' => params.use_imperial = false, + 'n' => render_options.narrow = true, + 'q' => render_options.quiet = true, + 'Q' => render_options.super_quiet = true, 'A' => params.ansi = true, 'T' => params.text_only = true, 't' => params.transparency = 150, @@ -62,9 +68,9 @@ pub const QueryParams = struct { } 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, "use_imperial")) { - params.units = .uscs; + params.use_imperial = true; } else if (std.mem.eql(u8, key, "use_metric")) { - params.units = .metric; + params.use_imperial = false; } else if (std.mem.eql(u8, key, "transparency")) { if (value) |v| { params.transparency = try std.fmt.parseInt(u8, v, 10); @@ -72,6 +78,8 @@ pub const QueryParams = struct { } } + if (params.use_imperial) |u| render_options.use_imperial = u; + params.render_options = render_options; return params; } }; @@ -81,7 +89,7 @@ test "parse empty query" { const params = try QueryParams.parse(allocator, ""); try std.testing.expect(params.format == null); try std.testing.expect(params.lang == null); - try std.testing.expect(params.units == null); + try std.testing.expect(params.use_imperial == null); } test "parse format parameter" { @@ -97,28 +105,28 @@ test "parse units with question mark" { // Test with just "u" (no question mark in query string) const params1 = try QueryParams.parse(allocator, "u"); - try std.testing.expectEqual(QueryParams.Units.uscs, params1.units.?); + try std.testing.expect(params1.use_imperial.?); // Test with "u=" (empty value) const params2 = try QueryParams.parse(allocator, "u="); - try std.testing.expectEqual(QueryParams.Units.uscs, params2.units.?); + try std.testing.expect(params2.use_imperial.?); // Test combined with other params const params3 = try QueryParams.parse(allocator, "format=3&u"); defer if (params3.format) |f| allocator.free(f); - try std.testing.expectEqual(QueryParams.Units.uscs, params3.units.?); + try std.testing.expect(params3.use_imperial.?); } test "parse units parameters" { const allocator = std.testing.allocator; const params_m = try QueryParams.parse(allocator, "m"); - try std.testing.expectEqual(QueryParams.Units.metric, params_m.units.?); + try std.testing.expect(!params_m.use_imperial.?); const params_u = try QueryParams.parse(allocator, "u"); - try std.testing.expectEqual(QueryParams.Units.uscs, params_u.units.?); + try std.testing.expect(params_u.use_imperial.?); const params_u_query = try QueryParams.parse(allocator, "u="); - try std.testing.expectEqual(QueryParams.Units.uscs, params_u_query.units.?); + try std.testing.expect(params_u_query.use_imperial.?); } test "parse multiple parameters" { @@ -128,7 +136,7 @@ test "parse multiple parameters" { defer if (params.lang) |l| allocator.free(l); try std.testing.expectEqualStrings("3", params.format.?); try std.testing.expectEqualStrings("de", params.lang.?); - try std.testing.expectEqual(QueryParams.Units.metric, params.units.?); + try std.testing.expect(!params.use_imperial.?); } test "parse transparency" { diff --git a/src/render/formatted.zig b/src/render/formatted.zig index 91f7da9..c5eca05 100644 --- a/src/render/formatted.zig +++ b/src/render/formatted.zig @@ -99,9 +99,10 @@ fn countInvisible(bytes: []const u8, format: Format) usize { pub const RenderOptions = struct { narrow: bool = false, + quiet: bool = false, + super_quiet: bool = false, days: u8 = 3, use_imperial: bool = false, - no_caption: bool = false, format: Format = .ansi, }; @@ -111,8 +112,11 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, options: Re const w = &output.writer; if (options.format == .html) try w.writeAll("
");
-    if (!options.no_caption)
-        try w.print("Weather report: {s}\n\n", .{data.locationDisplayName()});
+    if (!options.super_quiet)
+        try w.print(
+            "{s}{s}\n\n",
+            .{ if (!options.quiet) "Weather report: " else "", data.locationDisplayName() },
+        );
 
     try renderCurrent(w, data.current, options);
 
@@ -226,21 +230,33 @@ fn renderForecastDay(w: *std.Io.Writer, day: types.ForecastDay, options: RenderO
     try date_time.gofmt(date_stream.writer(), "Mon _2 Jan");
     const date_len = date_stream.pos;
 
-    try w.writeAll("                                                       ┌─────────────┐\n");
-    try w.print("┌──────────────────────────────┬───────────────────────┤  {s} ├───────────────────────┬──────────────────────────────┐\n", .{
-        date_str[0..date_len],
-    });
-    try w.writeAll("│            Morning           │             Noon      └──────┬──────┘     Evening           │             Night            │\n");
-    try w.writeAll("├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤\n");
+    if (!options.narrow) {
+        try w.writeAll("                                                       ┌─────────────┐\n");
+        try w.print("┌──────────────────────────────┬───────────────────────┤  {s} ├───────────────────────┬──────────────────────────────┐\n", .{
+            date_str[0..date_len],
+        });
+        try w.writeAll("│            Morning           │             Noon      └──────┬──────┘     Evening           │             Night            │\n");
+        try w.writeAll("├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤\n");
+    } else {
+        // narrow mode
+        try w.writeAll("                        ┌─────────────┐\n");
+        try w.print("┌───────────────────────┤  {s} ├──────────────────────┐\n", .{
+            date_str[0..date_len],
+        });
+        try w.writeAll("│             Noon      └──────┬──────┘      Night           │\n");
+        try w.writeAll("├──────────────────────────────┼─────────────────────────────┤\n");
+    }
 
+    const last_cell: u3 = if (options.narrow) 2 else 4;
     for (0..5) |line| {
         try w.writeAll("│ ");
         for (selected_hours[0..4], 0..) |maybe_hour, i| {
+            if (options.narrow and i % 2 == 0) continue;
             if (maybe_hour) |hour|
                 try renderHourlyCell(w, hour, line, options)
             else
                 try w.splatByteAll(' ', total_cell_width);
-            if (i < 3) {
+            if (i < last_cell - 1) {
                 try w.writeAll(" │ ");
             } else {
                 try w.writeAll(" │");
@@ -249,7 +265,10 @@ fn renderForecastDay(w: *std.Io.Writer, day: types.ForecastDay, options: RenderO
         try w.writeAll("\n");
     }
 
-    try w.writeAll("└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘\n");
+    if (!options.narrow)
+        try w.writeAll("└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘\n")
+    else
+        try w.writeAll("└──────────────────────────────┴─────────────────────────────┘\n");
 }
 
 const total_cell_width = 28;