diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b40e48..a5498b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,10 @@ jobs: url: https://github.com/${{github.repository}} manifest: https://github.com/${{github.repository}}/releases/latest/download/module.json download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/module.zip + license: https://github.com/${{github.repository}}/blob/main/LICENSE + readme: https://github.com/${{github.repository}}/blob/main/README + bugs: https://github.com/${{github.repository}}/issues + changelog: https://github.com/${{github.repository}}/blob/main/CHANGELOG - name: Build run: npm run build diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..7fc815b --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,5 @@ +# Commander - FoundryVTT at your fingertips + +## v0.1.0 (2022-02-19) +- Initial Release +- Complete functionality, but pending some improvements before calling it v1.0.0 \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..d737ae4 --- /dev/null +++ b/README @@ -0,0 +1,29 @@ +![Module Version](https://img.shields.io/github/v/release/ccjmk/commander?color=blue) +![FoundryVersion](https://img.shields.io/endpoint?url=https://foundryshields.com/version?url=https%3A%2F%2Fgithub.com%2Fccjmk%2Fcommander%2Freleases%2Fdownload%2Fv0.1.0-pre%2Fmodule.json) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/N4N88281M) + +# Commander - FoundryVTT at your fingertips + +Commander is a tool inspired vaguely inspired by the likes of `Launchy` or `Wox`, and a similar feeling like `SearchAnywhere`, that lets you run commands from a shortcut-invoked prompt. + +This module provides the command-line input and the API for registering new commands, and will provide some example and general-use commands. The command-line is opened by default by clicking Ctrl+Backtick (the ` right next to the 1 in english keyboards). + +It is not the intention of this module to provide commands specific to particular systems, but mostly the tooling and more generic commands applicable to anyone regardless of game system. If you have such a command that you want to share, [don't be afraid to open a pull request](https://github.com/ccjmk/commander/pulls)! + +## Executing Commands + +You can open the Commander widget by pressing the corresponding keybinding, configurable in-game, with the default been Ctrl+Backtick. *(the ` right next to the 1 in english keyboards)* + +Then you can start typing! Command suggestions will pop up as you type, you can auto-accept the selected suggestion with `Tab`/`Enter`, or select other suggestions using `Up` or `Down`. An `Enter` when no suggestion is selected sends the Command for execution. + +> For information on how to add new commands, please refer to [The Wiki](https://github.com/ccjmk/commander/wiki) + +## Licensing + +This project is being developed under the terms of the +[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT] for Foundry Virtual Tabletop. + +MIT - for more info please read the LICENSE file. + +[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT]: https://foundryvtt.com/article/license/ diff --git a/README.md b/README.md deleted file mode 100644 index 85cff49..0000000 --- a/README.md +++ /dev/null @@ -1,207 +0,0 @@ -# Commander - -Commander is a tool inspired vaguely inspired by the likes of `Launchy` or `Wox`, and a similar feeling like `SearchAnywhere`, that lets you run commands from a shortcut-invoked prompt. - -This module provides the command-line and the API for registering new commands, and will provide some example and general-use commands. The command-line is opened by clicking Ctrl+Backtick (the ` right next to the 1 in english keyboards). - -## API & Helpers - -The module provides an API available at the Module instance, alongside some helper functions. You can access them by doing: -```js -const {api, helpers} = game.modules.get('commander'); -``` - -### api.commands -Map of all registered command names and the respective commands. - -### api.execute (commandString) -Receives a string and tries to execute that command; first it parses the command name and checked if the command is allowed. If so, gets the command schema and tries to match the input to it, extracting the arguments. Then calls the command's handler function with said arguments. - -### api.register (command, replace?) -Receives a Command to register; the command is only registered if it passes integrity checks and it does not already exist (commands are identified by `name`). You can replace existing commands by sending a boolean flag as second argument. Commands have to be of the following shape: - -```ts -interface Command { - name: string; // must be lowercase - namespace: string; // unused for now but mandatory - description?: string; - schema: string; // must start with name, followed by argument names prefixed with '$' - args: Argument[]; - allow?: () => boolean; - handler: (...params: any) => any; -} -interface Argument { - name: string; // 'string'|'number'|'boolean'|'raw' names are reserved - type: ARGUMENT_TYPES; - suggestions?: (...params: any) => Suggestion[]; -} -enum ARGUMENT_TYPES { - 'string', // accepts spaces ONLY IF you write the next between quotes. - 'number', // accepts numbers with decimals. It's just parseFloat(arg), so be tame with the decimals. Consider yourself warned! - 'boolean', // accepts 'true', 'on', 'false', 'off' - 'raw', // returns the whole remaining input string. If used with other arguments this MUST BE LAST. -} -interface Suggestion { - content: string; // what is shown on the suggestion - icon?: string; // icon is a font-awesome class name, takes precedence over img - img?: string; - bold?: boolean; // not implemented yet - italics?: boolean; // not implemented yet -} -``` - -### helpers.hasRole (role) -Receives a string and tries to match it to a role from `CONST.USER_ROLES`. If it's a valid role, returns the allow callback function that will check for this role whenever a command using that function is invoked. Example: - -```js -// while defining a new command.. -api.register({ - name: "myCommand", - allow: helpers.hasRole('TRUSTED'), // this command can be invoked by a user with role TRUSTED or more - ... -}) -``` - -### helpers.hasPermissions (...permissions) -Receives a list of permission strings, and tries to match them to a permission from `CONST.USER_PERMISSIONS`. If all listed permissions are valid, returns the allow callback function that will check for ALL of these permissions whenever a command using that function is invoked. Example: - -```js -// while defining a new command.. -api.register({ - name: "myCommand", - allow: helpers.hasPermissions('ACTOR_CREATE', 'ITEM_CREATE'), // this command can be invoked by a user with both the ACTOR_CREATE and ITEM_CREATE permissions - ... -}) -``` - -## Creating and sharing commands -* PRs for adding new commands will be considered so long as they are system-agnostic. -* System-specific commands should live in their own module. - -## Hooks -The tool provides the following hooks: - -### commanderReady -> Called when Commander has finished initialization. At this point, the keybinding is functional and the API is available. Receives the Commander instance ready to be used. - -### commanderExecute - > Called when Commander has been asked to run a command, either via the provided widget or directly via the API. Receives the execution string requested to be executed. - -## Examples - -Lets register two commands: one to show a notification with the sum of two numbers, and another to log on the console the user input (except for the command), but only available for ASSISTANT and GAMEMASTER roles. - -### Sum -```js -let { api } = game.modules.get('commander') -api.register({ - name: "sum", - schema: "sum $a $b", // this is what you write to use this command, replacing $a and $b for numbers - args: [ - { name: "a", type: "number" }, - { name: "b", type: "number" }, - ], - handler: ({a, b}) => { // param names need to match the names or the args[] - ui.notifications?.info("the sum is "+(a+b)) - } -}) -// you can now open the command-line and type "sum 3 5" or.. -api.execute("sum 3 5"); -``` - -### LogInput -```js -let { api, helpers } = game.modules.get('commander') -api.register({ - name: "logInput", - schema: "logInput $text $args", // remember RAW arguments last. - args: [ - { name: "text", type: "string" }, - { name: "args", type: "raw" }, - ], - allow: helpers.hasRole('ASSISTANT'), // we use a helper to define the allow function - handler: ({text, args}) => { - console.log(`The text argument is: ${text}`) - console.log(args) - } -}) -// you can now open the command-line and type, or.. -api.execute(`logInput "this is a string with spaces" 123 .456 " asdas" !#$%^&*()`); -``` - -## Development - -### Prerequisites - -In order to build this module, recent versions of `node` and `npm` are -required. Most likely, using `yarn` also works, but only `npm` is officially -supported. We recommend using the latest lts version of `node`. If you use `nvm` -to manage your `node` versions, you can simply run - -``` -nvm install -``` - -in the project's root directory. - -You also need to install the project's dependencies. To do so, run - -``` -npm install -``` - -### Building - -You can build the project by running - -``` -npm run build -``` - -Alternatively, you can run - -``` -npm run build:watch -``` - -to watch for changes and automatically build as necessary. - -### Linking the built project to Foundry VTT - -In order to provide a fluent development experience, it is recommended to link -the built module to your local Foundry VTT installation's data folder. In -order to do so, first add a file called `foundryconfig.json` to the project root -with the following content: - -``` -{ - "dataPath": "/absolute/path/to/your/FoundryVTT" -} -``` - -(if you are using Windows, make sure to use `\` as a path separator instead of -`/`) - -Then run - -``` -npm run link-project -``` - -On Windows, creating symlinks requires administrator privileges, so unfortunately -you need to run the above command in an administrator terminal for it to work. - -### Creating a release - -The workflow works basically the same as the workflow of the [League Basic JS Module Template], please follow the -instructions given there. - -## Licensing - -This project is being developed under the terms of the -[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT] for Foundry Virtual Tabletop. - -MIT - for more info please read the LICENSE file. - -[League Basic JS Module Template]: https://github.com/League-of-Foundry-Developers/FoundryVTT-Module-Template -[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT]: https://foundryvtt.com/article/license/ diff --git a/src/module.json b/src/module.json index b209303..c45d17e 100644 --- a/src/module.json +++ b/src/module.json @@ -1,9 +1,18 @@ { "name": "commander", "title": "Commander", - "description": "Command launcher for running and registering commands executable solely by keyboard", + "description": "Command launcher for running and registering commands executable by keyboard", "version": "This is auto replaced", - "author": "ccjmk", + "authors": [ + { + "name": "ccjmk", + "url": "https://github.com/ccjmk" + }, + { + "name": "Miguel Galante", + "url": "https://www.linkedin.com/in/miguelgalante" + } + ], "minimumCoreVersion": "9", "compatibleCoreVersion": "9", "scripts": [], @@ -22,10 +31,10 @@ "url": "This is auto replaced", "manifest": "This is auto replaced", "download": "This is auto replaced", - "license": "", - "readme": "", - "bugs": "", - "changelog": "", + "license": "This is auto replaced", + "readme": "This is auto replaced", + "bugs": "This is auto replaced", + "changelog": "This is auto replaced", "system": [], "library": false } diff --git a/src/module/commandHandler.ts b/src/module/commandHandler.ts index 5942f14..f6703ac 100644 --- a/src/module/commandHandler.ts +++ b/src/module/commandHandler.ts @@ -200,8 +200,9 @@ function startsWithOverride(input: string) { function isValidCommand(command: any): command is Command { isValidStringField(command.name, 'name'); - isValidCommandName(command.name); + isValidLowercaseString(command.name); isValidStringField(command.namespace, 'namespace'); + isValidLowercaseString(command.namespace); isValidStringField(command.description, 'description', true); isValidStringField(command.schema, 'schema'); isValidSchema(command); @@ -286,7 +287,7 @@ function removeOrphanQuotes(input: string): string { return input; } -function isValidCommandName(name: any) { +function isValidLowercaseString(name: any) { const lowercaseName = name.toLocaleLowerCase().trim(); if (lowercaseName !== name) { throw new Error(localize('Handler.Reg.CommandNameNotLowercase')); diff --git a/src/module/commands/commandIndex.ts b/src/module/commands/commandIndex.ts index 3e8a7ac..b2d9382 100644 --- a/src/module/commands/commandIndex.ts +++ b/src/module/commands/commandIndex.ts @@ -5,7 +5,6 @@ import openSheetByNameCommand from './openSheetByName'; import openSheetByPlayerCommand from './openSheetByPlayer'; import showAllowedCommand from './showAllowedCommands'; import goTabCommand from './goTab'; -import suggestionsCommand from './examples/suggestionsExample'; import infoCommand from './info'; import tokenActiveEffectCommand from './tokenActiveEffect'; @@ -18,8 +17,6 @@ const registerCommands = (register: (command: Command, replace: boolean, silentE register(goTabCommand, false, true); register(infoCommand, false, true); register(tokenActiveEffectCommand, false, true); - - register(suggestionsCommand, false, true); // TODO delete after testing }; export default registerCommands; diff --git a/src/module/commands/examples/allArgsExample.ts b/src/module/commands/examples/allArgsExample.ts deleted file mode 100644 index d433731..0000000 --- a/src/module/commands/examples/allArgsExample.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Command from '../../command'; -import { ARGUMENT_TYPES, MODULE_NAMESPACE } from '../../utils/moduleUtils'; - -const allArgsCommand: Command = { - name: 'test', - namespace: MODULE_NAMESPACE, - schema: 'test $str $num $bool $raw', - args: [ - { - name: 'str', - type: ARGUMENT_TYPES.STRING, - }, - { - name: 'num', - type: ARGUMENT_TYPES.NUMBER, - }, - { - name: 'bool', - type: ARGUMENT_TYPES.BOOLEAN, - }, - { - name: 'raw', - type: ARGUMENT_TYPES.RAW, - }, - ], - handler: ({ str, num, bool, raw }) => { - ui.notifications?.info(`string: [${str}] - number: [${num}] - boolean: [${bool}]`); - ui.notifications?.info(`raw: [${raw}]`); - }, -}; -export default allArgsCommand; diff --git a/src/module/commands/examples/booleanArgExample.ts b/src/module/commands/examples/booleanArgExample.ts deleted file mode 100644 index 192c88f..0000000 --- a/src/module/commands/examples/booleanArgExample.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Command from '../../command'; -import { ARGUMENT_TYPES, MODULE_NAMESPACE } from '../../utils/moduleUtils'; - -const booleanArgCommand: Command = { - name: 'bool', - namespace: MODULE_NAMESPACE, - schema: 'bool $bool', - args: [ - { - name: 'bool', - type: ARGUMENT_TYPES.BOOLEAN, - }, - ], - handler: ({ bool }) => { - ui.notifications?.info(`[${bool}]`); - }, -}; -export default booleanArgCommand; diff --git a/src/module/commands/examples/numberArgExample.ts b/src/module/commands/examples/numberArgExample.ts deleted file mode 100644 index 4d1c732..0000000 --- a/src/module/commands/examples/numberArgExample.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Command from '../../command'; -import { ARGUMENT_TYPES, MODULE_NAMESPACE } from '../../utils/moduleUtils'; - -const numberArgCommand: Command = { - name: 'num', - namespace: MODULE_NAMESPACE, - schema: 'num $number', - args: [ - { - name: 'number', - type: ARGUMENT_TYPES.NUMBER, - }, - ], - handler: ({ number }) => { - ui.notifications?.info(`[${number}]`); - }, -}; -export default numberArgCommand; diff --git a/src/module/commands/examples/permissionsCreateActorExample.ts b/src/module/commands/examples/permissionsCreateActorExample.ts deleted file mode 100644 index ac52606..0000000 --- a/src/module/commands/examples/permissionsCreateActorExample.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Command from '../../command'; -import { MODULE_NAMESPACE } from '../../utils/moduleUtils'; - -const requireCreateActorsPermissionCommand: Command = { - name: 'onlyPermissionsCreateActor', - namespace: MODULE_NAMESPACE, - schema: 'onlyPermissionsCreateActor', - args: [], - allow: () => { - return true; - }, - handler: () => { - ui.notifications?.info(`You are a Trusted player, therefore you can run this command.`); - }, -}; -export default requireCreateActorsPermissionCommand; diff --git a/src/module/commands/examples/rawArgExample.ts b/src/module/commands/examples/rawArgExample.ts deleted file mode 100644 index 01bfdbc..0000000 --- a/src/module/commands/examples/rawArgExample.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Command from '../../command'; -import { ARGUMENT_TYPES, MODULE_NAMESPACE } from '../../utils/moduleUtils'; - -const rawArgCommand: Command = { - name: 'raw', - namespace: MODULE_NAMESPACE, - schema: 'raw $value', - args: [ - { - name: 'value', - type: ARGUMENT_TYPES.RAW, - }, - ], - handler: ({ value }) => { - ui.notifications?.info(`[${value}]`); - }, -}; -export default rawArgCommand; diff --git a/src/module/commands/examples/roleTrustedExample.ts b/src/module/commands/examples/roleTrustedExample.ts deleted file mode 100644 index fe84a44..0000000 --- a/src/module/commands/examples/roleTrustedExample.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Command from '../../command'; -import { hasRole } from '../../commandHandler'; -import { MODULE_NAMESPACE } from '../../utils/moduleUtils'; - -const onlyAllowTrustedCommand: Command = { - name: 'onlyTrusted', - namespace: MODULE_NAMESPACE, - schema: 'onlyTrusted', - args: [], - allow: () => hasRole('TRUSTED'), - handler: () => { - ui.notifications?.info(`You are a Trusted player, therefore you can run this command.`); - }, -}; -export default onlyAllowTrustedCommand; diff --git a/src/module/commands/examples/stringArgExample.ts b/src/module/commands/examples/stringArgExample.ts deleted file mode 100644 index 2d5edfa..0000000 --- a/src/module/commands/examples/stringArgExample.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Command from '../../command'; -import { ARGUMENT_TYPES, MODULE_NAMESPACE } from '../../utils/moduleUtils'; - -const stringArgCommand: Command = { - name: 'str', - namespace: MODULE_NAMESPACE, - schema: 'str $text', - args: [ - { - name: 'text', - type: ARGUMENT_TYPES.STRING, - }, - ], - handler: ({ text }) => { - ui.notifications?.info(`[${text}]`); - }, -}; -export default stringArgCommand; diff --git a/src/module/commands/examples/suggestionsExample.ts b/src/module/commands/examples/suggestionsExample.ts deleted file mode 100644 index ba6d991..0000000 --- a/src/module/commands/examples/suggestionsExample.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Command from '../../command'; -import { ARGUMENT_TYPES, getGame, MODULE_NAMESPACE } from '../../utils/moduleUtils'; - -const suggestionsCommand: Command = { - name: 'sug', - namespace: MODULE_NAMESPACE, - schema: 'sug $player $level $stuff $bool', - args: [ - { - name: 'player', - type: ARGUMENT_TYPES.STRING, - suggestions: () => { - const users = getGame().users?.values(); - if (!users) return []; - return [...users].map((u) => ({ content: u.name! })); - }, - }, - { - name: 'level', - type: ARGUMENT_TYPES.NUMBER, - suggestions: () => { - return Array.fromRange(20).map((n) => ({ content: n + 1 + '' })); - }, - }, - { - name: 'stuff', - type: ARGUMENT_TYPES.STRING, - }, - { - name: 'bool', - type: ARGUMENT_TYPES.BOOLEAN, - }, - ], - handler: ({ player, level, bool, stuff }) => { - ui.notifications?.info(`player: [${player}] - level: [${level}] - stuff: [${stuff}] -- bool: [${bool}]`); - }, -}; -export default suggestionsCommand; diff --git a/src/module/module.ts b/src/module/module.ts index 5d8be5f..a72d682 100644 --- a/src/module/module.ts +++ b/src/module/module.ts @@ -27,7 +27,9 @@ Hooks.once('setup', async () => { const module: Game.ModuleData & ModuleApi = getGame().modules.get(MODULE_NAMESPACE)!; module.api = { commands, register, execute }; module.helpers = { hasRole, hasPermissions }; +}); +Hooks.once('ready', () => { console.log(`${MODULE_NAME} | Commander ready..`); Hooks.callAll('commanderReady', module); });