diff options
| author | Hannes Schulze <43932053+hannesschulze@users.noreply.github.com> | 2024-11-04 12:58:21 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-11-04 12:58:21 +0100 |
| commit | 40efcf60a5f39674b5137f11de7bcfe37fb121b5 (patch) | |
| tree | 29e89bf90cd56d26f7432a52bf4bc90c51c632ce | |
| parent | f82081215e1f29140ab9c70e897ac62e9b456919 (diff) | |
| parent | 83d650b82e5209d2ca24cc1e6bf04e0644b3f615 (diff) | |
Merge pull request #4 from hannesschulze/3-calculator
Add a calculator to the launcher
| -rw-r--r-- | flake.lock | 12 | ||||
| -rw-r--r-- | flake.nix | 2 | ||||
| -rw-r--r-- | meson.build | 27 | ||||
| -rw-r--r-- | nix/package.nix | 3 | ||||
| -rw-r--r-- | res/icons/fluent-calculator-symbolic.svg | 3 | ||||
| -rw-r--r-- | res/icons/icons.gresource.xml | 1 | ||||
| -rw-r--r-- | src/application.c | 5 | ||||
| -rw-r--r-- | src/launcher-item.c | 73 | ||||
| -rw-r--r-- | src/launcher-item.h | 28 | ||||
| -rw-r--r-- | src/launcher.c | 83 | ||||
| -rw-r--r-- | src/launcher.h | 2 | ||||
| -rw-r--r-- | src/meson.build | 77 | ||||
| -rw-r--r-- | src/services/application-service.c | 211 | ||||
| -rw-r--r-- | src/services/application-service.h | 4 | ||||
| -rw-r--r-- | src/services/meson.build | 3 | ||||
| -rw-r--r-- | src/services/quick-answer-service.c | 360 | ||||
| -rw-r--r-- | src/services/quick-answer-service.h | 21 | ||||
| -rw-r--r-- | src/services/quick-answers/math-lexer.c | 248 | ||||
| -rw-r--r-- | src/services/quick-answers/math-parser.c | 414 | ||||
| -rw-r--r-- | src/services/quick-answers/math.c | 851 | ||||
| -rw-r--r-- | src/services/quick-answers/math.h | 148 | ||||
| -rw-r--r-- | src/services/quick-answers/math.test.c | 153 | ||||
| -rw-r--r-- | src/services/quick-answers/meson.build | 9 | ||||
| -rw-r--r-- | subprojects/mutest.wrap | 4 |
24 files changed, 2573 insertions, 169 deletions
@@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1714906307, - "narHash": "sha256-UlRZtrCnhPFSJlDQE7M0eyhgvuuHBTe1eJ9N9AQlJQ0=", + "lastModified": 1729880355, + "narHash": "sha256-RP+OQ6koQQLX5nw0NmcDrzvGL8HDLnyXt/jHhL1jwjM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "25865a40d14b3f9cf19f19b924e2ab4069b09588", + "rev": "18536bf04cd71abd345f9579158841376fdd0c5a", "type": "github" }, "original": { @@ -28,8 +28,10 @@ packages = [ pkgs.libnotify # for testing the notification daemon pkgs.clang-tools + pkgs.gdb ]; inputsFrom = [ foobar ]; + hardeningDisable = [ "fortify" ]; }; }; }); diff --git a/meson.build b/meson.build index d92faa4..f1cdb9d 100644 --- a/meson.build +++ b/meson.build @@ -8,17 +8,24 @@ gio_unix_dep = dependency('gio-unix-2.0', version: '>=2.78') gtk_dep = dependency('gtk4', version: '>=4.12') json_dep = dependency('json-glib-1.0', version: '>=1.8') nm_dep = dependency('libnm', version: '>=1.44') +gmp_dep = dependency('gmp', version: '>=6.3') m_dep = meson.get_compiler('c').find_library('m', required : false) -layershell_dep = dependency('gtk4-layer-shell', required: false) -gvc_dep = dependency('gvc', required: false) -if not layershell_dep.found() - layershell_proj = subproject('gtk4-layer-shell') - layershell_dep = layershell_proj.get_variable('gtk_layer_shell') -endif -if not gvc_dep.found() - gvc_proj = subproject('gvc') - gvc_dep = gvc_proj.get_variable('libgvc_dep') -endif +layershell_dep = dependency( + 'gtk4-layer-shell', + fallback: [ 'gtk4-layer-shell', 'gtk_layer_shell' ], + default_options: [ 'default_library=static' ], +) +gvc_dep = dependency( + 'gvc', + fallback: [ 'gvc', 'libgvc_dep' ], + default_options: [ 'default_library=static' ], +) +mutest_dep = dependency( + 'mutest', + fallback: [ 'mutest', 'mutest_dep' ], + default_options: [ 'static=true' ], + required: false, +) subdir('res') subdir('src') diff --git a/nix/package.nix b/nix/package.nix index 4c47e83..3b8be8f 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -15,6 +15,7 @@ glib, gtk4, json-glib, + gmp, librsvg, networkmanager, wayland, @@ -54,7 +55,7 @@ ''; nativeBuildInputs = [ makeWrapper git meson ninja vala sassc pkg-config gobject-introspection wayland-scanner ]; - buildInputs = [ glib gtk4 json-glib librsvg networkmanager wayland libpulseaudio alsa-lib upower brightnessctl ]; + buildInputs = [ glib gtk4 json-glib gmp librsvg networkmanager wayland libpulseaudio alsa-lib upower brightnessctl ]; meta = with lib; { homepage = "https://github.com/hannesschulze/foobar"; diff --git a/res/icons/fluent-calculator-symbolic.svg b/res/icons/fluent-calculator-symbolic.svg new file mode 100644 index 0000000..21b9799 --- /dev/null +++ b/res/icons/fluent-calculator-symbolic.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7 7C7 5.89543 7.89543 5 9 5H15C16.1046 5 17 5.89543 17 7V8C17 9.10457 16.1046 10 15 10H9C7.89543 10 7 9.10457 7 8V7ZM9 6.5C8.72386 6.5 8.5 6.72386 8.5 7V8C8.5 8.27614 8.72386 8.5 9 8.5H15C15.2761 8.5 15.5 8.27614 15.5 8V7C15.5 6.72386 15.2761 6.5 15 6.5H9ZM8.25 14.5C8.94036 14.5 9.5 13.9404 9.5 13.25C9.5 12.5596 8.94036 12 8.25 12C7.55964 12 7 12.5596 7 13.25C7 13.9404 7.55964 14.5 8.25 14.5ZM9.5 17.25C9.5 17.9404 8.94036 18.5 8.25 18.5C7.55964 18.5 7 17.9404 7 17.25C7 16.5596 7.55964 16 8.25 16C8.94036 16 9.5 16.5596 9.5 17.25ZM15.75 14.5C16.4404 14.5 17 13.9404 17 13.25C17 12.5596 16.4404 12 15.75 12C15.0596 12 14.5 12.5596 14.5 13.25C14.5 13.9404 15.0596 14.5 15.75 14.5ZM17 17.25C17 17.9404 16.4404 18.5 15.75 18.5C15.0596 18.5 14.5 17.9404 14.5 17.25C14.5 16.5596 15.0596 16 15.75 16C16.4404 16 17 16.5596 17 17.25ZM12 14.5C12.6904 14.5 13.25 13.9404 13.25 13.25C13.25 12.5596 12.6904 12 12 12C11.3096 12 10.75 12.5596 10.75 13.25C10.75 13.9404 11.3096 14.5 12 14.5ZM13.25 17.25C13.25 17.9404 12.6904 18.5 12 18.5C11.3096 18.5 10.75 17.9404 10.75 17.25C10.75 16.5596 11.3096 16 12 16C12.6904 16 13.25 16.5596 13.25 17.25ZM7.25 2C5.45507 2 4 3.45507 4 5.25V18.75C4 20.5449 5.45507 22 7.25 22H16.75C18.5449 22 20 20.5449 20 18.75V5.25C20 3.45507 18.5449 2 16.75 2H7.25ZM5.5 5.25C5.5 4.2835 6.2835 3.5 7.25 3.5H16.75C17.7165 3.5 18.5 4.2835 18.5 5.25V18.75C18.5 19.7165 17.7165 20.5 16.75 20.5H7.25C6.2835 20.5 5.5 19.7165 5.5 18.75V5.25Z" fill="#212121"/> +</svg> diff --git a/res/icons/icons.gresource.xml b/res/icons/icons.gresource.xml index 29cab10..a5061ca 100644 --- a/res/icons/icons.gresource.xml +++ b/res/icons/icons.gresource.xml @@ -48,5 +48,6 @@ <file compressed="true">fluent-chevron-right-symbolic.svg</file> <file compressed="true">fluent-checkmark-symbolic.svg</file> <file compressed="true">fluent-microphone-symbolic.svg</file> + <file compressed="true">fluent-calculator-symbolic.svg</file> </gresource> </gresources> 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'), +} diff --git a/subprojects/mutest.wrap b/subprojects/mutest.wrap new file mode 100644 index 0000000..6e99973 --- /dev/null +++ b/subprojects/mutest.wrap @@ -0,0 +1,4 @@ +[wrap-git] +url = https://github.com/ebassi/mutest.git +revision = 18a20071773f7c4b75e82a931ef9b916b273b3e5 +depth = 1 |
