Skip to content

Commit

Permalink
SLEIGHT-68, SLEIGHT-84, SLEIGHT-87: negative indicator, prep for rele…
Browse files Browse the repository at this point in the history
…ase, negative range validation
  • Loading branch information
synkarius committed Nov 20, 2022
1 parent ac2f7d5 commit 5e80f0e
Show file tree
Hide file tree
Showing 76 changed files with 1,587 additions and 601 deletions.
116 changes: 97 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,116 @@
# Sleight

Sleight is an app which creates voice dictation components and rules.
Sleight is a tool that lets you make and edit speech commands for other (free) software without knowing Python/etc.

## Usage
### What Other Software?

TODO: this section
[Dragonfly](https://dragonfly2.readthedocs.io) to start, but other frameworks are on the roadmap. (See the roadmap section below.)

## Export Formats
## What Problems Does this Solve?

TODO: this section
Creating voice commands in Python/ Vocola/ Talon is fairly technical. Sleight aims to lower the bar for all and increase velocity for power users.

## [npm](https://www.npmjs.com/)
Along with these productivity goals, Sleight is looking to improve [shareability/portability](#todo-export-formats) of voice commands and [grammar resilience](#todo-lockability) against framework changes/ updates.

## Where It's At Now

Sleight is very much alpha software at this point. Though it already supports a subset of the Dragonfly specification and can export full Dragonfly rules, there is much which needs to be done.

### Help Wanted

There are various ways you can contribute to Sleight:

- giving feedback: see the [feedback](#feedback) section below.
- expanding/narrowing/ordering the roadmap
- writing documentation
- code contributions
- demos (YouTube/etc.)

### Feedback

Sleight needs:

- bug reports
- UI/UX improvement suggestions
- validation suggestions
- are there ways to create or import invalid data which Sleight allows?
- feature requests
- please have _lots_ of patience here
- alternatively, open a PR ;)
- TypeScript/React best practices suggestions

## Roadmap Thus Far

This is a very rough roadmap at this point, and not necessary in order of importance.

- documentation
- demos
- keyboard shortcuts (customizable)
- exports to other frameworks
- Caster
- Vocola
- Talon
- model changes
- follow immutable model principle
- model version adapters
- aim to provide common "primitives" rather than implement any particular framework's specification
- add more
- simplify existing `Action`s
- get away from resemblance to Dragonfly's model
- web API
- read-only at first
- cleanup
- code
- UI/UX

## Design Philosophy

Sleight has thus far been designed with "strong opinions loosely held". Among them are the following.

### Libraries Usage Should Be Minimized

Sometimes bringing in a library is the best solution, but especially in the JS world, churn is high and packages break often. Therefore, to minimize maintenance, adequate consideration has to be given to the question of when to build versus when to "buy".

The approach that Sleight has taken thus far has been to mostly keep `packages.json` small, building simple utilities for simple jobs and including only a handful of libraries.

### Optimize for Popularity

When choosing a library or a framework, there are multiple ways to decide what's best. You could opt for performance, ergonomics, stability, or any number of other attributes.

Sleight is a project in its infancy, so it has thus far optimized for popularity: React over Vue or Angular; Electron over Tauri; etc. The main idea here is that using popular choices will have the best chance of attracting code contributors.

Popular choices will also likely be decent choices, even if they're not the best choices.

### TypeScript

Versus JavaScript? No competition.

### React Testing Library

RTL's philosophy overlaps (and inspired) Sleight's in two aspects.

1. Tests should access the DOM in the same way the user does. This seems like an obvious accessibility benefit, and forces the developer to care about accessibility if they care about testing.
2. Functional tests provide greater flexibility than unit tests. Fixing unit tests which broke because underlying implementations changed isn't a great use of anyone's time. Unit tests are necessary and useful, but should mainly cover implementation details which overly complicate functional tests.

## Running the Project

In the project directory, you can run:

### `npm start`
### `npm run react-start`

Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
This will run the app in the development mode.

The page will reload if you make edits.\
You can then open [http://localhost:3000](http://localhost:3000) to view it in the browser.

The page will reload if you make edits.
You will also see any lint errors in the console.

### `npm test`
### `npm run react-test`

Launches the test runner in the interactive watch mode.\
This launches the test runner in the interactive watch mode.
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.

### `npm run build`

Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
Builds the app for production to the `build` folder.
It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes.
6 changes: 3 additions & 3 deletions src/core/command-list/command-list-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import { DomainMapper } from '../mappers/mapper';
import { SpecDomainMapper } from '../mappers/spec-domain-mapper';
import { VariableDomainMapper } from '../mappers/variable-domain-mapper';

export const GLOBAL_CONTEXT = 'global';
const GLOBAL_CONTEXT = 'global';

export type CommandListItem = {
type CommandListItem = {
command: Command;
spec: Spec;
context?: Context;
actions: Action[];
variables: Variable[];
};

export type CommandListFilterCriteria = {
type CommandListFilterCriteria = {
contextSearch: string;
};

Expand Down
6 changes: 0 additions & 6 deletions src/core/common/common-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ import { UNSELECTED_ENUM, UNSELECTED_ID } from './consts';

export const alwaysTrue = <T>(_t: T): boolean => true;

export const alwaysFalse = <T>(_t: T): boolean => false;

export const notEmpty = (value: string) => value.trim().length > 0;

export const isEmpty = (value: string) => value.trim().length === 0;

export const identity = <T>(t: T) => t;

export const notFalsy = (t: unknown) => !!t;

export const isDefined = <T>(t: T | undefined): t is T => t !== undefined;

export const singletonArray = <T>(t: T): T[] => [t];
Expand All @@ -32,8 +28,6 @@ export const isIdSelected = (id?: string): boolean =>
export const isEnumSelected = (id?: string): boolean =>
!!id && id !== UNSELECTED_ENUM;

export const isTruthy = (t: unknown) => !!t;

export const replaceNonAlphaNumeric = (
value: string,
replacement: string
Expand Down
1 change: 0 additions & 1 deletion src/core/common/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const UNSELECTED_ENUM = 'unselected-sleight-enum';
export const USE_DEFAULT = 'Use Default';

// paths
export const EMPTY_PATH = '/';
export const COMMAND_LIST_PATH = '/commands';
export const ELEMENT_EDITOR_PATH = '/elements';
export const PREFERENCES_PATH = '/preferences';
Expand Down
4 changes: 2 additions & 2 deletions src/core/common/editing-context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import { DeleteModalConfig } from '../../ui/other-components/DeleteModalComponent';

export type SimpleEditingData<P> = {
type SimpleEditingData<P> = {
localDispatch: React.Dispatch<P>;
};
export type EditingData<P> = {
type EditingData<P> = {
// P is a payload
localDispatch: React.Dispatch<P>;
deleteModalConfig: DeleteModalConfig;
Expand Down
34 changes: 34 additions & 0 deletions src/core/common/field-groups-supplier.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { container } from '../../di/config/brandi-config';
import { Tokens } from '../../di/config/brandi-tokens';
import { Field } from '../../validation/validation-field';
import { isNone } from './maybe';

describe('field groups supplier tests', () => {
const fieldGroupsSupplier = container.get(Tokens.FieldGroupsSupplier);

it('should include all field groups found by naming convention', () => {
const allActionValueFields = Object.keys(Field)
.filter((i) => !isNaN(Number(i)))
.map((field) => +field)
.filter((field) => {
const fieldName = Field[field];
const prefix = fieldName.startsWith('AC_');
const suffix =
fieldName.endsWith('_RADIO') ||
fieldName.endsWith('_VALUE') ||
fieldName.endsWith('_VAR');
return prefix && suffix;
});

const missing = allActionValueFields
.map((field) => ({
name: Field[field],
metadata: fieldGroupsSupplier.getGroupByField(field),
}))
.filter((result) => isNone(result.metadata))
.map((result) => result.name)
.join(', ');

expect(missing).toBeFalsy();
});
});
151 changes: 151 additions & 0 deletions src/core/common/field-groups-supplier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { VariableType } from '../../data/model/variable/variable-types';
import { ExhaustivenessFailureError } from '../../error/exhaustiveness-failure-error';
import {
ActionValueFieldGroup,
groupFieldsOf,
} from '../../ui/model/action/action-value-type-name-group';
import {
bringAppPathGroup,
bringAppStarDirGroup,
bringAppTitleGroup,
} from '../../ui/model/action/bring-app/bring-app-action-value-field-group';
import { mimicWordsGroup } from '../../ui/model/action/mimic/mimic-action-value-field-group';
import {
mMoveXGroup,
mMoveYGroup,
mMouseButtonGroup,
mPauseGroup,
mRepeatGroup,
mDirectionGroup,
} from '../../ui/model/action/mouse/mouse-action-value-field-groups';
import { pSecondsGroup } from '../../ui/model/action/pause/pause-action-value-field-group';
import {
skDirectionGroup,
skInnerPauseGroup,
skKeyToSendGroup,
skOuterPauseGroup,
skRepeatGroup,
} from '../../ui/model/action/send-key/send-key-action-value-field-groups';
import { stTextGroup } from '../../ui/model/action/send-text/send-text-action-value-field-group';
import {
wfwExecutableGroup,
wfwTitleGroup,
wfwWaitSecondsGroup,
} from '../../ui/model/action/wait-for-window/wait-for-window-action-value-field-group';
import { Field } from '../../validation/validation-field';
import { maybe, Maybe } from './maybe';

export const enum FieldMetaDataType {
TEXT,
NUMBER,
ENUM,
ANY,
}

interface AbstractFieldMetaData {
type: FieldMetaDataType;
fields: Field[];
}

interface NumberFieldMetaData extends AbstractFieldMetaData {
type: FieldMetaDataType.NUMBER;
min?: number;
}

interface NonNumberFieldMetaData extends AbstractFieldMetaData {
type: FieldMetaDataType.TEXT | FieldMetaDataType.ENUM | FieldMetaDataType.ANY;
}

/** Can't just use ActionValueFieldGroup b/c type of CFA parameters is not static. */
type FieldMetaData = NumberFieldMetaData | NonNumberFieldMetaData;

/** Since field metadata is needed by validators and elsewhere, need a way to query it.
* By convention, the exhaustiveness-checking unit test for this will depend on
* a naming convention for now.
*/
export type FieldGroupsSupplier = {
getGroupByField: (field: Field) => Maybe<FieldMetaData>;
getAllGroups(): FieldMetaData[];
};

export class DefaultFieldGroupsSupplier implements FieldGroupsSupplier {
private map: Map<Field, FieldMetaData>;
constructor() {
this.map = new Map();
}

getGroupByField(field: Field): Maybe<FieldMetaData> {
if (!this.map.size) {
this.constructMapOnce();
}
return maybe(this.map.get(field));
}

getAllGroups(): FieldMetaData[] {
return [
this.convertFieldGroup(bringAppPathGroup),
this.convertFieldGroup(bringAppTitleGroup),
this.convertFieldGroup(bringAppStarDirGroup),
this.createCFAMetadata(),
this.convertFieldGroup(mimicWordsGroup),
this.convertFieldGroup(mMoveXGroup),
this.convertFieldGroup(mMoveYGroup),
this.convertFieldGroup(mMouseButtonGroup),
this.convertFieldGroup(mPauseGroup),
this.convertFieldGroup(mRepeatGroup),
this.convertFieldGroup(mDirectionGroup),
this.convertFieldGroup(pSecondsGroup),
this.convertFieldGroup(skKeyToSendGroup),
this.convertFieldGroup(skOuterPauseGroup),
this.convertFieldGroup(skInnerPauseGroup),
this.convertFieldGroup(skRepeatGroup),
this.convertFieldGroup(skDirectionGroup),
this.convertFieldGroup(stTextGroup),
this.convertFieldGroup(wfwTitleGroup),
this.convertFieldGroup(wfwExecutableGroup),
this.convertFieldGroup(wfwWaitSecondsGroup),
];
}

private createCFAMetadata() {
return {
type: FieldMetaDataType.ANY,
fields: [
Field.AC_CALL_FUNC_PARAMETER_RADIO,
Field.AC_CALL_FUNC_PARAMETER_VALUE,
Field.AC_CALL_FUNC_PARAMETER_VAR,
],
};
}

private constructMapOnce(): void {
const all = this.getAllGroups();
for (const group of all) {
group.fields.forEach((key) => this.map.set(key, group));
}
}

private convertFieldGroup(group: ActionValueFieldGroup): FieldMetaData {
const groupType = group.type;
switch (groupType) {
case VariableType.Enum.TEXT:
return {
type: FieldMetaDataType.TEXT,
fields: groupFieldsOf(group),
};
case VariableType.Enum.NUMBER:
return {
type: FieldMetaDataType.NUMBER,
fields: groupFieldsOf(group),
min: group.min,
};
case VariableType.Enum.ENUM:
return {
type: FieldMetaDataType.ENUM,
fields: groupFieldsOf(group),
};
default:
throw new ExhaustivenessFailureError(groupType);
}
}
}
Loading

0 comments on commit 5e80f0e

Please sign in to comment.