summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHannes Schulze <43932053+hannesschulze@users.noreply.github.com>2024-11-04 12:58:21 +0100
committerGitHub <noreply@github.com>2024-11-04 12:58:21 +0100
commit40efcf60a5f39674b5137f11de7bcfe37fb121b5 (patch)
tree29e89bf90cd56d26f7432a52bf4bc90c51c632ce /src
parentf82081215e1f29140ab9c70e897ac62e9b456919 (diff)
parent83d650b82e5209d2ca24cc1e6bf04e0644b3f615 (diff)
Merge pull request #4 from hannesschulze/3-calculator
Add a calculator to the launcher
Diffstat (limited to 'src')
-rw-r--r--src/application.c5
-rw-r--r--src/launcher-item.c73
-rw-r--r--src/launcher-item.h28
-rw-r--r--src/launcher.c83
-rw-r--r--src/launcher.h2
-rw-r--r--src/meson.build77
-rw-r--r--src/services/application-service.c211
-rw-r--r--src/services/application-service.h4
-rw-r--r--src/services/meson.build3
-rw-r--r--src/services/quick-answer-service.c360
-rw-r--r--src/services/quick-answer-service.h21
-rw-r--r--src/services/quick-answers/math-lexer.c248
-rw-r--r--src/services/quick-answers/math-parser.c414
-rw-r--r--src/services/quick-answers/math.c851
-rw-r--r--src/services/quick-answers/math.h148
-rw-r--r--src/services/quick-answers/math.test.c153
-rw-r--r--src/services/quick-answers/meson.build9
17 files changed, 2538 insertions, 152 deletions
diff --git a/src/application.c b/src/application.c
index a32e1ce..bad62b1 100644
--- a/src/application.c
+++ b/src/application.c
@@ -7,6 +7,7 @@
#include "services/battery-service.h"
#include "services/clock-service.h"
#include "services/brightness-service.h"
+#include "services/quick-answer-service.h"
#include "services/workspace-service.h"
#include "services/notification-service.h"
#include "services/audio-service.h"
@@ -38,6 +39,7 @@ struct _FoobarApplication
FoobarNetworkService* network_service;
FoobarBluetoothService* bluetooth_service;
FoobarApplicationService* application_service;
+ FoobarQuickAnswerService* quick_answer_service;
FoobarConfigurationService* configuration_service;
gulong config_handler_id;
FoobarServer* server_skeleton;
@@ -180,6 +182,7 @@ void foobar_application_activate( GApplication* app )
self->network_service = foobar_network_service_new( );
self->bluetooth_service = foobar_bluetooth_service_new( );
self->application_service = foobar_application_service_new( );
+ self->quick_answer_service = foobar_quick_answer_service_new( );
self->configuration_service = foobar_configuration_service_new( );
// Enforce a uniform style by forcing Adwaita and shipping our own icons.
@@ -231,6 +234,7 @@ void foobar_application_activate( GApplication* app )
self->launcher = foobar_launcher_new(
self->application_service,
+ self->quick_answer_service,
self->configuration_service );
g_object_ref( self->launcher );
@@ -358,6 +362,7 @@ void foobar_application_finalize( GObject* object )
g_clear_object( &self->network_service );
g_clear_object( &self->bluetooth_service );
g_clear_object( &self->application_service );
+ g_clear_object( &self->quick_answer_service );
g_clear_object( &self->configuration_service );
g_clear_object( &self->style_provider );
g_clear_object( &self->server_skeleton );
diff --git a/src/launcher-item.c b/src/launcher-item.c
new file mode 100644
index 0000000..c4de70b
--- /dev/null
+++ b/src/launcher-item.c
@@ -0,0 +1,73 @@
+#include "launcher-item.h"
+
+static void foobar_launcher_item_default_init( FoobarLauncherItemInterface* iface );
+
+G_DEFINE_INTERFACE( FoobarLauncherItem, foobar_launcher_item, G_TYPE_OBJECT )
+
+//
+// Static initialization for the launcher item interface.
+//
+void foobar_launcher_item_default_init( FoobarLauncherItemInterface* iface )
+{
+ g_object_interface_install_property( iface, g_param_spec_string(
+ "title",
+ "Title",
+ "The title text displayed in the launcher.",
+ NULL,
+ G_PARAM_READABLE ) );
+ g_object_interface_install_property( iface, g_param_spec_string(
+ "description",
+ "Description",
+ "Text displayed below the title.",
+ NULL,
+ G_PARAM_READABLE ) );
+ g_object_interface_install_property( iface, g_param_spec_object(
+ "icon",
+ "Icon",
+ "Icon to show for the item.",
+ G_TYPE_ICON,
+ G_PARAM_READABLE ) );
+}
+
+//
+// Get the title text displayed in the launcher.
+//
+gchar const* foobar_launcher_item_get_title( FoobarLauncherItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL );
+ FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self );
+ g_return_val_if_fail( iface->get_title != NULL, NULL );
+ return iface->get_title( self );
+}
+
+//
+// Get the text displayed below the title.
+//
+gchar const* foobar_launcher_item_get_description( FoobarLauncherItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL );
+ FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self );
+ g_return_val_if_fail( iface->get_description != NULL, NULL );
+ return iface->get_description( self );
+}
+
+//
+// Get the icon to show for the item.
+//
+GIcon* foobar_launcher_item_get_icon( FoobarLauncherItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL );
+ FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self );
+ g_return_val_if_fail( iface->get_icon != NULL, NULL );
+ return iface->get_icon( self );
+}
+
+//
+// Activate the item, e.g. by launching the application.
+//
+void foobar_launcher_item_activate( FoobarLauncherItem* self )
+{
+ g_return_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ) );
+ FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self );
+ if ( iface->activate ) { iface->activate( self ); }
+}
diff --git a/src/launcher-item.h b/src/launcher-item.h
new file mode 100644
index 0000000..b2d274a
--- /dev/null
+++ b/src/launcher-item.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_LAUNCHER_ITEM foobar_launcher_item_get_type( )
+
+G_DECLARE_INTERFACE( FoobarLauncherItem, foobar_launcher_item, FOOBAR, LAUNCHER_ITEM, GObject )
+
+struct _FoobarLauncherItemInterface
+{
+ GTypeInterface parent_interface;
+
+ gchar const* ( *get_title ) ( FoobarLauncherItem* self );
+ gchar const* ( *get_description )( FoobarLauncherItem* self );
+ GIcon* ( *get_icon ) ( FoobarLauncherItem* self );
+ void ( *activate ) ( FoobarLauncherItem* self );
+};
+
+gchar const* foobar_launcher_item_get_title ( FoobarLauncherItem* self );
+gchar const* foobar_launcher_item_get_description( FoobarLauncherItem* self );
+GIcon* foobar_launcher_item_get_icon ( FoobarLauncherItem* self );
+void foobar_launcher_item_activate ( FoobarLauncherItem* self );
+
+G_END_DECLS
+
diff --git a/src/launcher.c b/src/launcher.c
index da37715..f4b34d8 100644
--- a/src/launcher.c
+++ b/src/launcher.c
@@ -1,4 +1,5 @@
#include "launcher.h"
+#include "launcher-item.h"
#include "widgets/limit-container.h"
#include <gtk4-layer-shell.h>
#include <gdk/gdkkeysyms.h>
@@ -21,8 +22,11 @@ struct _FoobarLauncher
GtkWidget* list_view;
GtkWidget* limit_container;
GtkFilterListModel* filter_model;
+ GListStore* quick_answer_model;
+ GtkFlattenListModel* flatten_model;
GtkSingleSelection* selection_model;
FoobarApplicationService* application_service;
+ FoobarQuickAnswerService* quick_answer_service;
FoobarConfigurationService* configuration_service;
gulong config_handler_id;
};
@@ -119,9 +123,28 @@ void foobar_launcher_init( FoobarLauncher* self )
GtkWidget *separator = gtk_separator_new( GTK_ORIENTATION_HORIZONTAL );
+ // Set up the results list view and an event controller for auto-switching focus to the input.
+
+ GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_launcher_handle_item_setup ), NULL );
+
+ GtkCustomFilter* filter = gtk_custom_filter_new( foobar_launcher_filter_func, self, NULL );
+
+ self->filter_model = gtk_filter_list_model_new( NULL, GTK_FILTER( filter ) );
+
+ self->quick_answer_model = g_list_store_new( FOOBAR_TYPE_LAUNCHER_ITEM );
+
+ GListStore* flattened_models = g_list_store_new( G_TYPE_LIST_MODEL );
+ g_list_store_append( flattened_models, self->quick_answer_model );
+ g_list_store_append( flattened_models, self->filter_model );
+
+ self->flatten_model = gtk_flatten_list_model_new( G_LIST_MODEL( flattened_models ) );
+
+ self->selection_model = gtk_single_selection_new( G_LIST_MODEL( g_object_ref( self->flatten_model ) ) );
+
{
- GtkExpression* list_expr = gtk_constant_expression_new( GTK_TYPE_FILTER_LIST_MODEL, self->filter_model );
- GtkExpression* count_expr = gtk_property_expression_new( GTK_TYPE_FILTER_LIST_MODEL, list_expr, "n-items" );
+ GtkExpression* list_expr = gtk_constant_expression_new( GTK_TYPE_FLATTEN_LIST_MODEL, self->flatten_model );
+ GtkExpression* count_expr = gtk_property_expression_new( GTK_TYPE_FLATTEN_LIST_MODEL, list_expr, "n-items" );
GtkExpression* visible_params[] = { count_expr };
GtkExpression* visible_expr = gtk_cclosure_expression_new(
G_TYPE_BOOLEAN,
@@ -134,17 +157,6 @@ void foobar_launcher_init( FoobarLauncher* self )
gtk_expression_bind( visible_expr, separator, "visible", NULL );
}
- // Set up the results list view and an event controller for auto-switching focus to the input.
-
- GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( );
- g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_launcher_handle_item_setup ), NULL );
-
- GtkCustomFilter* filter = gtk_custom_filter_new( foobar_launcher_filter_func, self, NULL );
-
- self->filter_model = gtk_filter_list_model_new( NULL, GTK_FILTER( filter ) );
-
- self->selection_model = gtk_single_selection_new( G_LIST_MODEL( g_object_ref( self->filter_model ) ) );
-
GtkEventController* list_controller = gtk_event_controller_key_new( );
g_signal_connect( list_controller, "key-pressed", G_CALLBACK( foobar_launcher_handle_list_key ), self );
g_signal_connect( list_controller, "key-released", G_CALLBACK( foobar_launcher_handle_list_key ), self );
@@ -201,8 +213,11 @@ void foobar_launcher_finalize( GObject* object )
g_clear_signal_handler( &self->config_handler_id, self->configuration_service );
g_clear_object( &self->filter_model );
+ g_clear_object( &self->quick_answer_model );
+ g_clear_object( &self->flatten_model );
g_clear_object( &self->selection_model );
g_clear_object( &self->application_service );
+ g_clear_object( &self->quick_answer_service );
g_clear_object( &self->configuration_service );
g_clear_pointer( &self->search_terms, g_strfreev );
@@ -218,13 +233,16 @@ void foobar_launcher_finalize( GObject* object )
//
FoobarLauncher* foobar_launcher_new(
FoobarApplicationService* application_service,
+ FoobarQuickAnswerService* quick_answer_service,
FoobarConfigurationService* configuration_service )
{
g_return_val_if_fail( FOOBAR_IS_APPLICATION_SERVICE( application_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_QUICK_ANSWER_SERVICE( quick_answer_service ), NULL );
g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL );
FoobarLauncher* self = g_object_new( FOOBAR_TYPE_LAUNCHER, NULL );
self->application_service = g_object_ref( application_service );
+ self->quick_answer_service = g_object_ref( quick_answer_service );
self->configuration_service = g_object_ref( configuration_service );
// Set up the result list view's source model.
@@ -299,6 +317,17 @@ void foobar_launcher_handle_search_changed(
{
FoobarLauncher* self = (FoobarLauncher*)userdata;
+ // Update quick answer.
+
+ g_autoptr(FoobarQuickAnswer) answer = foobar_quick_answer_service_query( self->quick_answer_service, gtk_editable_get_text( editable ) );
+ g_list_store_remove_all( self->quick_answer_model );
+ if (answer)
+ {
+ g_list_store_append(self->quick_answer_model, answer);
+ }
+
+ // Update filter.
+
GStrvBuilder* terms_builder = g_strv_builder_new( );
g_autofree gchar* query = g_strdup( gtk_editable_get_text( editable ) );
gchar* query_start = query;
@@ -416,24 +445,24 @@ void foobar_launcher_handle_item_setup(
gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
gtk_widget_add_css_class( icon, "icon" );
- GtkWidget* name = gtk_label_new( NULL );
- gtk_label_set_justify( GTK_LABEL( name ), GTK_JUSTIFY_LEFT );
- gtk_label_set_xalign( GTK_LABEL( name ), 0 );
- gtk_label_set_wrap( GTK_LABEL( name ), FALSE );
- gtk_label_set_ellipsize( GTK_LABEL( name ), PANGO_ELLIPSIZE_END );
- gtk_widget_add_css_class( name, "name" );
+ GtkWidget* title = gtk_label_new( NULL );
+ gtk_label_set_justify( GTK_LABEL( title ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_xalign( GTK_LABEL( title ), 0 );
+ gtk_label_set_wrap( GTK_LABEL( title ), FALSE );
+ gtk_label_set_ellipsize( GTK_LABEL( title ), PANGO_ELLIPSIZE_END );
+ gtk_widget_add_css_class( title, "name" );
GtkWidget* description = gtk_label_new( NULL );
gtk_label_set_justify( GTK_LABEL( description ), GTK_JUSTIFY_LEFT );
gtk_label_set_xalign( GTK_LABEL( description ), 0 );
- gtk_label_set_wrap( GTK_LABEL( name ), FALSE );
+ gtk_label_set_wrap( GTK_LABEL( description ), FALSE );
gtk_label_set_ellipsize( GTK_LABEL( description ), PANGO_ELLIPSIZE_END );
gtk_widget_add_css_class( description, "description" );
GtkWidget* column = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
gtk_widget_set_hexpand( column, TRUE );
gtk_widget_set_valign( column, GTK_ALIGN_CENTER );
- gtk_box_append( GTK_BOX( column ), name );
+ gtk_box_append( GTK_BOX( column ), title );
gtk_box_append( GTK_BOX( column ), description );
GtkWidget* row = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
@@ -447,7 +476,7 @@ void foobar_launcher_handle_item_setup(
{
GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
- GtkExpression* icon_expr = gtk_property_expression_new( FOOBAR_TYPE_APPLICATION_ITEM, item_expr, "icon" );
+ GtkExpression* icon_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "icon" );
gtk_expression_bind( gtk_expression_ref( icon_expr ), icon, "gicon", list_item );
GtkExpression* visible_params[] = { icon_expr };
GtkExpression* visible_expr = gtk_cclosure_expression_new(
@@ -463,13 +492,13 @@ void foobar_launcher_handle_item_setup(
{
GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
- GtkExpression* name_expr = gtk_property_expression_new( FOOBAR_TYPE_APPLICATION_ITEM, item_expr, "name" );
- gtk_expression_bind( name_expr, name, "label", list_item );
+ GtkExpression* title_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "title" );
+ gtk_expression_bind( title_expr, title, "label", list_item );
}
{
GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
- GtkExpression* description_expr = gtk_property_expression_new( FOOBAR_TYPE_APPLICATION_ITEM, item_expr, "description" );
+ GtkExpression* description_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "description" );
gtk_expression_bind( gtk_expression_ref( description_expr ), description, "label", list_item );
GtkExpression* visible_params[] = { description_expr };
GtkExpression* visible_expr = gtk_cclosure_expression_new(
@@ -496,8 +525,8 @@ void foobar_launcher_handle_item_activate(
FoobarLauncher* self = (FoobarLauncher*)userdata;
GListModel* list = G_LIST_MODEL( gtk_list_view_get_model( view ) );
- FoobarApplicationItem* item = g_list_model_get_item( list, position );
- foobar_application_item_launch( item );
+ FoobarLauncherItem* item = g_list_model_get_item( list, position );
+ foobar_launcher_item_activate( item );
gtk_widget_set_visible( GTK_WIDGET( self ), FALSE );
}
diff --git a/src/launcher.h b/src/launcher.h
index 4c3e495..5d92607 100644
--- a/src/launcher.h
+++ b/src/launcher.h
@@ -3,6 +3,7 @@
#include <gtk/gtk.h>
#include "services/application-service.h"
#include "services/configuration-service.h"
+#include "services/quick-answer-service.h"
G_BEGIN_DECLS
@@ -11,6 +12,7 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE( FoobarLauncher, foobar_launcher, FOOBAR, LAUNCHER, GtkWindow )
FoobarLauncher* foobar_launcher_new ( FoobarApplicationService* application_service,
+ FoobarQuickAnswerService* quick_answer_service,
FoobarConfigurationService* configuration_service );
void foobar_launcher_apply_configuration( FoobarLauncher* self,
FoobarLauncherConfiguration const* config );
diff --git a/src/meson.build b/src/meson.build
index b089bce..b9383fb 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,41 +1,74 @@
foobar_sources = files(
- 'main.c',
'application.c',
'utils.c',
'panel.c',
'launcher.c',
+ 'launcher-item.c',
'control-center.c',
'notification-area.c',
)
+foobar_tests = {}
subdir('dbus')
subdir('services')
subdir('widgets')
+c_args = [
+ '-Wall',
+ '-Werror',
+ '-Wswitch-enum',
+ '-Wswitch-default',
+ '-Wunused',
+ '-Wuninitialized',
+]
+
+deps = [
+ glib_dep,
+ gio_dep,
+ gio_unix_dep,
+ gtk_dep,
+ json_dep,
+ gmp_dep,
+ nm_dep,
+ m_dep,
+ layershell_dep,
+ gvc_dep,
+]
+
+libfoobar = static_library(
+ meson.project_name(),
+ foobar_sources,
+ include_directories: include_directories('.'),
+ dependencies: deps,
+ install: false,
+ c_args: c_args
+)
+
+libfoobar_dep = declare_dependency(
+ link_with: libfoobar,
+ include_directories: include_directories('.'),
+ dependencies: deps,
+)
+
executable(
meson.project_name(),
+ 'main.c',
styles_resource,
icons_resource,
- foobar_sources,
- include_directories: include_directories('.'),
- dependencies: [
- glib_dep,
- gio_dep,
- gio_unix_dep,
- gtk_dep,
- json_dep,
- nm_dep,
- m_dep,
- layershell_dep,
- gvc_dep,
- ],
+ dependencies: [ libfoobar_dep ],
install: true,
- c_args: [
- '-Wall',
- '-Werror',
- '-Wswitch-enum',
- '-Wswitch-default',
- '-Wunused',
- '-Wuninitialized',
- ],
+ c_args: c_args
)
+
+if mutest_dep.found()
+ foreach name, sources : foobar_tests
+ test(
+ name,
+ executable(
+ 'foobar-test-' + name,
+ sources,
+ dependencies: [ libfoobar_dep, mutest_dep ],
+ ),
+ )
+ endforeach
+endif
diff --git a/src/services/application-service.c b/src/services/application-service.c
index 3885719..42835ac 100644
--- a/src/services/application-service.c
+++ b/src/services/application-service.c
@@ -1,4 +1,5 @@
#include "services/application-service.h"
+#include "launcher-item.h"
#include "utils.h"
#include <gtk/gtk.h>
#include <gio/gdesktopappinfo.h>
@@ -21,29 +22,38 @@ struct _FoobarApplicationItem
enum
{
APP_PROP_ID = 1,
- APP_PROP_NAME,
- APP_PROP_DESCRIPTION,
APP_PROP_EXECUTABLE,
APP_PROP_CATEGORIES,
- APP_PROP_ICON,
APP_PROP_FREQUENCY,
+ APP_PROP_TITLE,
+ APP_PROP_DESCRIPTION,
+ APP_PROP_ICON,
N_APP_PROPS,
};
static GParamSpec* app_props[N_APP_PROPS] = { 0 };
-static void foobar_application_item_class_init ( FoobarApplicationItemClass* klass );
-static void foobar_application_item_init ( FoobarApplicationItem* self );
-static void foobar_application_item_get_property( GObject* object,
- guint prop_id,
- GValue* value,
- GParamSpec* pspec );
-static void foobar_application_item_finalize ( GObject* object );
-static FoobarApplicationItem* foobar_application_item_new ( FoobarApplicationService* service );
-static void foobar_application_item_set_info ( FoobarApplicationItem* self,
- GAppInfo* value );
+static void foobar_application_item_class_init ( FoobarApplicationItemClass* klass );
+static void foobar_application_item_launcher_item_interface_init( FoobarLauncherItemInterface* iface );
+static void foobar_application_item_init ( FoobarApplicationItem* self );
+static void foobar_application_item_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_application_item_finalize ( GObject* object );
+static FoobarApplicationItem* foobar_application_item_new ( FoobarApplicationService* service );
+static gchar const* foobar_application_item_get_title ( FoobarLauncherItem* self );
+static gchar const* foobar_application_item_get_description ( FoobarLauncherItem* self );
+static GIcon* foobar_application_item_get_icon ( FoobarLauncherItem* self );
+static void foobar_application_item_activate ( FoobarLauncherItem* self );
+static void foobar_application_item_set_info ( FoobarApplicationItem* self,
+ GAppInfo* value );
-G_DEFINE_FINAL_TYPE( FoobarApplicationItem, foobar_application_item, G_TYPE_OBJECT )
+G_DEFINE_FINAL_TYPE_WITH_CODE(
+ FoobarApplicationItem,
+ foobar_application_item,
+ G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_LAUNCHER_ITEM, foobar_application_item_launcher_item_interface_init ) )
//
// FoobarApplicationService:
@@ -124,24 +134,13 @@ void foobar_application_item_class_init( FoobarApplicationItemClass* klass )
object_klass->get_property = foobar_application_item_get_property;
object_klass->finalize = foobar_application_item_finalize;
+ gpointer launcher_item_iface = g_type_default_interface_peek( FOOBAR_TYPE_LAUNCHER_ITEM );
app_props[APP_PROP_ID] = g_param_spec_string(
"id",
"ID",
"Application identifier string.",
NULL,
G_PARAM_READABLE );
- app_props[APP_PROP_NAME] = g_param_spec_string(
- "name",
- "Name",
- "Displayed name for the application.",
- NULL,
- G_PARAM_READABLE );
- app_props[APP_PROP_DESCRIPTION] = g_param_spec_string(
- "description",
- "Description",
- "Brief description of the application.",
- NULL,
- G_PARAM_READABLE );
app_props[APP_PROP_EXECUTABLE] = g_param_spec_string(
"executable",
"Executable",
@@ -154,12 +153,6 @@ void foobar_application_item_class_init( FoobarApplicationItemClass* klass )
"Semicolon-separated list of application categories.",
NULL,
G_PARAM_READABLE );
- app_props[APP_PROP_ICON] = g_param_spec_object(
- "icon",
- "Icon",
- "Icon to show for the application.",
- G_TYPE_ICON,
- G_PARAM_READABLE );
app_props[APP_PROP_FREQUENCY] = g_param_spec_int64(
"frequency",
"Frequency",
@@ -168,10 +161,30 @@ void foobar_application_item_class_init( FoobarApplicationItemClass* klass )
INT64_MAX,
0,
G_PARAM_READABLE );
+ app_props[APP_PROP_TITLE] = g_param_spec_override(
+ "title",
+ g_object_interface_find_property( launcher_item_iface, "title" ) );
+ app_props[APP_PROP_DESCRIPTION] = g_param_spec_override(
+ "description",
+ g_object_interface_find_property( launcher_item_iface, "description" ) );
+ app_props[APP_PROP_ICON] = g_param_spec_override(
+ "icon",
+ g_object_interface_find_property( launcher_item_iface, "icon" ) );
g_object_class_install_properties( object_klass, N_APP_PROPS, app_props );
}
//
+// Static initialization of the FoobarLauncherItem interface.
+//
+void foobar_application_item_launcher_item_interface_init( FoobarLauncherItemInterface* iface )
+{
+ iface->get_title = foobar_application_item_get_title;
+ iface->get_description = foobar_application_item_get_description;
+ iface->get_icon = foobar_application_item_get_icon;
+ iface->activate = foobar_application_item_activate;
+}
+
+//
// Instance initialization for application items.
//
void foobar_application_item_init( FoobarApplicationItem* self )
@@ -195,24 +208,24 @@ void foobar_application_item_get_property(
case APP_PROP_ID:
g_value_set_string( value, foobar_application_item_get_id( self ) );
break;
- case APP_PROP_NAME:
- g_value_set_string( value, foobar_application_item_get_name( self ) );
- break;
- case APP_PROP_DESCRIPTION:
- g_value_set_string( value, foobar_application_item_get_description( self ) );
- break;
case APP_PROP_EXECUTABLE:
g_value_set_string( value, foobar_application_item_get_executable( self ) );
break;
case APP_PROP_CATEGORIES:
g_value_set_string( value, foobar_application_item_get_categories( self ) );
break;
- case APP_PROP_ICON:
- g_value_set_object( value, foobar_application_item_get_icon( self ) );
- break;
case APP_PROP_FREQUENCY:
g_value_set_int64( value, foobar_application_item_get_frequency( self ) );
break;
+ case APP_PROP_TITLE:
+ g_value_set_string( value, foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM ( self ) ) );
+ break;
+ case APP_PROP_DESCRIPTION:
+ g_value_set_string( value, foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ) );
+ break;
+ case APP_PROP_ICON:
+ g_value_set_object( value, foobar_launcher_item_get_icon( FOOBAR_LAUNCHER_ITEM( self ) ) );
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
break;
@@ -251,62 +264,91 @@ gchar const* foobar_application_item_get_id( FoobarApplicationItem* self )
}
//
-// Get the application's human-readable name.
+// Get the application's executable name.
//
-gchar const* foobar_application_item_get_name( FoobarApplicationItem* self )
+gchar const* foobar_application_item_get_executable( FoobarApplicationItem* self )
{
g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
- return self->info ? g_app_info_get_display_name( self->info ) : NULL;
+ return self->info ? g_app_info_get_executable( self->info ) : NULL;
}
//
-// Get an optional short description for the application.
+// Get the semicolon-separated list of categories for this application.
//
-gchar const* foobar_application_item_get_description( FoobarApplicationItem* self )
+gchar const* foobar_application_item_get_categories( FoobarApplicationItem* self )
{
g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
- return self->info ? g_app_info_get_description( self->info ) : NULL;
+ return G_IS_DESKTOP_APP_INFO( self->info )
+ ? g_desktop_app_info_get_categories( G_DESKTOP_APP_INFO( self->info ) )
+ : NULL;
}
//
-// Get the application's executable name.
+// Get the number of times this application was opened using the launcher.
//
-gchar const* foobar_application_item_get_executable( FoobarApplicationItem* self )
+gint64 foobar_application_item_get_frequency( FoobarApplicationItem* self )
{
- g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
- return self->info ? g_app_info_get_executable( self->info ) : NULL;
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), 0 );
+
+ if ( !self->service ) { return 0; }
+ gpointer value = g_hash_table_lookup( self->service->frequencies, foobar_application_item_get_id( self ) );
+ return GPOINTER_TO_SIZE( value );
}
//
-// Get the semicolon-separated list of categories for this application.
+// Get the application's human-readable name.
//
-gchar const* foobar_application_item_get_categories( FoobarApplicationItem* self )
+gchar const* foobar_application_item_get_title( FoobarLauncherItem* item )
{
- g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
- return G_IS_DESKTOP_APP_INFO( self->info )
- ? g_desktop_app_info_get_categories( G_DESKTOP_APP_INFO( self->info ) )
- : NULL;
+ FoobarApplicationItem* self = ( FoobarApplicationItem* )item;
+ return self->info ? g_app_info_get_display_name( self->info ) : NULL;
+}
+
+//
+// Get an optional short description for the application.
+//
+gchar const* foobar_application_item_get_description( FoobarLauncherItem* item )
+{
+ FoobarApplicationItem* self = ( FoobarApplicationItem* )item;
+ return self->info ? g_app_info_get_description( self->info ) : NULL;
}
//
// Get an icon representing the application.
//
-GIcon* foobar_application_item_get_icon( FoobarApplicationItem* self )
+GIcon* foobar_application_item_get_icon( FoobarLauncherItem* item )
{
- g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
+ FoobarApplicationItem* self = ( FoobarApplicationItem* )item;
return self->info ? g_app_info_get_icon( self->info ) : NULL;
}
//
-// Get the number of times this application was opened using the launcher.
+// Launch the application, increasing its frequency by one.
//
-gint64 foobar_application_item_get_frequency( FoobarApplicationItem* self )
+void foobar_application_item_activate( FoobarLauncherItem* item )
{
- g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), 0 );
+ FoobarApplicationItem* self = ( FoobarApplicationItem* )item;
- if ( !self->service ) { return 0; }
- gpointer value = g_hash_table_lookup( self->service->frequencies, foobar_application_item_get_id( self ) );
- return GPOINTER_TO_SIZE( value );
+ if ( !self->info ) { return; }
+
+ g_autoptr( GError ) error = NULL;
+ if ( !g_app_info_launch( self->info, NULL, NULL, &error ) )
+ {
+ g_warning( "Unable to launch application: %s", error->message );
+ }
+
+ gchar const* id = foobar_application_item_get_id( self );
+ if ( !self->service || !id ) { return; }
+
+ gpointer value = g_hash_table_lookup( self->service->frequencies, id );
+ value = GSIZE_TO_POINTER( GPOINTER_TO_SIZE( value ) + 1 );
+ g_mutex_lock( &self->service->frequencies_mutex );
+ g_hash_table_insert( self->service->frequencies, g_strdup( id ), value );
+ g_mutex_unlock( &self->service->frequencies_mutex );
+
+ g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] );
+ gtk_sorter_changed( gtk_sort_list_model_get_sorter( self->service->sorted_items ), GTK_SORTER_CHANGE_DIFFERENT );
+ foobar_application_service_write_cache( self->service );
}
//
@@ -325,7 +367,7 @@ void foobar_application_item_set_info(
if ( value ) { self->info = g_object_ref( value ); }
g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ID] );
- g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_NAME] );
+ g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_TITLE] );
g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_DESCRIPTION] );
g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_EXECUTABLE] );
g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ICON] );
@@ -346,8 +388,8 @@ gboolean foobar_application_item_match(
for ( gchar const* const* it = terms; *it; ++it )
{
gchar const* term = *it;
- gchar const* name = foobar_application_item_get_name( self );
- gchar const* description = foobar_application_item_get_description( self );
+ gchar const* name = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( self ) );
+ gchar const* description = foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) );
gchar const* executable = foobar_application_item_get_executable( self );
gchar const* categories = foobar_application_item_get_categories( self );
gchar const* id = foobar_application_item_get_id( self );
@@ -363,35 +405,6 @@ gboolean foobar_application_item_match(
return TRUE;
}
-//
-// Launch the application, increasing its frequency by one.
-//
-void foobar_application_item_launch( FoobarApplicationItem* self )
-{
- g_return_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ) );
-
- if ( !self->info ) { return; }
-
- g_autoptr( GError ) error = NULL;
- if ( !g_app_info_launch( self->info, NULL, NULL, &error ) )
- {
- g_warning( "Unable to launch application: %s", error->message );
- }
-
- gchar const* id = foobar_application_item_get_id( self );
- if ( !self->service || !id ) { return; }
-
- gpointer value = g_hash_table_lookup( self->service->frequencies, id );
- value = GSIZE_TO_POINTER( GPOINTER_TO_SIZE( value ) + 1 );
- g_mutex_lock( &self->service->frequencies_mutex );
- g_hash_table_insert( self->service->frequencies, g_strdup( id ), value );
- g_mutex_unlock( &self->service->frequencies_mutex );
-
- g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] );
- gtk_sorter_changed( gtk_sort_list_model_get_sorter( self->service->sorted_items ), GTK_SORTER_CHANGE_DIFFERENT );
- foobar_application_service_write_cache( self->service );
-}
-
// ---------------------------------------------------------------------------------------------------------------------
// Service Implementation
// ---------------------------------------------------------------------------------------------------------------------
@@ -722,8 +735,8 @@ gint foobar_application_service_sort_func(
if ( freq_a > freq_b ) { return -1; }
if ( freq_a < freq_b ) { return 1; }
- gchar const* name_a = foobar_application_item_get_name( app_a );
- gchar const* name_b = foobar_application_item_get_name( app_b );
+ gchar const* name_a = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( app_a ) );
+ gchar const* name_b = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( app_b ) );
gint name_res = g_strcmp0( name_a, name_b );
if ( name_res ) { return name_res; }
diff --git a/src/services/application-service.h b/src/services/application-service.h
index 3c2622a..dd043fc 100644
--- a/src/services/application-service.h
+++ b/src/services/application-service.h
@@ -11,15 +11,11 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE( FoobarApplicationItem, foobar_application_item, FOOBAR, APPLICATION_ITEM, GObject )
gchar const* foobar_application_item_get_id ( FoobarApplicationItem* self );
-gchar const* foobar_application_item_get_name ( FoobarApplicationItem* self );
-gchar const* foobar_application_item_get_description( FoobarApplicationItem* self );
gchar const* foobar_application_item_get_executable ( FoobarApplicationItem* self );
gchar const* foobar_application_item_get_categories ( FoobarApplicationItem* self );
-GIcon* foobar_application_item_get_icon ( FoobarApplicationItem* self );
gint64 foobar_application_item_get_frequency ( FoobarApplicationItem* self );
gboolean foobar_application_item_match ( FoobarApplicationItem* self,
gchar const* const* terms );
-void foobar_application_item_launch ( FoobarApplicationItem* self );
G_DECLARE_FINAL_TYPE( FoobarApplicationService, foobar_application_service, FOOBAR, APPLICATION_SERVICE, GObject )
diff --git a/src/services/meson.build b/src/services/meson.build
index 990102b..368c2f1 100644
--- a/src/services/meson.build
+++ b/src/services/meson.build
@@ -8,5 +8,8 @@ foobar_sources += files(
'network-service.c',
'bluetooth-service.c',
'application-service.c',
+ 'quick-answer-service.c',
'configuration-service.c',
)
+
+subdir('quick-answers')
diff --git a/src/services/quick-answer-service.c b/src/services/quick-answer-service.c
new file mode 100644
index 0000000..670a72f
--- /dev/null
+++ b/src/services/quick-answer-service.c
@@ -0,0 +1,360 @@
+#include "services/quick-answer-service.h"
+#include "services/quick-answers/math.h"
+#include "launcher-item.h"
+#include <gdk/gdk.h>
+
+//
+// FoobarQuickAnswer:
+//
+// A launcher item representing a quick, one-line answer to the query entered by the user.
+//
+
+struct _FoobarQuickAnswer
+{
+ GObject parent_instance;
+ gchar* title;
+ gchar* value;
+ GIcon* icon;
+};
+
+enum
+{
+ ANSWER_PROP_VALUE = 1,
+ ANSWER_PROP_TITLE,
+ ANSWER_PROP_DESCRIPTION,
+ ANSWER_PROP_ICON,
+ N_ANSWER_PROPS,
+};
+
+static GParamSpec* answer_props[N_ANSWER_PROPS] = { 0 };
+
+static void foobar_quick_answer_class_init ( FoobarQuickAnswerClass* klass );
+static void foobar_quick_answer_launcher_item_interface_init( FoobarLauncherItemInterface* iface );
+static void foobar_quick_answer_init ( FoobarQuickAnswer* self );
+static void foobar_quick_answer_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_quick_answer_finalize ( GObject* object );
+static FoobarQuickAnswer* foobar_quick_answer_new ( void );
+static gchar const* foobar_quick_answer_get_title ( FoobarLauncherItem* self );
+static gchar const* foobar_quick_answer_get_description ( FoobarLauncherItem* self );
+static GIcon* foobar_quick_answer_get_icon ( FoobarLauncherItem* self );
+static void foobar_quick_answer_activate ( FoobarLauncherItem* self );
+static void foobar_quick_answer_set_title ( FoobarQuickAnswer* self,
+ gchar const* value );
+static void foobar_quick_answer_set_value ( FoobarQuickAnswer* self,
+ gchar const* value );
+static void foobar_quick_answer_set_icon ( FoobarQuickAnswer* self,
+ GIcon* value );
+
+G_DEFINE_FINAL_TYPE_WITH_CODE(
+ FoobarQuickAnswer,
+ foobar_quick_answer,
+ G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_LAUNCHER_ITEM, foobar_quick_answer_launcher_item_interface_init ) )
+
+//
+// FoobarQuickAnswerService:
+//
+// Service providing quick answers for search queries in the launcher (if available). This includes evaluating
+// mathematical expressions.
+//
+
+struct _FoobarQuickAnswerService
+{
+ GObject parent_instance;
+};
+
+static void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass );
+static void foobar_quick_answer_service_init ( FoobarQuickAnswerService* self );
+static FoobarQuickAnswer* foobar_quick_answer_service_query_math( gchar const* query );
+
+G_DEFINE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Quick Answers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for quick answers.
+//
+void foobar_quick_answer_class_init( FoobarQuickAnswerClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_quick_answer_get_property;
+ object_klass->finalize = foobar_quick_answer_finalize;
+
+ gpointer launcher_item_iface = g_type_default_interface_peek( FOOBAR_TYPE_LAUNCHER_ITEM );
+ answer_props[ANSWER_PROP_VALUE] = g_param_spec_string(
+ "value",
+ "Value",
+ "The raw value of the quick answer.",
+ NULL,
+ G_PARAM_READABLE );
+ answer_props[ANSWER_PROP_TITLE] = g_param_spec_override(
+ "title",
+ g_object_interface_find_property( launcher_item_iface, "title" ) );
+ answer_props[ANSWER_PROP_DESCRIPTION] = g_param_spec_override(
+ "description",
+ g_object_interface_find_property( launcher_item_iface, "description" ) );
+ answer_props[ANSWER_PROP_ICON] = g_param_spec_override(
+ "icon",
+ g_object_interface_find_property( launcher_item_iface, "icon" ) );
+ g_object_class_install_properties( object_klass, N_ANSWER_PROPS, answer_props );
+}
+
+//
+// Static initialization of the FoobarLauncherItem interface.
+//
+void foobar_quick_answer_launcher_item_interface_init( FoobarLauncherItemInterface* iface )
+{
+ iface->get_title = foobar_quick_answer_get_title;
+ iface->get_description = foobar_quick_answer_get_description;
+ iface->get_icon = foobar_quick_answer_get_icon;
+ iface->activate = foobar_quick_answer_activate;
+}
+
+//
+// Instance initialization for quick answers.
+//
+void foobar_quick_answer_init( FoobarQuickAnswer* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_quick_answer_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarQuickAnswer* self = (FoobarQuickAnswer*)object;
+
+ switch ( prop_id )
+ {
+ case ANSWER_PROP_VALUE:
+ g_value_set_string( value, foobar_quick_answer_get_value( self ) );
+ break;
+ case ANSWER_PROP_TITLE:
+ g_value_set_string( value, foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM ( self ) ) );
+ break;
+ case ANSWER_PROP_DESCRIPTION:
+ g_value_set_string( value, foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ) );
+ break;
+ case ANSWER_PROP_ICON:
+ g_value_set_object( value, foobar_launcher_item_get_icon( FOOBAR_LAUNCHER_ITEM( self ) ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for quick answers.
+//
+void foobar_quick_answer_finalize( GObject* object )
+{
+ FoobarQuickAnswer* self = (FoobarQuickAnswer*)object;
+
+ g_clear_pointer( &self->title, g_free );
+ g_clear_pointer( &self->value, g_free );
+ g_clear_object( &self->icon );
+
+ G_OBJECT_CLASS( foobar_quick_answer_parent_class )->finalize( object );
+}
+
+//
+// Create a new, default-initialized quick answer.
+//
+FoobarQuickAnswer* foobar_quick_answer_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_QUICK_ANSWER, NULL );
+}
+
+//
+// Get the raw value of the quick answer.
+//
+gchar const* foobar_quick_answer_get_value( FoobarQuickAnswer* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_QUICK_ANSWER( self ), NULL );
+ return self->value;
+}
+
+//
+// Get the title for the quick answer.
+//
+gchar const* foobar_quick_answer_get_title( FoobarLauncherItem* item )
+{
+ FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item;
+ return self->title;
+}
+
+//
+// Get the description shown below the title, always NULL for quick answers.
+//
+gchar const* foobar_quick_answer_get_description( FoobarLauncherItem* item )
+{
+ (void)item;
+ return NULL;
+}
+
+//
+// Get an icon representing the quick answer.
+//
+GIcon* foobar_quick_answer_get_icon( FoobarLauncherItem* item )
+{
+ FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item;
+ return self->icon;
+}
+
+//
+// Copy the quick answer's value into the clipboard.
+//
+void foobar_quick_answer_activate( FoobarLauncherItem* item )
+{
+ FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item;
+ if ( self->value )
+ {
+ GdkClipboard* clipboard = gdk_display_get_clipboard( gdk_display_get_default( ) );
+ gdk_clipboard_set_text( clipboard, self->value );
+ }
+}
+
+//
+// Update the raw value of the quick answer.
+//
+void foobar_quick_answer_set_value(
+ FoobarQuickAnswer* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) );
+
+ if ( g_strcmp0( self->value, value ) )
+ {
+ g_clear_pointer( &self->value, g_free );
+ self->value = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_VALUE] );
+ }
+}
+
+//
+// Update the title for the quick answer.
+//
+void foobar_quick_answer_set_title(
+ FoobarQuickAnswer* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) );
+
+ if ( g_strcmp0( self->title, value ) )
+ {
+ g_clear_pointer( &self->title, g_free );
+ self->title = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_TITLE] );
+ }
+}
+
+//
+// Update the icon representing the quick answer.
+//
+void foobar_quick_answer_set_icon(
+ FoobarQuickAnswer* self,
+ GIcon* value )
+{
+ g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) );
+
+ if ( self->icon != value )
+ {
+ g_clear_object( &self->icon );
+ self->icon = value ? g_object_ref( value ) : NULL;
+ g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_ICON] );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the quick answer service.
+//
+void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass )
+{
+ (void)klass;
+}
+
+//
+// Instance initialization for the quick answer service.
+//
+void foobar_quick_answer_service_init( FoobarQuickAnswerService* self )
+{
+ (void)self;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new quick answer service instance.
+//
+FoobarQuickAnswerService* foobar_quick_answer_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_QUICK_ANSWER_SERVICE, NULL );
+}
+
+//
+// Query the service for a quick answer.
+//
+FoobarQuickAnswer* foobar_quick_answer_service_query(
+ FoobarQuickAnswerService* self,
+ gchar const* query )
+{
+ (void)self;
+
+ FoobarQuickAnswer* result = foobar_quick_answer_service_query_math( query );
+ if ( result ) { return result; }
+
+ return NULL;
+}
+
+//
+// Try to parse the query as a mathematical expression and evaluate it.
+//
+FoobarQuickAnswer* foobar_quick_answer_service_query_math( gchar const* query )
+{
+ FoobarQuickAnswer* result = NULL;
+
+ gsize token_count;
+ g_autofree FoobarMathToken* tokens = foobar_math_lex( query, strlen( query ), &token_count );
+ if ( tokens )
+ {
+ FoobarMathExpression* expr = foobar_math_parse( tokens, token_count );
+ if ( expr )
+ {
+ // foobar_math_expression_print( expr, 0 );
+
+ FoobarMathValue val = { 0 };
+ if ( foobar_math_evaluate( expr, &val ) )
+ {
+ g_autofree gchar* value = foobar_math_value_to_string( val );
+ g_autofree gchar* title = g_strdup_printf( "= %s", value );
+ g_autoptr( GIcon ) icon = g_themed_icon_new( "fluent-calculator-symbolic" );
+ result = foobar_quick_answer_new( );
+ foobar_quick_answer_set_value( result, value );
+ foobar_quick_answer_set_title( result, title );
+ foobar_quick_answer_set_icon( result, icon );
+ foobar_math_value_free( val );
+ }
+
+ foobar_math_expression_free( expr );
+ }
+ }
+
+ return result;
+}
diff --git a/src/services/quick-answer-service.h b/src/services/quick-answer-service.h
new file mode 100644
index 0000000..adb0701
--- /dev/null
+++ b/src/services/quick-answer-service.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_QUICK_ANSWER foobar_quick_answer_get_type( )
+#define FOOBAR_TYPE_QUICK_ANSWER_SERVICE foobar_quick_answer_service_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarQuickAnswer, foobar_quick_answer, FOOBAR, QUICK_ANSWER, GObject )
+
+gchar const* foobar_quick_answer_get_value( FoobarQuickAnswer* self );
+
+G_DECLARE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, FOOBAR, QUICK_ANSWER_SERVICE, GObject )
+
+FoobarQuickAnswerService* foobar_quick_answer_service_new ( void );
+FoobarQuickAnswer* foobar_quick_answer_service_query ( FoobarQuickAnswerService* self,
+ gchar const* query );
+
+G_END_DECLS
diff --git a/src/services/quick-answers/math-lexer.c b/src/services/quick-answers/math-lexer.c
new file mode 100644
index 0000000..7fbe877
--- /dev/null
+++ b/src/services/quick-answers/math-lexer.c
@@ -0,0 +1,248 @@
+#include "services/quick-answers/math.h"
+
+//
+// FoobarMathToken:
+//
+// A token in a mathematical expression. The token contents are left as-is and need not be null-terminated. For example,
+// "sin(pi) - 3" would be represented as:
+// - IDENTIFIER: "sin"
+// - PAREN_OPEN: "("
+// - IDENTIFIER: "pi"
+// - PAREN_CLOSE: ")"
+// - MINUS: "-"
+// - NUMBER: "3"
+//
+
+//
+// Lexer:
+//
+// The lexer context/state which is passed around.
+//
+
+typedef struct _Lexer Lexer;
+
+struct _Lexer
+{
+ GArray* result;
+ gchar const* input;
+ gsize input_length;
+ gsize position;
+ gsize token_start;
+};
+
+static gboolean char_is_whitespace( char c );
+static gboolean char_is_digit ( char c );
+static gboolean char_is_identifier( char c );
+
+static char lexer_peek ( Lexer const* ctx,
+ gsize offset );
+static char lexer_pop ( Lexer* ctx );
+static void lexer_begin_token ( Lexer* ctx );
+static void lexer_commit_token ( Lexer* ctx,
+ FoobarMathTokenType type );
+static void lexer_commit_single( Lexer* ctx,
+ FoobarMathTokenType type );
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Lexing
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Divide an input string into a list of tokens.
+//
+// On success, this will return a newly allocated array of tokens which should be freed using g_free. The number of
+// elements will be written into out_count.
+//
+// On error, this will return NULL.
+//
+FoobarMathToken* foobar_math_lex(
+ gchar const* input,
+ gsize input_length,
+ gsize* out_count )
+{
+ g_autoptr( GArray ) result = g_array_new( FALSE, TRUE, sizeof( FoobarMathToken ) );
+
+ Lexer ctx = { 0 };
+ ctx.result = result;
+ ctx.input = input;
+ ctx.input_length = input_length;
+
+ while ( ctx.position < input_length )
+ {
+ if ( char_is_whitespace( lexer_peek( &ctx, 0 ) ) )
+ {
+ // Skip whitespace.
+
+ lexer_pop( &ctx );
+ }
+ else if ( char_is_digit( lexer_peek( &ctx, 0 ) ) )
+ {
+ // Process numbers as a whole, don't allow whitespace inbetween.
+
+ lexer_begin_token( &ctx );
+ while ( char_is_digit( lexer_peek( &ctx, 0 ) ) )
+ {
+ lexer_pop( &ctx );
+ }
+
+ if ( lexer_peek( &ctx, 0 ) == '.' && char_is_digit( lexer_peek( &ctx, 1 ) ) )
+ {
+ lexer_pop( &ctx );
+ while ( char_is_digit( lexer_peek( &ctx, 0 ) ) )
+ {
+ lexer_pop( &ctx );
+ }
+ }
+
+ lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_NUMBER );
+ }
+ else if ( char_is_identifier( lexer_peek( &ctx, 0 ) ) )
+ {
+ // Don't validate identifiers, leave that to the parser.
+
+ lexer_begin_token( &ctx );
+ while ( char_is_identifier( lexer_peek( &ctx, 0 ) ) )
+ {
+ lexer_pop( &ctx );
+ }
+ lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_IDENTIFIER );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '(' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_OPEN );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == ')' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_CLOSE );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '+' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PLUS );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '-' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MINUS );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '*' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MUL );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '/' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_DIV );
+ }
+ else if ( lexer_peek( &ctx, 0 ) == '^' )
+ {
+ lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_POW );
+ }
+ else
+ {
+ // Unknown character.
+
+ return NULL;
+ }
+ }
+
+ return g_array_steal( result, out_count );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Character Classification
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Check if a character is a whitespace symbol (space, tab, carriage return).
+//
+gboolean char_is_whitespace( char c )
+{
+ return c == ' ' || c == '\t' || c == '\r';
+}
+
+//
+// Check if a character is a decimal digit.
+//
+gboolean char_is_digit( char c )
+{
+ return c >= '0' && c <= '9';
+}
+
+//
+// Check if a character belongs to an identifier (like "sin", "pi", etc.).
+//
+gboolean char_is_identifier( char c )
+{
+ // Identifiers are all lowercase ASCII.
+
+ return c >= 'a' && c <= 'z';
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// State Helpers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Peek at the current character without consuming it.
+//
+// If this is the end of the input string, return 0.
+//
+char lexer_peek(
+ Lexer const* ctx,
+ gsize offset )
+{
+ gsize position = ctx->position + offset;
+ if ( position >= ctx->input_length ) { return 0; }
+
+ return ctx->input[position];
+}
+
+//
+// Consume the current character, moving forward to the next one.
+//
+// If this is the end of the input string, return 0.
+//
+char lexer_pop( Lexer* ctx )
+{
+ if ( ctx->position >= ctx->input_length ) { return 0; }
+
+ return ctx->input[ctx->position++];
+}
+
+//
+// Start recording a token by remembering the current position.
+//
+// The current character will be the first one in the token.
+//
+void lexer_begin_token( Lexer* ctx )
+{
+ ctx->token_start = ctx->position;
+}
+
+//
+// Stop recording a token, saving the string between the call to lexer_begin_token as the specified type.
+//
+// The current character will not be a part of the token.
+//
+void lexer_commit_token(
+ Lexer* ctx,
+ FoobarMathTokenType type )
+{
+ FoobarMathToken token = { 0 };
+ token.data = &ctx->input[ctx->token_start];
+ token.length = ctx->position - ctx->token_start;
+ token.type = type;
+ g_array_append_val( ctx->result, token );
+}
+
+//
+// Consume the current character and record it as a token.
+//
+// This is equivalent to calling lexer_begin_token, lexer_pop, and lexer_commit_token.
+//
+void lexer_commit_single(
+ Lexer* ctx,
+ FoobarMathTokenType type )
+{
+ lexer_begin_token( ctx );
+ lexer_pop( ctx );
+ lexer_commit_token( ctx, type );
+}
diff --git a/src/services/quick-answers/math-parser.c b/src/services/quick-answers/math-parser.c
new file mode 100644
index 0000000..17d976d
--- /dev/null
+++ b/src/services/quick-answers/math-parser.c
@@ -0,0 +1,414 @@
+#include "services/quick-answers/math.h"
+
+//
+// Parser:
+//
+// The parser context/state which is passed around.
+//
+
+typedef struct _Parser Parser;
+
+struct _Parser
+{
+ FoobarMathToken const* tokens;
+ gsize tokens_count;
+ gsize position;
+};
+
+//
+// InfixPrecedence:
+//
+// Value indicating how strongly infix operators should bind. The higher the value, the stronger they should bind.
+//
+
+typedef enum
+{
+ INFIX_PRECEDENCE_INVALID = -1,
+ INFIX_PRECEDENCE_NONE = 0,
+ INFIX_PRECEDENCE_ADDITIVE = 1,
+ INFIX_PRECEDENCE_MULTIPLICATIVE = 2,
+ INFIX_PRECEDENCE_EXPONENTS = 3,
+} InfixPrecedence;
+
+static FoobarMathExpression* parser_process ( Parser* ctx,
+ FoobarMathExpression* lhs,
+ InfixPrecedence min_precedence );
+static FoobarMathExpression* parser_process_single ( Parser* ctx,
+ gboolean allow_sign );
+static FoobarMathExpression* parser_process_identifier ( Parser* ctx );
+static FoobarMathExpression* parser_process_number ( Parser* ctx );
+static FoobarMathExpression* parser_process_function ( Parser* ctx,
+ FoobarMathFunction function );
+static FoobarMathExpression* parser_process_parens ( Parser* ctx );
+static FoobarMathExpression* parser_process_sign ( Parser* ctx );
+static FoobarMathToken const* parser_peek ( Parser const* ctx );
+static FoobarMathToken const* parser_pop ( Parser* ctx );
+static gboolean parser_match_identifier ( FoobarMathToken const* token,
+ gchar const* identifier );
+static InfixPrecedence parser_operator_precedence( FoobarMathToken const* token );
+static FoobarMathOperation parser_operator ( FoobarMathToken const* token );
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Parsing
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Transform a sequence of tokens into a mathematical expression.
+//
+// On success, this will return a newly allocated expression which should be freed using foobar_math_expression_free.
+//
+// On error, this will return NULL.
+//
+FoobarMathExpression* foobar_math_parse(
+ FoobarMathToken const* tokens,
+ gsize tokens_count )
+{
+ Parser ctx = { 0 };
+ ctx.tokens = tokens;
+ ctx.tokens_count = tokens_count;
+
+ FoobarMathExpression* lhs = parser_process_single( &ctx, TRUE );
+ if ( !lhs ) { return NULL; }
+
+ FoobarMathExpression* expr = parser_process( &ctx, lhs, INFIX_PRECEDENCE_NONE );
+ if ( !expr ) { return NULL; }
+
+ if ( parser_peek( &ctx ) )
+ {
+ foobar_math_expression_free( expr );
+ return NULL;
+ }
+
+ return expr;
+}
+
+//
+// Process tokens until the end of the current precedence scope (indicated by min_precedence), starting at an operator.
+//
+FoobarMathExpression* parser_process(
+ Parser* ctx,
+ FoobarMathExpression* lhs,
+ InfixPrecedence min_precedence )
+{
+ FoobarMathToken const* op = parser_peek( ctx );
+ InfixPrecedence op_precedence = parser_operator_precedence( op );
+ while ( op_precedence >= min_precedence )
+ {
+ parser_pop( ctx );
+
+ FoobarMathExpression* rhs = parser_process_single( ctx, FALSE );
+ if ( !rhs )
+ {
+ foobar_math_expression_free( lhs );
+ return NULL;
+ }
+
+ FoobarMathToken const* next_op = parser_peek( ctx );
+ while ( parser_operator_precedence( next_op ) > op_precedence )
+ {
+ rhs = parser_process( ctx, rhs, op_precedence + 1 );
+ if ( !rhs )
+ {
+ foobar_math_expression_free( lhs );
+ return NULL;
+ }
+
+ next_op = parser_peek( ctx );
+ }
+
+ lhs = foobar_math_expression_new_operation( parser_operator( op ), lhs, rhs );
+
+ op = parser_peek( ctx );
+ op_precedence = parser_operator_precedence( op );
+ }
+
+ return lhs;
+}
+
+//
+// Process a single value which may be either a constant, function, number, expression enclosed in parenthesis or
+// negation.
+//
+// If an unexpected token is encountered (i.e. an operator), this will return FALSE.
+//
+FoobarMathExpression* parser_process_single(
+ Parser* ctx,
+ gboolean allow_sign )
+{
+ FoobarMathToken const* token = parser_peek( ctx );
+ if ( !token ) { return NULL; }
+
+ switch ( token->type )
+ {
+ case FOOBAR_MATH_TOKEN_IDENTIFIER:
+ return parser_process_identifier( ctx );
+ case FOOBAR_MATH_TOKEN_NUMBER:
+ return parser_process_number( ctx );
+ case FOOBAR_MATH_TOKEN_PAREN_OPEN:
+ return parser_process_parens( ctx );
+ case FOOBAR_MATH_TOKEN_MINUS:
+ case FOOBAR_MATH_TOKEN_PLUS:
+ if ( !allow_sign ) { return NULL; }
+ return parser_process_sign( ctx );
+ case FOOBAR_MATH_TOKEN_PAREN_CLOSE:
+ case FOOBAR_MATH_TOKEN_MUL:
+ case FOOBAR_MATH_TOKEN_DIV:
+ case FOOBAR_MATH_TOKEN_POW:
+ return NULL;
+ default:
+ g_warn_if_reached( );
+ return NULL;
+ }
+}
+
+//
+// Parse an identifier.
+//
+// If the identifier is a function, this will expected parenthesis and an expression afterwards.
+//
+FoobarMathExpression* parser_process_identifier( Parser* ctx )
+{
+ FoobarMathToken const* token = parser_pop( ctx );
+ if ( parser_match_identifier( token, "pi" ) )
+ {
+ return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_PI );
+ }
+ else if ( parser_match_identifier( token, "e" ) )
+ {
+ return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_E );
+ }
+ else if ( parser_match_identifier( token, "exp" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_EXP );
+ }
+ else if ( parser_match_identifier( token, "sqrt" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SQRT );
+ }
+ else if ( parser_match_identifier( token, "sin" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SIN );
+ }
+ else if ( parser_match_identifier( token, "cos" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COS );
+ }
+ else if ( parser_match_identifier( token, "sec" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SEC );
+ }
+ else if ( parser_match_identifier( token, "csc" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_CSC );
+ }
+ else if ( parser_match_identifier( token, "tan" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_TAN );
+ }
+ else if ( parser_match_identifier( token, "cot" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COT );
+ }
+ else if ( parser_match_identifier( token, "arcsin" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSIN );
+ }
+ else if ( parser_match_identifier( token, "arccos" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOS );
+ }
+ else if ( parser_match_identifier( token, "arcsec" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSEC );
+ }
+ else if ( parser_match_identifier( token, "arccsc" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCSC );
+ }
+ else if ( parser_match_identifier( token, "arctan" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCTAN );
+ }
+ else if ( parser_match_identifier( token, "arccot" ) )
+ {
+ return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOT );
+ }
+ else
+ {
+ return NULL;
+ }
+}
+
+//
+// Process a numeric value.
+//
+FoobarMathExpression* parser_process_number( Parser* ctx )
+{
+ FoobarMathToken const* token = parser_pop( ctx );
+ FoobarMathValue value = { 0 };
+ if ( !foobar_math_value_from_string( token->data, token->length, &value ) )
+ {
+ return NULL;
+ }
+
+ return foobar_math_expression_new_value( value );
+}
+
+//
+// Process a function's parameter which should be enclosed in parenthesis.
+//
+FoobarMathExpression* parser_process_function(
+ Parser* ctx,
+ FoobarMathFunction function )
+{
+ FoobarMathToken const* opening_paren = parser_peek( ctx );
+ if ( !opening_paren || opening_paren->type != FOOBAR_MATH_TOKEN_PAREN_OPEN )
+ {
+ return NULL;
+ }
+
+ FoobarMathExpression* arg = parser_process_parens( ctx );
+ if ( !arg ) { return NULL; }
+
+ return foobar_math_expression_new_function( function, arg );
+}
+
+//
+// Process an expression enclosed in parenthesis.
+//
+FoobarMathExpression* parser_process_parens( Parser* ctx )
+{
+ parser_pop( ctx );
+ FoobarMathExpression* lhs = parser_process_single( ctx, TRUE );
+ if ( !lhs ) { return NULL; }
+ FoobarMathExpression* result = parser_process( ctx, lhs, INFIX_PRECEDENCE_NONE );
+ if ( !result ) { return NULL; }
+ FoobarMathToken const* closing_paren = parser_pop( ctx );
+ if ( !closing_paren || closing_paren->type != FOOBAR_MATH_TOKEN_PAREN_CLOSE )
+ {
+ foobar_math_expression_free( result );
+ return NULL;
+ }
+ return result;
+}
+
+//
+// Process a sign (- or +) which may be used for negation of an expression.
+//
+FoobarMathExpression* parser_process_sign( Parser* ctx )
+{
+ FoobarMathToken const* token = parser_pop( ctx );
+ FoobarMathExpression* result = parser_process_single( ctx, FALSE );
+ if ( !result ) { return NULL; }
+
+ if ( token->type == FOOBAR_MATH_TOKEN_MINUS )
+ {
+ return foobar_math_expression_new_function( FOOBAR_MATH_FUNCTION_NEGATE, result );
+ }
+ else
+ {
+ return result;
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// State Helpers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Peek at the current token without consuming it.
+//
+// If this is the end of the token list, return NULL.
+//
+FoobarMathToken const* parser_peek( Parser const* ctx )
+{
+ if ( ctx->position >= ctx->tokens_count ) { return NULL; }
+
+ return &ctx->tokens[ctx->position];
+}
+
+//
+// Consume the current token.
+//
+// If this is the end of the token list, return NULL.
+//
+FoobarMathToken const* parser_pop( Parser* ctx )
+{
+ if ( ctx->position >= ctx->tokens_count ) { return NULL; }
+
+ return &ctx->tokens[ctx->position++];
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Match a token's string against the given identifier string.
+//
+// This is necessary because the token's content is not a null-terminated string.
+//
+gboolean parser_match_identifier(
+ FoobarMathToken const* token,
+ gchar const* identifier )
+{
+ if ( strlen( identifier ) != token->length ) { return FALSE; }
+
+ return !strncmp( token->data, identifier, token->length );
+}
+
+//
+// Get the precedence of an infix operator token or INVALID.
+//
+InfixPrecedence parser_operator_precedence( FoobarMathToken const* token )
+{
+ if ( token )
+ {
+ switch ( token->type )
+ {
+ case FOOBAR_MATH_TOKEN_PLUS:
+ case FOOBAR_MATH_TOKEN_MINUS:
+ return INFIX_PRECEDENCE_ADDITIVE;
+ case FOOBAR_MATH_TOKEN_MUL:
+ case FOOBAR_MATH_TOKEN_DIV:
+ return INFIX_PRECEDENCE_MULTIPLICATIVE;
+ case FOOBAR_MATH_TOKEN_POW:
+ return INFIX_PRECEDENCE_EXPONENTS;
+ case FOOBAR_MATH_TOKEN_IDENTIFIER:
+ case FOOBAR_MATH_TOKEN_NUMBER:
+ case FOOBAR_MATH_TOKEN_PAREN_OPEN:
+ case FOOBAR_MATH_TOKEN_PAREN_CLOSE:
+ default:
+ break;
+ }
+ }
+
+ return INFIX_PRECEDENCE_INVALID;
+}
+
+//
+// Get the operation for an infix operator token. This requires the token to be valid.
+//
+FoobarMathOperation parser_operator( FoobarMathToken const* token )
+{
+ switch ( token->type )
+ {
+ case FOOBAR_MATH_TOKEN_PLUS:
+ return FOOBAR_MATH_OPERATION_ADD;
+ case FOOBAR_MATH_TOKEN_MINUS:
+ return FOOBAR_MATH_OPERATION_SUB;
+ case FOOBAR_MATH_TOKEN_MUL:
+ return FOOBAR_MATH_OPERATION_MUL;
+ case FOOBAR_MATH_TOKEN_DIV:
+ return FOOBAR_MATH_OPERATION_DIV;
+ case FOOBAR_MATH_TOKEN_POW:
+ return FOOBAR_MATH_OPERATION_POW;
+ case FOOBAR_MATH_TOKEN_IDENTIFIER:
+ case FOOBAR_MATH_TOKEN_NUMBER:
+ case FOOBAR_MATH_TOKEN_PAREN_OPEN:
+ case FOOBAR_MATH_TOKEN_PAREN_CLOSE:
+ default:
+ g_warn_if_reached( );
+ return 0;
+ }
+}
diff --git a/src/services/quick-answers/math.c b/src/services/quick-answers/math.c
new file mode 100644
index 0000000..69219e2
--- /dev/null
+++ b/src/services/quick-answers/math.c
@@ -0,0 +1,851 @@
+#define _GNU_SOURCE
+
+#include "services/quick-answers/math.h"
+#include <math.h>
+
+//
+// FoobarMathExpression:
+//
+// A fully parsed mathematical expression which can be evaluated, for example "sin(pi) - 3" would be represented as:
+// - Operation: SUB
+// - Function: SIN
+// - Constant: PI
+// - Value: 3
+//
+
+struct _FoobarMathExpression
+{
+ FoobarMathExpressionType type;
+
+ union
+ {
+ struct
+ {
+ FoobarMathValue v;
+ } value;
+
+ struct
+ {
+ FoobarMathFunction f;
+ FoobarMathExpression* input;
+ } function;
+
+ struct
+ {
+ FoobarMathConstant c;
+ } constant;
+
+ struct
+ {
+ FoobarMathOperation o;
+ FoobarMathExpression* lhs;
+ FoobarMathExpression* rhs;
+ } operation;
+ };
+};
+
+//
+// FoobarMathValue:
+//
+// Representation of a value. The goal of this memory structure is to store the value as an integer for as long as
+// possible. Because of this, there is a "decimal_places" field, so the actual value of an integer is
+// $v * 10^(-decimal_places)$.
+//
+// If absolutely necessary (for example, because we use a function like sin), the value is converted to a floating-point
+// value.
+//
+// Note that we use GMP to store the integer value to allow for arbitrarily large values. Because of this, the structure
+// should always be freed using foobar_math_value_free.
+//
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Expressions
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Helper to allocate a new FoobarMathExpression representing a value.
+//
+// Ownership of "value" is transferred to the new expression.
+//
+FoobarMathExpression* foobar_math_expression_new_value( FoobarMathValue value )
+{
+ FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 );
+ res->type = FOOBAR_MATH_EXPRESSION_VALUE;
+ res->value.v = value;
+ return res;
+}
+
+//
+// Helper to allocate a new FoobarMathExpression representing a function to be evaluated with another expression as its
+// parameter.
+//
+// Ownership of "input" is transferred to the new expression.
+//
+FoobarMathExpression* foobar_math_expression_new_function(
+ FoobarMathFunction function,
+ FoobarMathExpression* input )
+{
+ FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 );
+ res->type = FOOBAR_MATH_EXPRESSION_FUNCTION;
+ res->function.f = function;
+ res->function.input = input;
+ return res;
+}
+
+//
+// Helper to allocate a new FoobarMathExpression representing a known constant.
+//
+FoobarMathExpression* foobar_math_expression_new_constant( FoobarMathConstant constant )
+{
+ FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 );
+ res->type = FOOBAR_MATH_EXPRESSION_CONSTANT;
+ res->constant.c = constant;
+ return res;
+}
+
+//
+// Helper to allocate a new FoobarMathExpression representing an operation to be evaluated with two expressions as its
+// parameters.
+//
+// Ownership of "lhs" and "rhs" is transferred to the new expression.
+//
+FoobarMathExpression* foobar_math_expression_new_operation(
+ FoobarMathOperation operation,
+ FoobarMathExpression* lhs,
+ FoobarMathExpression* rhs )
+{
+ FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 );
+ res->type = FOOBAR_MATH_EXPRESSION_OPERATION;
+ res->operation.o = operation;
+ res->operation.lhs = lhs;
+ res->operation.rhs = rhs;
+ return res;
+}
+
+//
+// Release the memory associated with a mathematical expression, along with the expressions it owns.
+//
+void foobar_math_expression_free( FoobarMathExpression* expression )
+{
+ if ( expression )
+ {
+ switch ( expression->type )
+ {
+ case FOOBAR_MATH_EXPRESSION_VALUE:
+ foobar_math_value_free( expression->value.v );
+ break;
+ case FOOBAR_MATH_EXPRESSION_FUNCTION:
+ foobar_math_expression_free( expression->function.input );
+ break;
+ case FOOBAR_MATH_EXPRESSION_CONSTANT:
+ break;
+ case FOOBAR_MATH_EXPRESSION_OPERATION:
+ foobar_math_expression_free( expression->operation.lhs );
+ foobar_math_expression_free( expression->operation.rhs );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ g_free( expression );
+ }
+}
+
+//
+// Print a tree representation of a mathematical expression.
+//
+// This is mainly useful for debugging. "indentation" describes the current indentation level for the recursive call
+// (should be 0 initially).
+//
+void foobar_math_expression_print(
+ FoobarMathExpression const* expr,
+ gint indentation )
+{
+ for (gint i = 0; i < indentation; ++i) { g_print("| "); }
+ switch ( expr->type )
+ {
+ case FOOBAR_MATH_EXPRESSION_VALUE:
+ {
+ g_autofree gchar* val = foobar_math_value_to_string( expr->value.v );
+ g_print( "%s\n", val );
+ break;
+ }
+ case FOOBAR_MATH_EXPRESSION_FUNCTION:
+ switch ( expr->function.f )
+ {
+ case FOOBAR_MATH_FUNCTION_NEGATE:
+ g_print( "-\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_SIN:
+ g_print( "sin\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_COS:
+ g_print( "cos\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_SEC:
+ g_print( "sec\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_CSC:
+ g_print( "csc\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_TAN:
+ g_print( "tan\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_COT:
+ g_print( "cot\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCSIN:
+ g_print( "arcsin\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCOS:
+ g_print( "arccos\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCSEC:
+ g_print( "arcsec\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCSC:
+ g_print( "arccsc\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCTAN:
+ g_print( "arctan\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCOT:
+ g_print( "arccot\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_EXP:
+ g_print( "exp\n" );
+ break;
+ case FOOBAR_MATH_FUNCTION_SQRT:
+ g_print( "sqrt\n" );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+ foobar_math_expression_print( expr->function.input, indentation + 1 );
+ break;
+ case FOOBAR_MATH_EXPRESSION_CONSTANT:
+ switch ( expr->constant.c )
+ {
+ case FOOBAR_MATH_CONSTANT_PI:
+ g_print( "pi\n" );
+ break;
+ case FOOBAR_MATH_CONSTANT_E:
+ g_print( "e\n" );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+ break;
+ case FOOBAR_MATH_EXPRESSION_OPERATION:
+ switch ( expr->operation.o )
+ {
+ case FOOBAR_MATH_OPERATION_ADD:
+ g_print( "+\n" );
+ break;
+ case FOOBAR_MATH_OPERATION_SUB:
+ g_print( "-\n" );
+ break;
+ case FOOBAR_MATH_OPERATION_MUL:
+ g_print( "*\n" );
+ break;
+ case FOOBAR_MATH_OPERATION_DIV:
+ g_print( "/\n" );
+ break;
+ case FOOBAR_MATH_OPERATION_POW:
+ g_print( "^\n" );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+ foobar_math_expression_print( expr->operation.lhs, indentation + 1 );
+ foobar_math_expression_print( expr->operation.rhs, indentation + 1 );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+}
+
+//
+// Evaluate an expression, producing a value.
+//
+// On success (indicated by the return value TRUE), the value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_evaluate(
+ FoobarMathExpression const* expr,
+ FoobarMathValue* out_value )
+{
+ switch ( expr->type )
+ {
+ case FOOBAR_MATH_EXPRESSION_VALUE:
+ foobar_math_value_copy( expr->value.v, out_value );
+ return TRUE;
+ case FOOBAR_MATH_EXPRESSION_CONSTANT:
+ switch ( expr->constant.c )
+ {
+ case FOOBAR_MATH_CONSTANT_PI:
+ foobar_math_value_from_float( M_PIl, out_value );
+ return TRUE;
+ case FOOBAR_MATH_CONSTANT_E:
+ foobar_math_value_from_float( M_El, out_value );
+ return TRUE;
+ default:
+ g_warn_if_reached( );
+ return FALSE;
+ }
+ case FOOBAR_MATH_EXPRESSION_FUNCTION:
+ {
+ FoobarMathValue input = { 0 };
+ if ( !foobar_math_evaluate( expr->function.input, &input ) ) { return FALSE; }
+
+ if ( expr->function.f == FOOBAR_MATH_FUNCTION_NEGATE )
+ {
+ *out_value = input;
+ foobar_math_value_negate( out_value );
+ return TRUE;
+ }
+
+ long double input_float = foobar_math_value_to_float( input );
+ foobar_math_value_free( input );
+
+ long double result = NAN;
+ switch ( expr->function.f )
+ {
+ case FOOBAR_MATH_FUNCTION_SIN:
+ result = sinl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_COS:
+ result = cosl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_SEC:
+ result = 1.L / cosl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_CSC:
+ result = 1.L / sinl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_TAN:
+ result = tanl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_COT:
+ result = 1.L / tanl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCSIN:
+ result = asinl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCOS:
+ result = acosl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCSEC:
+ result = acosl( 1.L / input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCSC:
+ result = asinl( 1.L / input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCTAN:
+ result = atanl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_ARCCOT:
+ result = atanl( 1.L / input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_EXP:
+ result = expl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_SQRT:
+ result = sqrtl( input_float );
+ break;
+ case FOOBAR_MATH_FUNCTION_NEGATE:
+ default:
+ g_warn_if_reached( );
+ return FALSE;
+ }
+
+ if ( isnanl( result ) || isinfl( result ) ) { return FALSE; }
+
+ foobar_math_value_from_float( result, out_value );
+ return TRUE;
+ }
+ case FOOBAR_MATH_EXPRESSION_OPERATION:
+ {
+ FoobarMathValue lhs = { 0 };
+ if ( !foobar_math_evaluate( expr->operation.lhs, &lhs ) ) { return FALSE; }
+ FoobarMathValue rhs = { 0 };
+ if ( !foobar_math_evaluate( expr->operation.rhs, &rhs ) )
+ {
+ foobar_math_value_free( lhs );
+ return FALSE;
+ }
+
+ gboolean success = FALSE;
+ switch ( expr->operation.o )
+ {
+ case FOOBAR_MATH_OPERATION_ADD:
+ success = foobar_math_value_add( &lhs, &rhs, out_value );
+ break;
+ case FOOBAR_MATH_OPERATION_SUB:
+ success = foobar_math_value_sub( &lhs, &rhs, out_value );
+ break;
+ case FOOBAR_MATH_OPERATION_MUL:
+ success = foobar_math_value_mul( &lhs, &rhs, out_value );
+ break;
+ case FOOBAR_MATH_OPERATION_DIV:
+ success = foobar_math_value_div( &lhs, &rhs, out_value );
+ break;
+ case FOOBAR_MATH_OPERATION_POW:
+ success = foobar_math_value_pow( &lhs, &rhs, out_value );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ foobar_math_value_free( lhs );
+ foobar_math_value_free( rhs );
+
+ return success;
+ }
+ default:
+ g_warn_if_reached( );
+ return FALSE;
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Values
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Initialize a new integer value to zero.
+//
+// After this, the structure should be freed using foobar_math_value_free.
+//
+void foobar_math_value_new_int( FoobarMathValue* out_value )
+{
+ out_value->type = FOOBAR_MATH_VALUE_INT;
+ mpz_init( out_value->int_value.v );
+ out_value->int_value.decimal_places = 0;
+}
+
+//
+// Initialize a new value from its floating point representation.
+//
+// Although technically not necessary, you should still call foobar_math_value_free on the returned value.
+//
+void foobar_math_value_from_float(
+ long double value,
+ FoobarMathValue* out_value )
+{
+ out_value->type = FOOBAR_MATH_VALUE_FLOAT;
+ out_value->float_value.v = value;
+}
+
+//
+// Parse a value from its string representation. This will always return an integer representation.
+//
+// On success (return value TRUE), the structure should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_from_string(
+ gchar const* input,
+ gsize input_length,
+ FoobarMathValue* out_value )
+{
+ g_autofree gchar* without_sep = g_new0( gchar, input_length + 1 );
+ gsize sep_pos = input_length - 1;
+ gsize j = 0;
+ for ( gsize i = 0; i < input_length; ++i )
+ {
+ if ( input[i] == '.' )
+ {
+ sep_pos = i;
+ }
+ else
+ {
+ without_sep[j++] = input[i];
+ }
+ }
+
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = input_length - 1 - sep_pos;
+ if ( mpz_init_set_str( out_value->int_value.v, without_sep, 10 ) == -1 )
+ {
+ mpz_clear( out_value->int_value.v );
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+//
+// Create a copy of a value, allocating necessary memory.
+//
+// After this, the structure should be freed using foobar_math_value_free.
+//
+void foobar_math_value_copy(
+ FoobarMathValue value,
+ FoobarMathValue* out_value )
+{
+ *out_value = value;
+
+ switch ( value.type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ mpz_init_set( out_value->int_value.v, value.int_value.v );
+ break;
+ case FOOBAR_MATH_VALUE_FLOAT:
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+}
+
+//
+// Release memory associated with a value.
+//
+// This is necessary because the integer representation is stored using GMP, which can store arbitrarily large values.
+//
+void foobar_math_value_free( FoobarMathValue value )
+{
+ switch ( value.type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ mpz_clear( value.int_value.v );
+ break;
+ case FOOBAR_MATH_VALUE_FLOAT:
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Operations
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Operation equivalent to "value *= -1".
+//
+void foobar_math_value_negate( FoobarMathValue* value )
+{
+ switch ( value->type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ mpz_neg( value->int_value.v, value->int_value.v );
+ break;
+ case FOOBAR_MATH_VALUE_FLOAT:
+ value->float_value.v = -value->float_value.v;
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+}
+
+//
+// Operation setting "out_value = lhs + rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_add(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( foobar_math_value_unify_integers( lhs, rhs ) )
+ {
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places;
+ mpz_add( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v );
+ return TRUE;
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ foobar_math_value_from_float( lhs_float + rhs_float, out_value );
+ return TRUE;
+}
+
+//
+// Operation setting "out_value = lhs - rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_sub(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( foobar_math_value_unify_integers( lhs, rhs ) )
+ {
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places;
+ mpz_sub( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v );
+ return TRUE;
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ foobar_math_value_from_float( lhs_float - rhs_float, out_value );
+ return TRUE;
+}
+
+//
+// Operation setting "out_value = lhs * rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_mul(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT )
+ {
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places + rhs->int_value.decimal_places;
+ mpz_mul( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v );
+ return TRUE;
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ foobar_math_value_from_float( lhs_float * rhs_float, out_value );
+ return TRUE;
+}
+
+//
+// Operation setting "out_value = lhs / rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_div(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT )
+ {
+ if ( mpz_sgn( rhs->int_value.v ) == 0 ) { return FALSE; }
+
+ if ( rhs->int_value.decimal_places > lhs->int_value.decimal_places )
+ {
+ foobar_math_value_unify_integers( lhs, rhs );
+ }
+
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places - rhs->int_value.decimal_places;
+ mpz_t rem;
+ mpz_init( rem );
+ mpz_divmod( out_value->int_value.v, rem, lhs->int_value.v, rhs->int_value.v );
+ gboolean result_is_int = mpz_sgn( rem ) == 0;
+ mpz_clear( rem );
+
+ if ( result_is_int ) { return TRUE; }
+ foobar_math_value_free( *out_value );
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ long double result = lhs_float / rhs_float;
+ if ( isnanl( result ) || isinfl( result ) ) { return FALSE; }
+ foobar_math_value_from_float( result, out_value );
+ return TRUE;
+}
+
+//
+// Operation setting "out_value = lhs ^ rhs".
+//
+// After this, the result value should be freed using foobar_math_value_free.
+//
+gboolean foobar_math_value_pow(
+ FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value )
+{
+ if ( lhs->type == FOOBAR_MATH_VALUE_INT &&
+ rhs->type == FOOBAR_MATH_VALUE_INT &&
+ rhs->int_value.decimal_places == 0 &&
+ mpz_fits_ulong_p( rhs->int_value.v ) )
+ {
+ unsigned long exp = mpz_get_ui( rhs->int_value.v );
+ foobar_math_value_new_int( out_value );
+ out_value->int_value.decimal_places = lhs->int_value.decimal_places * exp;
+ mpz_pow_ui( out_value->int_value.v, lhs->int_value.v, exp );
+ return TRUE;
+ }
+
+ long double lhs_float = foobar_math_value_to_float( *lhs );
+ long double rhs_float = foobar_math_value_to_float( *rhs );
+ long double result = powl( lhs_float, rhs_float );
+ if ( isnanl( result ) || isinfl( result ) ) { return FALSE; }
+ foobar_math_value_from_float( result, out_value );
+ return TRUE;
+}
+
+//
+// Ensure that two integer values have the same number of decimal places.
+//
+// This may change the in-memory-representation of a value, but not its actual value. It only increases the precision of
+// one value
+//
+// If either a or b is not stored as an integer, this will return FALSE.
+//
+gboolean foobar_math_value_unify_integers(
+ FoobarMathValue* a,
+ FoobarMathValue* b )
+{
+ if ( a->type != FOOBAR_MATH_VALUE_INT || b->type != FOOBAR_MATH_VALUE_INT ) { return FALSE; }
+
+ // Extend the less precise representation so both numbers have the same number of decimal places.
+
+ gsize decimal_places = MAX( a->int_value.decimal_places, b->int_value.decimal_places );
+
+ mpz_t exp;
+ mpz_init( exp );
+
+ if ( decimal_places > a->int_value.decimal_places )
+ {
+ mpz_ui_pow_ui( exp, 10, decimal_places - a->int_value.decimal_places );
+ mpz_mul( a->int_value.v, a->int_value.v, exp );
+ }
+
+ if ( decimal_places > b->int_value.decimal_places )
+ {
+ mpz_ui_pow_ui( exp, 10, decimal_places - b->int_value.decimal_places );
+ mpz_mul( b->int_value.v, b->int_value.v, exp );
+ }
+
+ mpz_clear( exp );
+
+ a->int_value.decimal_places = b->int_value.decimal_places = decimal_places;
+
+ return TRUE;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Conversions
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Format a value as a string.
+//
+// The result is a null-terminated string which should be freed using g_free.
+//
+gchar* foobar_math_value_to_string( FoobarMathValue value )
+{
+ switch ( value.type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ {
+ gsize cap = mpz_sizeinbase( value.int_value.v, 10 );
+ cap = MAX( cap, value.int_value.decimal_places + 1 ); // pad with zeroes if necessary
+ gchar* result = g_new0( gchar, cap + 3 ); // length + null terminator + sign + separator
+ mpz_get_str( result, 10, value.int_value.v );
+ gsize len = strlen( result ); // actual size may be 1 char less (+ len now also includes sign)
+ gboolean has_sign = result[0] == '-';
+ if ( has_sign )
+ {
+ // add the sign back later, makes everything else easier
+ result += 1;
+ len -= 1;
+ }
+ // pad with leading zeroes if necessary
+ gsize padded_len = MAX( len, value.int_value.decimal_places + 1 );
+ memmove( &result[padded_len - len], result, len + 1 );
+ memset( result, '0', padded_len - len );
+ len = padded_len;
+ gboolean trailing_zeros = TRUE;
+ gsize decimal_places = value.int_value.decimal_places;
+ gsize actual_decimal_places = decimal_places;
+ for ( gsize i = 0; i < decimal_places; ++i )
+ {
+ if ( trailing_zeros && result[len - 1 - i] == '0' )
+ {
+ result[len - 1 - i] = '\0';
+ actual_decimal_places -= 1;
+ }
+ else
+ {
+ trailing_zeros = FALSE;
+ }
+ }
+ if ( actual_decimal_places > 0 )
+ {
+ memmove( &result[len + 1 - decimal_places], &result[len - decimal_places], decimal_places + 1 );
+ result[len - decimal_places] = '.';
+ }
+ if ( has_sign )
+ {
+ result -= 1;
+ len += 1;
+ }
+ return result;
+ }
+ case FOOBAR_MATH_VALUE_FLOAT:
+ {
+ gchar* result = g_strdup_printf( "%.30Lf", value.float_value.v );
+
+ // trim trailing zeros
+ size_t len = strlen( result );
+ for ( gsize i = 0; i < len; ++i )
+ {
+ if ( result[len - 1 - i] == '.' )
+ {
+ result[len - 1 - i] = '\0';
+ break;
+ }
+ else if ( result[len - 1 - i] == '0' )
+ {
+ result[len - 1 - i] = '\0';
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return result;
+ }
+ default:
+ g_warn_if_reached( );
+ return NULL;
+ }
+}
+
+//
+// Convert a value to its floating-point representation.
+//
+// This may be a lossy conversion and should only be done if absolutely necessary.
+//
+long double foobar_math_value_to_float( FoobarMathValue value )
+{
+ switch ( value.type )
+ {
+ case FOOBAR_MATH_VALUE_INT:
+ {
+ mpz_t exp, integer, frac;
+ mpz_inits( exp, integer, frac, NULL );
+ mpz_ui_pow_ui( exp, 10, value.int_value.decimal_places );
+ mpz_mod( frac, value.int_value.v, exp );
+ mpz_div( integer, value.int_value.v, exp );
+ mpf_t f_exp, f_frac;
+ mpf_inits( f_exp, f_frac, NULL );
+ mpf_set_z( f_exp, exp );
+ mpf_set_z( f_frac, frac );
+ mpf_div( f_frac, f_frac, f_exp );
+ long double result = mpz_get_d( integer ) + mpf_get_d( f_frac );
+ mpf_clears( f_exp, f_frac, NULL );
+ mpz_clears( exp, integer, frac, NULL );
+ return result;
+ }
+ case FOOBAR_MATH_VALUE_FLOAT:
+ return value.float_value.v;
+ default:
+ g_warn_if_reached( );
+ return 0;
+ }
+}
diff --git a/src/services/quick-answers/math.h b/src/services/quick-answers/math.h
new file mode 100644
index 0000000..fafa73d
--- /dev/null
+++ b/src/services/quick-answers/math.h
@@ -0,0 +1,148 @@
+#pragma once
+
+#include <gio/gio.h>
+#include <gmp.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+ FOOBAR_MATH_VALUE_INT,
+ FOOBAR_MATH_VALUE_FLOAT,
+} FoobarMathValueType;
+
+typedef struct _FoobarMathValue FoobarMathValue;
+
+struct _FoobarMathValue
+{
+ FoobarMathValueType type;
+
+ union
+ {
+ struct
+ {
+ mpz_t v;
+ gsize decimal_places;
+ } int_value;
+
+ struct
+ {
+ long double v;
+ } float_value;
+ };
+};
+
+typedef enum
+{
+ FOOBAR_MATH_TOKEN_IDENTIFIER,
+ FOOBAR_MATH_TOKEN_NUMBER,
+ FOOBAR_MATH_TOKEN_PAREN_OPEN,
+ FOOBAR_MATH_TOKEN_PAREN_CLOSE,
+ FOOBAR_MATH_TOKEN_PLUS,
+ FOOBAR_MATH_TOKEN_MINUS,
+ FOOBAR_MATH_TOKEN_MUL,
+ FOOBAR_MATH_TOKEN_DIV,
+ FOOBAR_MATH_TOKEN_POW,
+} FoobarMathTokenType;
+
+typedef struct _FoobarMathToken FoobarMathToken;
+
+struct _FoobarMathToken
+{
+ FoobarMathTokenType type;
+ gchar const* data;
+ gsize length;
+};
+
+typedef enum
+{
+ FOOBAR_MATH_EXPRESSION_VALUE,
+ FOOBAR_MATH_EXPRESSION_FUNCTION,
+ FOOBAR_MATH_EXPRESSION_CONSTANT,
+ FOOBAR_MATH_EXPRESSION_OPERATION,
+} FoobarMathExpressionType;
+
+typedef enum
+{
+ FOOBAR_MATH_FUNCTION_NEGATE,
+ FOOBAR_MATH_FUNCTION_SIN,
+ FOOBAR_MATH_FUNCTION_COS,
+ FOOBAR_MATH_FUNCTION_SEC,
+ FOOBAR_MATH_FUNCTION_CSC,
+ FOOBAR_MATH_FUNCTION_TAN,
+ FOOBAR_MATH_FUNCTION_COT,
+ FOOBAR_MATH_FUNCTION_ARCSIN,
+ FOOBAR_MATH_FUNCTION_ARCCOS,
+ FOOBAR_MATH_FUNCTION_ARCSEC,
+ FOOBAR_MATH_FUNCTION_ARCCSC,
+ FOOBAR_MATH_FUNCTION_ARCTAN,
+ FOOBAR_MATH_FUNCTION_ARCCOT,
+ FOOBAR_MATH_FUNCTION_EXP,
+ FOOBAR_MATH_FUNCTION_SQRT,
+} FoobarMathFunction;
+
+typedef enum
+{
+ FOOBAR_MATH_CONSTANT_PI,
+ FOOBAR_MATH_CONSTANT_E,
+} FoobarMathConstant;
+
+typedef enum
+{
+ FOOBAR_MATH_OPERATION_ADD,
+ FOOBAR_MATH_OPERATION_SUB,
+ FOOBAR_MATH_OPERATION_MUL,
+ FOOBAR_MATH_OPERATION_DIV,
+ FOOBAR_MATH_OPERATION_POW,
+} FoobarMathOperation;
+
+typedef struct _FoobarMathExpression FoobarMathExpression;
+
+FoobarMathToken* foobar_math_lex ( gchar const* input,
+ gsize input_length,
+ gsize* out_count );
+FoobarMathExpression* foobar_math_parse ( FoobarMathToken const* tokens,
+ gsize tokens_count );
+FoobarMathExpression* foobar_math_expression_new_value ( FoobarMathValue value );
+FoobarMathExpression* foobar_math_expression_new_function ( FoobarMathFunction function,
+ FoobarMathExpression* input );
+FoobarMathExpression* foobar_math_expression_new_constant ( FoobarMathConstant constant );
+FoobarMathExpression* foobar_math_expression_new_operation( FoobarMathOperation operation,
+ FoobarMathExpression* lhs,
+ FoobarMathExpression* rhs );
+void foobar_math_expression_print ( FoobarMathExpression const* expr,
+ gint indentation );
+gboolean foobar_math_evaluate ( FoobarMathExpression const* expr,
+ FoobarMathValue* out_value );
+void foobar_math_expression_free ( FoobarMathExpression* expression );
+void foobar_math_value_new_int ( FoobarMathValue* out_value );
+void foobar_math_value_from_float ( long double value,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_from_string ( gchar const* input,
+ gsize input_length,
+ FoobarMathValue* out_value );
+void foobar_math_value_copy ( FoobarMathValue value,
+ FoobarMathValue* out_value );
+void foobar_math_value_free ( FoobarMathValue value );
+void foobar_math_value_negate ( FoobarMathValue* value );
+gboolean foobar_math_value_add ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_sub ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_mul ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_div ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_pow ( FoobarMathValue* lhs,
+ FoobarMathValue* rhs,
+ FoobarMathValue* out_value );
+gboolean foobar_math_value_unify_integers ( FoobarMathValue* a,
+ FoobarMathValue* b );
+gchar* foobar_math_value_to_string ( FoobarMathValue value );
+long double foobar_math_value_to_float ( FoobarMathValue value );
+
+G_END_DECLS
diff --git a/src/services/quick-answers/math.test.c b/src/services/quick-answers/math.test.c
new file mode 100644
index 0000000..416b576
--- /dev/null
+++ b/src/services/quick-answers/math.test.c
@@ -0,0 +1,153 @@
+#include "services/quick-answers/math.h"
+#include <mutest.h>
+
+#define SERIALIZATION_TEST( value, identifier ) \
+ static void serialization_value_##identifier##_spec( void ) \
+ { \
+ FoobarMathValue val; \
+ \
+ gchar const input[] = #value; \
+ mutest_expect( \
+ "parse result", \
+ mutest_bool_value( foobar_math_value_from_string( input, sizeof(input) - 1, &val) ), \
+ mutest_to_be_true, \
+ NULL ); \
+ g_autofree gchar* output = foobar_math_value_to_string( val ); \
+ mutest_expect( \
+ "string representation", \
+ mutest_string_value( output ), \
+ mutest_to_be, \
+ input, \
+ NULL ); \
+ \
+ foobar_math_value_free( val ); \
+ }
+
+SERIALIZATION_TEST( 0, 0 )
+SERIALIZATION_TEST( 1, 1 )
+SERIALIZATION_TEST( -1, neg_1 )
+SERIALIZATION_TEST( 1.2, 1_2 )
+SERIALIZATION_TEST( -1.2, neg_1_2 )
+SERIALIZATION_TEST( 1.30002, 1_30002 )
+SERIALIZATION_TEST( -1.30002, neg_1_30002 )
+SERIALIZATION_TEST( 0.2, 0_2 )
+SERIALIZATION_TEST( -0.2, neg_0_2 )
+SERIALIZATION_TEST( 0.00002, 0_00002 )
+SERIALIZATION_TEST( -0.00002, neg_0_00002 )
+SERIALIZATION_TEST( 123, 123 )
+SERIALIZATION_TEST( 123.456, 123_456 )
+SERIALIZATION_TEST( -123, neg_123 )
+SERIALIZATION_TEST( -123.456, neg_123_456 )
+
+#undef SERIALIZATION_TEST
+
+static void serialization_suite( void )
+{
+ mutest_it( "value 0", serialization_value_0_spec );
+ mutest_it( "value 1", serialization_value_1_spec );
+ mutest_it( "value -1", serialization_value_neg_1_spec );
+ mutest_it( "value 1.2", serialization_value_1_2_spec );
+ mutest_it( "value -1.2", serialization_value_neg_1_2_spec );
+ mutest_it( "value 1.30002", serialization_value_1_30002_spec );
+ mutest_it( "value -1.30002", serialization_value_neg_1_30002_spec );
+ mutest_it( "value 0.2", serialization_value_0_2_spec );
+ mutest_it( "value -0.2", serialization_value_neg_0_2_spec );
+ mutest_it( "value 0.00002", serialization_value_0_00002_spec );
+ mutest_it( "value -0.00002", serialization_value_neg_0_00002_spec );
+ mutest_it( "value 123", serialization_value_123_spec );
+ mutest_it( "value 123.456", serialization_value_123_456_spec );
+ mutest_it( "value -123", serialization_value_neg_123_spec );
+ mutest_it( "value -123.456", serialization_value_neg_123_456_spec );
+}
+#define OPERATION_TEST( op, value_a, value_b, value_res, identifier ) \
+ static void operation_##identifier##_spec( void ) \
+ { \
+ FoobarMathValue a, b, res; \
+ \
+ gchar const input_a[] = #value_a; \
+ gchar const input_b[] = #value_b; \
+ mutest_expect( \
+ "input a parsed", \
+ mutest_bool_value( foobar_math_value_from_string( input_a, sizeof(input_a) - 1, &a) ), \
+ mutest_to_be_true, \
+ NULL ); \
+ mutest_expect( \
+ "input b parsed", \
+ mutest_bool_value( foobar_math_value_from_string( input_b, sizeof(input_b) - 1, &b) ), \
+ mutest_to_be_true, \
+ NULL ); \
+ foobar_math_value_##op( &a, &b, &res ); \
+ g_autofree gchar* output = foobar_math_value_to_string( res ); \
+ mutest_expect( \
+ "result", \
+ mutest_string_value( output ), \
+ mutest_to_be, \
+ #value_res, \
+ NULL ); \
+ \
+ foobar_math_value_free( a ); \
+ foobar_math_value_free( b ); \
+ foobar_math_value_free( res ); \
+ }
+
+OPERATION_TEST( add, 123, 456, 579, 123_plus_456)
+OPERATION_TEST( add, 123.456, 11, 134.456, 123_456_plus_11)
+OPERATION_TEST( add, 11, 123.456, 134.456, 11_plus_123_456)
+OPERATION_TEST( sub, 123, 456, -333, 123_minus_456)
+OPERATION_TEST( sub, 123.456, 11, 112.456, 123_456_minus_11)
+OPERATION_TEST( sub, 11, 123.456, -112.456, 11_minus_123_456)
+OPERATION_TEST( mul, 123, 3, 369, 123_times_3 )
+OPERATION_TEST( mul, 12, 0.05, 0.6, 12_times_0_05 )
+OPERATION_TEST( mul, 0.05, 12, 0.6, 0_05_times_12 )
+OPERATION_TEST( div, 369, 3, 123, 369_div_3 )
+OPERATION_TEST( div, 369, 2, 184.5, 369_div_2 )
+OPERATION_TEST( div, 12, 0.05, 240, 12_div_0_05 )
+OPERATION_TEST( div, 0.08, 4, 0.02, 0_08_div_4 )
+OPERATION_TEST( pow, 2, 3, 8, 2_pow_3 )
+OPERATION_TEST( pow, 4, 0.5, 2, 4_pow_0_5 )
+
+#undef OPERATION_TEST
+
+static void addition_suite( void )
+{
+ mutest_it( "calculate 123 + 456", operation_123_plus_456_spec );
+ mutest_it( "calculate 123.456 + 11", operation_123_456_plus_11_spec );
+ mutest_it( "calculate 11 + 123.456", operation_11_plus_123_456_spec );
+}
+
+static void subtraction_suite( void )
+{
+ mutest_it( "calculate 123 - 456", operation_123_minus_456_spec );
+ mutest_it( "calculate 123.456 - 11", operation_123_456_minus_11_spec );
+ mutest_it( "calculate 11 - 123.456", operation_11_minus_123_456_spec );
+}
+
+static void multiplication_suite( void )
+{
+ mutest_it( "calculate 123 * 3", operation_123_times_3_spec );
+ mutest_it( "calculate 12 * 0.05", operation_12_times_0_05_spec );
+ mutest_it( "calculate 0.05 * 12", operation_0_05_times_12_spec );
+}
+
+static void division_suite( void )
+{
+ mutest_it( "calculate 369 / 3", operation_369_div_3_spec );
+ mutest_it( "calculate 369 / 2", operation_369_div_2_spec );
+ mutest_it( "calculate 12 / 0.05", operation_12_div_0_05_spec );
+ mutest_it( "calculate 0.08 / 4", operation_0_08_div_4_spec );
+}
+
+static void power_suite( void )
+{
+ mutest_it( "calculate 2 ^ 3", operation_2_pow_3_spec );
+ mutest_it( "calculate 4 ^ 0.5", operation_4_pow_0_5_spec );
+}
+
+MUTEST_MAIN(
+ mutest_describe( "Serialization", serialization_suite );
+ mutest_describe( "Addition", addition_suite );
+ mutest_describe( "Subtraction", subtraction_suite );
+ mutest_describe( "Multiplication", multiplication_suite );
+ mutest_describe( "Division", division_suite );
+ mutest_describe( "Power", power_suite );
+)
diff --git a/src/services/quick-answers/meson.build b/src/services/quick-answers/meson.build
new file mode 100644
index 0000000..ae468c2
--- /dev/null
+++ b/src/services/quick-answers/meson.build
@@ -0,0 +1,9 @@
+foobar_sources += files(
+ 'math-lexer.c',
+ 'math-parser.c',
+ 'math.c',
+)
+
+foobar_tests += {
+ 'math': files('math.test.c'),
+}