Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1538 (2023-01-15 18:26:42)

feat: Add status categories/types to eventually control behaviour
<!--- Provide a general summary of your changes in the Title above -->

# Description

- Add enum `StatusType`:

```js
export enum StatusType {
    TODO = 'TODO',
    DONE = 'DONE',
    IN_PROGRESS = 'IN_PROGRESS',
    CANCELLED = 'CANCELLED',
    NON_TASK = 'NON_TASK',
    EMPTY = 'EMPTY',
}
```

- Each status now has one StatusType value
- Edit Status modal now allows editing of type


## Motivation and Context

In my own vault I have struggled to use the new statuses, because I have no control over which statuses are treated as Done. Some of my custom statuses represent tasks that are incomplete, and they are currently excluded by `not done` queries.

This is a partial attempt at giving users control over this.

## How has this been tested?

- New tests
- Running in Obsidian

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/docs/getting-started/statuses.md b/docs/getting-started/statuses.md
index a4abedab..d9e86089 100644
--- a/docs/getting-started/statuses.md
+++ b/docs/getting-started/statuses.md
@@ -44,12 +44,12 @@ This table shows the statuses provided by default:
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md -->
 
-| Status Character    | Status Name | Next Status Character | Needs Custom Styling |
-| ------------------- | ----------- | --------------------- | -------------------- |
-| `space` | Todo | `x` | No |
-| `/` | In Progress | `x` | Yes |
-| `x` | Done | `space` | No |
-| `-` | Cancelled | `space` | Yes |
+| Status Character    | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| ------------------- | ----------- | --------------------- | ----------- | -------------------- |
+| `space` | Todo | `x` | `TODO` | No |
+| `/` | In Progress | `x` | `IN_PROGRESS` | Yes |
+| `x` | Done | `space` | `DONE` | No |
+| `-` | Cancelled | `space` | `CANCELLED` | Yes |
 
 <!-- placeholder to force blank line after table --> <!-- endInclude -->
 
@@ -89,27 +89,27 @@ Remember to set up your chosen CSS Snippet or Theme before setting up the custom
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md -->
 
-| Status Character    | Status Name | Next Status Character | Needs Custom Styling |
-| ------------------- | ----------- | --------------------- | -------------------- |
-| `>` | Forwarded | `x` | Yes |
-| `<` | Schedule | `x` | Yes |
-| `?` | Question | `x` | Yes |
-| `!` | Important | `x` | Yes |
-| `"` | Quote | `x` | Yes |
-| `-` | Canceled | `x` | Yes |
-| `*` | Star | `x` | Yes |
-| `l` | Location | `x` | Yes |
-| `i` | Info | `x` | Yes |
-| `S` | Amount/savings/money | `x` | Yes |
-| `I` | Idea/lightbulb | `x` | Yes |
-| `f` | Fire | `x` | Yes |
-| `k` | Key | `x` | Yes |
-| `u` | Up | `x` | Yes |
-| `d` | Down | `x` | Yes |
-| `w` | Win | `x` | Yes |
-| `p` | Pros | `x` | Yes |
-| `c` | Cons | `x` | Yes |
-| `b` | Bookmark | `x` | Yes |
+| Status Character    | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| ------------------- | ----------- | --------------------- | ----------- | -------------------- |
+| `>` | Forwarded | `x` | `TODO` | Yes |
+| `<` | Schedule | `x` | `TODO` | Yes |
+| `?` | Question | `x` | `TODO` | Yes |
+| `!` | Important | `x` | `TODO` | Yes |
+| `"` | Quote | `x` | `TODO` | Yes |
+| `-` | Canceled | `x` | `CANCELLED` | Yes |
+| `*` | Star | `x` | `TODO` | Yes |
+| `l` | Location | `x` | `TODO` | Yes |
+| `i` | Info | `x` | `TODO` | Yes |
+| `S` | Amount/savings/money | `x` | `TODO` | Yes |
+| `I` | Idea/lightbulb | `x` | `TODO` | Yes |
+| `f` | Fire | `x` | `TODO` | Yes |
+| `k` | Key | `x` | `TODO` | Yes |
+| `u` | Up | `x` | `TODO` | Yes |
+| `d` | Down | `x` | `TODO` | Yes |
+| `w` | Win | `x` | `TODO` | Yes |
+| `p` | Pros | `x` | `TODO` | Yes |
+| `c` | Cons | `x` | `TODO` | Yes |
+| `b` | Bookmark | `x` | `TODO` | Yes |
 
 <!-- placeholder to force blank line after table --> <!-- endInclude -->
 
@@ -117,28 +117,28 @@ Remember to set up your chosen CSS Snippet or Theme before setting up the custom
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md -->
 
-| Status Character    | Status Name | Next Status Character | Needs Custom Styling |
-| ------------------- | ----------- | --------------------- | -------------------- |
-| `>` | Forward | `x` | Yes |
-| `D` | Deferred/Scheduled | `x` | Yes |
-| `?` | Question | `x` | Yes |
-| `!` | Important | `x` | Yes |
-| `+` | Add | `x` | Yes |
-| `R` | Research | `x` | Yes |
-| `i` | Idea | `x` | Yes |
-| `B` | Brainstorm | `x` | Yes |
-| `P` | Pro | `x` | Yes |
-| `C` | Con | `x` | Yes |
-| `I` | Info | `x` | Yes |
-| `Q` | Quote | `x` | Yes |
-| `N` | Note | `x` | Yes |
-| `b` | Bookmark | `x` | Yes |
-| `p` | Paraphrase | `x` | Yes |
-| `E` | Example | `x` | Yes |
-| `L` | Location | `x` | Yes |
-| `A` | Answer | `x` | Yes |
-| `r` | Reward | `x` | Yes |
-| `c` | Choice | `x` | Yes |
+| Status Character    | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| ------------------- | ----------- | --------------------- | ----------- | -------------------- |
+| `>` | Forward | `x` | `TODO` | Yes |
+| `D` | Deferred/Scheduled | `x` | `TODO` | Yes |
+| `?` | Question | `x` | `TODO` | Yes |
+| `!` | Important | `x` | `TODO` | Yes |
+| `+` | Add | `x` | `TODO` | Yes |
+| `R` | Research | `x` | `TODO` | Yes |
+| `i` | Idea | `x` | `TODO` | Yes |
+| `B` | Brainstorm | `x` | `TODO` | Yes |
+| `P` | Pro | `x` | `TODO` | Yes |
+| `C` | Con | `x` | `TODO` | Yes |
+| `I` | Info | `x` | `TODO` | Yes |
+| `Q` | Quote | `x` | `TODO` | Yes |
+| `N` | Note | `x` | `TODO` | Yes |
+| `b` | Bookmark | `x` | `TODO` | Yes |
+| `p` | Paraphrase | `x` | `TODO` | Yes |
+| `E` | Example | `x` | `TODO` | Yes |
+| `L` | Location | `x` | `TODO` | Yes |
+| `A` | Answer | `x` | `TODO` | Yes |
+| `r` | Reward | `x` | `TODO` | Yes |
+| `c` | Choice | `x` | `TODO` | Yes |
 
 <!-- placeholder to force blank line after table --> <!-- endInclude -->
 
diff --git a/docs/how-to/set-up-custom-statuses.md b/docs/how-to/set-up-custom-statuses.md
index 8fe35061..30b3bb0c 100644
--- a/docs/how-to/set-up-custom-statuses.md
+++ b/docs/how-to/set-up-custom-statuses.md
@@ -73,11 +73,11 @@ Suppose that you wanted to create a set of 3 statuses that cycle between each ot
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md -->
 
-| Status Character    | Status Name | Next Status Character | Needs Custom Styling |
-| ------------------- | ----------- | --------------------- | -------------------- |
-| `!` | Important | `D` | Yes |
-| `D` | Doing - Important | `X` | Yes |
-| `X` | Done - Important | `!` | Yes |
+| Status Character    | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| ------------------- | ----------- | --------------------- | ----------- | -------------------- |
+| `!` | Important | `D` | `TODO` | Yes |
+| `D` | Doing - Important | `X` | `TODO` | Yes |
+| `X` | Done - Important | `!` | `DONE` | Yes |
 
 <!-- placeholder to force blank line after table --> <!-- endInclude -->
 
diff --git a/src/Config/CustomStatusModal.ts b/src/Config/CustomStatusModal.ts
index 153ce53f..871c165c 100644
--- a/src/Config/CustomStatusModal.ts
+++ b/src/Config/CustomStatusModal.ts
@@ -1,5 +1,5 @@
 import { Modal, Notice, Setting, TextComponent } from 'obsidian';
-import { StatusConfiguration } from '../StatusConfiguration';
+import { StatusConfiguration, StatusType } from '../StatusConfiguration';
 import type TasksPlugin from '../main';
 import { StatusValidator } from '../StatusValidator';
 import { Status } from '../Status';
@@ -11,6 +11,8 @@ export class CustomStatusModal extends Modal {
     statusName: string;
     statusNextSymbol: string;
     statusAvailableAsCommand: boolean;
+    type: StatusType;
+
     saved: boolean = false;
     error: boolean = false;
     constructor(public plugin: TasksPlugin, statusType: StatusConfiguration) {
@@ -19,6 +21,7 @@ export class CustomStatusModal extends Modal {
         this.statusName = statusType.name;
         this.statusNextSymbol = statusType.nextStatusIndicator;
         this.statusAvailableAsCommand = statusType.availableAsCommand;
+        this.type = statusType.type;
     }
 
     /**
@@ -30,6 +33,7 @@ export class CustomStatusModal extends Modal {
             this.statusName,
             this.statusNextSymbol,
             this.statusAvailableAsCommand,
+            this.type,
         );
     }
 
@@ -90,6 +94,25 @@ export class CustomStatusModal extends Modal {
                 );
             });
 
+        new Setting(settingDiv)
+            .setName('Task Status Type')
+            .setDesc('Control how the status behaves for searching and toggling')
+            .addDropdown((dropdown) => {
+                const types = [
+                    StatusType.TODO,
+                    StatusType.IN_PROGRESS,
+                    StatusType.DONE,
+                    StatusType.CANCELLED,
+                    StatusType.NON_TASK,
+                ];
+                types.forEach((s) => {
+                    dropdown.addOption(s, s);
+                });
+                dropdown.setValue(this.type).onChange((v) => {
+                    this.type = Status.getTypeFromStatusTypeString(v);
+                });
+            });
+
         if (Status.tasksPluginCanCreateCommandsForStatuses()) {
             new Setting(settingDiv)
                 .setName('Available as command')
diff --git a/src/Config/Settings.ts b/src/Config/Settings.ts
index c15a6b65..11534358 100644
--- a/src/Config/Settings.ts
+++ b/src/Config/Settings.ts
@@ -1,3 +1,5 @@
+import { StatusConfiguration } from '../StatusConfiguration';
+import { Status } from '../Status';
 import { StatusSettings } from './StatusSettings';
 import { Feature } from './Feature';
 import type { FeatureFlag } from './Feature';
@@ -77,6 +79,13 @@ export const getSettings = (): Settings => {
         }
     }
 
+    // In case saves pre-dated StatusConfiguration.type
+    // TODO Special case for indicator 'X' or 'x' (just in case)
+    settings.statusSettings.customStatusTypes = settings.statusSettings.customStatusTypes.map((s) => {
+        const newType = Status.getTypeFromStatusTypeString(s.type);
+        return new StatusConfiguration(s.indicator, s.name, s.nextStatusIndicator, s.availableAsCommand, newType);
+    });
+
     return { ...settings };
 };
 
diff --git a/src/Config/SettingsTab.ts b/src/Config/SettingsTab.ts
index 4feb2d21..5c7e120f 100644
--- a/src/Config/SettingsTab.ts
+++ b/src/Config/SettingsTab.ts
@@ -1,5 +1,5 @@
 import { Notice, PluginSettingTab, Setting, debounce } from 'obsidian';
-import { StatusConfiguration } from '../StatusConfiguration';
+import { StatusConfiguration, StatusType } from '../StatusConfiguration';
 import type TasksPlugin from '../main';
 import { StatusRegistry } from '../StatusRegistry';
 import { Status } from '../Status';
@@ -403,7 +403,10 @@ export class SettingsTab extends PluginSettingTab {
                 .setButtonText('Add New Task Status')
                 .setCta()
                 .onClick(async () => {
-                    StatusSettings.addCustomStatus(statusSettings, new StatusConfiguration('', '', '', false));
+                    StatusSettings.addCustomStatus(
+                        statusSettings,
+                        new StatusConfiguration('', '', '', false, StatusType.TODO),
+                    );
                     await updateAndSaveStatusSettings(statusSettings, settings);
                 });
         });
diff --git a/src/Config/StatusSettings.ts b/src/Config/StatusSettings.ts
index 91b46ede..04c956fb 100644
--- a/src/Config/StatusSettings.ts
+++ b/src/Config/StatusSettings.ts
@@ -1,5 +1,6 @@
-import { StatusConfiguration } from '../StatusConfiguration';
+import type { StatusConfiguration } from '../StatusConfiguration';
 import type { StatusRegistry } from '../StatusRegistry';
+import { Status } from '../Status';
 
 /**
  * Class for encapsulating the settings that control custom statuses.
@@ -45,7 +46,7 @@ export class StatusSettings {
         originalStatus: StatusConfiguration,
         newStatus: StatusConfiguration,
     ): boolean {
-        const index = statusSettings.customStatusTypes.indexOf(originalStatus);
+        const index = this.findStatusIndex(originalStatus, statusSettings);
         if (index <= -1) {
             return false;
         }
@@ -53,6 +54,20 @@ export class StatusSettings {
         return true;
     }
 
+    /**
+     * This is a workaround for the fact that statusSettings.customStatusTypes.indexOf(statusConfiguration)
+     * stopped finding identical statuses since the addition of StatusConfiguration.type.
+     * @param statusConfiguration
+     * @param statusSettings
+     * @private
+     */
+    private static findStatusIndex(statusConfiguration: StatusConfiguration, statusSettings: StatusSettings) {
+        const originalStatusAsStatus = new Status(statusConfiguration);
+        return statusSettings.customStatusTypes.findIndex((s) => {
+            return new Status(s).previewText() == originalStatusAsStatus.previewText();
+        });
+    }
+
     /**
      * Delete the given custom status.
      * Returns true if deleted, and false if not.
@@ -63,7 +78,7 @@ export class StatusSettings {
      * @param status
      */
     public static deleteCustomStatus(statusSettings: StatusSettings, status: StatusConfiguration) {
-        const index = statusSettings.customStatusTypes.indexOf(status);
+        const index = this.findStatusIndex(status, statusSettings);
         if (index <= -1) {
             return false;
         }
@@ -98,10 +113,7 @@ export class StatusSettings {
                 );
             });
             if (!hasStatus) {
-                StatusSettings.addCustomStatus(
-                    statusSettings,
-                    new StatusConfiguration(importedStatus[0], importedStatus[1], importedStatus[2], false),
-                );
+                StatusSettings.addCustomStatus(statusSettings, Status.createFromImportedValue(importedStatus));
             } else {
                 notices.push(`The status ${importedStatus[1]} (${importedStatus[0]}) is already added.`);
             }
diff --git a/src/Status.ts b/src/Status.ts
index 35662678..1111f305 100644
--- a/src/Status.ts
+++ b/src/Status.ts
@@ -1,4 +1,4 @@
-import { StatusConfiguration } from './StatusConfiguration';
+import { StatusConfiguration, StatusType } from './StatusConfiguration';
 
 /**
  * Tracks the possible states that a task can be in.
@@ -89,6 +89,13 @@ export class Status {
         return this.configuration.availableAsCommand;
     }
 
+    /**
+     * Returns the status type. See {@link StatusType} for details.
+     */
+    public get type(): StatusType {
+        return this.configuration.type;
+    }
+
     /**
      * Creates an instance of Status. The registry will be added later in the case
      * of the default statuses.
@@ -104,14 +111,14 @@ export class Status {
      * The default Done status. Goes to Todo when toggled.
      */
     static makeDone(): Status {
-        return new Status(new StatusConfiguration('x', 'Done', ' ', true));
+        return new Status(new StatusConfiguration('x', 'Done', ' ', true, StatusType.DONE));
     }
 
     /**
      * A default status of empty, used when things go wrong.
      */
     static makeEmpty(): Status {
-        return new Status(new StatusConfiguration('', 'EMPTY', '', true));
+        return new Status(new StatusConfiguration('', 'EMPTY', '', true, StatusType.EMPTY));
     }
 
     /**
@@ -119,21 +126,53 @@ export class Status {
      * User may later be able to override this to go to In Progress instead.
      */
     static makeTodo(): Status {
-        return new Status(new StatusConfiguration(' ', 'Todo', 'x', true));
+        return new Status(new StatusConfiguration(' ', 'Todo', 'x', true, StatusType.TODO));
     }
 
     /**
      * The default Cancelled status. Goes to Todo when toggled.
      */
     static makeCancelled(): Status {
-        return new Status(new StatusConfiguration('-', 'Cancelled', ' ', true));
+        return new Status(new StatusConfiguration('-', 'Cancelled', ' ', true, StatusType.CANCELLED));
     }
 
     /**
      * The default In Progress status. Goes to Done when toggled.
      */
     static makeInProgress(): Status {
-        return new Status(new StatusConfiguration('/', 'In Progress', 'x', true));
+        return new Status(new StatusConfiguration('/', 'In Progress', 'x', true, StatusType.IN_PROGRESS));
+    }
+
+    /**
+     * Return the StatusType to use for an indicator, if it is not in the StatusRegistry.
+     * The core indicators are recognised.
+     * Other indicators are treated as StatusType.TODO
+     * @param indicator
+     */
+    static getTypeForUnknownIndicator(indicator: string): StatusType {
+        switch (indicator) {
+            case 'x':
+            case 'X':
+                return StatusType.DONE;
+            case '/':
+                return StatusType.IN_PROGRESS;
+            case '-':
+                return StatusType.CANCELLED;
+            case '':
+                return StatusType.EMPTY;
+            case ' ':
+            default:
+                return StatusType.TODO;
+        }
+    }
+
+    /**
+     * Convert text that was saved from a StatusType value back to a StatusType.
+     * Returns StatusType.TODO if the string is not valid.
+     * @param statusTypeAsString
+     */
+    static getTypeFromStatusTypeString(statusTypeAsString: string): StatusType {
+        return StatusType[statusTypeAsString as keyof typeof StatusType] || StatusType.TODO;
     }
 
     /**
@@ -141,10 +180,24 @@ export class Status {
      *
      * This can be useful when StatusRegistry does not recognise an indicator,
      * and we do not want to expose the user's data to the Status.EMPTY status.
+     *
+     * The type is set to TODO.
      * @param unknownIndicator
      */
     static createUnknownStatus(unknownIndicator: string) {
-        return new Status(new StatusConfiguration(unknownIndicator, 'Unknown', 'x', false));
+        return new Status(new StatusConfiguration(unknownIndicator, 'Unknown', 'x', false, StatusType.TODO));
+    }
+
+    /**
+     * Helper function for bulk-importing settings from arrays of strings.
+     *
+     * The type is deduced, for a few common status indicators.
+     * @param imported An array of indicator, name, next indicator
+     */
+    static createFromImportedValue(imported: [string, string, string]) {
+        const indicator = imported[0];
+        const type = Status.getTypeForUnknownIndicator(indicator);
+        return new Status(new StatusConfiguration(indicator, imported[1], imported[2], false, type));
     }
 
     /**
@@ -166,7 +219,7 @@ export class Status {
         if (Status.tasksPluginCanCreateCommandsForStatuses() && this.availableAsCommand) {
             commandNotice = 'Available as a command.';
         }
-        return `- [${this.indicator}] ${this.name}, next status is '${this.nextStatusIndicator}'. ${commandNotice}`;
+        return `- [${this.indicator}] ${this.name}, next status is '${this.nextStatusIndicator}', type is '${this.configuration.type}'. ${commandNotice}`;
     }
 
     /**
diff --git a/src/StatusConfiguration.ts b/src/StatusConfiguration.ts
index 0d807d8c..e0ee9cde 100644
--- a/src/StatusConfiguration.ts
+++ b/src/StatusConfiguration.ts
@@ -1,3 +1,15 @@
+/**
+ * Collection of status types supported by the plugin.
+ */
+export enum StatusType {
+    TODO = 'TODO',
+    DONE = 'DONE',
+    IN_PROGRESS = 'IN_PROGRESS',
+    CANCELLED = 'CANCELLED',
+    NON_TASK = 'NON_TASK',
+    EMPTY = 'EMPTY',
+}
+
 /**
  * This is the object stored by the Obsidian configuration and used to create the status
  * objects for the session
@@ -38,6 +50,11 @@ export class StatusConfiguration {
      */
     public readonly availableAsCommand: boolean;
 
+    /**
+     * Returns the status type. See {@link StatusType} for details.
+     */
+    public readonly type: StatusType;
+
     /**
      * Creates an instance of Status. The registry will be added later in the case
      * of the default statuses.
@@ -46,12 +63,20 @@ export class StatusConfiguration {
      * @param {string} name
      * @param {Status} nextStatusIndicator
      * @param {boolean} availableAsCommand
+     * @param {StatusType} type
      * @memberof Status
      */
-    constructor(indicator: string, name: string, nextStatusIndicator: string, availableAsCommand: boolean) {
+    constructor(
+        indicator: string,
+        name: string,
+        nextStatusIndicator: string,
+        availableAsCommand: boolean,
+        type: StatusType = StatusType.TODO, // TODO Remove default value
+    ) {
         this.indicator = indicator;
         this.name = name;
         this.nextStatusIndicator = nextStatusIndicator;
         this.availableAsCommand = availableAsCommand;
+        this.type = type;
     }
 }


Test patch:
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md
index 785db7b4..b798cc36 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md
@@ -1,11 +1,11 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character    | Status Name | Next Status Character | Needs Custom Styling |
-| ------------------- | ----------- | --------------------- | -------------------- |
-| `space` | Todo | `x` | No |
-| `/` | In Progress | `x` | Yes |
-| `x` | Done | `space` | No |
-| `-` | Cancelled | `space` | Yes |
+| Status Character    | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| ------------------- | ----------- | --------------------- | ----------- | -------------------- |
+| `space` | Todo | `x` | `TODO` | No |
+| `/` | In Progress | `x` | `IN_PROGRESS` | Yes |
+| `x` | Done | `space` | `DONE` | No |
+| `-` | Cancelled | `space` | `CANCELLED` | Yes |
 
 
 <!-- placeholder to force blank line after table -->
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
index 5fb6447b..6aaec429 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
@@ -1,10 +1,10 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character    | Status Name | Next Status Character | Needs Custom Styling |
-| ------------------- | ----------- | --------------------- | -------------------- |
-| `!` | Important | `D` | Yes |
-| `D` | Doing - Important | `X` | Yes |
-| `X` | Done - Important | `!` | Yes |
+| Status Character    | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| ------------------- | ----------- | --------------------- | ----------- | -------------------- |
+| `!` | Important | `D` | `TODO` | Yes |
+| `D` | Doing - Important | `X` | `TODO` | Yes |
+| `X` | Done - Important | `!` | `DONE` | Yes |
 
 
 <!-- placeholder to force blank line after table -->
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md
index ac361ee9..b5bc6b93 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md
@@ -1,27 +1,27 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character    | Status Name | Next Status Character | Needs Custom Styling |
-| ------------------- | ----------- | --------------------- | -------------------- |
-| `>` | Forward | `x` | Yes |
-| `D` | Deferred/Scheduled | `x` | Yes |
-| `?` | Question | `x` | Yes |
-| `!` | Important | `x` | Yes |
-| `+` | Add | `x` | Yes |
-| `R` | Research | `x` | Yes |
-| `i` | Idea | `x` | Yes |
-| `B` | Brainstorm | `x` | Yes |
-| `P` | Pro | `x` | Yes |
-| `C` | Con | `x` | Yes |
-| `I` | Info | `x` | Yes |
-| `Q` | Quote | `x` | Yes |
-| `N` | Note | `x` | Yes |
-| `b` | Bookmark | `x` | Yes |
-| `p` | Paraphrase | `x` | Yes |
-| `E` | Example | `x` | Yes |
-| `L` | Location | `x` | Yes |
-| `A` | Answer | `x` | Yes |
-| `r` | Reward | `x` | Yes |
-| `c` | Choice | `x` | Yes |
+| Status Character    | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| ------------------- | ----------- | --------------------- | ----------- | -------------------- |
+| `>` | Forward | `x` | `TODO` | Yes |
+| `D` | Deferred/Scheduled | `x` | `TODO` | Yes |
+| `?` | Question | `x` | `TODO` | Yes |
+| `!` | Important | `x` | `TODO` | Yes |
+| `+` | Add | `x` | `TODO` | Yes |
+| `R` | Research | `x` | `TODO` | Yes |
+| `i` | Idea | `x` | `TODO` | Yes |
+| `B` | Brainstorm | `x` | `TODO` | Yes |
+| `P` | Pro | `x` | `TODO` | Yes |
+| `C` | Con | `x` | `TODO` | Yes |
+| `I` | Info | `x` | `TODO` | Yes |
+| `Q` | Quote | `x` | `TODO` | Yes |
+| `N` | Note | `x` | `TODO` | Yes |
+| `b` | Bookmark | `x` | `TODO` | Yes |
+| `p` | Paraphrase | `x` | `TODO` | Yes |
+| `E` | Example | `x` | `TODO` | Yes |
+| `L` | Location | `x` | `TODO` | Yes |
+| `A` | Answer | `x` | `TODO` | Yes |
+| `r` | Reward | `x` | `TODO` | Yes |
+| `c` | Choice | `x` | `TODO` | Yes |
 
 
 <!-- placeholder to force blank line after table -->
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md
index 4bf1e441..dbf83996 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md
@@ -1,26 +1,26 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character    | Status Name | Next Status Character | Needs Custom Styling |
-| ------------------- | ----------- | --------------------- | -------------------- |
-| `>` | Forwarded | `x` | Yes |
-| `<` | Schedule | `x` | Yes |
-| `?` | Question | `x` | Yes |
-| `!` | Important | `x` | Yes |
-| `"` | Quote | `x` | Yes |
-| `-` | Canceled | `x` | Yes |
-| `*` | Star | `x` | Yes |
-| `l` | Location | `x` | Yes |
-| `i` | Info | `x` | Yes |
-| `S` | Amount/savings/money | `x` | Yes |
-| `I` | Idea/lightbulb | `x` | Yes |
-| `f` | Fire | `x` | Yes |
-| `k` | Key | `x` | Yes |
-| `u` | Up | `x` | Yes |
-| `d` | Down | `x` | Yes |
-| `w` | Win | `x` | Yes |
-| `p` | Pros | `x` | Yes |
-| `c` | Cons | `x` | Yes |
-| `b` | Bookmark | `x` | Yes |
+| Status Character    | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| ------------------- | ----------- | --------------------- | ----------- | -------------------- |
+| `>` | Forwarded | `x` | `TODO` | Yes |
+| `<` | Schedule | `x` | `TODO` | Yes |
+| `?` | Question | `x` | `TODO` | Yes |
+| `!` | Important | `x` | `TODO` | Yes |
+| `"` | Quote | `x` | `TODO` | Yes |
+| `-` | Canceled | `x` | `CANCELLED` | Yes |
+| `*` | Star | `x` | `TODO` | Yes |
+| `l` | Location | `x` | `TODO` | Yes |
+| `i` | Info | `x` | `TODO` | Yes |
+| `S` | Amount/savings/money | `x` | `TODO` | Yes |
+| `I` | Idea/lightbulb | `x` | `TODO` | Yes |
+| `f` | Fire | `x` | `TODO` | Yes |
+| `k` | Key | `x` | `TODO` | Yes |
+| `u` | Up | `x` | `TODO` | Yes |
+| `d` | Down | `x` | `TODO` | Yes |
+| `w` | Win | `x` | `TODO` | Yes |
+| `p` | Pros | `x` | `TODO` | Yes |
+| `c` | Cons | `x` | `TODO` | Yes |
+| `b` | Bookmark | `x` | `TODO` | Yes |
 
 
 <!-- placeholder to force blank line after table -->
diff --git a/tests/DocsSamplesForStatuses.test.ts b/tests/DocsSamplesForStatuses.test.ts
index b829ada4..0de0fcf0 100644
--- a/tests/DocsSamplesForStatuses.test.ts
+++ b/tests/DocsSamplesForStatuses.test.ts
@@ -3,7 +3,6 @@ import { verify } from 'approvals/lib/Providers/Jest/JestApprovals';
 
 import { StatusRegistry } from '../src/StatusRegistry';
 import { Status } from '../src/Status';
-import { StatusConfiguration } from '../src/StatusConfiguration';
 import * as StatusSettingsHelpers from '../src/Config/StatusSettingsHelpers';
 
 function getPrintableIndicator(indicator: string) {
@@ -13,13 +12,16 @@ function getPrintableIndicator(indicator: string) {
 
 function verifyStatusesAsMarkdownTable(statuses: Status[]) {
     let commandsTable = '<!-- placeholder to force blank line before table -->\n\n';
-    commandsTable += '| Status Character    | Status Name | Next Status Character | Needs Custom Styling |\n';
-    commandsTable += '| ------------------- | ----------- | --------------------- | -------------------- |\n';
+    commandsTable +=
+        '| Status Character    | Status Name | Next Status Character | Status Type | Needs Custom Styling |\n';
+    commandsTable +=
+        '| ------------------- | ----------- | --------------------- | ----------- | -------------------- |\n';
     for (const status of statuses) {
         const statusCharacter = getPrintableIndicator(status.indicator);
         const nextStatusCharacter = getPrintableIndicator(status.nextStatusIndicator);
+        const type = getPrintableIndicator(status.type);
         const needsCustomStyling = status.indicator !== ' ' && status.indicator !== 'x' ? 'Yes' : 'No';
-        commandsTable += `| ${statusCharacter} | ${status.name} | ${nextStatusCharacter} | ${needsCustomStyling} |\n`;
+        commandsTable += `| ${statusCharacter} | ${status.name} | ${nextStatusCharacter} | ${type} | ${needsCustomStyling} |\n`;
     }
     commandsTable += '\n\n<!-- placeholder to force blank line after table -->\n';
     let options = new Options();
@@ -30,9 +32,7 @@ function verifyStatusesAsMarkdownTable(statuses: Status[]) {
 function constructStatuses(importedStatuses: Array<[string, string, string]>) {
     const statuses: Status[] = [];
     importedStatuses.forEach((importedStatus) => {
-        statuses.push(
-            new Status(new StatusConfiguration(importedStatus[0], importedStatus[1], importedStatus[2], false)),
-        );
+        statuses.push(Status.createFromImportedValue(importedStatus));
     });
     return statuses;
 }
diff --git a/tests/Status.test.ts b/tests/Status.test.ts
index 89ffff2d..7365457c 100644
--- a/tests/Status.test.ts
+++ b/tests/Status.test.ts
@@ -3,29 +3,33 @@
  */
 import moment from 'moment';
 import { Status } from '../src/Status';
-import { StatusConfiguration } from '../src/StatusConfiguration';
+import { StatusConfiguration, StatusType } from '../src/StatusConfiguration';
 
 jest.mock('obsidian');
 window.moment = moment;
 
 describe('Status', () => {
     it('preview text', () => {
-        const configuration = new Status(new StatusConfiguration('P', 'Pro', 'Con', true));
-        expect(configuration.previewText()).toEqual("- [P] Pro, next status is 'Con'. ");
+        const configuration = new Status(new StatusConfiguration('P', 'Pro', 'Con', true, StatusType.TODO));
+        expect(configuration.previewText()).toEqual("- [P] Pro, next status is 'Con', type is 'TODO'. ");
     });
 
     it('default configurations', () => {
-        expect(Status.DONE.previewText()).toEqual("- [x] Done, next status is ' '. ");
-        expect(Status.EMPTY.previewText()).toEqual("- [] EMPTY, next status is ''. ");
-        expect(Status.TODO.previewText()).toEqual("- [ ] Todo, next status is 'x'. ");
+        expect(Status.DONE.previewText()).toEqual("- [x] Done, next status is ' ', type is 'DONE'. ");
+        expect(Status.EMPTY.previewText()).toEqual("- [] EMPTY, next status is '', type is 'EMPTY'. ");
+        expect(Status.TODO.previewText()).toEqual("- [ ] Todo, next status is 'x', type is 'TODO'. ");
     });
 
     it('factory methods for default statuses', () => {
-        expect(Status.makeDone().previewText()).toEqual("- [x] Done, next status is ' '. ");
-        expect(Status.makeEmpty().previewText()).toEqual("- [] EMPTY, next status is ''. ");
-        expect(Status.makeTodo().previewText()).toEqual("- [ ] Todo, next status is 'x'. ");
-        expect(Status.makeCancelled().previewText()).toEqual("- [-] Cancelled, next status is ' '. ");
-        expect(Status.makeInProgress().previewText()).toEqual("- [/] In Progress, next status is 'x'. ");
+        expect(Status.makeDone().previewText()).toEqual("- [x] Done, next status is ' ', type is 'DONE'. ");
+        expect(Status.makeEmpty().previewText()).toEqual("- [] EMPTY, next status is '', type is 'EMPTY'. ");
+        expect(Status.makeTodo().previewText()).toEqual("- [ ] Todo, next status is 'x', type is 'TODO'. ");
+        expect(Status.makeCancelled().previewText()).toEqual(
+            "- [-] Cancelled, next status is ' ', type is 'CANCELLED'. ",
+        );
+        expect(Status.makeInProgress().previewText()).toEqual(
+            "- [/] In Progress, next status is 'x', type is 'IN_PROGRESS'. ",
+        );
     });
 
     it('should initialize with valid properties', () => {
@@ -35,13 +39,14 @@ describe('Status', () => {
         const next = 'x';
 
         // Act
-        const status = new Status(new StatusConfiguration(indicator, name, next, false));
+        const status = new Status(new StatusConfiguration(indicator, name, next, false, StatusType.IN_PROGRESS));
 
         // Assert
         expect(status).not.toBeNull();
         expect(status!.indicator).toEqual(indicator);
         expect(status!.name).toEqual(name);
         expect(status!.nextStatusIndicator).toEqual(next);
+        expect(status!.type).toEqual(StatusType.IN_PROGRESS);
         expect(status!.isCompleted()).toEqual(false);
     });
 
@@ -52,7 +57,7 @@ describe('Status', () => {
         const next = ' ';
 
         // Act
-        const status = new Status(new StatusConfiguration(indicator, name, next, false));
+        const status = new Status(new StatusConfiguration(indicator, name, next, false, StatusType.DONE));
 
         // Assert
         expect(status).not.toBeNull();
@@ -62,9 +67,29 @@ describe('Status', () => {
         expect(status!.isCompleted()).toEqual(true);
     });
 
+    it('should deduce type for unknown indicators', () => {
+        expect(Status.getTypeForUnknownIndicator(' ')).toEqual(StatusType.TODO);
+        expect(Status.getTypeForUnknownIndicator('!')).toEqual(StatusType.TODO); // Unknown character treated as TODO
+        expect(Status.getTypeForUnknownIndicator('x')).toEqual(StatusType.DONE);
+        expect(Status.getTypeForUnknownIndicator('X')).toEqual(StatusType.DONE);
+        expect(Status.getTypeForUnknownIndicator('/')).toEqual(StatusType.IN_PROGRESS);
+        expect(Status.getTypeForUnknownIndicator('-')).toEqual(StatusType.CANCELLED);
+        expect(Status.getTypeForUnknownIndicator('')).toEqual(StatusType.EMPTY);
+    });
+
+    it('should deduce type from StatusType text', () => {
+        expect(Status.getTypeFromStatusTypeString('TODO')).toEqual(StatusType.TODO);
+        expect(Status.getTypeFromStatusTypeString('DONE')).toEqual(StatusType.DONE);
+        expect(Status.getTypeFromStatusTypeString('IN_PROGRESS')).toEqual(StatusType.IN_PROGRESS);
+        expect(Status.getTypeFromStatusTypeString('CANCELLED')).toEqual(StatusType.CANCELLED);
+        expect(Status.getTypeFromStatusTypeString('NON_TASK')).toEqual(StatusType.NON_TASK);
+        expect(Status.getTypeFromStatusTypeString('EMPTY')).toEqual(StatusType.EMPTY);
+        expect(Status.getTypeFromStatusTypeString('i do not exist')).toEqual(StatusType.TODO);
+    });
+
     it('should construct a Status for unknown symbol', () => {
         // Arrange
-        const indicator = 'U';
+        const indicator = '/';
 
         // Act
         const status = Status.createUnknownStatus(indicator);
@@ -74,6 +99,29 @@ describe('Status', () => {
         expect(status!.indicator).toEqual(indicator);
         expect(status!.name).toEqual('Unknown');
         expect(status!.nextStatusIndicator).toEqual('x');
+        // Even though the type *could* be deduced as IN_PROGRESS, createUnknownStatus() is used when
+        // the user has not defined the meaning of a status indicator, so treat everything as TODO.
+        expect(status!.type).toEqual(StatusType.TODO);
         expect(status!.isCompleted()).toEqual(false);
     });
+
+    it('should construct a Status from a core imported value', () => {
+        const imported: [string, string, string] = ['/', 'in progress', 'x'];
+        const status = Status.createFromImportedValue(imported);
+        expect(status.indicator).toEqual('/');
+        expect(status.name).toEqual('in progress');
+        expect(status.nextStatusIndicator).toEqual('x');
+        expect(status.type).toEqual(StatusType.IN_PROGRESS); // should deduce IN_PROGRESS from indicator '/'
+        expect(status.availableAsCommand).toEqual(false);
+    });
+
+    it('should construct a Status from a custom imported value', () => {
+        const imported: [string, string, string] = ['P', 'Pro', 'C'];
+        const status = Status.createFromImportedValue(imported);
+        expect(status.indicator).toEqual('P');
+        expect(status.name).toEqual('Pro');
+        expect(status.nextStatusIndicator).toEqual('C');
+        expect(status.type).toEqual(StatusType.TODO);
+        expect(status.availableAsCommand).toEqual(false);
+    });
 });