commit 2b4d06b37c9c928e8d74d8d86447d99cc8ea5796 Author: Tait Hoyem Date: Mon Jul 25 13:31:14 2022 -0600 Initial commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..18d33dc --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +CC=clang +CFLAGS=-O2 -g -Wall -I/usr/include -I/usr/X11R6/include `pkg-config --cflags glib-2.0 gobject-2.0 atk-bridge-2.0 atspi-2 speech-dispatcher` +LDFLAGS=-pthread -L/usr/X11R6/lib -lm `pkg-config --libs glib-2.0 gobject-2.0 atk-bridge-2.0 atspi-2 speech-dispatcher` +EXAMPLES = $(patsubst %.c,%,$(wildcard *.c)) + +default: + $(CC) -o speak-selection $< $(CFLAGS) $(LDFLAGS) speak-selection.c diff --git a/README.md b/README.md new file mode 100644 index 0000000..89ea314 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Speak Selection (SSN) + +This program is dead simple. +While running, it wil speak out newly selected pieces of text to a user. + +It does not include any screen reading functionality. +For that, I would shamelessly recommend a project I work on called [Odilia}(https://odilia.app) +or a project I don't work on: [Orca](https://help.gnome.org/users/orca/stable/introduction.html.en). + +## Credits + +* Blatently ripped of the code from [at-spi2-examples](https://github.com/infapi00/at-spi2-examples/) + +## Requirements + +* `at-spi2-core` +* `libspeechd` +* `gcc` or `clang` diff --git a/speak-selection b/speak-selection new file mode 100755 index 0000000..be4f7b3 Binary files /dev/null and b/speak-selection differ diff --git a/speak-selection.c b/speak-selection.c new file mode 100644 index 0000000..31c02c0 --- /dev/null +++ b/speak-selection.c @@ -0,0 +1,205 @@ +/* + * For each focus/selection change, it prints the following for each + * object getting the focus/selection: + * (appname, name, rolename, [state-set]) + * + * You can specify if you want to filter by application. Use --help + * for more information. + */ + +#include +#include +#include +#include +#include +#include + +gchar *filter_name = NULL; +SPDConnection *spd_conn = NULL; + +const static gchar* +atspi_state_get_name(gint state) +{ + GTypeClass *type_class; + GEnumValue *value; + + type_class = g_type_class_ref(ATSPI_TYPE_STATE_TYPE); + g_return_val_if_fail(G_IS_ENUM_CLASS (type_class), ""); + + value = g_enum_get_value(G_ENUM_CLASS (type_class), state); + + return value->value_nick; +} + +static gchar* +get_state_set(AtspiAccessible *accessible) +{ + AtspiStateSet *state_set = atspi_accessible_get_state_set(accessible); + GArray *states = atspi_state_set_get_states(state_set); + gchar *result = g_strdup_printf("["); + gchar *aux = NULL; + gint i; + AtspiStateType state; + + for(i = 0; i < states->len; i++) { + state = g_array_index(states, gint, i); + + aux = result; + if(i < states->len -1) + result = g_strdup_printf("%s%s,", aux, atspi_state_get_name(state)); + else + result = g_strdup_printf("%s%s", aux, atspi_state_get_name(state)); + g_free(aux); + } + + aux = result; + result = g_strconcat(aux, "]", NULL); + g_free(aux); + + g_array_free(states, TRUE); + g_object_unref(state_set); + + return result; +} + +static gchar* +get_label(AtspiAccessible *accessible) +{ + GArray *relations; + AtspiRelation *relation; + gint i; + gchar *result = ""; + + relations = atspi_accessible_get_relation_set(accessible, NULL); + if(relations == NULL) { + return ""; + } + + for(i = 0; i < relations->len; i++) { + relation = g_array_index(relations, AtspiRelation*, i); + + if(atspi_relation_get_relation_type (relation) == ATSPI_RELATION_LABELLED_BY) { + result = atspi_accessible_get_name(atspi_relation_get_target (relation, 0), NULL); + } + } + + if(relations != NULL) + g_array_free(relations, TRUE); + + return result; +} + +static void +print_info(AtspiAccessible *accessible, + gchar *app_name) +{ + AtspiText *text = atspi_accessible_get_text_iface(accessible); + gchar *name = "NULL"; + gchar *role_name = "NULL"; + gchar *state_set = NULL; + gint length_of_string; + GError **error = NULL; + + if(accessible != NULL) { + name = atspi_accessible_get_name(accessible, NULL); + if((name == NULL) || (g_strcmp0(name, "") == 0)) { + name = get_label(accessible); + if((name == NULL) || (g_strcmp0(name, "") == 0)) { + length_of_string = atspi_text_get_character_count(text, error); + name = atspi_text_get_text(text, 0, length_of_string, error); + } + } + role_name = atspi_accessible_get_role_name(accessible, NULL); + } + + state_set = get_state_set(accessible); + g_print("(%s, %s, %s, %s)\n", app_name, name, role_name, state_set); + spd_sayf(spd_conn, SPD_TEXT, "%s, %s", name, role_name); + g_free(state_set); +} + +static void +on_event(AtspiEvent *event, + void *data) +{ + AtspiAccessible *application = NULL; + gchar *app_name = NULL; + + if(event->source == NULL) + return; + + /* We only care about focus/selection gain */ + if(!event->detail1) + return; + + application = atspi_accessible_get_application(event->source, NULL); + if(application == NULL) + return; + + app_name = atspi_accessible_get_name(application, NULL); + + if((filter_name != NULL) && (g_strcmp0 (app_name, filter_name) != 0)) + goto clean; + + print_info(event->source, app_name); + +clean: + g_free(app_name); +} + +static gchar* +parse_args(int *argc, + char ***argv) +{ + GError *error = NULL; + GOptionContext *context; + static gchar *name = NULL; + static GOptionEntry entries [] = + { + {"application", 'a', 0, G_OPTION_ARG_STRING, &name, "Application name", NULL}, + {NULL,}, + }; + + context = g_option_context_new(""); + g_option_context_add_main_entries(context, entries, NULL); + if(!g_option_context_parse (context, argc, argv, &error)) + { + g_print("%s\n", error->message); + g_print("Use --help for more information.\n"); + exit(0); + } + + return name; +} + +int main(int argc, gchar **argv) +{ + AtspiEventListener *listener; + char username[64]; + + filter_name = parse_args(&argc, &argv); + if(!filter_name) { + g_print("NOTE: Application name to filter not specified. Showing " + "focus/selection changes for any application.\n"); + } + if(getlogin_r(username, 64)) { + g_print("USERNAME not found. Error.\n"); + exit(1); + } + spd_conn = spd_open("speak_selection", "main", username, SPD_MODE_SINGLE); + if(!spd_conn){ + g_print("Error establishing speech dispatcher connection. Fatal error."); + exit(1); + } + + atspi_init(); + + listener = atspi_event_listener_new(on_event, NULL, NULL); + + //atspi_event_listener_register(listener, "object:state-changed:focused", NULL); + atspi_event_listener_register(listener, "object:text-caret-moved", NULL); + + atspi_event_main(); + spd_close(spd_conn); + return 0; +}