Introduction Link to heading
Welcome to the final post in this series! Up until this point we have been doing mostly functional updates to our app. In this section, all of our functional updates are complete and the last thing we need to do is make our CLI look nice for the users. You can catch up by reading part 2 here
Note: The final code for this series is complete and lives in the git repo here. Feel free to take a look and if you want to look at the code yourself
Picking Up Where We Left Off Link to heading
At the end of part two, we have a functional CLI where we return the entire json response. This is functional but we want to clean up the output a bit to make it worth using. We are going to change color based on the type of weather and also provide certain emojis based on conditions and temperature.
Text Color Based On Weather Conditions Link to heading
The first update we are going to make is updating the color of the text based on the type of weather is happening at the location we looked up. We are going to use Chameleon
to generate the preset we want and then print a nicely formatted version of our weather data.
fn handleDifferentWeather(app: App, data: Root) !void {
var defaultPreset: Chameleon.RuntimeChameleon = undefined;
const normalizedTemperature = if (std.mem.eql(u8, "metric", app.units)) (data.data.values.temperature.? * 1.8) + 32 else data.data.values.temperature.?;
switch (data.data.values.weatherCode.?) {
1000, 1100 => {
defaultPreset = try app.c.bold().yellow().createPreset();
if (normalizedTemperature < 65.0) {
defaultPreset = try app.c.bold().blueBright().createPreset();
}
},
1101, 1102, 1001, 2000, 2100 => {
defaultPreset = try app.c.bold().dim().createPreset();
},
4000, 4001, 4200, 4201 => {
defaultPreset = try app.c.bold().blue().createPreset();
},
5000, 5001, 5100, 5101, 6000, 6001, 6200, 6201, 7000, 7101, 7102 => {
defaultPreset = try app.c.bold().white().createPreset();
},
8000 => {
defaultPreset = try app.c.bold().black().createPreset();
},
else => {
defaultPreset = try app.c.bold().green().createPreset();
},
}
try defaultPreset.printOut(" Weather For {s}:\n", .{
data.location.name,
});
try defaultPreset.printOut(" > Temperature: {d:.0}°", .{data.data.values.temperature.?});
if (std.mem.eql(u8, "imperial", app.units)) {
try defaultPreset.printOut("F\n", .{});
} else {
try defaultPreset.printOut("C\n", .{});
}
if (data.data.values.temperatureApparent.? != data.data.values.temperature.?) {
try defaultPreset.printOut(" > Real Feel: {d:.0}°", .{data.data.values.temperatureApparent.?});
if (std.mem.eql(u8, "imperial", app.units)) {
try defaultPreset.printOut("F\n", .{});
} else {
try defaultPreset.printOut("C\n", .{});
}
}
try defaultPreset.printOut(" > Wind Speed: {d:.0} m/s\n", .{data.data.values.windSpeed.?});
try defaultPreset.printOut(" > Chance Of Rain: {d:.0}%\n", .{data.data.values.precipitationProbability.?});
try defaultPreset.printOut(" > Humidity: {d:.0}%\n", .{data.data.values.humidity.?});
try defaultPreset.printOut(" > UV Index: {d:.0}\n", .{data.data.values.uvIndex.?});
}
Now we just have to replace our json.stringfy
to be a call to our new handleDifferentWeather
function.
fn fetchWeatherData(app: App) !void {
...
.
.
try handleDifferentWeather(app, parsed.value);
}
The important piece of logic here is the switch statement on the weather codes. We want to modify the Chamelon preset based on the values we have. In order to find out which colors to pick I took a look at the docs describing the weather codes here. I’m choosing to use a preset here so that we don’t have to keep formatting the output once we pick a color. Additionally, I chose to normalize the temperature to make any future comparisions easier for myself. If we run this now and try a few different place we should see the text change colors based on the current weather happening at those locations!
Right now we actually don’t print the matching type of weather based on the weather code. Let’s fix that by creating a hashmap that we can use to lookup the weather code returned and print the string. First we will create a function to build this hashmap and invoke it at the start of our CLI. Additionally to make this happen at compile time since its static we can make it a comptime function so we don’t have any overhead when setting this up when we just want to run the CLI without rebuilding it.
Comptime is a unique feature of zig that lets us force some code to be run at compile time. By default if nothing about a struct relies on runtime it will be comptime
and we don’t need to include the comptime
keyword in the definition of our object.
pub const WeatherCodes = struct {
// this is comptime by default since nothing about this struct relies on the runtime
const codes = [_]struct { code: u32, description: []const u8 }{
.{ .code = 0, .description = "unknown" },
.{ .code = 1000, .description = "clear" },
.{ .code = 1100, .description = "mostly clear" },
.{ .code = 1101, .description = "partly cloudy" },
.{ .code = 1102, .description = "mostly cloudy" },
.{ .code = 1001, .description = "cloudy" },
.{ .code = 2000, .description = "foggy" },
.{ .code = 2100, .description = "lightly foggy" },
.{ .code = 4000, .description = "drizzling" },
.{ .code = 4001, .description = "raining" },
.{ .code = 4200, .description = "light raining" },
.{ .code = 4201, .description = "heavy raining" },
.{ .code = 5000, .description = "snowing" },
.{ .code = 5001, .description = "flurring" },
.{ .code = 5100, .description = "light snowing" },
.{ .code = 5101, .description = "heavy snowing" },
.{ .code = 6000, .description = "freezing drizzle" },
.{ .code = 6001, .description = "freezing raining" },
.{ .code = 6200, .description = "light freezing raining" },
.{ .code = 6201, .description = "heavy freezing raining" },
.{ .code = 7000, .description = "ice pelleting" },
.{ .code = 7101, .description = "heavy ice pelleting" },
.{ .code = 7102, .description = "light ice pelleting" },
.{ .code = 8000, .description = "thunderstorming" },
};
pub fn getDescription(code: u32) ?[]const u8 {
// This will be optimized by the compiler into an efficient lookup
inline for (codes) |entry| {
if (entry.code == code) return entry.description;
}
return null;
}
};
Now we can just our function from earlier to use WeatherCodes.getDescription()
to return the string we want.
fn handleDifferentWeather(app: App, data: Root) !void {
...
.
.
.
const currentWeather = WeatherCodes.getDescription(weatherCode).?;
try defaultPreset.printOut(" > Currently it is {s} outside\n", .{ currentWeather });
.
.
}
Let’s take this a step further and also include emojis based on the weather and temperature. We are going to use that normalizedTemperature
value here so we dont have to do two different comparisions for each temperature range we want to define.
fn getEmojiTemperature(code: f64) []const u8 {
switch (@as(i32, @intFromFloat(code))) {
-150...32 => {
return "🥶";
},
33...60 => {
return "😬";
},
61...85 => {
return "😁";
},
else => {
return "🥵";
},
}
}
fn getEmojiWeather(code: u32) []const u8 {
switch (code) {
1000 => {
return "☀️";
},
1100 => {
return "🌤️";
},
1101 => {
return "⛅";
},
1102 => {
return "🌥️";
},
1001 => {
return "☁️";
},
2000, 2100 => {
return "🌁";
},
4000, 4001, 4200, 4201 => {
return "🌧️";
},
5000, 5001, 5100, 5101 => {
return "🌨️";
},
6000, 6001, 6200, 6201, 7000, 7101, 7102 => {
return "🧊🌧️";
},
8000 => {
return "⛈️";
},
else => {
return "🥵";
},
}
}
Feel free to change these to any emojis you want to use, but these were the ones I decided to use for this CLI. Now all we have to do is use them in our function and our CLI is complete!
fn handleDifferentWeather(app: App, data: Root) !void {
var defaultPreset: Chameleon.RuntimeChameleon = undefined;
const normalizedTemperature = if (std.mem.eql(u8, "metric", app.units)) (data.data.values.temperature.? * 1.8) + 32 else data.data.values.temperature.?;
switch (data.data.values.weatherCode.?) {
1000, 1100 => {
defaultPreset = try app.c.bold().yellow().createPreset();
if (normalizedTemperature < 65.0) {
defaultPreset = try app.c.bold().blueBright().createPreset();
}
},
1101, 1102, 1001, 2000, 2100 => {
defaultPreset = try app.c.bold().dim().createPreset();
},
4000, 4001, 4200, 4201 => {
defaultPreset = try app.c.bold().blue().createPreset();
},
5000, 5001, 5100, 5101, 6000, 6001, 6200, 6201, 7000, 7101, 7102 => {
defaultPreset = try app.c.bold().white().createPreset();
},
8000 => {
defaultPreset = try app.c.bold().black().createPreset();
},
else => {
defaultPreset = try app.c.bold().green().createPreset();
},
}
try defaultPreset.printOut(" Weather For {s} : {s}\n", .{ data.location.name, getEmojiTemperature(normalizedTemperature) });
const weatherCode = data.data.values.weatherCode.?;
const currentWeather = WeatherCodes.getDescription(weatherCode).?;
try defaultPreset.printOut(" > Currently it is {s} outside {s}\n", .{ currentWeather, getEmojiWeather(weatherCode) });
try defaultPreset.printOut(" > Temperature: {d:.0}°", .{data.data.values.temperature.?});
if (std.mem.eql(u8, "imperial", app.units)) {
try defaultPreset.printOut("F\n", .{});
} else {
try defaultPreset.printOut("C\n", .{});
}
if (data.data.values.temperatureApparent.? != data.data.values.temperature.?) {
try defaultPreset.printOut(" > Real Feel: {d:.0}°", .{data.data.values.temperatureApparent.?});
if (std.mem.eql(u8, "imperial", app.units)) {
try defaultPreset.printOut("F\n", .{});
} else {
try defaultPreset.printOut("C\n", .{});
}
}
try defaultPreset.printOut(" > Wind Speed: {d:.0} m/s\n", .{data.data.values.windSpeed.?});
try defaultPreset.printOut(" > Chance Of Rain: {d:.0}%\n", .{data.data.values.precipitationProbability.?});
try defaultPreset.printOut(" > Humidity: {d:.0}%\n", .{data.data.values.humidity.?});
try defaultPreset.printOut(" > UV Index: {d:.0}\n", .{data.data.values.uvIndex.?});
}
Now that we have that all worked out we have reached the end of this series! Hopefully it taught you a few things about how to work through building basic API calls and CLI applications in zig. The entire finished code for this lives on my github! Feel free to check it out for the complete code for this project.
If you want to keep in touch I have my socials listed on the homepage, and you can subscribe to my RSS feed