Caster icon indicating copy to clipboard operation
Caster copied to clipboard

Grammar API

Open lexxish opened this issue 5 years ago • 29 comments

Grammar is hardcoded into caster and difficult to change. Allow grammars to be changed by separating the grammars from the behavior.

lexxish avatar May 22 '19 02:05 lexxish

This is a challenging subject as this is something that would be better addressed in the dragonfly platform. However if there is a way to implement it on top of dragonfly that I'm more willing to pursue.

Either way this will be a long-term goal as there's a lot of other pressing implementations such as Python three, cross-platform, reloading grammars and so on. This is something we'll revisit in the future.

LexiconCode avatar May 22 '19 04:05 LexiconCode

Either way this will be a long-term goal as there's a lot of other pressing implementations such as Python three, cross-platform, reloading grammars and so on. This is something we'll revisit in the future.

I have some time and can work on this

lexxish avatar May 23 '19 16:05 lexxish

Sure, as long as it's backwards compatible with dragonfly and filter rules.

LexiconCode avatar May 23 '19 16:05 LexiconCode

Yes it won't effect either of those. I will decouple it from Caster and build it just on top of dragonfly. Filter rules will still be in Caster's control.

lexxish avatar May 23 '19 16:05 lexxish

Danesprite is very approachable. You might consider contacting Dane Finlay @Danesprite about the possibility of integrating it directly into dragonfly.

LexiconCode avatar May 23 '19 16:05 LexiconCode

I could see this being part of dragonfly or being its own module. However I would lean towards its own module that depends on dragonfly so that it can be modified without touching dragonfly. I plan to initially build out the grammar for desktops. But in the future, it may have other devices like mobile for example. @Danesprite thoughts?

lexxish avatar May 23 '19 17:05 lexxish

This sounds similar to dragonfly's Configuration toolkit. This _audacity.py module is a good example of using it. That repository contains separate _audacity-en.txt and _audacity-nl.txt files for using the grammar with English or Dutch.

Perhaps Caster could use the configuration toolkit as another way to modify grammars without changing the source code? It's doable, but changing Caster's modules to use it would be pretty tedious.

drmfinlay avatar May 24 '19 04:05 drmfinlay

Could we break this down into multiple subsections using - [ ]. This allows to track progress in the roadmap board.

LexiconCode avatar Jul 01 '19 15:07 LexiconCode

This sounds similar to dragonfly's Configuration toolkit. This _audacity.py module is a good example of using it. That repository contains separate _audacity-en.txt and _audacity-nl.txt files for using the grammar with English or Dutch.

Perhaps Caster could use the configuration toolkit as another way to modify grammars without changing the source code? It's doable, but changing Caster's modules to use it would be pretty tedious.

@lexxish I think this is what we need and it's a bonus that it's built into Dragonfly. Thoughts?

LexiconCode avatar Aug 01 '19 18:08 LexiconCode

Generally sounds like the right idea but one concern I have is if they are text files how can we represent extras? If they are kept as python files we can represent extras and use dependency injection to handle different grammars.

lexxish avatar Aug 01 '19 19:08 lexxish

@Danesprite might have more knowledge on the subject than I. I'll try some experimenting later today!

LexiconCode avatar Aug 01 '19 19:08 LexiconCode

I'm not sure if that's possible with the configuration toolkit as it currently stands.

drmfinlay avatar Aug 02 '19 13:08 drmfinlay

@lexxish I think it might be worthwhile exploring what it would take to evolve the configuration toolkit within dragonfly to accommodate extras. In the event that it's deemed not feasible using dependency injection probably is the way to go.

LexiconCode avatar Aug 05 '19 01:08 LexiconCode

As a side note the configuration toolkit does work with caster. Testing extras is still needed.

#
# This file is a command-module for Dragonfly.
# (c) Copyright 2008 by Biddy
# Licensed under the LGPL, see <http://www.gnu.org/licenses/>
#

"""
Command-module for **Audacity**, an audio-editing application
=============================================================
This module offers voice-commands to control 
`Audacity <http://audacity.sourceforge.net/>`_, an open 
source application for recording and editing sounds.

Customization
-------------
Users can edit the spoken-form of this module's commands
in its configuration file.  This is useful for
translations, for example.

"""

from dragonfly import (Config)
from castervoice.lib.imports import *

#---------------------------------------------------------------------------
# Initialize this module's configuration.

config = Config("Audacity control")

config.lang                    = Section("Language section")
config.lang.new_project        = Item("[start] new project", doc="Spec starts a new project.")
config.lang.open_project       = Item("open project", doc="Spec opens an existing project.")
config.lang.close_project      = Item("close [this] project", doc="Spec closes currently open project.")
config.lang.save_project       = Item("save [this] project", doc="Spec saves currently open project.")
config.lang.selection_tool     = Item("[open] selection tool", doc="Spec opens the selection tool.")
config.lang.envelope_tool      = Item("[open] envelope tool", doc="Spec opens the envelope tool.")
config.lang.edit_tool          = Item("[open] edit tool", doc="Spec opens the edit tool.")
config.lang.zoom_tool          = Item("[open] zoom tool", doc="Spec opens the zoom tool.")
config.lang.timeshift_tool     = Item("[open] timeshift tool", doc="Spec opens the timeshift tool.")
config.lang.multi_tool         = Item("[open] multi tool", doc="Spec opens the multitool.")
config.lang.next_toolbar_tool  = Item("[move to | go to] next tool", doc="Spec move to the next tool in toolbar.")
config.lang.previous_toolbar_tool = Item("[move to | go to] previous tool", doc="Spec move to the previous tool in toolbar.")
config.lang.silence            = Item("[insert | create] silence", doc="Spec insert silence at current position.")
config.lang.duplicate          = Item("[make] duplicate", doc="Spec makes duplicate.")
config.lang.find_zero_crossings= Item("[find | search] zero crossings", doc="Spec finds zero crossings.")
config.lang.play_stop          = Item("[start | stop] playing", doc="Spec starts or stops playback.")
config.lang.loop               = Item("[start] loop", doc="Spec starts a loop.")
config.lang.pause              = Item("pause [playback | recording]", doc="Spec pauses current playback or recording.")
config.lang.record             = Item("[start | begin] recording", doc="Spec starts recording.")
config.lang.play_cursor_selection = Item("[play] cursor selection", doc="Spec plays one second at cursor position.")
config.lang.zoom_in            = Item("zoom in", doc="Spec zooms in on current view.")
config.lang.zoom_normal        = Item("[return to] standard zoom level", doc="Spec zoom in or out to standard zoom level.")
config.lang.zoom_out           = Item("zoom out", doc="Spec zooms out on current view.")
config.lang.fit_window         = Item("fit [track] to window", doc="Spec fits the track in current window size.")
config.lang.fit_vertically     = Item("fit all tracks [to window]", doc="Spec makes all tracks visible in current window size.")
config.lang.zoom_selection     = Item("zoom [in] selection", doc="Spec zooms in on your current selection.")
config.lang.import_audio       = Item("import audio", doc="Spec imports audio from your PC.")
config.lang.create_label       = Item("[create | make] label", doc="Spec creates a label.")
config.lang.repeat_last_effect = Item("repeat last effect", doc="Spec repeats the last effect.")
config.lang.ripple_delete      = Item("ripple delete selection", doc="Spec deletes current selection and removes empty space.")
config.lang.split_delete       = Item("split delete selection", doc="Spec deletes current selection while leaving space empty.")
config.lang.split_new          = Item("split new", doc="Spec splits new.")
config.lang.join               = Item("join track", doc="Spec removes all empty spaces from track.")
config.lang.disjoin            = Item("disjoin track", doc="Spec brings back empty spaces in track.")
config.lang.mute_all_tracks    = Item("mute all [tracks]", doc="Spec mutes all tracks in file.")
config.lang.unmute_all_tracks  = Item("unmute all [tracks]", doc="Spec unmutes all tracks in file.")
config.lang.collapse_all_tracks= Item("collapse all [tracks]", doc="Spec collapses all tracks in file.")
config.lang.expand_all_tracks  = Item("expand all [tracks]", doc="Spec expands all tracks in file.")
config.lang.export_track       = Item("export track", doc="Spec exports track to preferred file format.")
config.lang.export_selection   = Item("export selection", doc="Spec exports selection to preferred file format.")

#config.generate_config_file()
config.load()


#---------------------------------------------------------------------------
# Create this module's grammar and the context under which it'll be active.

#---------------------------------------------------------------------------
# This module's main keystroke mapping rule.

class audacityRule(MergeRule):
    name="audacityRule"
    mapping={
             config.lang.new_project:            R(Key("c-n")),
             config.lang.open_project:           R(Key("c-o")),
             config.lang.close_project:          R(Key("c-w")),
             config.lang.save_project:           R(Key("c-s")),
             config.lang.selection_tool:         R(Key("f1")),
             config.lang.envelope_tool:          R(Key("f2")),
             config.lang.edit_tool:              R(Key("f3")),
             config.lang.zoom_tool:              R(Key("f4")),
             config.lang.timeshift_tool:         R(Key("f5")),
             config.lang.multi_tool:             R(Key("f6")),
             config.lang.next_toolbar_tool:      R(Key("d")),
             config.lang.previous_toolbar_tool:  R(Key("a")),
             config.lang.silence:                R(Key("c-l")),
             config.lang.duplicate:              R(Key("c-d")),
             config.lang.find_zero_crossings:    R(Key("z")),
             config.lang.play_stop:              R(Key("space")),
             config.lang.loop:                   R(Key("l")),
             config.lang.pause:                  R(Key("p")),
             config.lang.record:                 R(Key("r")),
             config.lang.play_cursor_selection:  R(Key("b")),
             config.lang.zoom_in:                R(Key("c-1")),
             config.lang.zoom_normal:            R(Key("c-2")),
             config.lang.zoom_out:               R(Key("c-3")),
             config.lang.fit_window:             R(Key("c-f")),
             config.lang.fit_vertically:         R(Key("cs-f")),
             config.lang.zoom_selection:         R(Key("c-e")),
             config.lang.import_audio:           R(Key("cs-i")),
             config.lang.create_label:           R(Key("c-b")),
             config.lang.repeat_last_effect:     R(Key("c-r")),
             config.lang.ripple_delete:          R(Key("c-k")),
             config.lang.split_delete:           R(Key("ca-k")),
             config.lang.split_new:              R(Key("ca-i")),
             config.lang.join:                   R(Key("c-j")),
             config.lang.disjoin:                R(Key("ca-j")),
             config.lang.mute_all_tracks:        R(Key("c-u")),
             config.lang.unmute_all_tracks:      R(Key("cs-u")),
             config.lang.collapse_all_tracks:    R(Key("cs-c")),
             config.lang.expand_all_tracks:      R(Key("cs-x")),

             # Customised hotkeys.
             # (editable in Audacity; Preferences; Keyboard)
             config.lang.export_track:           R(Key("ca-e")),
             config.lang.export_selection:       R(Key("cs-e")),
            }

# Add the action rule to the grammar instance.

context = AppContext(executable="audacity", title="audacity")
control.non_ccr_app_rule(audacityRule(), context=context)

LexiconCode avatar Aug 05 '19 01:08 LexiconCode

Well, when we touched translations way back, bothering people writing grammars with writing _(spec) instead of spec (gettext) seemed too much. This seems to have a bit more disadvantages, it is very verbose, and something for generation and loading (according to some rules) of the ini files would have to be written. I would suggest as alternatives either gettext directly, or a custom solution based on gettext and reflection (no changes to existing rules, but not sure about performance and code complexity).

comodoro avatar Aug 05 '19 12:08 comodoro

Well, when we touched translations way back, bothering people writing grammars with writing _(spec) instead of spec (gettext) seemed too much. This seems to have a bit more disadvantages, it is very verbose, and something for generation and loading (according to some rules) of the ini files would have to be written. I would suggest as alternatives either gettext directly, or a custom solution based on gettext and reflection (no changes to existing rules, but not sure about performance and code complexity).

I agree writing the grammars with an API is verbose. Although I think that's true of any API that would be developed. What we have above is pretty much the same as application standard like browser or ccr standard The good news is we don't have to create the config by hand it's automatically or generated with config.generate_config_file() An example of a config _audacity-en.txt. Which pretty much means the burden of the API falls to maintainers rather that the end-users.

Your suggestion regarding gettext is an interesting alternative. Is there a snippet somewhere or file that demonstrates close to how you see it being used?

LexiconCode avatar Aug 06 '19 15:08 LexiconCode

Yes, sorry, I have looked at the gettext module and it doesn't look like it is flexible enough for anything other than traditional localization. It goes like:

[some imports]
class audacityRule(MergeRule):
    name="audacityRule"
    mapping={
             _("new project"):            R(Key("c-n")),
             ...
    }

and some code to load translations at startup, probably _caster.py, files with actual translations are .po compiled into .mo in the appropriate folders. The _() function does the translating. Details everywhere, https://www.google.com/search?q=translation+with+gettext+python

It is one of the most widely used ways of localization, I wonder why, as it does not look as pretty as one would think is possible. It would take care of other strings (like extras...) and switching between translations though.

A custom solution like the one above (or modified Dragonfly one) looks possible: defining the Config object might be automatic from the ini (or whatever format) file. But I don't think you have made thing easier for whoever wants to modify the grammars. It is certainly easier to replace an utterance ("spec"), but not adding one nor adding a rule. Then there are questions about complexity (more code to maintain, you have to load the correct set of strings according to some rules), performance, introducing new bugs.

comodoro avatar Aug 07 '19 17:08 comodoro

My preference is still to use Python. This gives us syntax checking, highlighting and IDE tooling. It also allows for extras to be easily added into grammar standards like they are for Browser.. I'd be pretty happy with moving of all voice commands to grammar/standard Python files and allowing those files to be overridden locally (possibly with an inheritance style override system).

An actual API may offer some small benefits on top of the above, but won't look quite as clean in the rule files so I'm still on the fence there. The only specific benefit I can think of to an actual API is better handling of conflicting grammar versions - for example if your local grammar doesn't have a specific command that you pull from upstream then you can provide some default for the API call. I should add that even if it were a full blown API my preference is to remain in python. Creating the ability to create simple rules in a DSL seems reasonable though.

lexxish avatar Aug 07 '19 19:08 lexxish

You mean externalizing strings like it is in browser.py and symbolspecs py? Yes, to make commands consistent when they are used in more than rule, something like this may be a good idea. Not sure where else it is though, perhaps tab navigation accross apps?

I thought the point of MappingRule was that it is so simple that you do not need DSLs. Also I do not know what API means in this context, I was thinking more in the direction of translations.

comodoro avatar Aug 10 '19 16:08 comodoro

Some further thoughts for discussion.

What I've considered an API in the context of Caster is a stable interface for customizing existing commands. Given that definition of scope as for adding new, editing and removing commands in an existing grammar is out of scope for the API.

As for mechanisms to utilize the API I think there is two clear purposes could be summarized as translation and customization grammars. Perhaps it would be easier to come up with two solutions rather than one overarching mechanism.

My primary thought for this is simply if someone uses something like _audacity-en.txt for translation and customization the system becomes very convoluted. Further complexity can be seen when that translation is updated upstream. User customization shouldn't be mixed with translation in the same file.

I thought would be for the mechanism to translate first then apply customizations. The format to which these files take don't have to be necessarily the same.

LexiconCode avatar Aug 12 '19 04:08 LexiconCode

Ultimately, Through filter rules (soon to be Ttransformers) I plan to solve the 90% of user customizations straight through a UI. This editing command/action, adding new and removing commands from existing grammars. That last 10% people need to create their own grammars and functions in python. Building something without in depth knowledge of Python or programming yet powerful maintaining the flexibility that we currently enjoy.

Technically this can be done with or without the API but it would be more stable with such an interface.

LexiconCode avatar Aug 12 '19 04:08 LexiconCode

You mean externalizing strings like it is in browser.py and symbolspecs py? Yes, to make commands consistent when they are used in more than rule, something like this may be a good idea. Not sure where else it is though, perhaps tab navigation accross apps?

I was thinking we would always externalize grammars so they can be replaced but rule filters would be an option too.

I thought the point of MappingRule was that it is so simple that you do not need DSLs. Also I do not know what API means in this context, I was thinking more in the direction of translations.

I don't feel a need for a DSL right now. Sorry I brought it up as it might be off topic.

What I've considered an API in the context of Caster is a stable interface for customizing existing commands. Given that definition of scope as for adding new, editing and removing commands in an existing grammar is out of scope for the API.

I'm not sure I follow. Let me write some pseudo code.

Here is how I thought an API might look where Grammar.LEFT returns "lease". Then you could easily change the API to return "left" if you prefer that.

class MyMappingRule(MapingRule):
     mapping = {
          Grammar.LEFT : Key("left"),

We could also enforce the use of Grammar by changing the interface of MappingRule to require a command as the mapping key.

Then during bootup - before loading rules - you could call Grammar.LEFT = "left" if you wanted a custom grammar.

Would you consider this a Grammar API?

This could also be accomplished with rule filters. The only downside there is it may be easier to break something since the compiler isn't checking that you put a valid command into the rule filter. The upside is your grammars may be more visible in the rule files in some cases. There is also the question of whether extras can be handled with rule filters?

I'm curious which method others prefer - filters, externalized grammar, both or something else?

Ultimately, Through filter rules (soon to be Ttransformers) I plan to solve the 90% of user customizations straight through a UI. This editing command/action, adding new and removing commands from existing grammars. That last 10% people need to create their own grammars and functions in python. Building something without in depth knowledge of Python or programming yet powerful maintaining the flexibility that we currently enjoy.

I prefer to use a file rather than UI since it's easier to edit files by voice IMO. However it could make a lot of sense to have both options since the file could be more difficult for some.

lexxish avatar Aug 12 '19 19:08 lexxish

What I've considered an API in the context of Caster is a stable interface for customizing existing commands. Given that definition of scope as for adding new, editing and removing commands in an existing grammar is out of scope for the API.

I'm not sure I follow. Let me write some pseudo code.

Here is how I thought an API might look where Grammar.LEFT returns "lease". Then you could easily change the API to return "left" if you prefer that.

class MyMappingRule(MapingRule):
     mapping = {
          Grammar.LEFT : Key("left"),

We could also enforce the use of Grammar by changing the interface of MappingRule to require a command as the mapping key.

Then during bootup - before loading rules - you could call Grammar.LEFT = "left" if you wanted a custom grammar.

Would you consider this a Grammar API?

Yes I would consider that a Grammar API. Grammar.LEFT is a stable interface instead of relying on an action, command, or spec. Using Grammar.LEFT does not add New, Edit or Remove commands in an existing grammar. What it does do is provide an interface -> Grammar.LEFT for another mechanism to perform those functions. Perhaps I miss using the word interface if so sorry for the confusion.

This could also be accomplished with rule filters. The only downside there is it may be easier to break something since the compiler isn't checking that you put a valid command into the rule filter. The upside is your grammars may be more visible in the rule files in some cases. There is also the question of whether extras can be handled with rule filters?

I'm curious which method others prefer - filters, externalized grammar, both or something else?

I would prefer a mechanism for generating translations from existing grammars as seen with the dragonfly config implementation. Does mean we have to use it but a similar concept. Followed by filter rules for user customizations. I would also say that this allows both utilization of Python files and other formats.

Handling extras would have to be figured out. I'll need to know a little bit more to evaluate whether not filter rules can handle extras according to your expectations.

Can you give me an example of what you mean by representing extras?

Filter Rules will have to be tweaked to handle Grammar.LEFT instead of just specs. Validating the filter rules when added would be pretty trivial.

I prefer to use a file rather than UI since it's easier to edit files by voice IMO. However it could make a lot of sense to have both options since the file could be more difficult for some.

The UI should not interfere with the ability to edit files by voice or that nature. Truth be told I prefer files ATM. However I recognize in order for Caster to grow were going to need something more.

LexiconCode avatar Aug 13 '19 00:08 LexiconCode

I would prefer a mechanism for generating translations from existing grammars as seen with the dragonfly config implementation. Does mean we have to use it but a similar concept. Followed by filter rules for user customizations. I would also say that this allows both utilization of Python files and other formats.

What do you mean by "user customizations"? Do you mean customization to actions rather than grammars?

From a code clarity perspective it is less clear if the grammar is defined as one string and replaced as another string elsewhere.

Handling extras would have to be figured out. I'll need to know a little bit more to evaluate whether not filter rules can handle extras according to your expectations.

Can you give me an example of what you mean by representing extras?

In this case browser.extras can be replaced just like any other grammar commands. Since extras is actually python code and not just a string, how would we represent it in the configuration file? It could be a method call for all we know? Perhaps we use a hybrid approach where extras are configured with python config files and commands are configured with the dragonfly configuration toolkit. Further, can rule filters replace extras?

Regardless extras are much less likely to be changed, so perhaps an MVP wouldn't even include extra configuration.

lexxish avatar Aug 14 '19 19:08 lexxish

I would prefer a mechanism for generating translations from existing grammars as seen with the dragonfly config implementation. Does mean we have to use it but a similar concept. Followed by filter rules for user customizations. I would also say that this allows both utilization of Python files and other formats.

What do you mean by "user customizations"? Do you mean customization to actions rather than grammars?

From a code clarity perspective it is less clear if the grammar is defined as one string and replaced as another string elsewhere.

The scope Filter Rules is for user customizations to grammars/rules without editing source code. I can see your point but it doesn't matter as these edits are explicit by the user. Therefore there is no conflict with expectations like "defined as one string and replaced as another string elsewhere".

Filter Rules can do a number of things already with existing grammars without the further enhancements made by the rewrite coming up. Filter Rules creating new grammars/rules is out of scope.

First of all some definitions. This picture needs to be updated. For the sake of talking about this so were on the same page I will use the definitions defined in the picture. image

Filter rules

  • Can replace specific specs or actions independently, commands, rule pronunciations.
  • Create new commands for an existing rule
  • global replacement of SPEC (for specs only), EXTRA, DEFAULT, NOT_SPECS (for extras and defaults, but not specs), and ANY (for specs, extras, and defaults).

Filter rules will be changed to Transformers. It'll be vastly easier to add new types of Transformers with the new upcoming implementation.

With a new implementation we could even swap out grammars.

Can you give me an example of what you mean by representing extras?

Thank you for your response and I'll take some time to digest it. My time is a bit limited ATM so I'll get back to you soon. I think this discussion is very productive so thanks you for your thoughts.

LexiconCode avatar Aug 14 '19 19:08 LexiconCode

Since extras is actually python code and not just a string, how would we represent it in the configuration file? It could be a method call for all we know? Perhaps we use a hybrid approach where extras are configured with python config files and commands are configured with the dragonfly configuration toolkit. Further, can rule filters replace extras?

Regardless extras are much less likely to be changed, so perhaps an MVP wouldn't even include extra configuration.

I like the idea of a hybrid approach it seems like it's the least amount of development effort. Let's take a close look at this once the rewrite ready for testing.

Further, can rule filters replace extras?

As I understand it the new Transformers have complete access to Extra rules therefore they are replaceable.

LexiconCode avatar Aug 15 '19 04:08 LexiconCode

From a code clarity perspective it is less clear if the grammar is defined as one string and replaced as another string elsewhere.

Sorry I posted this by accident. It was in my original reply, but after some thought I intended to remove it. It's not that it's a false statement, but just am unsure if it's a concern.

The scope Filter Rules is for user customizations to grammars/rules without editing source code. I can see your point but it doesn't matter as these edits are explicit by the user. Therefore there is no conflict with expectations like "defined as one string and replaced as another string elsewhere".

I'm not sure I agree with the bolded statement. For example say we decide to change the default string for a command but we are not using python variables to represent that command. In this case it would break filter rules that replaced that command for users who pull the change from master. I'm not sure how big of a problem this is and it's more important that we get replaceable grammars implemented than worry about it.

lexxish avatar Aug 21 '19 16:08 lexxish

I would love to see a Grammar API and a mechanism to update in engine user customization rather than changing source code. However...

The transformers have been dropped from the rewrite although minimally implemented for simplified filter rules. Transformers work however they need higher level of abstraction.

Therefore without a mechanism to customize specs in engine the kind of up a creek without a paddle. Currently the main mechanism is to override source code grammars with ones placed in the user directory. API style grammars have been removed to keep the user experience consistent with the rest of the grammars and standard dragonfly/caster documentation for now.

I kept the grammars you made lexxish in the share folder of caster. There's definitely a need for a grammar API although it needs to not interfere with the standard grammar.

LexiconCode avatar Dec 01 '19 01:12 LexiconCode

I've reopened the issue. A grammar API opens the door for multilingual support as long as the speech recognition engine supports the language.

However there are some challenges that need to be addressed. These challenges affect developers more than an end-user that does not modify code. lexxish has made a great example of the grammar API. The Commands with corresponding filenames below is representative of that example within the context of the Google Chrome application.

Current Design

browser_shared.py ZOOM_OUT_N_TIMES = "zoom out [<n>]"

browser_shared_commands.py browser_shared.ZOOM_OUT_N_TIMES: R(Key("c-minus/20")) * Repeat(extra="n")

chrome.py "focus notification": R(Key("a-n")),

The above designdesign standardizes (spec defined in one place) as well as provides stable construct the API. However solving both of those issues at once leads to do some design issues.

  1. Having a shared API from multiple contexts with joined with individual contexts that are specific to that domain make it difficult to understand and edit. Browser in this case we need to view three files as a developer to understand the overall grammar created.

  2. With the paradigm of using a variable as the command name it makes it harder to understand the spec definition from within the file.

  3. This distances the project from the dragonfly API paradigm. For those getting into the codebase keeping the design within a similar construct to dragonfly allows for easier adoption. This is less of a barrier for experienced programmers but it is a barrier nonetheless.

  4. This paradigm is forced on the user and developer to use the Grammar API. This also encapsulates the issues raised in the first three points.

Therefore a proposal which leverages the groundwork presented by lexxish. It separates the concern of standardizing specs and creating an grammar API.

~~browser_shared.py~~ ~~ZOOM_OUT_N_TIMES = "zoom out [<n>]"~~ no longer needed unless the user defines it.

~~browser_shared_commands.py~~ would be moved to chrome.py ~~browser_shared.ZOOM_OUT_N_TIMES: R(Key("c-minus/20")) * Repeat(extra="n")~~

chrome.py

"focus notification":   R(Key("a-n")),

"zoom out [<n>]":   R(Key("c-minus/20"), api=browser_shared.ZOOM_OUT_N_TIMES) * Repeat(extra="n")

Proposal This should address the four design issues. Conceptually if the APIs spec defined (in a config file) it could override the default spec.

  1. In the example above the Commands are centralized is in chrome.py. This makes it much easier to edit understand the commands involved and not having to keep track of different contexts and files across the same domain.

  2. Grammars are easy to read because the default spec is included without abstraction. This is because the spec is not a variable and contains a default. For the developer its easy to understand what the command does and how to trigger it from within the file.

  3. More in line with the dragonfly API and documentation. At the end of the day Caster builds on top of the dragonfly API. What seen in the Dragonfly documentation is still be transferable knowledge to the Caster codebase with grammar design.

  4. In the proposed commands above it doesn't force the API on the user as they can redefine the spec without utilizing the API.

Other benefits

You can move the grammar API definitions from a Python file to a config toml file. This makes it machine-readable. Further extraction without the end-user having any knowledge of the API through a GUI. User is shown the defaults, makes an edit s which is then saved to grammar API config.

For those that edit the the configuration file there are benefits as well. Because the API is not defined by default populating existing specs it becomes user driven. Meaning they only add the grammar API references that they want to change.

Understanding the impact for developers and maintainers My proposal would need some changes made in order to override specs from the api to the Caster codebase whereas lexxish design currently works.

I acknowledge the burden to maintain sane defaults across grammars is placed firmly in the developers and maintainer responsibility. However I believe the benefits gained to the separation of concerns outweigh that. Ultimately we do have control over the defaults and users have a single source of truth for defining specs.

Other considerations

I think this might be even more useful if it gets at the optionally the Command. In part because of someone changes the spec without adjusting certain parameters the command becomes invalid. For example

"zoom out [<n>]": R(Key("c-minus/20"), api=browser_shared.ZOOM_OUT_N_TIMES) * Repeat(extra="n")

In the config file with the renamed spec browser_shared.ZOOM_OUT_N_TIMES: zoom away # it's missing [<n>] when it still defined in the action.

LexiconCode avatar Feb 21 '20 20:02 LexiconCode