implement view options n,q,Q,0,1,2

This commit is contained in:
Emil Lerch 2026-01-06 15:34:17 -08:00
parent 5e1c2fb44c
commit 98747a3d30
Signed by: lobo
GPG key ID: A7B62D657EF764F8
4 changed files with 81 additions and 53 deletions

View file

@ -153,20 +153,21 @@ fn handleWeatherInternal(
if (params.lang) |l| req_alloc.free(l); if (params.lang) |l| req_alloc.free(l);
} }
var render_options = params.render_options;
// Determine if imperial units should be used // Determine if imperial units should be used
// Priority: explicit ?u or ?m > lang=us > US IP > default metric // Priority: explicit ?u or ?m > lang=us > US IP > default metric
const use_imperial = blk: { if (params.use_imperial == null) {
if (params.units) |u| // User did not ask for anything explicitly
break :blk u == .uscs;
if (params.lang) |lang| // Check if lang=us
if (params.lang) |lang| {
if (std.mem.eql(u8, lang, "us")) 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)) if (!render_options.use_imperial and client_ip.len > 0 and opts.geoip.isUSIp(client_ip))
break :blk true; render_options.use_imperial = true; // this is a US IP
break :blk false; }
};
// Add coordinates header using response allocator // 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 }); 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); break :blk try json.render(req_alloc, weather);
} }
if (std.mem.eql(u8, fmt, "v2")) 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, "%")) 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 // 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 { } else {
const format: formatted.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}",
.{ 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; if (render_options.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 }); 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 allocator = std.testing.allocator;
const params_u = try QueryParams.parse(allocator, "u"); 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"); 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"); const params_lang = try QueryParams.parse(allocator, "lang=us");
defer allocator.free(params_lang.lang.?); defer allocator.free(params_lang.lang.?);

View file

@ -38,10 +38,10 @@ pub const help_page =
\\ 2 # current weather + today's + tomorrow's forecast \\ 2 # current weather + today's + tomorrow's forecast
\\ A # ignore User-Agent and force ANSI output format (terminal) \\ A # ignore User-Agent and force ANSI output format (terminal)
\\ d # * restrict output to standard console font glyphs \\ 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) \\ F # do not show the "Follow" line (not necessary - this version does not have a follow line)
\\ n # * narrow version (only day and night) \\ n # narrow version (only day and night)
\\ q # * quiet version (no "Weather report" text) \\ q # quiet version (no "Weather report" text)
\\ Q # * superquiet version (no "Weather report", no city name) \\ Q # superquiet version (no "Weather report", no city name)
\\ T # switch terminal sequences off (no colors) \\ T # switch terminal sequences off (no colors)
\\ \\
\\PNG options: \\PNG options:

View file

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const RenderOptions = @import("../render/formatted.zig").RenderOptions;
///Units: ///Units:
/// ///
@ -22,22 +23,21 @@ pub const QueryParams = struct {
format: ?[]const u8 = null, format: ?[]const u8 = null,
lang: ?[]const u8 = null, lang: ?[]const u8 = null,
location: ?[]const u8 = null, location: ?[]const u8 = null,
units: ?Units = null,
transparency: ?u8 = null, transparency: ?u8 = null,
/// 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
text_only: bool = false, text_only: bool = false,
/// This is necessary because it it imporant to know if the user explicitly
pub const Units = enum { /// requested imperial/metric
metric, use_imperial: ?bool = null,
uscs, render_options: RenderOptions,
};
pub fn parse(allocator: std.mem.Allocator, query_string: []const u8) !QueryParams { 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 iter = std.mem.splitScalar(u8, query_string, '&');
var render_options = RenderOptions{};
while (iter.next()) |pair| { while (iter.next()) |pair| {
if (pair.len == 0) continue; if (pair.len == 0) continue;
@ -47,8 +47,14 @@ pub const QueryParams = struct {
if (key.len == 1) { if (key.len == 1) {
switch (key[0]) { switch (key[0]) {
'u' => params.units = .uscs, '0' => render_options.days = 0,
'm' => params.units = .metric, '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, 'A' => params.ansi = true,
'T' => params.text_only = true, 'T' => params.text_only = true,
't' => params.transparency = 150, 't' => params.transparency = 150,
@ -62,9 +68,9 @@ pub const QueryParams = struct {
} else if (std.mem.eql(u8, key, "location")) { } else if (std.mem.eql(u8, key, "location")) {
params.location = if (value) |v| try allocator.dupe(u8, v) else null; params.location = if (value) |v| try allocator.dupe(u8, v) else null;
} else if (std.mem.eql(u8, key, "use_imperial")) { } 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")) { } else if (std.mem.eql(u8, key, "use_metric")) {
params.units = .metric; params.use_imperial = false;
} else if (std.mem.eql(u8, key, "transparency")) { } else if (std.mem.eql(u8, key, "transparency")) {
if (value) |v| { if (value) |v| {
params.transparency = try std.fmt.parseInt(u8, v, 10); 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; return params;
} }
}; };
@ -81,7 +89,7 @@ test "parse empty query" {
const params = try QueryParams.parse(allocator, ""); const params = try QueryParams.parse(allocator, "");
try std.testing.expect(params.format == null); try std.testing.expect(params.format == null);
try std.testing.expect(params.lang == 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" { test "parse format parameter" {
@ -97,28 +105,28 @@ test "parse units with question mark" {
// Test with just "u" (no question mark in query string) // Test with just "u" (no question mark in query string)
const params1 = try QueryParams.parse(allocator, "u"); 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) // Test with "u=" (empty value)
const params2 = try QueryParams.parse(allocator, "u="); 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 // Test combined with other params
const params3 = try QueryParams.parse(allocator, "format=3&u"); const params3 = try QueryParams.parse(allocator, "format=3&u");
defer if (params3.format) |f| allocator.free(f); 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" { test "parse units parameters" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const params_m = try QueryParams.parse(allocator, "m"); 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"); 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="); 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" { test "parse multiple parameters" {
@ -128,7 +136,7 @@ test "parse multiple parameters" {
defer if (params.lang) |l| allocator.free(l); defer if (params.lang) |l| allocator.free(l);
try std.testing.expectEqualStrings("3", params.format.?); try std.testing.expectEqualStrings("3", params.format.?);
try std.testing.expectEqualStrings("de", params.lang.?); 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" { test "parse transparency" {

View file

@ -99,9 +99,10 @@ fn countInvisible(bytes: []const u8, format: Format) usize {
pub const RenderOptions = struct { pub const RenderOptions = struct {
narrow: bool = false, narrow: bool = false,
quiet: bool = false,
super_quiet: bool = false,
days: u8 = 3, days: u8 = 3,
use_imperial: bool = false, use_imperial: bool = false,
no_caption: bool = false,
format: Format = .ansi, format: Format = .ansi,
}; };
@ -111,8 +112,11 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, options: Re
const w = &output.writer; const w = &output.writer;
if (options.format == .html) try w.writeAll("<pre>"); if (options.format == .html) try w.writeAll("<pre>");
if (!options.no_caption) if (!options.super_quiet)
try w.print("Weather report: {s}\n\n", .{data.locationDisplayName()}); try w.print(
"{s}{s}\n\n",
.{ if (!options.quiet) "Weather report: " else "", data.locationDisplayName() },
);
try renderCurrent(w, data.current, options); 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"); try date_time.gofmt(date_stream.writer(), "Mon _2 Jan");
const date_len = date_stream.pos; const date_len = date_stream.pos;
try w.writeAll(" ┌─────────────┐\n"); if (!options.narrow) {
try w.print("┌──────────────────────────────┬───────────────────────┤ {s} ├───────────────────────┬──────────────────────────────┐\n", .{ try w.writeAll(" ┌─────────────┐\n");
date_str[0..date_len], try w.print("┌──────────────────────────────┬───────────────────────┤ {s} ├───────────────────────┬──────────────────────────────┐\n", .{
}); date_str[0..date_len],
try w.writeAll("│ Morning │ Noon └──────┬──────┘ Evening │ Night │\n"); });
try w.writeAll("├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤\n"); 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| { for (0..5) |line| {
try w.writeAll(""); try w.writeAll("");
for (selected_hours[0..4], 0..) |maybe_hour, i| { for (selected_hours[0..4], 0..) |maybe_hour, i| {
if (options.narrow and i % 2 == 0) continue;
if (maybe_hour) |hour| if (maybe_hour) |hour|
try renderHourlyCell(w, hour, line, options) try renderHourlyCell(w, hour, line, options)
else else
try w.splatByteAll(' ', total_cell_width); try w.splatByteAll(' ', total_cell_width);
if (i < 3) { if (i < last_cell - 1) {
try w.writeAll(""); try w.writeAll("");
} else { } else {
try w.writeAll(""); 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");
} }
try w.writeAll("└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘\n"); if (!options.narrow)
try w.writeAll("└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘\n")
else
try w.writeAll("└──────────────────────────────┴─────────────────────────────┘\n");
} }
const total_cell_width = 28; const total_cell_width = 28;