Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion data/cities.csv
Original file line number Diff line number Diff line change
Expand Up @@ -1529,7 +1529,7 @@ Mexicali,1032686,MX,America/Tijuana,32.62781,-115.45446
Mexico City,12294193,MX,America/Mexico_City,19.42847,-99.12766
Miami,441003,US,America/New_York,25.77427,-80.19366
Mianyang,1550000,CN,Asia/Shanghai,31.46784,104.68168
"Mianzhu, Deyang, Sichuan",510000,CN,Asia/Shanghai,31.33786,104.22057
Mianzhu,510000,CN,Asia/Shanghai,31.33786,104.22057
Miguel Hidalgo,372889,MX,America/Mexico_City,19.43411,-99.20024
Milan,1371498,IT,Europe/Rome,45.46427,9.18951
Milton Keynes,256385,GB,Europe/London,52.04172,-0.75583
Expand Down
56 changes: 56 additions & 0 deletions include/arg_definitions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* Contains definitions for command line arguments.
*
* Each line is an invocation of a macro whose arguments define a command line argument of a particular type.
*
* The macro arguments are:
* - The source-code variable name of the argument definition.
* - The short and long versions of the argument.
* - The type of the argument (where applicable).
* - The help text for the argument.
*
* To use these definitions, define the 4 macros below so that they use the arguments, then
* include this file in the source code.
*
* This approach is based on the idea of "X macros" (https://en.wikipedia.org/wiki/X_macro)
*
*/

#if !defined(INCLUDE_ARG_DEFINITION_DBL0)
#define INCLUDE_ARG_DEFINITION_DBL0(...)
#endif

#if !defined(INCLUDE_ARG_DEFINITION_STR0)
#define INCLUDE_ARG_DEFINITION_STR0(...)
#endif

#if !defined(INCLUDE_ARG_DEFINITION_LIT0)
#define INCLUDE_ARG_DEFINITION_LIT0(...)
#endif

#if !defined(INCLUDE_ARG_DEFINITION_INT0)
#define INCLUDE_ARG_DEFINITION_INT0(...)
#endif

INCLUDE_ARG_DEFINITION_DBL0(latitude_arg, "a", "latitude", "<degrees>", "Observer latitude [-90°, 90°] (default: 0.0)");
INCLUDE_ARG_DEFINITION_DBL0(longitude_arg, "o", "longitude", "<degrees>", "Observer longitude [-180°, 180°] (default: 0.0)");
INCLUDE_ARG_DEFINITION_DBL0(threshold_arg, "t", "threshold", "<float>", "Only render stars brighter than this magnitude (default: 5.0)");
INCLUDE_ARG_DEFINITION_DBL0(label_arg, "l", "label-thresh", "<float>", "Label stars brighter than this magnitude (default: 0.25)");
INCLUDE_ARG_DEFINITION_DBL0(speed_arg, "s", "speed", "<float>", "Animation speed multiplier (default: 1.0)");
INCLUDE_ARG_DEFINITION_DBL0(ratio_arg, "r", "aspect-ratio", "<float>", "Override the calculated terminal cell aspect ratio. Use this if your projection is not 'square.' A value around 2.0 works well for most cases");
INCLUDE_ARG_DEFINITION_STR0(datetime_arg, "d", "datetime", "<yyyy-mm-ddThh:mm:ss>", "Observation datetime in UTC");
INCLUDE_ARG_DEFINITION_STR0(city_arg, "i", "city", "<city_name>", "Use the latitude and longitude of the provided city. If the name contains multiple words, enclose the name in single or double quotes. For a list of available cities, see: https://github.com/da-luce/astroterm/blob/v" PROJ_VERSION "/data/cities.csv");
INCLUDE_ARG_DEFINITION_LIT0(color_arg, "c", "color", "Enable terminal colors");
INCLUDE_ARG_DEFINITION_LIT0(constell_arg, "C", "constellations", "Draw constellation stick figures. Note: a constellation is only drawn if all stars in the figure are over the threshold");
INCLUDE_ARG_DEFINITION_LIT0(grid_arg, "g", "grid", "Draw an azimuthal grid");
INCLUDE_ARG_DEFINITION_LIT0(unicode_arg, "u", "unicode", "Use unicode characters");
INCLUDE_ARG_DEFINITION_LIT0(quit_arg, "q", "quit-on-any", "Quit on any keypress (default is to quit on 'q' or 'ESC' only)");
INCLUDE_ARG_DEFINITION_LIT0(meta_arg, "m", "metadata", "Display metadata");
INCLUDE_ARG_DEFINITION_LIT0(help_arg, "h", "help", "Print this help message");
INCLUDE_ARG_DEFINITION_LIT0(completions_arg, "b", "bash-completions", "Print bash completions");
INCLUDE_ARG_DEFINITION_LIT0(version_arg, "v", "version", "Display version info and exit");
INCLUDE_ARG_DEFINITION_INT0(fps_arg, "f", "fps", "<int>", "Frames per second (default: 24)");

#undef INCLUDE_ARG_DEFINITION_DBL0
#undef INCLUDE_ARG_DEFINITION_STR0
#undef INCLUDE_ARG_DEFINITION_LIT0
#undef INCLUDE_ARG_DEFINITION_INT0
4 changes: 4 additions & 0 deletions include/city.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ CityData *get_city(const char *name);
*/
void free_city(CityData *city);

/* Apply a callback and some associated data to all city definitions
*/
void iter_cities(void (*callback)(const CityData *city, void *data), void *data);

#endif // CITY_H
6 changes: 6 additions & 0 deletions include/split_lines.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef SPLIT_LINES_H
#define SPLIT_LINES_H

char **split_lines(char *data, int *line_count_out);

#endif // SPLIT_LINES_H
97 changes: 64 additions & 33 deletions src/city.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "city.h"
#include "cities.h"
#include "macros.h"
#include "split_lines.h"

#include <ctype.h>
#include <stdint.h>
Expand Down Expand Up @@ -104,10 +105,6 @@ CityData *get_city(const char *name)
return NULL;
}

// Convert the byte array into an array of lines
char **lines = NULL;
size_t line_count = 0;

char *data = malloc(cities_len + 1);
if (data == NULL)
{
Expand All @@ -118,35 +115,14 @@ CityData *get_city(const char *name)
memcpy(data, cities, cities_len);
data[cities_len] = '\0';

const char *line = strtok(data, "\n");
while (line != NULL)
// Convert the byte array into an array of lines
int line_count = 0;
char **lines = split_lines(data, &line_count);
if (lines == NULL)
{
char *line_copy = strdup(line);
if (line_copy == NULL)
{
perror("Memory allocation failed");
free(normalized_name);
free(data);
return NULL;
}

char **temp = realloc(lines, (line_count + 1) * sizeof(char *));
if (temp == NULL)
{
perror("Memory allocation failed");
free(normalized_name);
free(data);
for (size_t i = 0; i < line_count; i++)
{
free(lines[i]);
}
free(lines);
return NULL;
}
lines = temp;

lines[line_count++] = line_copy;
line = strtok(NULL, "\n");
free(normalized_name);
free(data);
return NULL;
}

// Perform binary search with the normalized name
Expand Down Expand Up @@ -183,7 +159,7 @@ CityData *get_city(const char *name)
}

// Clean up lines and data
for (size_t i = 0; i < line_count; i++)
for (int i = 0; i < line_count; i++)
{
free(lines[i]);
}
Expand All @@ -202,3 +178,58 @@ void free_city(CityData *city)
free(city);
}
}

/**
* @brief Iterates over all cities and applies a callback function to each.
*
* This function traverses the collection of cities and calls the provided
* callback function for each city. The callback receives a pointer to the
* city's data and a user-defined data pointer.
*
* @param callback A function pointer to be called for each city.
* @param user_data A pointer to user-defined data that will be passed to the
* callback function for each city.
*/
void iter_cities(void (*callback)(const CityData *city, void *data), void *user_data)
{
if (callback == NULL)
{
return;
}

// Get the city data as a buffer using split_lines
char *data = malloc(cities_len + 1);
memcpy(data, cities, cities_len);
data[cities_len] = '\0';

int line_count = 0;
char **lines = split_lines(data, &line_count);
if (lines == NULL)
{
free(data);
return;
}

for (int i = 1; i < line_count; i++)
{
char *line = lines[i];
const char *city_name, *latitude_str, *longitude_str;

city_name = strtok(line, ",");
strtok(NULL, ","); // Skip population
strtok(NULL, ","); // Skip country code
strtok(NULL, ","); // Skip timezone
latitude_str = strtok(NULL, ",");
longitude_str = strtok(NULL, ",");

if (city_name && latitude_str && longitude_str)
{
CityData city_data;
city_data.city_name = city_name;
city_data.latitude = atof(latitude_str);
city_data.longitude = atof(longitude_str);

callback(&city_data, user_data);
}
}
}
83 changes: 55 additions & 28 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -243,40 +243,28 @@ int main(int argc, char *argv[])
return EXIT_SUCCESS;
}

void print_city_name_quoted(const CityData *city, void *unused)
{
printf("\"%s\"\n", city->city_name);
}

void parse_options(int argc, char *argv[], struct Conf *config)
{
struct arg_dbl *latitude_arg = arg_dbl0("a", "latitude", "<degrees>", "Observer latitude [-90°, 90°] (default: 0.0)");
struct arg_dbl *longitude_arg = arg_dbl0("o", "longitude", "<degrees>", "Observer longitude [-180°, 180°] (default: 0.0)");
struct arg_str *datetime_arg = arg_str0("d", "datetime", "<yyyy-mm-ddThh:mm:ss>", "Observation datetime in UTC");
struct arg_dbl *threshold_arg =
arg_dbl0("t", "threshold", "<float>", "Only render stars brighter than this magnitude (default: 5.0)");
struct arg_dbl *label_arg =
arg_dbl0("l", "label-thresh", "<float>", "Label stars brighter than this magnitude (default: 0.25)");
struct arg_int *fps_arg = arg_int0("f", "fps", "<int>", "Frames per second (default: 24)");
struct arg_dbl *speed_arg = arg_dbl0("s", "speed", "<float>", "Animation speed multiplier (default: 1.0)");
struct arg_lit *color_arg = arg_lit0("c", "color", "Enable terminal colors");
struct arg_lit *constell_arg = arg_lit0("C", "constellations",
"Draw constellation stick figures. Note: a constellation is only "
"drawn if all stars in the figure are over the threshold");
struct arg_lit *grid_arg = arg_lit0("g", "grid", "Draw an azimuthal grid");
struct arg_lit *unicode_arg = arg_lit0("u", "unicode", "Use unicode characters");
struct arg_lit *quit_arg = arg_lit0("q", "quit-on-any", "Quit on any keypress (default is to quit on 'q' or 'ESC' only)");
struct arg_lit *meta_arg = arg_lit0("m", "metadata", "Display metadata");
struct arg_lit *help_arg = arg_lit0("h", "help", "Print this help message");
struct arg_dbl *ratio_arg = arg_dbl0("r", "aspect-ratio", "<float>",
"Override the calculated terminal cell aspect ratio. Use this if your projection is "
"not 'square.' A value around 2.0 works well for most cases");
struct arg_str *city_arg =
arg_str0("i", "city", "<city_name>",
"Use the latitude and longitude of the provided city. If the name contains multiple words, "
"enclose the name in single or double quotes. For a list of available cities, see: "
"https://github.com/da-luce/astroterm/blob/v" PROJ_VERSION "/data/cities.csv");
struct arg_lit *version_arg = arg_lit0("v", "version", "Display version info and exit");
#define INCLUDE_ARG_DEFINITION_DBL0(token, short_name, long_name, datatype, glossary) \
struct arg_dbl *token = arg_dbl0(short_name, long_name, datatype, glossary);
#define INCLUDE_ARG_DEFINITION_STR0(token, short_name, long_name, datatype, glossary) \
struct arg_str *token = arg_str0(short_name, long_name, datatype, glossary);
#define INCLUDE_ARG_DEFINITION_LIT0(token, short_name, long_name, glossary) \
struct arg_lit *token = arg_lit0(short_name, long_name, glossary);
#define INCLUDE_ARG_DEFINITION_INT0(token, short_name, long_name, datatype, glossary) \
struct arg_int *token = arg_int0(short_name, long_name, datatype, glossary);
#include "arg_definitions.h"
struct arg_end *end = arg_end(20);

void *argtable[] = {latitude_arg, longitude_arg, datetime_arg, threshold_arg, label_arg, fps_arg,
speed_arg, color_arg, constell_arg, grid_arg, unicode_arg, quit_arg,
meta_arg, ratio_arg, help_arg, city_arg, version_arg, end};
meta_arg, ratio_arg, help_arg, completions_arg,
city_arg, version_arg, end};

int nerrors = arg_parse(argc, argv, argtable);

Expand All @@ -288,6 +276,45 @@ void parse_options(int argc, char *argv[], struct Conf *config)
exit(EXIT_SUCCESS);
}

if (completions_arg->count > 0)
{
// Print bash completions
printf("# Bash completions for astroterm\n");
printf("ASTROTERM_OPTIONS=(\n");
#define INCLUDE_ARG_DEFINITION_DBL0(token, short_name, long_name, datatype, glossary) \
printf(" -%s\n", short_name); printf(" --%s\n", long_name);
#define INCLUDE_ARG_DEFINITION_STR0(token, short_name, long_name, datatype, glossary) \
printf(" -%s\n", short_name); printf(" --%s\n", long_name);
#define INCLUDE_ARG_DEFINITION_LIT0(token, short_name, long_name, glossary) \
printf(" -%s\n", short_name); printf(" --%s\n", long_name);
#define INCLUDE_ARG_DEFINITION_INT0(token, short_name, long_name, datatype, glossary) \
printf(" -%s\n", short_name); printf(" --%s\n", long_name);
#include "arg_definitions.h"
printf(")\n\n");
printf("ASTROTERM_CITIES=(\n");
iter_cities(&print_city_name_quoted, NULL);
printf(")\n");
printf("_astroterm_completions() {\n");
printf(" local word=\"${COMP_WORDS[COMP_CWORD]}\"\n");
printf(" local prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n");
printf(" case \"$prev\" in\n");
printf(" -i|--city)\n");
printf(" readarray -t _cities < <(IFS=: compgen -W \"$( printf '%%q:' \"${ASTROTERM_CITIES[@]}\" )\" -- \"${word}\")\n");
printf(" COMPREPLY=()\n");
printf(" for city in \"${_cities[@]}\"; do\n");
printf(" printf -v city_esc '%%q ' \"$city\"\n");
printf(" COMPREPLY+=(\"$city_esc\")\n");
printf(" done\n");
printf(" ;;\n");
printf(" *)\n");
printf(" COMPREPLY=( $(compgen -W \"${ASTROTERM_OPTIONS[*]}\" -- \"${word}\") )\n");
printf(" ;;\n");
printf(" esac\n");
printf("}\n");
printf("complete -o nospace -F _astroterm_completions astroterm\n");
exit(EXIT_SUCCESS);
}

if (nerrors > 0)
{
arg_print_errors(stderr, end, argv[0]);
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ project_source_files += [
files('stopwatch.c'),
files('term.c'),
files('city.c'),
files('split_lines.c'),
]

# NOTE: We add main.c separately in the root Meson.build file to avoid duplicate "main" functions when compiling tests
Expand Down
53 changes: 53 additions & 0 deletions src/split_lines.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "split_lines.h"

/**
* Splits a buffer into lines.
*
* @param data The buffer to split (will be modified).
* @param line_count_out Pointer to int to store the number of lines.
* @return Array of lines, or NULL on error. Caller must free each line and the array.
*/
char **split_lines(char *data, int *line_count_out)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, this was factored out since the same logic is used in get_city and the new iter_city?

{
char **lines = NULL;
size_t line_count = 0;
const char *line = strtok(data, "\n");
while (line != NULL)
{
char *line_copy = strdup(line);
if (line_copy == NULL)
{
perror("Memory allocation failed");
for (size_t i = 0; i < line_count; i++)
{
free(lines[i]);
}
free(lines);
return NULL;
}

char **temp = realloc(lines, (line_count + 1) * sizeof(char *));
if (temp == NULL)
{
perror("Memory allocation failed");
free(line_copy);
for (size_t i = 0; i < line_count; i++)
{
free(lines[i]);
}
free(lines);
return NULL;
}
lines = temp;

lines[line_count++] = line_copy;
line = strtok(NULL, "\n");
}
if (line_count_out)
*line_count_out = (int)line_count;
return lines;
}