aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSpacedio <spacedio@thernusen.net>2026-02-26 19:20:14 -0500
committerSpacedio <spacedio@thernusen.net>2026-02-26 19:20:14 -0500
commitdb657412e6ae93341ed42d59df6aef564a739a1f (patch)
tree6f67f55f6dc61fb0afa8829a950f0dbf1a0aeadf /src
downloadlightdm-mini-greeter-master.tar.gz
Initial commit after cloneHEADmaster
Diffstat (limited to 'src')
-rw-r--r--src/app.c60
-rw-r--r--src/app.h36
-rw-r--r--src/callbacks.c152
-rw-r--r--src/callbacks.h15
-rw-r--r--src/compat.c25
-rw-r--r--src/compat.h12
-rw-r--r--src/config.c435
-rw-r--r--src/config.h68
-rw-r--r--src/focus_ring.c105
-rw-r--r--src/focus_ring.h31
-rw-r--r--src/main.c28
-rw-r--r--src/ui.c430
-rw-r--r--src/ui.h24
-rw-r--r--src/utils.c70
-rw-r--r--src/utils.h14
15 files changed, 1505 insertions, 0 deletions
diff --git a/src/app.c b/src/app.c
new file mode 100644
index 0000000..0877b55
--- /dev/null
+++ b/src/app.c
@@ -0,0 +1,60 @@
+/* Application Initialization Things */
+#include <stdlib.h>
+
+#include <gtk/gtk.h>
+#include <lightdm.h>
+
+#include "app.h"
+#include "callbacks.h"
+#include "config.h"
+
+
+/* Initialize the Greeter & UI */
+App *initialize_app(int argc, char **argv)
+{
+ g_log_set_always_fatal(G_LOG_LEVEL_CRITICAL);
+ gtk_init(&argc, &argv);
+
+ // Allocate & Initialize
+ App *app = malloc(sizeof(App));
+ if (app == NULL) {
+ g_error("Could not allocate memory for App");
+ }
+
+ app->config = initialize_config();
+ app->greeter = lightdm_greeter_new();
+ app->ui = initialize_ui(app->config);
+
+ // Connect Greeter & UI Signals
+ g_signal_connect(app->greeter, "authentication-complete",
+ G_CALLBACK(authentication_complete_cb), app);
+ app->password_callback_id =
+ g_signal_connect(GTK_ENTRY(APP_PASSWORD_INPUT(app)), "activate",
+ G_CALLBACK(handle_password), app);
+ // This was added to fix a bug where the background window would be focused
+ // instead of the main window, preventing users from entering their password.
+ // It's undocument & probably not necessary any more. Investigate & remove.
+ for (int m = 0; m < APP_MONITOR_COUNT(app); m++) {
+ g_signal_connect(GTK_WIDGET(APP_BACKGROUND_WINDOWS(app)[m]),
+ "key-press-event",
+ G_CALLBACK(handle_tab_key), app);
+ }
+ g_signal_connect(GTK_WIDGET(APP_MAIN_WINDOW(app)), "key-press-event",
+ G_CALLBACK(handle_hotkeys), app);
+ // Update the current time every 15 seconds
+ if (app->config->show_sys_info) {
+ handle_time_update(app);
+ g_timeout_add_seconds(15, G_SOURCE_FUNC(handle_time_update), app);
+ }
+
+ return app;
+}
+
+
+/* Free any dynamically allocated memory */
+void destroy_app(App *app)
+{
+ destroy_config(app->config);
+ free(app->ui);
+ free(app);
+}
diff --git a/src/app.h b/src/app.h
new file mode 100644
index 0000000..4bfdab8
--- /dev/null
+++ b/src/app.h
@@ -0,0 +1,36 @@
+#ifndef APP_H
+#define APP_H
+
+#include <lightdm.h>
+
+#include "config.h"
+#include "focus_ring.h"
+#include "ui.h"
+
+
+typedef struct App_ {
+ Config *config;
+ LightDMGreeter *greeter;
+ UI *ui;
+ FocusRing *session_ring;
+
+ // Signal Handler ID for the `handle_password` callback
+ gulong password_callback_id;
+} App;
+
+
+App *initialize_app(int argc, char **argv);
+void destroy_app(App *app);
+
+/* Config Member Accessors */
+#define APP_LOGIN_USER(app) (app)->config->login_user
+
+/* UI Member Accessors */
+#define APP_BACKGROUND_WINDOWS(app) (app)->ui->background_windows
+#define APP_MONITOR_COUNT(app) (app)->ui->monitor_count
+#define APP_MAIN_WINDOW(app) (app)->ui->main_window
+#define APP_PASSWORD_INPUT(app) (app)->ui->password_input
+#define APP_FEEDBACK_LABEL(app) (app)->ui->feedback_label
+#define APP_TIME_LABEL(app) (app)->ui->time_label
+
+#endif
diff --git a/src/callbacks.c b/src/callbacks.c
new file mode 100644
index 0000000..7cefa42
--- /dev/null
+++ b/src/callbacks.c
@@ -0,0 +1,152 @@
+/* Callback Functions for LightDM & GTK */
+#include <gtk/gtk.h>
+#include <lightdm.h>
+#include <string.h>
+#include <time.h>
+
+#include "app.h"
+#include "utils.h"
+#include "focus_ring.h"
+#include "callbacks.h"
+#include "compat.h"
+
+static void set_ui_feedback_label(App *app, gchar *feedback_text);
+
+
+/* LightDM Callbacks */
+
+/* Start the Selected Session Once Fully Authenticated.
+ *
+ * The callback will clear & re-enable the input widget, and re-add the
+ * `handle_password` callback so the user can try again if authentication
+ * fails.
+ */
+void authentication_complete_cb(LightDMGreeter *greeter, App *app)
+{
+ if (lightdm_greeter_get_is_authenticated(greeter)) {
+ const gchar *session = focus_ring_get_value(app->session_ring);
+
+ g_message("Attempting to start session: %s", session);
+
+ gboolean session_started_successfully =
+ !lightdm_greeter_start_session_sync(greeter, session, NULL);
+
+ if (!session_started_successfully) {
+ g_message("Unable to start session");
+ }
+ } else {
+ g_message("Authentication failed");
+ if (strlen(app->config->invalid_password_text) > 0) {
+ set_ui_feedback_label(app, app->config->invalid_password_text);
+ }
+ begin_authentication_as_default_user(app);
+ }
+ gtk_entry_set_text(GTK_ENTRY(APP_PASSWORD_INPUT(app)), "");
+ gtk_editable_set_editable(GTK_EDITABLE(APP_PASSWORD_INPUT(app)), TRUE);
+ app->password_callback_id =
+ g_signal_connect(GTK_ENTRY(APP_PASSWORD_INPUT(app)), "activate",
+ G_CALLBACK(handle_password), app);
+}
+
+
+
+/* GUI Callbacks */
+
+/* Attempt to Authenticate When a Password is Entered.
+ *
+ * The callback disables itself & the input widget to prevent two
+ * authentication attempts from running at the same time - which would cause
+ * LightDM to throw a critical error.
+ */
+void handle_password(GtkWidget *password_input, App *app)
+{
+ if (app->password_callback_id != 0) {
+ g_signal_handler_disconnect(GTK_ENTRY(APP_PASSWORD_INPUT(app)),
+ app->password_callback_id);
+ app->password_callback_id = 0;
+ }
+
+ if (!lightdm_greeter_get_is_authenticated(app->greeter)) {
+ gtk_editable_set_editable(GTK_EDITABLE(password_input), FALSE);
+ if (!lightdm_greeter_get_in_authentication(app->greeter)) {
+ begin_authentication_as_default_user(app);
+ }
+ g_message("Using entered password to authenticate");
+ const gchar *password_text =
+ gtk_entry_get_text(GTK_ENTRY(password_input));
+ compat_greeter_respond(app->greeter, password_text, NULL);
+ } else {
+ g_message("Password entered while already authenticated");
+ }
+}
+
+
+/* Select the Password input if the Tab Key is Pressed */
+gboolean handle_tab_key(GtkWidget *widget, GdkEvent *event, App *app)
+{
+ (void) widget; // Window accessible through app.
+
+ GdkEventKey *key_event = (GdkEventKey *) event;
+ if (event->type == GDK_KEY_PRESS && key_event->keyval == GDK_KEY_Tab) {
+ g_message("Handling Tab Key Press");
+ gtk_window_present(GTK_WINDOW(APP_MAIN_WINDOW(app)));
+ gtk_widget_grab_focus(GTK_WIDGET(APP_PASSWORD_INPUT(app)));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/* Shutdown, Restart, Hibernate, Suspend, or Switch Sessions if the correct
+ * keys are pressed.
+ */
+gboolean handle_hotkeys(GtkWidget *widget, GdkEventKey *event, App *app)
+{
+ (void) widget;
+ Config *config = app->config;
+ FocusRing *sessions = app->session_ring;
+
+ if (event->state & config->mod_bit) {
+ if (event->keyval == config->suspend_key && lightdm_get_can_suspend()) {
+ lightdm_suspend(NULL);
+ } else if (event->keyval == config->hibernate_key &&
+ lightdm_get_can_hibernate()) {
+ lightdm_hibernate(NULL);
+ } else if (event->keyval == config->restart_key &&
+ lightdm_get_can_restart()) {
+ lightdm_restart(NULL);
+ } else if (event->keyval == config->shutdown_key &&
+ lightdm_get_can_shutdown()) {
+ lightdm_shutdown(NULL);
+ } else if (event->keyval == config->session_key && sessions != NULL) {
+ gchar *new_session = focus_ring_next(sessions);
+ set_ui_feedback_label(app, new_session);
+ } else {
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/** Determine the current time & update the time GtkLabel.
+ */
+gboolean handle_time_update(App *app)
+{
+ time_t now = time(NULL);
+ struct tm *local_now = localtime(&now);
+ gchar date_string[30];
+ strftime(date_string, 29, "%H:%M", local_now);
+ gtk_label_set_text(GTK_LABEL(APP_TIME_LABEL(app)), date_string);
+
+ return TRUE;
+}
+
+/* Set the Feedback Label's text & ensure it is visible. */
+static void set_ui_feedback_label(App *app, gchar *feedback_text)
+{
+ if (!gtk_widget_get_visible(APP_FEEDBACK_LABEL(app))) {
+ gtk_widget_show(APP_FEEDBACK_LABEL(app));
+ }
+ gtk_label_set_text(GTK_LABEL(APP_FEEDBACK_LABEL(app)), feedback_text);
+}
diff --git a/src/callbacks.h b/src/callbacks.h
new file mode 100644
index 0000000..2b18a54
--- /dev/null
+++ b/src/callbacks.h
@@ -0,0 +1,15 @@
+#ifndef CALLBACKS_H
+#define CALLBACKS_H
+
+#include <lightdm.h>
+
+#include "app.h"
+
+
+void authentication_complete_cb(LightDMGreeter *greeter, App *app);
+void handle_password(GtkWidget *password_input, App *app);
+gboolean handle_tab_key(GtkWidget *widget, GdkEvent *event, App *app);
+gboolean handle_hotkeys(GtkWidget *widget, GdkEventKey *event, App *app);
+gboolean handle_time_update(App *app);
+
+#endif
diff --git a/src/compat.c b/src/compat.c
new file mode 100644
index 0000000..74b683d
--- /dev/null
+++ b/src/compat.c
@@ -0,0 +1,25 @@
+/* Backwards-Compatible Functions for LightDM */
+#include <lightdm.h>
+
+#include "compat.h"
+
+gboolean compat_greeter_authenticate(LightDMGreeter *greeter, const gchar *username, GError **error)
+{
+#ifdef LIGHTDM_1_19_1_LOWER
+ lightdm_greeter_authenticate(greeter, username);
+ return TRUE;
+#else
+ return lightdm_greeter_authenticate(greeter, username, error);
+#endif
+}
+
+gboolean compat_greeter_respond(LightDMGreeter *greeter, const gchar *response, GError **error)
+{
+#ifdef LIGHTDM_1_19_1_LOWER
+ lightdm_greeter_respond(greeter, response);
+ return TRUE;
+#else
+ return lightdm_greeter_respond(greeter, response, error);
+#endif
+
+}
diff --git a/src/compat.h b/src/compat.h
new file mode 100644
index 0000000..10546fa
--- /dev/null
+++ b/src/compat.h
@@ -0,0 +1,12 @@
+#ifndef COMPAT_H
+#define COMPAT_H
+
+#include <lightdm.h>
+
+#include "defines.h"
+
+// v1.19.2 of LightDM introduced GError arguments but Debian jessie & stretch are not updated yet
+gboolean compat_greeter_authenticate(LightDMGreeter *greeter, const gchar *username, GError **error);
+gboolean compat_greeter_respond(LightDMGreeter *greeter, const gchar *response, GError **error);
+
+#endif
diff --git a/src/config.c b/src/config.c
new file mode 100644
index 0000000..0d7b21b
--- /dev/null
+++ b/src/config.c
@@ -0,0 +1,435 @@
+/* Functions related to the Configuration */
+#include <stdlib.h>
+#include <string.h>
+
+#include <gdk/gdk.h>
+#include <glib.h>
+
+#include "config.h"
+#include "utils.h"
+
+
+static gchar *parse_greeter_string(GKeyFile *keyfile, const char *group_name,
+ const char *key_name, const gchar *fallback);
+static gint parse_greeter_integer(GKeyFile *keyfile, const char *group_name,
+ const char *key_name, const gint fallback);
+static gboolean parse_greeter_boolean(GKeyFile *keyfile, const char *group_name,
+ const char *key_name, const gboolean fallback);
+static GdkRGBA *parse_greeter_color_key(GKeyFile *keyfile, const char *key_name, const char *default_str);
+static guint parse_greeter_hotkey_keyval(GKeyFile *keyfile, const char *key_name, const char default_char);
+static gunichar *parse_greeter_password_char(GKeyFile *keyfile);
+static gfloat parse_greeter_password_alignment(GKeyFile *keyfile);
+static gboolean is_rtl_keymap_layout(void);
+gboolean input_string_equals(gchar *input_str, const gchar * const fixed_str);
+
+/* Initialize the configuration, sourcing the greeter's configuration file */
+Config *initialize_config(void)
+{
+ Config *config = malloc(sizeof(Config));
+ if (config == NULL) {
+ g_error("Could not allocate memory for Config");
+ }
+
+ // Load the key-value file
+ GKeyFile *keyfile = g_key_file_new();
+ GError *keyerror = NULL;
+ gboolean keyfile_loaded = g_key_file_load_from_file(
+ keyfile, CONFIG_FILE, G_KEY_FILE_NONE, &keyerror);
+ if (!keyfile_loaded) {
+ if (keyerror != NULL) {
+ g_error("Could not load configuration file: %s", keyerror->message);
+ free(keyerror);
+ } else {
+ g_error("Could not load configuration file.");
+ }
+ }
+
+ // Parse values from the keyfile into a Config.
+ config->login_user =
+ g_strchomp(g_key_file_get_string(keyfile, "greeter", "user", NULL));
+ if (strcmp(config->login_user, "CHANGE_ME") == 0) {
+ g_message("User configuration value is unchanged.");
+ }
+ config->show_password_label =
+ parse_greeter_boolean(keyfile, "greeter", "show-password-label", TRUE);
+ config->password_label_text = parse_greeter_string(
+ keyfile, "greeter", "password-label-text", "Password:");
+ config->invalid_password_text = parse_greeter_string(
+ keyfile, "greeter", "invalid-password-text", "Invalid Password");
+ config->show_input_cursor =
+ parse_greeter_boolean(keyfile, "greeter", "show-input-cursor", TRUE);
+ config->password_alignment = parse_greeter_password_alignment(keyfile);
+ config->password_input_width = parse_greeter_integer(
+ keyfile, "greeter", "password-input-width", -1);
+ config->show_image_on_all_monitors = parse_greeter_boolean(
+ keyfile, "greeter", "show-image-on-all-monitors", FALSE);
+ config->show_sys_info = parse_greeter_boolean(
+ keyfile, "greeter", "show-sys-info", FALSE);
+
+ // Parse Hotkey Settings
+ config->suspend_key = parse_greeter_hotkey_keyval(keyfile, "suspend-key", 'u');
+ config->hibernate_key = parse_greeter_hotkey_keyval(keyfile, "hibernate-key", 'h');
+ config->restart_key = parse_greeter_hotkey_keyval(keyfile, "restart-key", 'r');
+ config->shutdown_key = parse_greeter_hotkey_keyval(keyfile, "shutdown-key", 's');
+ config->session_key = parse_greeter_hotkey_keyval(keyfile, "session-key", 'e');
+ gchar *mod_key =
+ g_key_file_get_string(keyfile, "greeter-hotkeys", "mod-key", NULL);
+ if (mod_key == NULL) {
+ config->mod_bit = GDK_SUPER_MASK;
+ } else {
+ g_strchomp(mod_key);
+ if (strcmp(mod_key, "control") == 0) {
+ config->mod_bit = GDK_CONTROL_MASK;
+ } else if (strcmp(mod_key, "alt") == 0) {
+ config->mod_bit = GDK_MOD1_MASK;
+ } else if (strcmp(mod_key, "meta") == 0) {
+ config->mod_bit = GDK_SUPER_MASK;
+ } else {
+ g_error("Invalid mod-key configuration value: '%s'\n", mod_key);
+ }
+ }
+
+ // Parse Theme Settings
+ // Font
+ config->font =
+ parse_greeter_string(keyfile, "greeter-theme", "font", "Sans");
+ config->font_size =
+ parse_greeter_string(keyfile, "greeter-theme", "font-size", "1em");
+ config->font_weight =
+ parse_greeter_string(keyfile, "greeter-theme", "font-weight", "bold");
+ config->font_style =
+ parse_greeter_string(keyfile, "greeter-theme", "font-style", "normal");
+ config->text_color =
+ parse_greeter_color_key(keyfile, "text-color", "#080800");
+ config->error_color =
+ parse_greeter_color_key(keyfile, "error-color", "#F8F8F0");
+ // Background
+ config->background_image =
+ g_key_file_get_string(keyfile, "greeter-theme", "background-image", NULL);
+ if (config->background_image == NULL || strcmp(config->background_image, "") == 0) {
+ config->background_image = (gchar *) "\"\"";
+ }
+ config->background_color =
+ parse_greeter_color_key(keyfile, "background-color", "#1B1D1E");
+ config->background_image_size =
+ parse_greeter_string(keyfile, "greeter-theme", "background-image-size", "auto");
+ // Window
+ config->window_color =
+ parse_greeter_color_key(keyfile, "window-color", "#F92672");
+ config->border_color =
+ parse_greeter_color_key(keyfile, "border-color", "#080800");
+ config->border_width = parse_greeter_string(
+ keyfile, "greeter-theme", "border-width", "2px");
+ // Password
+ config->password_char =
+ parse_greeter_password_char(keyfile);
+ config->password_color =
+ parse_greeter_color_key(keyfile, "password-color", "#F8F8F0");
+ config->password_background_color =
+ parse_greeter_color_key(keyfile, "password-background-color", "#1B1D1E");
+ gchar *temp_password_border_color = g_key_file_get_string(
+ keyfile, "greeter-theme", "password-border-color", NULL);
+ if (temp_password_border_color == NULL) {
+ config->password_border_color = config->border_color;
+ } else {
+ free(temp_password_border_color);
+ config->password_border_color =
+ parse_greeter_color_key(keyfile, "password-border-color", "#080800");
+ }
+ config->password_border_width = parse_greeter_string(
+ keyfile, "greeter-theme", "password-border-width", config->border_width);
+ config->password_border_radius = parse_greeter_string(
+ keyfile, "greeter-theme", "password-border-radius", "0.341125em");
+ // System Info
+ gchar *temp_sys_info_color = g_key_file_get_string(
+ keyfile, "greeter-theme", "sys-info-color", NULL);
+ if (temp_sys_info_color == NULL) {
+ config->sys_info_color = config->text_color;
+ } else {
+ config->sys_info_color = parse_greeter_color_key(
+ keyfile, "sys-info-color", "#080800");
+ }
+ config->sys_info_font = parse_greeter_string(keyfile, "greeter-theme", "sys-info-font", config->font);
+ config->sys_info_font_size =
+ parse_greeter_string(keyfile, "greeter-theme", "sys-info-font-size", config->font_size);
+ config->sys_info_margin =
+ parse_greeter_string(keyfile, "greeter-theme", "sys-info-margin", "-5px -5px -5px");
+
+
+ gint layout_spacing =
+ parse_greeter_integer(keyfile, "greeter-theme", "layout-space", 15);
+ if (layout_spacing < 0) {
+ config->layout_spacing = (guint) (-1 * layout_spacing);
+ } else {
+ config->layout_spacing = (guint) layout_spacing;
+ }
+
+ config->x_pos = g_key_file_get_double(keyfile, "greeter-theme", "x-pos", NULL);
+ config->y_pos = g_key_file_get_double(keyfile, "greeter-theme", "y-pos", NULL);
+
+ g_key_file_free(keyfile);
+
+ return config;
+}
+
+
+/* Cleanup any memory allocated for the Config */
+void destroy_config(Config *config)
+{
+ free(config->login_user);
+ free(config->font);
+ free(config->font_size);
+ free(config->font_weight);
+ free(config->font_style);
+ free(config->text_color);
+ free(config->error_color);
+ free(config->background_image);
+ free(config->background_color);
+ free(config->background_image_size);
+ free(config->window_color);
+ free(config->border_color);
+ free(config->border_width);
+ free(config->password_label_text);
+ free(config->invalid_password_text);
+ free(config->password_char);
+ free(config->password_color);
+ free(config->password_background_color);
+ free(config->password_border_color);
+ free(config->password_border_width);
+ free(config->password_border_radius);
+ free(config->sys_info_color);
+ free(config->sys_info_font_size);
+ free(config->sys_info_margin);
+ free(config);
+}
+
+
+/* Parse a string from the config file, returning a copy of the fallback value
+ * if the key is not present in the group.
+ */
+static gchar *parse_greeter_string(GKeyFile *keyfile, const char *group_name,
+ const char *key_name, const gchar *fallback)
+{
+ gchar *parsed_string = g_key_file_get_string(keyfile, group_name, key_name, NULL);
+ if (parsed_string == NULL) {
+ g_warning("Could not find value for %s.%s - falling back to '%s'",
+ group_name, key_name, fallback);
+ return g_strdup(fallback);
+ } else {
+ return parsed_string;
+ }
+}
+
+/* Parse an integer from the config file, returning the fallback value if the
+ * key is not present in the group, or if the value is not an integer.
+ */
+static gint parse_greeter_integer(GKeyFile *keyfile, const char *group_name,
+ const char *key_name, const gint fallback)
+{
+ GError *parse_error = NULL;
+ gint parse_result = g_key_file_get_integer(
+ keyfile, group_name, key_name, &parse_error);
+ if (parse_error != NULL) {
+ if (parse_error->code == G_KEY_FILE_ERROR_INVALID_VALUE) {
+ // Read the value as a string so we can log it
+ gchar *value = g_key_file_get_string(
+ keyfile, group_name, key_name, NULL);
+ g_warning("Invalid integer for %s.%s: `%s`",
+ group_name, key_name, value);
+ free(value);
+ }
+ g_error_free(parse_error);
+ return fallback;
+ }
+ return parse_result;
+}
+
+/* Parse a boolean from the config file, returning the fallback value if the
+ * key is not present in the group or cannot be parsed as a boolean.
+ */
+static gboolean parse_greeter_boolean(GKeyFile *keyfile, const char *group_name,
+ const char *key_name, const gboolean fallback)
+{
+ GError *parse_error = NULL;
+ gboolean parse_result = g_key_file_get_boolean(
+ keyfile, group_name, key_name, &parse_error);
+ if (!parse_result && parse_error != NULL) {
+ if (parse_error->code == G_KEY_FILE_ERROR_INVALID_VALUE) {
+ // Read the value as a string so we can log it
+ gchar *value = g_key_file_get_string(
+ keyfile, group_name, key_name, NULL);
+ g_warning("Invalid boolean for %s.%s: `%s`\n",
+ group_name, key_name, value);
+ g_free(value);
+ }
+ g_error_free(parse_error);
+ return fallback;
+ }
+ return parse_result;
+}
+
+/* Parse a greeter-colors group key into a newly-allocated GdkRGBA value */
+static GdkRGBA *parse_greeter_color_key(GKeyFile *keyfile, const char *key_name, const char *default_str)
+{
+ gchar *color_string = g_key_file_get_string(
+ keyfile, "greeter-theme", key_name, NULL);
+ if (color_string == NULL) {
+ g_warning("Could not find value for %s.%s - falling back to '%s'",
+ "greeter-theme", key_name, default_str);
+ }
+
+ GdkRGBA *default_color = malloc(sizeof(GdkRGBA));
+ gboolean default_was_parsed = gdk_rgba_parse(default_color, default_str);
+ if (!default_was_parsed) {
+ g_critical("Could not parse the default '%s' setting: %s", key_name, default_str);
+ }
+
+ if (color_string != NULL) {
+ if (strstr(color_string, "#") != NULL) {
+ // Remove quotations from hex color strings
+ remove_char(color_string, '"');
+ remove_char(color_string, '\'');
+ }
+
+ GdkRGBA *color = malloc(sizeof(GdkRGBA));
+ gboolean color_was_parsed = gdk_rgba_parse(color, color_string);
+ if (color_was_parsed) {
+ free(default_color);
+ return color;
+ }
+ g_warning("Could not parse the '%s' setting: %s - falling back to default of %s",
+ key_name, color_string, default_str);
+ free(color);
+ }
+ return default_color;
+}
+
+/* Parse a greeter-hotkeys key into the GDKkeyval of it's first character */
+static guint parse_greeter_hotkey_keyval(GKeyFile *keyfile, const char *key_name, const char default_char)
+{
+ gchar *key = g_key_file_get_string(
+ keyfile, "greeter-hotkeys", key_name, NULL);
+
+ guint32 key_code;
+ if (key == NULL) {
+ key_code = (guint32) default_char;
+ } else if (strcmp(key, "") == 0) {
+ g_warning("Configuration contains empty key for '%s' - falling back to default of %c\n", key_name, default_char);
+ key_code = (guint32) default_char;
+ } else {
+ key_code = (guint32) key[0];
+ }
+
+ return gdk_unicode_to_keyval(key_code);
+}
+
+/* Parse the password masking character that should be displayed when typing
+ * into the password input.
+ *
+ * We first attempt to parse a literal -1 or 0, where -1 means to use the
+ * default character & 0 means to display no characters when typing a password.
+ *
+ * If that parsing fails, we attempt to parse the field as a string & use the
+ * first character of the string. If the string is empty or parsing fails, we
+ * fall back to the default characer.
+ *
+ * Since the related gtk_entry function takes a unsigned int, we use pointers,
+ * where NULL means "use the default" & all other values indicate an argument
+ * to the `gtk_entry_set_invisible_char` function.
+ *
+ */
+static gunichar *parse_greeter_password_char(GKeyFile *keyfile)
+{
+ const char *const group_name = "greeter-theme";
+ const char *const key_name = "password-character";
+ GError *parse_error = NULL;
+
+ // Attempt the int parsing
+
+ const gint int_result = g_key_file_get_integer(
+ keyfile, group_name, key_name, &parse_error);
+ // Matches -1
+ if (int_result == -1) {
+ return NULL;
+ }
+
+ gunichar *result = malloc(sizeof(gunichar));
+ // Matches 0
+ if (int_result == 0 && parse_error == NULL) {
+ *result = 0;
+ return result;
+ } else if (parse_error != NULL) {
+ g_error_free(parse_error);
+ }
+
+ // Atempt the string parsing
+
+ gchar *str_result = g_key_file_get_string(
+ keyfile, group_name, key_name, NULL);
+
+ // Invalid or 0-length string
+ if (str_result == NULL || strlen(str_result) == 0) {
+ if (str_result != NULL) {
+ free(str_result);
+ }
+ free(result);
+ return NULL;
+ }
+
+ // Convert to unicode code points
+ gunichar *unicode_str = g_utf8_to_ucs4(str_result, -1, NULL, NULL, NULL);
+ free(str_result);
+
+ if (unicode_str == NULL) {
+ free(result);
+ return NULL;
+ }
+
+ *result = unicode_str[0];
+ g_free(unicode_str);
+ return result;
+}
+
+/* Parse the password input alignment, properly handling RTL layouts.
+ *
+ * Note that the gboolean returned by this function is meant to be used with
+ * the `gtk_entry_set_alignment` function.
+ */
+static gfloat parse_greeter_password_alignment(GKeyFile *keyfile)
+{
+ gfloat alignment;
+
+ gchar *password_alignment_text = parse_greeter_string(
+ keyfile, "greeter", "password-alignment", "right");
+ gboolean is_rtl = is_rtl_keymap_layout();
+
+ if (input_string_equals(password_alignment_text, "left")) {
+ alignment = is_rtl ? 1 : 0;
+ } else if (input_string_equals(password_alignment_text, "center")) {
+ alignment = 0.5;
+ } else {
+ alignment = is_rtl ? 0 : 1;
+ }
+ free(password_alignment_text);
+ return alignment;
+}
+
+/* Determine if the default Display's Keymap is in the Right-to-Left direction
+ */
+static gboolean is_rtl_keymap_layout(void)
+{
+ GdkDisplay *display = gdk_display_get_default();
+ if (display == NULL) {
+ return FALSE;
+ }
+ GdkKeymap *keymap = gdk_keymap_get_for_display(display);
+ PangoDirection text_direction = gdk_keymap_get_direction(keymap);
+ return text_direction == PANGO_DIRECTION_RTL;
+}
+
+/* Take a string from the config file, trim any whitespace & check it's
+ * equality with a fixed string.
+ */
+gboolean input_string_equals(gchar *input_str, const gchar * const fixed_str) {
+ return strcmp(g_strchomp(input_str), fixed_str) == 0;
+}
diff --git a/src/config.h b/src/config.h
new file mode 100644
index 0000000..51e0f26
--- /dev/null
+++ b/src/config.h
@@ -0,0 +1,68 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#include <gdk/gdk.h>
+#include <glib.h>
+
+#ifndef CONFIG_FILE
+#define CONFIG_FILE "/etc/lightdm/lightdm-mini-greeter.conf"
+#endif
+
+
+// Represents the System's Greeter Configuration. Parsed from `CONFIG_FILE`.
+typedef struct Config_ {
+ gchar *login_user;
+ gboolean show_password_label;
+ gchar *password_label_text;
+ gchar *invalid_password_text;
+ gboolean show_input_cursor;
+ gfloat password_alignment;
+ gint password_input_width;
+ gboolean show_image_on_all_monitors;
+ gboolean show_sys_info;
+
+ /* Theme Configuration */
+ gchar *font;
+ gchar *font_size;
+ gchar *font_weight;
+ gchar *font_style;
+ GdkRGBA *text_color;
+ GdkRGBA *error_color;
+ // Windows
+ gchar *background_image;
+ GdkRGBA *background_color;
+ gchar *background_image_size;
+ GdkRGBA *window_color;
+ GdkRGBA *border_color;
+ gchar *border_width;
+ guint layout_spacing;
+ // Window position
+ gdouble x_pos;
+ gdouble y_pos;
+ // Password Input
+ gunichar *password_char;
+ GdkRGBA *password_color;
+ GdkRGBA *password_background_color;
+ GdkRGBA *password_border_color;
+ gchar *password_border_width;
+ gchar *password_border_radius;
+ // System Info
+ gchar *sys_info_font;
+ gchar *sys_info_font_size;
+ GdkRGBA *sys_info_color;
+ gchar *sys_info_margin;
+
+ /* Hotkeys */
+ guint mod_bit;
+ guint shutdown_key;
+ guint restart_key;
+ guint hibernate_key;
+ guint suspend_key;
+ guint session_key;
+} Config;
+
+
+Config *initialize_config(void);
+void destroy_config(Config *config);
+
+#endif
diff --git a/src/focus_ring.c b/src/focus_ring.c
new file mode 100644
index 0000000..45f64b2
--- /dev/null
+++ b/src/focus_ring.c
@@ -0,0 +1,105 @@
+/* Functions related to a ring of focusable items */
+
+#include "focus_ring.h"
+
+static gint find_by_value(gconstpointer data, gconstpointer user_data);
+
+
+/* Initialize the FocusRing with the given options, value getter, & label
+ *
+ * Returns NULL if the passed GList is NULL.
+ * Throws an error if cannot allocate memory for the FocusRing.
+ */
+FocusRing *initialize_focus_ring(const GList *options, gchar *(*getter_function)(gconstpointer), const gchar *const label)
+{
+ if (options == NULL) {
+ return NULL;
+ }
+
+ FocusRing *ring = malloc(sizeof(FocusRing));
+ if (ring == NULL) {
+ g_error("Could not allocate memory for FocusRing: %s", label);
+ }
+
+ ring->selected = options;
+ ring->beginning = options;
+ ring->end = g_list_last((GList *) options);
+
+ ring->getter_function = getter_function;
+
+ return ring;
+}
+
+/* Free only the FocusRing & not the underlying GList */
+void destroy_focus_ring(FocusRing *ring)
+{
+ free(ring);
+}
+
+/* Focus the next item in the ring, or loop back to the first item. */
+gchar *focus_ring_next(FocusRing *ring)
+{
+ if (ring->selected->next == NULL) {
+ ring->selected = ring->beginning;
+ } else {
+ ring->selected = ring->selected->next;
+ }
+ return focus_ring_get_value(ring);
+}
+
+/* Focus the previous item in the ring, or loop back to the last item. */
+gchar *focus_ring_prev(FocusRing *ring)
+{
+ if (ring->selected->prev == NULL) {
+ ring->selected = ring->end;
+ } else {
+ ring->selected = ring->selected->prev;
+ }
+ return focus_ring_get_value(ring);
+}
+
+/* Get the currently selected data. */
+gconstpointer focus_ring_get_selected(FocusRing *ring)
+{
+ return ring->selected->data;
+}
+
+/* Get the inner value of the currently selected item. */
+gchar *focus_ring_get_value(FocusRing *ring)
+{
+ return ring->getter_function(focus_ring_get_selected(ring));
+}
+
+
+/* Internal data used by the find_by_value & focus_ring_scroll_to_value
+ * functions.
+ */
+struct FindByValueData {
+ FocusRing *ring;
+ const gchar *target_value;
+};
+
+/* Attempt to scroll the ring to the item with the matching inner value.
+ *
+ * Does nothing if the item cannot be found.
+ */
+gchar *focus_ring_scroll_to_value(FocusRing *ring, const gchar *target_value)
+{
+ struct FindByValueData user_data = { .ring = ring, .target_value = target_value };
+ GList *find_result = g_list_find_custom((GList *) ring->beginning, (void *) &user_data, &find_by_value);
+ if (find_result != NULL) {
+ ring->selected = find_result;
+ }
+ return focus_ring_get_value(ring);
+}
+
+/* Determine if the given data's inner value matches what we're looking for.
+ *
+ * Used as callback to the g_list_find_custom function.
+ */
+static gint find_by_value(gconstpointer data, gconstpointer user_data)
+{
+ struct FindByValueData *local_user_data = (struct FindByValueData *) user_data;
+
+ return g_strcmp0(local_user_data->ring->getter_function(data), local_user_data->target_value);
+}
diff --git a/src/focus_ring.h b/src/focus_ring.h
new file mode 100644
index 0000000..74e9002
--- /dev/null
+++ b/src/focus_ring.h
@@ -0,0 +1,31 @@
+#ifndef FOCUSRING_H
+#define FOCUSRING_H
+
+#include <gdk/gdk.h>
+
+
+/* A FocusRing is an scrollable list that wraps around to the beginning/end
+ * when the selection is moved out-of-bounds.
+ */
+typedef struct FocusRing_ {
+ /* Points to current selection */
+ const GList *selected;
+ /* Points to beginning of list */
+ const GList *beginning;
+ /* Points to end of list */
+ const GList *end;
+
+ /* Function to retrieve the target "inner value" from the GList's data */
+ gchar *(*getter_function)(gconstpointer);
+} FocusRing;
+
+FocusRing *initialize_focus_ring(const GList *options, gchar *(*getter_function)(gconstpointer), const gchar *const label);
+void destroy_focus_ring(FocusRing *ring);
+
+gchar *focus_ring_next(FocusRing *ring);
+gchar *focus_ring_prev(FocusRing *ring);
+gconstpointer focus_ring_get_selected(FocusRing *ring);
+gchar *focus_ring_get_value(FocusRing *ring);
+gchar *focus_ring_scroll_to_value(FocusRing *ring, const gchar *target_value);
+
+#endif
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..99351c9
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,28 @@
+/* lightdm-mini-greeter - A minimal GTK LightDM Greeter */
+#include <sys/mman.h>
+
+#include <gtk/gtk.h>
+
+#include "app.h"
+#include "utils.h"
+
+
+int main(int argc, char **argv)
+{
+ mlockall(MCL_CURRENT | MCL_FUTURE); // Keep data out of any swap devices
+
+ App *app = initialize_app(argc, argv);
+
+ connect_to_lightdm_daemon(app->greeter);
+ begin_authentication_as_default_user(app);
+ make_session_focus_ring(app);
+
+ for (int m = 0; m < APP_MONITOR_COUNT(app); m++) {
+ gtk_widget_show_all(GTK_WIDGET(APP_BACKGROUND_WINDOWS(app)[m]));
+ }
+ gtk_widget_show_all(GTK_WIDGET(APP_MAIN_WINDOW(app)));
+ gtk_window_present(APP_MAIN_WINDOW(app));
+ gtk_main();
+
+ destroy_app(app);
+}
diff --git a/src/ui.c b/src/ui.c
new file mode 100644
index 0000000..6d2d42c
--- /dev/null
+++ b/src/ui.c
@@ -0,0 +1,430 @@
+/* Functions related to the GUI. */
+#define _GNU_SOURCE
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <gtk/gtk.h>
+#include <glib.h>
+#include <lightdm.h>
+
+#include "callbacks.h"
+#include "ui.h"
+#include "utils.h"
+
+
+static UI *new_ui(void);
+static void setup_background_windows(Config *config, UI *ui);
+static GtkWindow *new_background_window(GdkMonitor *monitor);
+static void set_window_to_monitor_size(GdkMonitor *monitor, GtkWindow *window);
+static void hide_mouse_cursor(GtkWidget *window, gpointer user_data);
+static void move_mouse_to_background_window(void);
+static void setup_main_window(Config *config, UI *ui);
+static void place_main_window(GtkWidget *main_window, gpointer user_data);
+static void create_and_attach_layout_container(UI *ui);
+static void create_and_attach_sys_info_label(Config *config, UI *ui);
+static void create_and_attach_password_field(Config *config, UI *ui);
+static void create_and_attach_feedback_label(UI *ui);
+static void attach_config_colors_to_screen(Config *config);
+
+
+/* Initialize the Main Window & it's Children */
+UI *initialize_ui(Config *config)
+{
+ UI *ui = new_ui();
+
+ setup_background_windows(config, ui);
+ move_mouse_to_background_window();
+ setup_main_window(config, ui);
+ create_and_attach_layout_container(ui);
+ create_and_attach_sys_info_label(config, ui);
+ create_and_attach_password_field(config, ui);
+ create_and_attach_feedback_label(ui);
+ attach_config_colors_to_screen(config);
+
+ return ui;
+}
+
+
+/* Create a new UI with all values initialized to NULL */
+static UI *new_ui(void)
+{
+ UI *ui = malloc(sizeof(UI));
+ if (ui == NULL) {
+ g_error("Could not allocate memory for UI");
+ }
+ ui->background_windows = NULL;
+ ui->monitor_count = 0;
+ ui->main_window = NULL;
+ ui->layout_container = NULL;
+ ui->password_label = NULL;
+ ui->password_input = NULL;
+ ui->feedback_label = NULL;
+
+ return ui;
+}
+
+
+/* Create a Background Window for Every Monitor */
+static void setup_background_windows(Config *config, UI *ui)
+{
+ GdkDisplay *display = gdk_display_get_default();
+ ui->monitor_count = gdk_display_get_n_monitors(display);
+ ui->background_windows = malloc((uint) ui->monitor_count * sizeof (GtkWindow *));
+ for (int m = 0; m < ui->monitor_count; m++) {
+ GdkMonitor *monitor = gdk_display_get_monitor(display, m);
+ if (monitor == NULL) {
+ break;
+ }
+
+ GtkWindow *background_window = new_background_window(monitor);
+ ui->background_windows[m] = background_window;
+
+ gboolean show_background_image =
+ (gdk_monitor_is_primary(monitor) || config->show_image_on_all_monitors) &&
+ (strcmp(config->background_image, "\"\"") != 0);
+ if (show_background_image) {
+ GtkStyleContext *style_context =
+ gtk_widget_get_style_context(GTK_WIDGET(background_window));
+ gtk_style_context_add_class(style_context, "with-image");
+ }
+ }
+}
+
+
+/* Create & Configure a Background Window for a Monitor */
+static GtkWindow *new_background_window(GdkMonitor *monitor)
+{
+ GtkWindow *background_window = GTK_WINDOW(gtk_window_new(
+ GTK_WINDOW_TOPLEVEL));
+ gtk_window_set_type_hint(background_window, GDK_WINDOW_TYPE_HINT_DESKTOP);
+ gtk_window_set_keep_below(background_window, TRUE);
+ gtk_widget_set_name(GTK_WIDGET(background_window), "background");
+
+ // Set Window Size to Monitor Size
+ set_window_to_monitor_size(monitor, background_window);
+
+ g_signal_connect(background_window, "realize", G_CALLBACK(hide_mouse_cursor),
+ NULL);
+ // TODO: is this needed?
+ g_signal_connect(background_window, "destroy", G_CALLBACK(gtk_main_quit),
+ NULL);
+
+ return background_window;
+}
+
+
+/* Set the Window's Minimum Size to the Default Screen's Size */
+static void set_window_to_monitor_size(GdkMonitor *monitor, GtkWindow *window)
+{
+ GdkRectangle geometry;
+ gdk_monitor_get_geometry(monitor, &geometry);
+ gtk_widget_set_size_request(
+ GTK_WIDGET(window),
+ geometry.width,
+ geometry.height
+ );
+ gtk_window_move(window, geometry.x, geometry.y);
+ gtk_window_set_resizable(window, FALSE);
+}
+
+
+/* Hide the mouse cursor when it is hovered over the given widget.
+ *
+ * Note: This has no effect when used with a GtkEntry widget.
+ */
+static void hide_mouse_cursor(GtkWidget *widget, gpointer user_data)
+{
+ GdkDisplay *display = gdk_display_get_default();
+ GdkCursor *blank_cursor = gdk_cursor_new_for_display(display, GDK_BLANK_CURSOR);
+ GdkWindow *window = gtk_widget_get_window(widget);
+ if (window != NULL) {
+ gdk_window_set_cursor(window, blank_cursor);
+ }
+}
+
+
+/* Move the mouse cursor to the upper-left corner of the primary screen.
+ *
+ * This is necessary for hiding the mouse cursor because we cannot hide the
+ * mouse cursor when it is hovered over the GtkEntry password input. Instead,
+ * we hide the cursor when it is over the background windows and then move the
+ * mouse to the corner of the screen where it should hover over the background
+ * window or main window instead.
+ */
+static void move_mouse_to_background_window(void)
+{
+ GdkDisplay *display = gdk_display_get_default();
+ GdkDevice *mouse = gdk_seat_get_pointer(gdk_display_get_default_seat(display));
+ GdkScreen *screen = gdk_display_get_default_screen(display);
+
+ gdk_device_warp(mouse, screen, 0, 0);
+}
+
+
+/* Create & Configure the Main Window */
+static void setup_main_window(Config *config, UI *ui)
+{
+ GtkWindow *main_window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
+
+ gtk_container_set_border_width(GTK_CONTAINER(main_window), config->layout_spacing);
+ gtk_widget_set_name(GTK_WIDGET(main_window), "main");
+
+ g_signal_connect(main_window, "show", G_CALLBACK(place_main_window), config);
+ g_signal_connect(main_window, "realize", G_CALLBACK(hide_mouse_cursor), NULL);
+ g_signal_connect(main_window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
+
+ ui->main_window = main_window;
+}
+
+
+/* Move the Main Window to the Center of the Primary Monitor
+ *
+ * This is done after the main window is shown(via the "show" signal) so that
+ * the width of the window is properly calculated. Otherwise the returned size
+ * will not include the size of the password label text.
+ */
+static void place_main_window(GtkWidget *main_window, gpointer user_data)
+{
+ // Get the Geometry of the Primary Monitor
+ GdkDisplay *display = gdk_display_get_default();
+ GdkMonitor *primary_monitor = gdk_display_get_primary_monitor(display);
+ GdkRectangle primary_monitor_geometry;
+ gdk_monitor_get_geometry(primary_monitor, &primary_monitor_geometry);
+
+ // Get the Geometry of the Window
+ gint window_width, window_height;
+ gtk_window_get_size(GTK_WINDOW(main_window), &window_width, &window_height);
+
+ Config *config = (Config *) user_data;
+ gint xpos = (gint) (primary_monitor_geometry.width * config->x_pos);
+ gint ypos = (gint) (primary_monitor_geometry.height * config->y_pos);
+
+ gtk_window_move(
+ GTK_WINDOW(main_window),
+ primary_monitor_geometry.x + xpos - window_width / 2,
+ primary_monitor_geometry.y + ypos - window_height / 2);
+}
+
+
+/* Add a Layout Container for All Displayed Widgets */
+static void create_and_attach_layout_container(UI *ui)
+{
+ ui->layout_container = GTK_GRID(gtk_grid_new());
+ gtk_grid_set_column_spacing(ui->layout_container, 5);
+ gtk_grid_set_row_spacing(ui->layout_container, 5);
+
+ gtk_container_add(GTK_CONTAINER(ui->main_window),
+ GTK_WIDGET(ui->layout_container));
+}
+
+/* Create a container for the system information & current time.
+ *
+ * Set the system information text by querying LightDM & the Config, but leave
+ * the time blank & let the timer update it.
+ */
+static void create_and_attach_sys_info_label(Config *config, UI *ui)
+{
+ if (!config->show_sys_info) {
+ return;
+ }
+ // container for system info & time
+ ui->info_container = GTK_GRID(gtk_grid_new());
+ gtk_grid_set_column_spacing(ui->info_container, 0);
+ gtk_grid_set_row_spacing(ui->info_container, 5);
+ gtk_widget_set_name(GTK_WIDGET(ui->info_container), "info");
+
+ // system info: <user>@<hostname>
+ const gchar *hostname = lightdm_get_hostname();
+ gchar *output_string;
+ int output_string_length = asprintf(&output_string, "%s@%s",
+ config->login_user, hostname);
+ if (output_string_length >= 0) {
+ ui->sys_info_label = gtk_label_new(output_string);
+ } else {
+ g_warning("Could not allocate memory for system info string.");
+ ui->sys_info_label = gtk_label_new("");
+ }
+ gtk_label_set_xalign(GTK_LABEL(ui->sys_info_label), 0.0f);
+ gtk_widget_set_name(GTK_WIDGET(ui->sys_info_label), "sys-info");
+
+ // time: filled out by callback
+ ui->time_label = gtk_label_new("");
+ gtk_label_set_xalign(GTK_LABEL(ui->time_label), 1.0f);
+ gtk_widget_set_hexpand(GTK_WIDGET(ui->time_label), TRUE);
+ gtk_widget_set_name(GTK_WIDGET(ui->time_label), "time-info");
+
+ // attach labels to info container, attach info container to layout.
+ gtk_grid_attach(
+ ui->info_container, GTK_WIDGET(ui->sys_info_label), 0, 0, 1, 1);
+ gtk_grid_attach(
+ ui->info_container, GTK_WIDGET(ui->time_label), 1, 0, 1, 1);
+ gtk_grid_attach(
+ ui->layout_container, GTK_WIDGET(ui->info_container), 0, 0, 2, 1);
+}
+
+
+/* Add a label & entry field for the user's password.
+ *
+ * If the `show_password_label` member of `config` is FALSE,
+ * `ui->password_label` is left as NULL.
+ */
+static void create_and_attach_password_field(Config *config, UI *ui)
+{
+ ui->password_input = gtk_entry_new();
+ gtk_entry_set_visibility(GTK_ENTRY(ui->password_input), FALSE);
+ if (config->password_char != NULL) {
+ gtk_entry_set_invisible_char(GTK_ENTRY(ui->password_input), *config->password_char);
+ }
+ gtk_entry_set_alignment(GTK_ENTRY(ui->password_input),
+ config->password_alignment);
+ // TODO: The width is usually a little shorter than we specify. Is there a
+ // way to force this exact character width?
+ // Maybe use 2 GtkBoxes instead of a GtkGrid?
+ gtk_entry_set_width_chars(GTK_ENTRY(ui->password_input),
+ config->password_input_width);
+ gtk_widget_set_name(GTK_WIDGET(ui->password_input), "password");
+ const gint top = config->show_sys_info ? 1 : 0;
+ gtk_grid_attach(ui->layout_container, ui->password_input, 1, top, 1, 1);
+
+ if (config->show_password_label) {
+ ui->password_label = gtk_label_new(config->password_label_text);
+ gtk_label_set_justify(GTK_LABEL(ui->password_label), GTK_JUSTIFY_RIGHT);
+ gtk_grid_attach_next_to(ui->layout_container, ui->password_label,
+ ui->password_input, GTK_POS_LEFT, 1, 1);
+ }
+}
+
+
+/* Add a label for feedback to the user */
+static void create_and_attach_feedback_label(UI *ui)
+{
+ ui->feedback_label = gtk_label_new("");
+ gtk_label_set_justify(GTK_LABEL(ui->feedback_label), GTK_JUSTIFY_CENTER);
+ gtk_widget_set_no_show_all(ui->feedback_label, TRUE);
+ gtk_widget_set_name(GTK_WIDGET(ui->feedback_label), "error");
+
+ GtkWidget *attachment_point;
+ gint width;
+ if (ui->password_label == NULL) {
+ attachment_point = ui->password_input;
+ width = 1;
+ } else {
+ attachment_point = ui->password_label;
+ width = 2;
+ }
+
+ gtk_grid_attach_next_to(ui->layout_container, ui->feedback_label,
+ attachment_point, GTK_POS_BOTTOM, width, 1);
+}
+
+/* Attach a style provider to the screen, using color options from config */
+static void attach_config_colors_to_screen(Config *config)
+{
+ GtkCssProvider* provider = gtk_css_provider_new();
+
+ GdkRGBA *caret_color;
+ if (config->show_input_cursor) {
+ caret_color = config->password_color;
+ } else {
+ caret_color = config->password_background_color;
+ }
+
+ char *css;
+ int css_string_length = asprintf(&css,
+ "* {\n"
+ "font-family: %s;\n"
+ "font-size: %s;\n"
+ "font-weight: %s;\n"
+ "font-style: %s;\n"
+ "}\n"
+ "label {\n"
+ "color: %s;\n"
+ "}\n"
+ "label#error {\n"
+ "color: %s;\n"
+ "}\n"
+ "#background {\n"
+ "background-color: %s;\n"
+ "}\n"
+ "#background.with-image {\n"
+ "background-image: image(url(%s), %s);\n"
+ "background-repeat: no-repeat;\n"
+ "background-size: %s;\n"
+ "background-position: center;\n"
+ "}\n"
+ "#main, #password {\n"
+ "border-width: %s;\n"
+ "border-color: %s;\n"
+ "border-style: solid;\n"
+ "}\n"
+ "#main {\n"
+ "background-color: %s;\n"
+ "}\n"
+ "#password {\n"
+ "color: %s;\n"
+ "caret-color: %s;\n"
+ "background-color: %s;\n"
+ "border-width: %s;\n"
+ "border-color: %s;\n"
+ "border-radius: %s;\n"
+ "background-image: none;\n"
+ "box-shadow: none;\n"
+ "border-image-width: 0;\n"
+ "}\n"
+ "#info {\n"
+ "margin: %s;\n"
+ "}\n"
+ "#info label {\n"
+ "font-family: %s;\n"
+ "font-size: %s;\n"
+ "color: %s;\n"
+ "}\n"
+
+ // *
+ , config->font
+ , config->font_size
+ , config->font_weight
+ , config->font_style
+ // label
+ , gdk_rgba_to_string(config->text_color)
+ // label#error
+ , gdk_rgba_to_string(config->error_color)
+ // #background
+ , gdk_rgba_to_string(config->background_color)
+ // #background.image-background
+ , config->background_image
+ , gdk_rgba_to_string(config->background_color)
+ , config->background_image_size
+ // #main, #password
+ , config->border_width
+ , gdk_rgba_to_string(config->border_color)
+ // #main
+ , gdk_rgba_to_string(config->window_color)
+ // #password
+ , gdk_rgba_to_string(config->password_color)
+ , gdk_rgba_to_string(caret_color)
+ , gdk_rgba_to_string(config->password_background_color)
+ , config->password_border_width
+ , gdk_rgba_to_string(config->password_border_color)
+ , config->password_border_radius
+ // #info
+ , config->sys_info_margin
+ // #info label
+ , config->sys_info_font
+ , config->sys_info_font_size
+ , gdk_rgba_to_string(config->sys_info_color)
+ );
+
+ if (css_string_length >= 0) {
+ gtk_css_provider_load_from_data(provider, css, -1, NULL);
+
+ GdkScreen *screen = gdk_screen_get_default();
+ gtk_style_context_add_provider_for_screen(
+ screen, GTK_STYLE_PROVIDER(provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER + 1);
+ }
+
+
+ g_object_unref(provider);
+}
diff --git a/src/ui.h b/src/ui.h
new file mode 100644
index 0000000..3d84aaf
--- /dev/null
+++ b/src/ui.h
@@ -0,0 +1,24 @@
+#ifndef UI_H
+#define UI_H
+
+#include <gtk/gtk.h>
+#include "config.h"
+
+
+typedef struct UI_ {
+ GtkWindow **background_windows;
+ int monitor_count;
+ GtkWindow *main_window;
+ GtkGrid *layout_container;
+ GtkGrid *info_container;
+ GtkWidget *sys_info_label;
+ GtkWidget *time_label;
+ GtkWidget *password_label;
+ GtkWidget *password_input;
+ GtkWidget *feedback_label;
+} UI;
+
+
+UI *initialize_ui(Config *config);
+
+#endif
diff --git a/src/utils.c b/src/utils.c
new file mode 100644
index 0000000..38a34de
--- /dev/null
+++ b/src/utils.c
@@ -0,0 +1,70 @@
+/* General Utility Functions */
+#include <lightdm.h>
+
+#include "app.h"
+#include "compat.h"
+#include "lightdm/session.h"
+#include "utils.h"
+#include "focus_ring.h"
+
+static gchar *get_session_key(gconstpointer data);
+
+
+/* Connect to the LightDM daemon or exit with an error */
+void connect_to_lightdm_daemon(LightDMGreeter *greeter)
+{
+ if (!lightdm_greeter_connect_sync(greeter, NULL)) {
+ g_critical("Could not connect to the LightDM daemon");
+ }
+}
+
+
+/* Begin authentication as the default user, or exit with an error */
+void begin_authentication_as_default_user(App *app)
+{
+ const gchar *default_user = APP_LOGIN_USER(app);
+ if (g_strcmp0(default_user, NULL) == 0) {
+ g_critical("A default user has not been not set");
+ } else {
+ g_message("Beginning authentication as the default user: %s",
+ default_user);
+ compat_greeter_authenticate(app->greeter, default_user, NULL);
+ }
+}
+
+
+/* Remove every occurence of a character from a string */
+void remove_char(char *str, char garbage) {
+
+ char *src, *dst;
+ for (src = dst = str; *src != '\0'; src++) {
+ *dst = *src;
+ if (*dst != garbage) dst++;
+ }
+ *dst = '\0';
+}
+
+
+/* Get Sessions & Build the Focus Ring */
+void make_session_focus_ring(App *app)
+{
+ const gchar *default_session =
+ lightdm_greeter_get_default_session_hint(app->greeter);
+ const GList *sessions = lightdm_get_sessions();
+ FocusRing *session_ring = initialize_focus_ring(sessions, &get_session_key, "sessions");
+
+ if (default_session != NULL) {
+ focus_ring_scroll_to_value(session_ring, default_session);
+ }
+ g_message("Initial session set to: %s", focus_ring_get_value(session_ring));
+
+ app->session_ring = session_ring;
+}
+/* Retrieves the `key` field of a session, used to pull current session out of
+ * a FocusRing.
+ */
+static gchar *get_session_key(gconstpointer data)
+{
+ LightDMSession *session = (LightDMSession *) data;
+ return (gchar *) lightdm_session_get_key(session);
+}
diff --git a/src/utils.h b/src/utils.h
new file mode 100644
index 0000000..6351e7c
--- /dev/null
+++ b/src/utils.h
@@ -0,0 +1,14 @@
+#ifndef UTILS_H
+#define UTILS_H
+
+#include <lightdm.h>
+
+#include "app.h"
+
+
+void connect_to_lightdm_daemon(LightDMGreeter *greeter);
+void make_session_focus_ring(App *app);
+void begin_authentication_as_default_user(App *app);
+void remove_char(char *str, char garbage);
+
+#endif