--- /dev/null
+/*#############################################################################
+# #
+# collecty - A system statistics collection daemon for IPFire #
+# Copyright (C) 2025 IPFire Development Team #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+#############################################################################*/
+
+#include <argp.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <systemd/sd-bus.h>
+
+const char* argp_program_version = PACKAGE_VERSION;
+
+static const char* doc = "The collecty client that can draw graphs";
+
+typedef struct collecty_client_ctx {
+ // DBus
+ sd_bus* bus;
+
+ // Name of the graph
+ const char* graph;
+
+ // Output Format
+ const char* format;
+
+ // Output
+ FILE* f;
+} collecty_client_ctx;
+
+static error_t parse(int key, char* arg, struct argp_state* state) {
+ collecty_client_ctx* ctx = state->input;
+
+ switch (key) {
+ // Called for each argument
+ case ARGP_KEY_ARG:
+ // Take the graph name as first argument
+ if (!ctx->graph) {
+ ctx->graph = arg;
+ return 0;
+
+ // Otherwise show help
+ } else {
+ argp_usage(state);
+ }
+ break;
+
+ // Called once all arguments have been parsed
+ case ARGP_KEY_END:
+ // Not enough arguments
+ if (state->arg_num < 1)
+ argp_usage(state);
+
+ break;
+
+ default:
+ return ARGP_ERR_UNKNOWN;
+ }
+
+ return 0;
+}
+
+static int render(collecty_client_ctx* ctx) {
+ const void* buffer = NULL;
+ sd_bus_message* m = NULL;
+ char path[PATH_MAX];
+ size_t bytes_written = 0;
+ size_t length = 0;
+ int r;
+
+ // Format the path
+ r = snprintf(path, sizeof(path), "/org/ipfire/collecty1/graph/%s", ctx->graph);
+ if (r < 0)
+ goto ERROR;
+
+ // Call the render function
+ r = sd_bus_call_method(ctx->bus, "org.ipfire.collecty1", path,
+ "org.ipfire.collecty1.Graph", "Render", NULL, &m, "s", "");
+ if (r < 0) {
+ perror("Failed to call the Render() method");
+ goto ERROR;
+ }
+
+ // Read the response
+ r = sd_bus_message_read_array(m, 'y', &buffer, &length);
+ if (r < 0) {
+ perror("Failed to read the response");
+ goto ERROR;
+ }
+
+ // Write the buffer to the output
+ bytes_written = fwrite(buffer, 1, length, ctx->f);
+ if (bytes_written < length) {
+ perror("Failed to write output");
+ r = -errno;
+ goto ERROR;
+ }
+
+ERROR:
+ if (m)
+ sd_bus_message_unref(m);
+
+ return r;
+}
+
+int main(int argc, char* argv[]) {
+ struct argp parser = {
+ .parser = parse,
+ .doc = doc,
+ };
+ int arg_index = 0;
+ int r;
+
+ // Allocate a new context
+ collecty_client_ctx ctx = {
+ .f = stdout,
+ };
+
+ // Parse command line arguments
+ r = argp_parse(&parser, argc, argv, 0, &arg_index, &ctx);
+ if (r)
+ goto ERROR;
+
+ // Connect to the bus
+ r = sd_bus_open_system(&ctx.bus);
+ if (r < 0) {
+ perror("Failed to connect to system bus");
+ goto ERROR;
+ }
+
+ // Render the graph
+ r = render(&ctx);
+
+ERROR:
+ if (ctx.bus)
+ sd_bus_unref(ctx.bus);
+
+ return (r) ? EXIT_FAILURE : EXIT_SUCCESS;
+}