aligned ansi/initial html implementation
This commit is contained in:
parent
df4f84f603
commit
a314fc39d4
4 changed files with 283 additions and 168 deletions
|
|
@ -1,123 +0,0 @@
|
|||
const std = @import("std");
|
||||
const Format = @import("formatted.zig").Format;
|
||||
|
||||
pub const InvisibleByteCountingWriter = struct {
|
||||
child: *std.Io.Writer,
|
||||
invisible_bytes: usize = 0,
|
||||
format: Format,
|
||||
writer: std.Io.Writer,
|
||||
|
||||
pub fn init(child: *std.Io.Writer, format: Format, buffer: []u8) InvisibleByteCountingWriter {
|
||||
return .{
|
||||
.child = child,
|
||||
.format = format,
|
||||
.writer = .{
|
||||
.vtable = &.{
|
||||
.drain = drain,
|
||||
},
|
||||
.buffer = buffer,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
|
||||
const self: *InvisibleByteCountingWriter = @alignCast(@fieldParentPtr("writer", w));
|
||||
|
||||
// Count invisible bytes in buffered data
|
||||
self.invisible_bytes += countInvisible(w.buffered(), self.format);
|
||||
|
||||
// Count invisible bytes in incoming data
|
||||
for (data[0 .. data.len - 1]) |bytes| {
|
||||
self.invisible_bytes += countInvisible(bytes, self.format);
|
||||
}
|
||||
const pattern = data[data.len - 1];
|
||||
for (0..splat) |_| {
|
||||
self.invisible_bytes += countInvisible(pattern, self.format);
|
||||
}
|
||||
|
||||
// Pass through to child writer
|
||||
return self.child.writeSplatHeader(w.buffered(), data, splat);
|
||||
}
|
||||
|
||||
fn countInvisible(bytes: []const u8, format: Format) usize {
|
||||
var count: usize = 0;
|
||||
var i: usize = 0;
|
||||
|
||||
switch (format) {
|
||||
.plain_text => {
|
||||
// Count multi-byte UTF-8 continuation bytes
|
||||
for (bytes) |byte| {
|
||||
if (byte & 0xC0 == 0x80) count += 1;
|
||||
}
|
||||
return count;
|
||||
},
|
||||
.ansi => {
|
||||
while (i < bytes.len) {
|
||||
if (bytes[i] == '\x1b' and i + 1 < bytes.len and bytes[i + 1] == '[') {
|
||||
const start = i;
|
||||
i += 2;
|
||||
while (i < bytes.len and bytes[i] != 'm') : (i += 1) {}
|
||||
if (i < bytes.len) i += 1;
|
||||
count += i - start;
|
||||
} else {
|
||||
// Also count UTF-8 continuation bytes
|
||||
if (bytes[i] & 0xC0 == 0x80) count += 1;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
.html => {
|
||||
while (i < bytes.len) {
|
||||
if (bytes[i] == '<') {
|
||||
const start = i;
|
||||
while (i < bytes.len and bytes[i] != '>') : (i += 1) {}
|
||||
if (i < bytes.len) i += 1;
|
||||
count += i - start;
|
||||
} else {
|
||||
// Also count UTF-8 continuation bytes
|
||||
if (bytes[i] & 0xC0 == 0x80) count += 1;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
};
|
||||
|
||||
test "countInvisible - plain text with UTF-8" {
|
||||
const testing = std.testing;
|
||||
try testing.expectEqual(0, InvisibleByteCountingWriter.countInvisible("hello", .plain_text));
|
||||
try testing.expectEqual(1, InvisibleByteCountingWriter.countInvisible("°C", .plain_text)); // ° is 2 bytes
|
||||
try testing.expectEqual(2, InvisibleByteCountingWriter.countInvisible("↑", .plain_text)); // ↑ is 3 bytes
|
||||
try testing.expectEqual(3, InvisibleByteCountingWriter.countInvisible("°C ↑", .plain_text));
|
||||
}
|
||||
|
||||
test "countInvisible - ANSI escape sequences" {
|
||||
const testing = std.testing;
|
||||
try testing.expectEqual(0, InvisibleByteCountingWriter.countInvisible("hello", .ansi));
|
||||
try testing.expectEqual(14, InvisibleByteCountingWriter.countInvisible("\x1b[38;5;82mhello\x1b[0m", .ansi));
|
||||
try testing.expectEqual(1, InvisibleByteCountingWriter.countInvisible("°C", .ansi)); // UTF-8 still counted
|
||||
try testing.expectEqual(15, InvisibleByteCountingWriter.countInvisible("\x1b[38;5;82m°C\x1b[0m", .ansi));
|
||||
}
|
||||
|
||||
test "countInvisible - HTML tags" {
|
||||
const testing = std.testing;
|
||||
try testing.expectEqual(0, InvisibleByteCountingWriter.countInvisible("hello", .html));
|
||||
try testing.expectEqual(25, InvisibleByteCountingWriter.countInvisible("<span class=\"c82\">hello</span>", .html));
|
||||
try testing.expectEqual(1, InvisibleByteCountingWriter.countInvisible("°C", .html)); // UTF-8 still counted
|
||||
}
|
||||
|
||||
test "InvisibleByteCountingWriter - integration" {
|
||||
const testing = std.testing;
|
||||
var buf: [64]u8 = undefined;
|
||||
var fixed = std.Io.Writer.fixed(&buf);
|
||||
var counting = InvisibleByteCountingWriter.init(&fixed, .ansi, &.{});
|
||||
|
||||
try counting.writer.writeAll("test");
|
||||
try counting.writer.print("\x1b[38;5;82m{d}\x1b[0m", .{42});
|
||||
try counting.writer.flush();
|
||||
|
||||
try testing.expectEqual(14, counting.invisible_bytes); // 14 for ANSI codes only
|
||||
}
|
||||
|
|
@ -11,9 +11,47 @@ fn degreeToArrow(deg: f32) []const u8 {
|
|||
pub const Format = enum {
|
||||
plain_text,
|
||||
ansi,
|
||||
html, // TODO: implement
|
||||
html,
|
||||
};
|
||||
|
||||
fn countInvisible(bytes: []const u8, format: Format) usize {
|
||||
var count: usize = 0;
|
||||
var i: usize = 0;
|
||||
|
||||
switch (format) {
|
||||
.plain_text => for (bytes) |byte| { // Count multi-byte UTF-8 continuation bytes
|
||||
if (byte & 0xC0 == 0x80) count += 1;
|
||||
},
|
||||
.ansi => while (i < bytes.len) {
|
||||
if (bytes[i] == '\x1b' and i + 1 < bytes.len and bytes[i + 1] == '[') {
|
||||
const start = i;
|
||||
i += 2;
|
||||
while (i < bytes.len and bytes[i] != 'm') : (i += 1) {}
|
||||
if (i < bytes.len) i += 1;
|
||||
count += i - start;
|
||||
} else {
|
||||
// Also count UTF-8 continuation bytes
|
||||
if (bytes[i] & 0xC0 == 0x80) count += 1;
|
||||
i += 1;
|
||||
}
|
||||
},
|
||||
.html => while (i < bytes.len) {
|
||||
if (bytes[i] == '<') {
|
||||
const start = i;
|
||||
while (i < bytes.len and bytes[i] != '>') : (i += 1) {}
|
||||
if (i < bytes.len) i += 1;
|
||||
count += i - start;
|
||||
} else {
|
||||
// Also count UTF-8 continuation bytes
|
||||
if (bytes[i] & 0xC0 == 0x80) count += 1;
|
||||
i += 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
pub const RenderOptions = struct {
|
||||
narrow: bool = false,
|
||||
days: u8 = 3,
|
||||
|
|
@ -54,47 +92,67 @@ fn renderCurrent(w: *std.Io.Writer, current: types.CurrentCondition, options: Re
|
|||
|
||||
const art = getWeatherArt(current.weather_code);
|
||||
const sign: u8 = if (temp >= 0) '+' else '-';
|
||||
const abs_temp = if (temp >= 0) temp else -temp;
|
||||
const abs_temp = @abs(temp);
|
||||
const fl_sign: u8 = if (feels_like >= 0) '+' else '-';
|
||||
const abs_fl = if (feels_like >= 0) feels_like else -feels_like;
|
||||
const abs_fl = @abs(feels_like);
|
||||
|
||||
if (options.format == .plain_text) {
|
||||
try w.print("{s} {s}\n", .{ art[0], current.condition });
|
||||
try w.print("{s} {c}{d:.0}({c}{d:.0}) {s}\n", .{ art[1], sign, abs_temp, fl_sign, abs_fl, temp_unit });
|
||||
try w.print("{s} {s} {d:.0} {s}\n", .{ art[2], degreeToArrow(current.wind_deg), wind_speed, wind_unit });
|
||||
if (current.visibility_km) |_| {
|
||||
const visibility = if (options.use_imperial) current.visiblityMph().? else current.visibility_km.?;
|
||||
const vis_unit = if (options.use_imperial) "mi" else "km";
|
||||
try w.print("{s} {d:.0} {s}\n", .{ art[3], visibility, vis_unit });
|
||||
} else {
|
||||
try w.print("{s}\n", .{std.mem.trimRight(u8, art[3], " ")});
|
||||
}
|
||||
try w.print("{s} {d:.1} {s}\n", .{ art[4], precip, precip_unit });
|
||||
} else {
|
||||
const temp_color_code = tempColor(current.temp_c);
|
||||
const wind_color_code = windColor(current.wind_kph);
|
||||
const cloud_color = "\x1b[38;5;250m";
|
||||
const reset = "\x1b[0m";
|
||||
switch (options.format) {
|
||||
.plain_text => {
|
||||
try w.print("{s} {s}\n", .{ art[0], current.condition });
|
||||
try w.print("{s} {c}{d:.0}({c}{d:.0}) {s}\n", .{ art[1], sign, abs_temp, fl_sign, abs_fl, temp_unit });
|
||||
try w.print("{s} {s} {d:.0} {s}\n", .{ art[2], degreeToArrow(current.wind_deg), wind_speed, wind_unit });
|
||||
if (current.visibility_km) |_| {
|
||||
const visibility = if (options.use_imperial) current.visiblityMph().? else current.visibility_km.?;
|
||||
const vis_unit = if (options.use_imperial) "mi" else "km";
|
||||
try w.print("{s} {d:.0} {s}\n", .{ art[3], visibility, vis_unit });
|
||||
} else {
|
||||
try w.print("{s}\n", .{std.mem.trimRight(u8, art[3], " ")});
|
||||
}
|
||||
try w.print("{s} {d:.1} {s}\n", .{ art[4], precip, precip_unit });
|
||||
},
|
||||
.ansi => {
|
||||
const temp_color_code = tempColor(current.temp_c);
|
||||
const wind_color_code = windColor(current.wind_kph);
|
||||
const cloud_color = "\x1b[38;5;250m";
|
||||
const reset = "\x1b[0m";
|
||||
|
||||
try w.print("{s}{s}{s} {s}\n", .{ cloud_color, art[0], reset, current.condition });
|
||||
try w.print("{s}{s}{s} \x1b[38;5;{d}m{c}{d:.0}({c}{d:.0}){s} {s}\n", .{ cloud_color, art[1], reset, temp_color_code, sign, abs_temp, fl_sign, abs_fl, reset, temp_unit });
|
||||
try w.print("{s}{s}{s} {s} \x1b[38;5;{d}m{d:.0}{s} {s}\n", .{ cloud_color, art[2], reset, degreeToArrow(current.wind_deg), wind_color_code, wind_speed, reset, wind_unit });
|
||||
if (current.visibility_km) |_| {
|
||||
const visibility = if (options.use_imperial) current.visiblityMph().? else current.visibility_km.?;
|
||||
const vis_unit = if (options.use_imperial) "mi" else "km";
|
||||
try w.print("{s}{s}{s} {d:.0} {s}\n", .{ cloud_color, art[3], reset, visibility, vis_unit });
|
||||
} else {
|
||||
try w.print("{s}{s}{s}\n", .{ cloud_color, std.mem.trimRight(u8, art[3], " "), reset });
|
||||
}
|
||||
try w.print("{s}{s}{s} {d:.1} {s}\n", .{ cloud_color, art[4], reset, precip, precip_unit });
|
||||
try w.print("{s}{s}{s} {s}\n", .{ cloud_color, art[0], reset, current.condition });
|
||||
try w.print("{s}{s}{s} \x1b[38;5;{d}m{c}{d:.0}({c}{d:.0}){s} {s}\n", .{ cloud_color, art[1], reset, temp_color_code, sign, abs_temp, fl_sign, abs_fl, reset, temp_unit });
|
||||
try w.print("{s}{s}{s} {s} \x1b[38;5;{d}m{d:.0}{s} {s}\n", .{ cloud_color, art[2], reset, degreeToArrow(current.wind_deg), wind_color_code, wind_speed, reset, wind_unit });
|
||||
if (current.visibility_km) |_| {
|
||||
const visibility = if (options.use_imperial) current.visiblityMph().? else current.visibility_km.?;
|
||||
const vis_unit = if (options.use_imperial) "mi" else "km";
|
||||
try w.print("{s}{s}{s} {d:.0} {s}\n", .{ cloud_color, art[3], reset, visibility, vis_unit });
|
||||
} else {
|
||||
try w.print("{s}{s}{s}\n", .{ cloud_color, std.mem.trimRight(u8, art[3], " "), reset });
|
||||
}
|
||||
try w.print("{s}{s}{s} {d:.1} {s}\n", .{ cloud_color, art[4], reset, precip, precip_unit });
|
||||
},
|
||||
.html => {
|
||||
const temp_color = ansiToHex(tempColor(current.temp_c));
|
||||
const wind_color = ansiToHex(windColor(current.wind_kph));
|
||||
const cloud_color = "#bcbcbc"; // 250
|
||||
|
||||
try w.print("<span style=\"color:{s}\">{s}</span> {s}\n", .{ cloud_color, art[0], current.condition });
|
||||
try w.print("<span style=\"color:{s}\">{s}</span> <span style=\"color:{s}\">{c}{d:.0}({c}{d:.0})</span> {s}\n", .{ cloud_color, art[1], temp_color, sign, abs_temp, fl_sign, abs_fl, temp_unit });
|
||||
try w.print("<span style=\"color:{s}\">{s}</span> {s} <span style=\"color:{s}\">{d:.0}</span> {s}\n", .{ cloud_color, art[2], degreeToArrow(current.wind_deg), wind_color, wind_speed, wind_unit });
|
||||
if (current.visibility_km) |_| {
|
||||
const visibility = if (options.use_imperial) current.visiblityMph().? else current.visibility_km.?;
|
||||
const vis_unit = if (options.use_imperial) "mi" else "km";
|
||||
try w.print("<span style=\"color:{s}\">{s}</span> {d:.0} {s}\n", .{ cloud_color, art[3], visibility, vis_unit });
|
||||
} else {
|
||||
try w.print("<span style=\"color:{s}\">{s}</span>\n", .{ cloud_color, std.mem.trimRight(u8, art[3], " ") });
|
||||
}
|
||||
try w.print("<span style=\"color:{s}\">{s}</span> {d:.1} {s}\n", .{ cloud_color, art[4], precip, precip_unit });
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn renderForecastDay(w: *std.Io.Writer, day: types.ForecastDay, options: RenderOptions) !void {
|
||||
var date_str: [11]u8 = undefined;
|
||||
if (day.hourly.len < 4) {
|
||||
const max_temp = if (options.use_imperial) day.max_temp_c * 1.8 + 32 else day.max_temp_c;
|
||||
const min_temp = if (options.use_imperial) day.min_temp_c * 1.8 + 32 else day.min_temp_c;
|
||||
const max_temp = if (options.use_imperial) day.maxTempFahrenheit() else day.max_temp_c;
|
||||
const min_temp = if (options.use_imperial) day.minTempFahrenheit() else day.min_temp_c;
|
||||
const temp_unit = if (options.use_imperial) "°F" else "°C";
|
||||
const art = getWeatherArt(day.weather_code);
|
||||
|
||||
|
|
@ -140,7 +198,6 @@ fn renderHourlyCell(w: *std.Io.Writer, hour: types.HourlyForecast, line: usize,
|
|||
visibility = 3,
|
||||
precipitation = 4,
|
||||
};
|
||||
const InvisibleByteCountingWriter = @import("InvisibleByteCountingWriter.zig").InvisibleByteCountingWriter;
|
||||
|
||||
const art = getWeatherArt(hour.weather_code);
|
||||
|
||||
|
|
@ -148,30 +205,77 @@ fn renderHourlyCell(w: *std.Io.Writer, hour: types.HourlyForecast, line: usize,
|
|||
const art_width = 14; // includes spacer between art and data. This is display width, not actual
|
||||
var buf: [64]u8 = undefined; // We need more than total_width because total_width is display width, not bytes
|
||||
var cell_writer = std.Io.Writer.fixed(&buf);
|
||||
var counting_writer = InvisibleByteCountingWriter.init(&cell_writer, options.format, &.{});
|
||||
const cw = &counting_writer.writer;
|
||||
const cw = &cell_writer;
|
||||
|
||||
try w.print("{s} ", .{art[line]});
|
||||
switch (options.format) {
|
||||
.ansi => {
|
||||
try w.print("\x1b[38;5;250m{s}\x1b[0m ", .{art[line]});
|
||||
},
|
||||
.html => {
|
||||
try w.print("<span style=\"color:#bcbcbc\">{s}</span> ", .{art[line]});
|
||||
},
|
||||
.plain_text => {
|
||||
try w.print("{s} ", .{art[line]});
|
||||
},
|
||||
}
|
||||
|
||||
switch (@as(Line, @enumFromInt(line))) {
|
||||
.condition => {
|
||||
try cw.writeAll(hour.condition);
|
||||
},
|
||||
.temperature => {
|
||||
const temp = if (options.use_imperial) hour.temp_c * 1.8 + 32 else hour.temp_c;
|
||||
const feels_like = if (options.use_imperial) hour.feels_like_c * 1.8 + 32 else hour.feels_like_c;
|
||||
const temp = if (options.use_imperial) hour.tempFahrenheit() else hour.temp_c;
|
||||
const feels_like = if (options.use_imperial) hour.feelsLikeFahrenheit() else hour.feels_like_c;
|
||||
const temp_unit = if (options.use_imperial) "°F" else "°C";
|
||||
const sign: u8 = if (temp >= 0) '+' else '-';
|
||||
const abs_temp = if (temp >= 0) temp else -temp;
|
||||
const abs_temp = @abs(temp);
|
||||
const fl_sign: u8 = if (feels_like >= 0) '+' else '-';
|
||||
const abs_fl = if (feels_like >= 0) feels_like else -feels_like;
|
||||
try cw.print("{c}{d:.0}({c}{d:.0}) {s}", .{ sign, abs_temp, fl_sign, abs_fl, temp_unit });
|
||||
const abs_fl = @abs(feels_like);
|
||||
switch (options.format) {
|
||||
.ansi => {
|
||||
const color = tempColor(hour.temp_c);
|
||||
try cw.print("\x1b[38;5;{d}m{c}{d:.0}({c}{d:.0})\x1b[0m {s}", .{
|
||||
color,
|
||||
sign,
|
||||
abs_temp,
|
||||
fl_sign,
|
||||
abs_fl,
|
||||
temp_unit,
|
||||
});
|
||||
},
|
||||
.html => {
|
||||
const color = ansiToHex(tempColor(hour.temp_c));
|
||||
try cw.print("<span style=\"color:{s}\">{c}{d:.0}({c}{d:.0})</span> {s}", .{
|
||||
color,
|
||||
sign,
|
||||
abs_temp,
|
||||
fl_sign,
|
||||
abs_fl,
|
||||
temp_unit,
|
||||
});
|
||||
},
|
||||
.plain_text => {
|
||||
try cw.print("{c}{d:.0}({c}{d:.0}) {s}", .{ sign, abs_temp, fl_sign, abs_fl, temp_unit });
|
||||
},
|
||||
}
|
||||
},
|
||||
.wind => {
|
||||
const wind_speed = if (options.use_imperial) hour.windMph() else hour.wind_kph;
|
||||
const wind_unit = if (options.use_imperial) "mph" else "km/h";
|
||||
const arrow = degreeToArrow(hour.wind_deg);
|
||||
try cw.print("{s} {d:.0} {s}", .{ arrow, wind_speed, wind_unit });
|
||||
switch (options.format) {
|
||||
.ansi => {
|
||||
const color = windColor(hour.wind_kph);
|
||||
try cw.print("{s} \x1b[38;5;{d}m{d:.0}\x1b[0m {s}", .{ arrow, color, wind_speed, wind_unit });
|
||||
},
|
||||
.html => {
|
||||
const color = ansiToHex(windColor(hour.wind_kph));
|
||||
try cw.print("{s} <span style=\"color:{s}\">{d:.0}</span> {s}", .{ arrow, color, wind_speed, wind_unit });
|
||||
},
|
||||
.plain_text => {
|
||||
try cw.print("{s} {d:.0} {s}", .{ arrow, wind_speed, wind_unit });
|
||||
},
|
||||
}
|
||||
},
|
||||
.visibility => {
|
||||
if (hour.visibility_km) |_| {
|
||||
|
|
@ -188,9 +292,13 @@ fn renderHourlyCell(w: *std.Io.Writer, hour: types.HourlyForecast, line: usize,
|
|||
}
|
||||
try cw.flush();
|
||||
const buffered = cell_writer.buffered();
|
||||
const display_width = art_width + buffered.len - counting_writer.invisible_bytes;
|
||||
const invisible = countInvisible(buffered, options.format);
|
||||
const display_width = art_width + buffered.len - invisible;
|
||||
try w.writeAll(buffered);
|
||||
try w.splatByteAll(' ', @max(@as(isize, total_width) - @as(isize, @intCast(display_width)), 0));
|
||||
try w.splatByteAll(
|
||||
' ',
|
||||
@max(@as(isize, @intCast(total_width)) - @as(isize, @intCast(display_width)), 0),
|
||||
);
|
||||
}
|
||||
|
||||
const DateFormat = enum {
|
||||
|
|
@ -267,6 +375,39 @@ fn windColor(wind_kph: f32) u8 {
|
|||
return 196;
|
||||
}
|
||||
|
||||
fn ansiToHex(code: u8) []const u8 {
|
||||
return switch (code) {
|
||||
21 => "#0000ff",
|
||||
27 => "#005fff",
|
||||
33 => "#0087ff",
|
||||
39 => "#00afff",
|
||||
45 => "#00d7ff",
|
||||
46 => "#00ffff",
|
||||
47 => "#00ffd7",
|
||||
48 => "#00ffaf",
|
||||
49 => "#00ff87",
|
||||
50 => "#00ff5f",
|
||||
51 => "#00ff00",
|
||||
82 => "#5fff00",
|
||||
118 => "#87ff00",
|
||||
154 => "#afff00",
|
||||
190 => "#d7ff00",
|
||||
196 => "#ff0000",
|
||||
202 => "#ff5f00",
|
||||
208 => "#ff8700",
|
||||
214 => "#ffaf00",
|
||||
220 => "#ffd700",
|
||||
226 => "#ffff00",
|
||||
241 => "#626262",
|
||||
242 => "#6c6c6c",
|
||||
243 => "#767676",
|
||||
246 => "#949494",
|
||||
250 => "#bcbcbc",
|
||||
253 => "#dadada",
|
||||
else => "#ffffff",
|
||||
};
|
||||
}
|
||||
|
||||
fn getWeatherArt(code: types.WeatherCode) [5][]const u8 {
|
||||
return switch (@intFromEnum(code)) {
|
||||
800 => .{ // Clear
|
||||
|
|
@ -750,3 +891,56 @@ test "plain text format - MetNo real data" {
|
|||
|
||||
try std.testing.expectEqualStrings(expected, output);
|
||||
}
|
||||
test "ansi format - MetNo real data - phoenix" {
|
||||
const allocator = std.testing.allocator;
|
||||
const MetNo = @import("../weather/MetNo.zig");
|
||||
|
||||
const json_data = @embedFile("../tests/metno-phoenix.json");
|
||||
|
||||
const weather_data = try MetNo.parse(undefined, allocator, json_data);
|
||||
defer weather_data.deinit();
|
||||
|
||||
const output = try render(allocator, weather_data, .{ .format = .ansi, .days = 3, .use_imperial = true });
|
||||
defer allocator.free(output);
|
||||
|
||||
// const file = try std.fs.cwd().createFile("/tmp/formatted_output.txt", .{});
|
||||
// defer file.close();
|
||||
// try file.writeAll(output);
|
||||
|
||||
const expected = @embedFile("../tests/metno-phoenix-tmp.ansi");
|
||||
|
||||
try std.testing.expectEqualStrings(expected, output);
|
||||
}
|
||||
|
||||
test "countInvisible - plain text with UTF-8" {
|
||||
const testing = std.testing;
|
||||
try testing.expectEqual(0, countInvisible("hello", .plain_text));
|
||||
try testing.expectEqual(1, countInvisible("°C", .plain_text)); // ° is 2 bytes
|
||||
try testing.expectEqual(2, countInvisible("↑", .plain_text)); // ↑ is 3 bytes
|
||||
try testing.expectEqual(3, countInvisible("°C ↑", .plain_text));
|
||||
}
|
||||
|
||||
test "countInvisible - ANSI escape sequences" {
|
||||
const testing = std.testing;
|
||||
try testing.expectEqual(0, countInvisible("hello", .ansi));
|
||||
try testing.expectEqual(14, countInvisible("\x1b[38;5;82mhello\x1b[0m", .ansi));
|
||||
try testing.expectEqual(1, countInvisible("°C", .ansi)); // UTF-8 still counted
|
||||
try testing.expectEqual(15, countInvisible("\x1b[38;5;82m°C\x1b[0m", .ansi));
|
||||
}
|
||||
|
||||
test "countInvisible - HTML tags" {
|
||||
const testing = std.testing;
|
||||
try testing.expectEqual(0, countInvisible("hello", .html));
|
||||
try testing.expectEqual(25, countInvisible("<span class=\"c82\">hello</span>", .html));
|
||||
try testing.expectEqual(1, countInvisible("°C", .html)); // UTF-8 still counted
|
||||
}
|
||||
|
||||
test "countInvisible - ansi full string" {
|
||||
const str = "\x1b[38;5;250m .--. \x1b[0m \x1b[38;5;154m+61(+61)\x1b[0m °F ";
|
||||
try std.testing.expectEqual(28, str.len - countInvisible(str, .ansi));
|
||||
}
|
||||
|
||||
test "countInvisible - ansi formatted" {
|
||||
const str = "\x1b[38;5;154m+61(+61)\x1b[0m °F";
|
||||
try std.testing.expectEqual(11, str.len - countInvisible(str, .ansi));
|
||||
}
|
||||
|
|
|
|||
38
src/tests/metno-phoenix-tmp.ansi
Normal file
38
src/tests/metno-phoenix-tmp.ansi
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
Weather report: 33.4484,-112.0741
|
||||
|
||||
[38;5;250m [0m Cloudy
|
||||
[38;5;250m .--. [0m [38;5;154m+61(+61)[0m °F
|
||||
[38;5;250m .-( ). [0m ↘ [38;5;242m2[0m mph
|
||||
[38;5;250m (___.__)__)[0m
|
||||
[38;5;250m [0m 0.0 in
|
||||
|
||||
┌─────────────┐
|
||||
┌──────────────────────────────┬───────────────────────┤ Sun 4 Jan ├───────────────────────┬──────────────────────────────┐
|
||||
│ Morning │ Noon └──────┬──────┘ Evening │ Night │
|
||||
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
|
||||
│ [38;5;250m [0m Cloudy │ [38;5;250m \ / [0m Partly cloudy │ [38;5;250m \ / [0m Partly cloudy │ [38;5;250m \ / [0m Clear │
|
||||
│ [38;5;250m .--. [0m [38;5;154m+61(+61)[0m °F │ [38;5;250m _ /"".-. [0m [38;5;118m+56(+56)[0m °F │ [38;5;250m _ /"".-. [0m [38;5;82m+50(+50)[0m °F │ [38;5;250m .-. [0m [38;5;190m+65(+65)[0m °F │
|
||||
│ [38;5;250m .-( ). [0m ↘ [38;5;242m2[0m mph │ [38;5;250m \_( ). [0m ↖ [38;5;241m1[0m mph │ [38;5;250m \_( ). [0m ↖ [38;5;242m3[0m mph │ [38;5;250m ― ( ) ― [0m ↖ [38;5;242m3[0m mph │
|
||||
│ [38;5;250m (___.__)__) [0m │ [38;5;250m /(___(__) [0m │ [38;5;250m /(___(__) [0m │ [38;5;250m `-' [0m │
|
||||
│ [38;5;250m [0m 0.0 in | 0% │ [38;5;250m [0m 0.0 in | 0% │ [38;5;250m [0m 0.0 in | 0% │ [38;5;250m / \ [0m 0.0 in | 0% │
|
||||
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
|
||||
┌─────────────┐
|
||||
┌──────────────────────────────┬───────────────────────┤ Mon 5 Jan ├───────────────────────┬──────────────────────────────┐
|
||||
│ Morning │ Noon └──────┬──────┘ Evening │ Night │
|
||||
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
|
||||
│ [38;5;250m \ / [0m Fair │ [38;5;250m \ / [0m Partly cloudy │ [38;5;250m \ / [0m Partly cloudy │ [38;5;250m \ / [0m Partly cloudy │
|
||||
│ [38;5;250m _ /"".-. [0m [38;5;190m+68(+68)[0m °F │ [38;5;250m _ /"".-. [0m [38;5;118m+57(+57)[0m °F │ [38;5;250m _ /"".-. [0m [38;5;82m+53(+53)[0m °F │ [38;5;250m _ /"".-. [0m [38;5;154m+61(+61)[0m °F │
|
||||
│ [38;5;250m \_( ). [0m ↙ [38;5;241m2[0m mph │ [38;5;250m \_( ). [0m ↖ [38;5;242m3[0m mph │ [38;5;250m \_( ). [0m ↘ [38;5;242m2[0m mph │ [38;5;250m \_( ). [0m ← [38;5;242m3[0m mph │
|
||||
│ [38;5;250m /(___(__) [0m │ [38;5;250m /(___(__) [0m │ [38;5;250m /(___(__) [0m │ [38;5;250m /(___(__) [0m │
|
||||
│ [38;5;250m [0m 0.0 in | 0% │ [38;5;250m [0m 0.0 in | 0% │ [38;5;250m [0m 0.0 in | 0% │ [38;5;250m [0m 0.0 in | 0% │
|
||||
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
|
||||
┌─────────────┐
|
||||
┌──────────────────────────────┬───────────────────────┤ Tue 6 Jan ├───────────────────────┬──────────────────────────────┐
|
||||
│ Morning │ Noon └──────┬──────┘ Evening │ Night │
|
||||
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
|
||||
│ [38;5;250m [0m Cloudy │ [38;5;250m \ / [0m Partly cloudy │ [38;5;250m \ / [0m Partly cloudy │ [38;5;250m \ / [0m Partly cloudy │
|
||||
│ [38;5;250m .--. [0m [38;5;190m+66(+66)[0m °F │ [38;5;250m _ /"".-. [0m [38;5;118m+58(+58)[0m °F │ [38;5;250m _ /"".-. [0m [38;5;82m+53(+53)[0m °F │ [38;5;250m _ /"".-. [0m [38;5;82m+49(+49)[0m °F │
|
||||
│ [38;5;250m .-( ). [0m ↘ [38;5;242m2[0m mph │ [38;5;250m \_( ). [0m ← [38;5;242m3[0m mph │ [38;5;250m \_( ). [0m ← [38;5;242m2[0m mph │ [38;5;250m \_( ). [0m ← [38;5;241m2[0m mph │
|
||||
│ [38;5;250m (___.__)__) [0m │ [38;5;250m /(___(__) [0m │ [38;5;250m /(___(__) [0m │ [38;5;250m /(___(__) [0m │
|
||||
│ [38;5;250m [0m 0.0 in | 0% │ [38;5;250m [0m 0.0 in | 0% │ [38;5;250m [0m 0.0 in | 0% │ [38;5;250m [0m 0.0 in | 0% │
|
||||
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
|
||||
|
|
@ -161,6 +161,12 @@ pub const HourlyForecast = struct {
|
|||
precip_mm: f32,
|
||||
visibility_km: ?f32,
|
||||
|
||||
pub fn tempFahrenheit(self: HourlyForecast) f32 {
|
||||
return celsiusToFahrenheit(self.temp_c);
|
||||
}
|
||||
pub fn feelsLikeFahrenheit(self: HourlyForecast) f32 {
|
||||
return celsiusToFahrenheit(self.feels_like_c);
|
||||
}
|
||||
pub fn windMph(self: HourlyForecast) f32 {
|
||||
return self.wind_kph * miles_per_km;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue