diff --git a/Tasks.txt b/Tasks.txt
new file mode 100644
index 0000000000..606ef01774
Binary files /dev/null and b/Tasks.txt differ
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000000..90c3548de8
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,70 @@
+plugins {
+ id 'java'
+ id 'application'
+ id 'checkstyle'
+ id 'com.github.johnrengelman.shadow' version '7.1.2'
+ id 'org.openjfx.javafxplugin' version '0.1.0'
+}
+
+// Configure the Java compiler options
+tasks.withType(JavaCompile).configureEach {
+ options.compilerArgs << "-Xlint:unchecked"
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0'
+ testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0'
+
+ String javaFxVersion = '17.0.7'
+
+ implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win'
+ implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac'
+ implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux'
+ implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win'
+ implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac'
+ implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux'
+ implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win'
+ implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac'
+ implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux'
+ implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win'
+ implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac'
+ implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux'
+}
+
+test {
+ useJUnitPlatform()
+
+ testLogging {
+ events "passed", "skipped", "failed"
+
+ showExceptions true
+ exceptionFormat "full"
+ showCauses true
+ showStackTraces true
+ showStandardStreams = false
+ }
+}
+
+application {
+ //mainClass.set("seedu.duke.Duke")
+ mainClass.set("catbot.CatBotEntrypoint")
+}
+
+shadowJar {
+ archiveBaseName = "catbot"
+ archiveClassifier = null
+ dependsOn("distZip", "distTar")
+}
+
+run{
+ standardInput = System.in
+}
+
+javafx {
+ version = "17.0.7"
+ modules = [ 'javafx.controls', 'javafx.fxml' ]
+}
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000000..eb761a9b9a
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,434 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
new file mode 100644
index 0000000000..39efb6e4ac
--- /dev/null
+++ b/config/checkstyle/suppressions.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/docs/README.md b/docs/README.md
index 8077118ebe..5ff134cfdc 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,29 +1,130 @@
-# User Guide
+# CatBot User Guide
+
+Hi! CatBot is a friendly and informal task assistant.
## Features
-### Feature-ABC
+### It's really uncertain...
+It's trying its best.
+
+### You can type to do things
+Like the 1337coder you are. :)
-Description of the feature.
+## Usage
-### Feature-XYZ
+### `list` - view tracked tasks
-Description of the feature.
+Command: `list`
-## Usage
+Expected outcome:
+Lists all tasks currently tracked, or infoms you if you have no currently tracked tasks.
+
+### `mark` - mark a task as done
+
+Command: `mark `
+
+where `` is the corresponding number of the task when displayed with `list`.
+
+Example of usage:
+`mark 5`
+
+Expected outcome:
+Marks the fifth task in your list as done, or informs you that the index is out of range.
+
+### `unmark` - mark a task as not done
+
+Command: `unmark `
+
+where `` is the corresponding number of the task when displayed with `list`.
+
+Example of usage:
+`unmark 5`
+
+Expected outcome:
+Marks the fifth task in your list as not done, or informs you that the index is out of range.
+
+### `delete` - stop tracking a task
+
+Command: `delete `
+
+where `` is the corresponding number of the task when displayed with `list`.
+
+Example of usage:
+`delete 5`
+
+Expected outcome:
+Removes the task that was the fifth task before deletion, or informs you that the index is out of range.
+
+### `todo` - create a task with a simple description
+
+Command: `todo `
+where `` is any text without `/` (character reserved for commands).
-### `Keyword` - Describe action
+Example of usage:
+`todo tP brainstorming`
+
+Expected outcome:
+Starts tracking a task called "tP brainstorming".
+
+### `deadline` - create a task with a deadline
+
+Command: `deadline /by `
+
+where `` is any text without `/` (character reserved for commands),
+and `` is a date in `YYYY-MM-DD` format.
+
+Example of usage:
+`deadline iP is due /by 2023-09-22`
+
+Expected outcome:
+Starts tracking a task called "iP is due" that has a due date that is 22nd September, 2023.
+
+### `event` - create a task with start and end dates
+
+Command: `event /from /to `
+
+where `` is any text without `/` (character reserved for commands),
+and `` is a date in `YYYY-MM-DD` format.
+
+Example of usage:
+`event iP-related panic /from 2023-09-15 /to 2023-09-22`
+
+Expected outcome:
+Starts tracking an event called "iP-related panic" that started on 15th September 2023, and is still ongoing at the time of writing.
+
+### `edit` - change the details of a task
-Describe the action and its outcome.
+Command: `edit / `
+
+where `` is the corresponding number of the task when displayed with `list`,
+`` is a word describing the specific information to change (see elaboration and examples below),
+and `value` is any text without `/` (character reserved for commands), or a date in `YYYY-MM-DD` format (depending on the ``).
+
+Other than the description, which uses the `desc` parameter, all other parameters follow those used during task creation.
+Edit does not change the type of task (does not change a `todo` to a `deadline`).
Example of usage:
+`edit 2 /by 2023-09-25`
+
+Expected outcome:
+If the second task in the list is a `deadline` with a due date of 22nd September, extends its due date back by three days to 25th September.
+
+### `find` - find tasks with matching descriptions
-`keyword (optional arguments)`
+Command: `find `
+
+where `` is any text without `/` (character reserved for commands).
+Matches partial descriptions as well.
+
+Example of usage:
+`find iP`
Expected outcome:
+Provides an unnumbered list of tasks whose descriptions contain the text searched, or informs you that there are no matches.
+
+### `bye` - close the application
-Description of the outcome.
+Command: `bye`
-```
-expected output
-```
+Expected outcome:
+Catbot says bye, and the application closes in a few seconds.
diff --git a/docs/Ui.png b/docs/Ui.png
new file mode 100644
index 0000000000..493d70743b
Binary files /dev/null and b/docs/Ui.png differ
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..033e24c4cd
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..66c01cfeba
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000000..fcb6fca147
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000000..6689b85bee
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java
deleted file mode 100644
index 5d313334cc..0000000000
--- a/src/main/java/Duke.java
+++ /dev/null
@@ -1,10 +0,0 @@
-public class Duke {
- public static void main(String[] args) {
- String logo = " ____ _ \n"
- + "| _ \\ _ _| | _____ \n"
- + "| | | | | | | |/ / _ \\\n"
- + "| |_| | |_| | < __/\n"
- + "|____/ \\__,_|_|\\_\\___|\n";
- System.out.println("Hello from\n" + logo);
- }
-}
diff --git a/src/main/java/catbot/CatBotEntrypoint.java b/src/main/java/catbot/CatBotEntrypoint.java
new file mode 100644
index 0000000000..602f6f7dfa
--- /dev/null
+++ b/src/main/java/catbot/CatBotEntrypoint.java
@@ -0,0 +1,22 @@
+package catbot;
+
+import catbot.bot.CatBot;
+import catbot.io.CatBotJavaFxIo;
+import catbot.io.UserIo;
+import catbot.task.TaskList;
+
+/**
+ * Entrypoint for the CatBot Assistant.
+ * Contains a public static void main to run.
+ */
+public class CatBotEntrypoint {
+
+ public static void main(String[] args) {
+ CatBot catBot = new CatBot(new TaskList("Tasks.txt"));
+ UserIo userIo = new CatBotJavaFxIo();
+ userIo.initialize();
+ catBot.initialize(userIo);
+ userIo.takeoverExecutionLogic(catBot);
+ }
+
+}
diff --git a/src/main/java/catbot/bot/Bot.java b/src/main/java/catbot/bot/Bot.java
new file mode 100644
index 0000000000..5287aebc1d
--- /dev/null
+++ b/src/main/java/catbot/bot/Bot.java
@@ -0,0 +1,27 @@
+package catbot.bot;
+
+import catbot.io.UserIo;
+
+/**
+ * Object that contains the full supported feature set of an assistant.
+ */
+public interface Bot {
+
+ /**
+ * Sets up the bot for use.
+ * Intended to include the creation of commands.
+ *
+ * @param userIo the IO object the bot will use to communicate with the user.
+ */
+ void initialize(UserIo userIo);
+
+ /**
+ * Runs a command based on the given command and argument, if applicable.
+ * If a matching command does not exist, may run a default command.
+ * Any such behaviour is defined by the bot.
+ *
+ * @param commandArgumentStruct a struct containing information about the command to run, and its arguments.
+ */
+ void run(CommandArgumentStruct commandArgumentStruct);
+
+}
diff --git a/src/main/java/catbot/bot/CatBot.java b/src/main/java/catbot/bot/CatBot.java
new file mode 100644
index 0000000000..7e2e98354c
--- /dev/null
+++ b/src/main/java/catbot/bot/CatBot.java
@@ -0,0 +1,193 @@
+package catbot.bot;
+
+import java.util.Optional;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+import catbot.internal.CommandMap;
+import catbot.internal.CommandPattern;
+import catbot.internal.NamedParameterMap;
+import catbot.io.ErrorIndicatorIo;
+import catbot.io.UserIo;
+import catbot.task.Deadline;
+import catbot.task.Event;
+import catbot.task.Task;
+import catbot.task.TaskList;
+import catbot.task.Todo;
+
+/**
+ * Main runnable class for the CatBot assistant.
+ */
+public class CatBot implements Bot {
+
+ //region Fields
+
+ private UserIo io;
+ private CommandMap commands;
+ private final TaskList taskList;
+
+ //endregion
+
+ //region Constructor
+
+ /**
+ * Constructs a CatBot using the provided TaskList.
+ *
+ * @param taskList taskList for CatBot to operate on.
+ */
+ public CatBot(TaskList taskList) {
+ this.taskList = taskList;
+ }
+
+ //endregion
+
+ //region Bot
+
+ @Override
+ public void initialize(UserIo userIo) {
+ initializeFields(userIo);
+ addSupportedCommandsToCommandMap();
+ }
+
+ @Override
+ public void run(CommandArgumentStruct commandArgumentStruct) {
+ if (commandArgumentStruct == null) {
+ return;
+ }
+ commands.run(commandArgumentStruct.getCommand(), commandArgumentStruct.getArgument());
+ }
+
+ //endregion
+
+ //region Internal Helpers
+
+ private void initializeFields(UserIo userIo) {
+ this.io = userIo;
+ this.commands = new CommandMap();
+ assert taskList != null;
+ }
+
+ private void addSupportedCommandsToCommandMap() {
+ /*
+ * TLDR: this method is a list of commands, and therefore follows list logic.
+ * The following method is really long, but does not compromise readability in my opinion.
+ * It works like a list of entries, and the entries are independent except for bi-consumers,
+ * which acts to reduce redundant copy-pasting.
+ * PATTERNS: command patterns are created using generators.
+ * BI-CONSUMERS: bi-consumers are created out of otherwise repetitive combinations of patterns and behaviours.
+ * eg: runIfValidIndexElseIndicateError uses an integerPattern and runs it through taskList's
+ * ifValidIndexElse. Such behaviour is necessary for all simple modifications (mark, unmark, delete).
+ * COMMANDS: every command is independent. If a command is giving a problem, look for the string that identifies
+ * the command, and debug from there. All other commands are irrelevant to the debugging of that
+ * command, and there is no higher-level interpretation of flow necessary.
+ * */
+
+ CommandPattern integerPattern = CatBotCommandPatterns.getIntegerPatternGenerator()
+ .generateUsingDefault(io::indicateInvalidInteger);
+ CommandPattern slashPattern = CatBotCommandPatterns.getSlashPatternGenerator()
+ .generateUsingDefault(CatBotCommandPatterns.NO_DEFAULT);
+ CommandPattern stringPattern = CatBotCommandPatterns.getStringPatternGenerator()
+ .generateUsingDefault(CatBotCommandPatterns.NO_DEFAULT);
+
+ commands.setDefaultCommand(io::indicateInvalidCommand)
+ .addCommand("bye", args -> {
+ io.cleanup();
+ prepareToClose();
+ })
+ .addCommand("list", args -> io.displayTaskList(taskList));
+
+ // User doing simple modification to existing tasks (through IntegerPattern, and Index)
+ // noinspection FunctionalExpressionCanBeFolded for better readability
+ BiConsumer> runIfValidIndexElseIndicateError = (args, lambda) ->
+ integerPattern.ifParsableElseDefault(args,
+ integer -> taskList.ifValidIndexElse(integer,
+ validIndex -> lambda.accept(validIndex),
+ invalidIndex -> io.indicateInvalidIndex(invalidIndex, taskList.getIndexBounds())
+ ));
+
+ //noinspection SpellCheckingInspection for "unmark"
+ commands.addCommand("mark",
+ string -> runIfValidIndexElseIndicateError.accept(string,
+ validIndex -> {
+ taskList.markTask(validIndex);
+ io.displayTaskModified(taskList, validIndex);
+ }
+ )
+ )
+ .addCommand("unmark",
+ string -> runIfValidIndexElseIndicateError.accept(string,
+ validIndex -> {
+ taskList.unmarkTask(validIndex);
+ io.displayTaskModified(taskList, validIndex);
+ }
+ )
+ )
+ .addCommand("delete",
+ string -> runIfValidIndexElseIndicateError.accept(string,
+ validIndex -> io.displayTaskDeleted(taskList.removeTask(validIndex))
+ )
+ );
+
+ // User creating new tasks (with SlashPattern)
+ BiConsumer,
+ Optional>>
+ createTaskIfValidElseWarn = (args, bifunction) -> slashPattern.ifParsableElseDefault(args,
+ namedParameterMap -> bifunction.apply(
+ namedParameterMap,
+ io::indicateArgumentInvalid
+ ).ifPresent(task -> {
+ taskList.addTask(task);
+ io.displayTaskAdded(taskList);
+ })
+ );
+
+ commands.addCommand("todo",
+ args -> createTaskIfValidElseWarn.accept(args, Todo::createIfValidElse)
+ )
+ .addCommand("event",
+ args -> createTaskIfValidElseWarn.accept(args, Event::createIfValidElse)
+ )
+ .addCommand("deadline",
+ args -> createTaskIfValidElseWarn.accept(args, Deadline::createIfValidElse)
+ );
+
+ // User filtering for tasks
+ commands.addCommand("find",
+ args -> stringPattern.ifParsableElseDefault(args,
+ str -> io.displayTaskListWithoutNumber(taskList.findInDescriptions(str)))
+ );
+
+ // User editing tasks (with more control)
+
+ BiConsumer>
+ editTaskIfValidIndexElseIndicate = (string, biconsumer) -> slashPattern.ifParsableElseDefault(string,
+ map -> runIfValidIndexElseIndicateError.accept(map.remove(""),
+ integer -> biconsumer.accept(integer, map)
+ )
+ );
+
+ commands.addCommand("edit",
+ str -> editTaskIfValidIndexElseIndicate.accept(str, (index, map) -> {
+ taskList.editTask(index, map);
+ io.displayTaskModified(taskList, index);
+ })
+ );
+
+
+ }
+
+ private void prepareToClose() {
+ // Helped by ChatGPT
+ ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
+ executorService.schedule(() -> System.exit(0), 2, TimeUnit.SECONDS);
+ executorService.shutdown();
+ }
+
+ //endregion
+
+}
diff --git a/src/main/java/catbot/bot/CatBotCommandPatterns.java b/src/main/java/catbot/bot/CatBotCommandPatterns.java
new file mode 100644
index 0000000000..cf695067fe
--- /dev/null
+++ b/src/main/java/catbot/bot/CatBotCommandPatterns.java
@@ -0,0 +1,103 @@
+package catbot.bot;
+
+import java.util.function.Consumer;
+
+import catbot.internal.CommandPattern;
+import catbot.internal.CommandPatternGenerator;
+import catbot.internal.NamedParameterMap;
+import catbot.internal.Parser;
+
+/**
+ * Class containing {@link CommandPattern CommandPatterns} and {@link CommandPatternGenerator CommandPatternGenerators}
+ * used by the CatBot Assistant.
+ */
+public abstract class CatBotCommandPatterns {
+
+ //region Fields
+
+ /**
+ * A consumer that throws an error when called.
+ * Intended to assert that a CommandPattern does not result in fallthrough behaviour;
+ * ie all possible inputs are handled.
+ */
+ public static final Consumer NO_DEFAULT = s -> {
+ assert false; //SHOULD NOT BE CALLED
+ };
+ private static final IntegerPatternGenerator integerPatternGenerator = new IntegerPatternGenerator();
+ private static final SlashArgumentPatternGenerator slashPatternGenerator = new SlashArgumentPatternGenerator();
+ private static final StringPatternGenerator stringPatternGenerator = new StringPatternGenerator();
+
+ //endregion
+
+ //region Integer Pattern
+
+ /**
+ * Gets a singleton instance of {@link IntegerPatternGenerator}.
+ * @return the generator.
+ */
+ public static CommandPatternGenerator getIntegerPatternGenerator() {
+ return integerPatternGenerator;
+ }
+ private static class IntegerPatternGenerator implements CommandPatternGenerator {
+
+ @Override
+ public CommandPattern generateUsingDefault(Consumer invalidInput) {
+ return (args, consumer) -> {
+ try {
+ consumer.accept(Integer.parseInt(args));
+ } catch (NumberFormatException nfe) {
+ invalidInput.accept(args);
+ }
+ };
+ }
+ }
+ //endregion
+
+ //region Slash Arguments Pattern
+
+ /**
+ * Gets a singleton instance of {@link SlashArgumentPatternGenerator}.
+ * @return the generator.
+ */
+ public static CommandPatternGenerator getSlashPatternGenerator() {
+ return slashPatternGenerator;
+ }
+
+ private static class SlashArgumentPatternGenerator implements CommandPatternGenerator {
+
+ @Override
+ public CommandPattern generateUsingDefault(Consumer ignored) {
+ return new CommandPattern<>() {
+
+ private final Parser slashParser = Parser.with("/", true);
+
+ @Override
+ public void ifParsableElseDefault(String args, Consumer consumer) {
+ consumer.accept(slashParser.parse(args));
+ }
+ };
+ }
+ }
+
+ //endregion
+
+ //region String Pattern
+
+ /**
+ * Gets a singleton instance of {@link StringPatternGenerator}.
+ * @return the generator.
+ */
+ public static CommandPatternGenerator getStringPatternGenerator() {
+ return stringPatternGenerator;
+ }
+
+ private static class StringPatternGenerator implements CommandPatternGenerator {
+
+ @Override
+ public CommandPattern generateUsingDefault(Consumer ignored) {
+ return (args, consumer) -> consumer.accept(args);
+ }
+ }
+
+ //endregion
+}
diff --git a/src/main/java/catbot/bot/CommandArgumentStruct.java b/src/main/java/catbot/bot/CommandArgumentStruct.java
new file mode 100644
index 0000000000..61666bfdc9
--- /dev/null
+++ b/src/main/java/catbot/bot/CommandArgumentStruct.java
@@ -0,0 +1,38 @@
+package catbot.bot;
+
+/**
+ * Simple container for a command and argument pair, both stored as Strings.
+ */
+public class CommandArgumentStruct {
+ private final String command;
+ private final String argument;
+
+ /**
+ * Constructor for a CommandArgumentStruct.
+ *
+ * @param command string that represents the command to run.
+ * @param argument string that represents the argument for the command.
+ */
+ public CommandArgumentStruct(String command, String argument) {
+ this.command = command;
+ this.argument = argument;
+ }
+
+ /**
+ * Retrieves the argument to be given while running the command.
+ *
+ * @return the argument.
+ */
+ public String getArgument() {
+ return argument;
+ }
+
+ /**
+ * Retrieves the command to run, as a String.
+ *
+ * @return the command.
+ */
+ public String getCommand() {
+ return command;
+ }
+}
diff --git a/src/main/java/catbot/internal/Bounds.java b/src/main/java/catbot/internal/Bounds.java
new file mode 100644
index 0000000000..88621332f4
--- /dev/null
+++ b/src/main/java/catbot/internal/Bounds.java
@@ -0,0 +1,49 @@
+package catbot.internal;
+
+/**
+ * Represents a range of integers by keeping track of their lower and upper bounds.
+ */
+public class Bounds {
+ private final int lowerBound;
+ private final int upperBound;
+
+ /**
+ * Constructs a bound using the provided lower and upper bounds, inclusive.
+ * If lowerBound is greater than upper bound, the bound does not contain any integer.
+ *
+ * @param lowerBound lower bound of the integer range.
+ * @param upperBound upper bound of the inteer range.
+ */
+ public Bounds(int lowerBound, int upperBound) {
+ this.lowerBound = lowerBound;
+ this.upperBound = upperBound;
+ }
+
+ /**
+ * Checks if the provided integer is within the bounds.
+ *
+ * @param i integer to check.
+ * @return true if between lower and upper bounds, inclusive.
+ */
+ public boolean contains(int i) {
+ return this.lowerBound <= i && i <= this.upperBound;
+ }
+
+ /**
+ * Retrieves the inclusive lower bound of the Bound.
+ *
+ * @return lower bound, inclusive.
+ */
+ public int getLower() {
+ return lowerBound;
+ }
+
+ /**
+ * Retrieves the inclusive upper bound of the Bound.
+ *
+ * @return upper bound, inclusive.
+ */
+ public int getUpper() {
+ return upperBound;
+ }
+}
diff --git a/src/main/java/catbot/internal/Command.java b/src/main/java/catbot/internal/Command.java
new file mode 100644
index 0000000000..e51a53c5b5
--- /dev/null
+++ b/src/main/java/catbot/internal/Command.java
@@ -0,0 +1,13 @@
+package catbot.internal;
+
+/**
+ * A functional interface for delayed and dynamic execution of commands.
+ */
+public interface Command {
+
+ /**
+ * Runs command.
+ * @param args string representing arguments to pass to the command.
+ */
+ void run(String args);
+}
diff --git a/src/main/java/catbot/internal/CommandMap.java b/src/main/java/catbot/internal/CommandMap.java
new file mode 100644
index 0000000000..37096d1e5b
--- /dev/null
+++ b/src/main/java/catbot/internal/CommandMap.java
@@ -0,0 +1,52 @@
+package catbot.internal;
+
+import java.util.HashMap;
+
+/**
+ * Object to store commands as key-value pairs, intended for use specifically with text-triggered functionality.
+ * Key is intended to be a String used to invoke its corresponding functionality.
+ * Value is a Command, which is a functional interface.
+ */
+public class CommandMap {
+
+ private final HashMap commandMap = new HashMap<>();
+ private Command defaultCommand;
+ //endregion
+
+ /**
+ * Adds an invocation-command pair to the map. Supports pipelining.
+ *
+ * @param invocation the String that calls the command.
+ * @param lambda the Command that triggers through the corresponding String.
+ * @return this, for pipelining
+ */
+ public CommandMap addCommand(String invocation, Command lambda) {
+ commandMap.put(invocation, lambda);
+ return this;
+ }
+
+ /**
+ * Initializes or replaces the default command that runs when an invocation does not match any command.
+ *
+ * @param defaultCommand Command to run.
+ * @return this, for pipelining.
+ */
+ public CommandMap setDefaultCommand(Command defaultCommand) {
+ this.defaultCommand = defaultCommand;
+ return this;
+ }
+
+ /**
+ * Runs the command corresponding to the invocation, passing it a String argument.
+ *
+ * @param invocation String to use as the key value to retrieve the relevant Command.
+ * @param argument String to pass as an argument to the Command.
+ */
+ public void run(String invocation, String argument) {
+ if (commandMap.containsKey(invocation)) {
+ commandMap.get(invocation).run(argument);
+ } else if (defaultCommand != null) {
+ defaultCommand.run(invocation);
+ }
+ }
+}
diff --git a/src/main/java/catbot/internal/CommandPattern.java b/src/main/java/catbot/internal/CommandPattern.java
new file mode 100644
index 0000000000..ffb3a04c26
--- /dev/null
+++ b/src/main/java/catbot/internal/CommandPattern.java
@@ -0,0 +1,21 @@
+package catbot.internal;
+
+import java.util.function.Consumer;
+
+/**
+ * Object that parses a String using a predefined pattern, and has a default behaviour if the pattern does not apply.
+ *
+ * @param type of output generated by the String if the pattern applies, to be fed to a Consumer.
+ */
+public interface CommandPattern {
+
+ /**
+ * Runs the given Consumer if the pattern applies to the given String, feeding the output generated to the Consumer.
+ * Otherwise, runs the default behaviour.
+ *
+ * @param args String to apply the pattern to.
+ * @param consumer Consumer that accepts the output generated by successful pattern application.
+ */
+ void ifParsableElseDefault(String args, Consumer consumer);
+
+}
diff --git a/src/main/java/catbot/internal/CommandPatternGenerator.java b/src/main/java/catbot/internal/CommandPatternGenerator.java
new file mode 100644
index 0000000000..211b0ae759
--- /dev/null
+++ b/src/main/java/catbot/internal/CommandPatternGenerator.java
@@ -0,0 +1,23 @@
+package catbot.internal;
+
+import java.util.function.Consumer;
+
+/**
+ * Object that is used to generate a CommandPattern.
+ * Meant to allow a lambda to be passed to the pattern on creation, while allowing it to be a functional interface.
+ *
+ * @param return type of the generated CommandPattern.
+ * @see CommandPattern type of object generated.
+ */
+public interface CommandPatternGenerator {
+
+ /**
+ * Returns a CommandPattern created with the provided Consumer as a fallthrough option.
+ * The default consumer will be run if the pattern cannot be applied to the provided string.
+ *
+ * @param invalidInput the default Consumer that accepts the provided command, if the pattern cannot be applied.
+ * @return CommandPattern generated.
+ * @see CommandPattern
+ */
+ CommandPattern generateUsingDefault(Consumer invalidInput);
+}
diff --git a/src/main/java/catbot/internal/NamedParameterMap.java b/src/main/java/catbot/internal/NamedParameterMap.java
new file mode 100644
index 0000000000..ff2e71814e
--- /dev/null
+++ b/src/main/java/catbot/internal/NamedParameterMap.java
@@ -0,0 +1,108 @@
+package catbot.internal;
+
+import java.util.HashMap;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Maps Strings to Strings with limited functionality,
+ * suitable to be used as small containers for parameter-argument pairs.
+ */
+public class NamedParameterMap {
+
+ private final HashMap parameters;
+
+ /**
+ * Constructor for a NamedParameterMap.
+ */
+ public NamedParameterMap() {
+ parameters = new HashMap<>();
+ }
+
+ /**
+ * Checks if the map has a value mapped to the given key.
+ *
+ * @param key the key for which to check if a corresponding value exists.
+ * @return true if a value exists for the key, false otherwise.
+ */
+ public boolean containsKey(String key) {
+ return parameters.containsKey(key);
+ }
+
+ /**
+ * Changes the key of a value to a new key, if the corresponding value exists.
+ *
+ * @param oldKey key to identify the value to change the key of.
+ * @param newKey new key to associate the value to.
+ */
+ public void moveToNewKey(String oldKey, String newKey) {
+ if (!parameters.containsKey(newKey) && parameters.containsKey(oldKey)) {
+ parameters.put(newKey, parameters.get(oldKey));
+ parameters.remove(oldKey);
+ }
+ }
+
+ /**
+ * Removes a key-value pair from the map.
+ *
+ * @param key key of the key-value pair to remove.
+ * @return value of the removed key-value pair.
+ */
+ public String remove(String key) {
+ if (!containsKey(key)) {
+ return null;
+ }
+
+ return parameters.remove(key);
+ }
+
+ /**
+ * Retrieves a Set of all keys.
+ *
+ * @return set of all keys.
+ */
+ public Set keySet() {
+ return parameters.keySet();
+ }
+
+ /**
+ * Retrieves the value corresponding to the given key.
+ *
+ * @param key key of the key-value pair to find.
+ * @return value of the key-value pair found, null otherwise.
+ */
+ public String get(String key) {
+ return parameters.get(key);
+ }
+
+ /**
+ * Adds a parameter and its argument as a key-value pair into the NamedParameterMap.
+ *
+ * @param parameterName key with which the specified value is to be associated
+ * @param parameterValue value to be associated with the specified key
+ * @return this, for pipelining
+ */
+ public NamedParameterMap addNamedParameter(String parameterName, String parameterValue) {
+ parameters.put(parameterName, parameterValue);
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ // full credit to IntelliJ, which is smarter than I am
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ NamedParameterMap that = (NamedParameterMap) o;
+ return Objects.equals(parameters, that.parameters);
+ }
+
+ @Override
+ public int hashCode() {
+ // full credit to IntelliJ, which is smarter than I am
+ return Objects.hash(parameters);
+ }
+}
diff --git a/src/main/java/catbot/internal/ObjectStorage.java b/src/main/java/catbot/internal/ObjectStorage.java
new file mode 100644
index 0000000000..de73469ccf
--- /dev/null
+++ b/src/main/java/catbot/internal/ObjectStorage.java
@@ -0,0 +1,34 @@
+package catbot.internal;
+
+import java.util.function.Supplier;
+
+/**
+ * Object that stores and retrieves a specific type of object to disk.
+ *
+ * @param type of object to read and write.
+ */
+public interface ObjectStorage {
+
+ /**
+ * Writes the given object to disk.
+ *
+ * @param object object to write.
+ */
+ void write(T object);
+
+ /**
+ * Reads and returns a given object if possible, otherwise calls the default supplier.
+ *
+ * @return stored object if successful, provided object otherwise.
+ * @see java.util.Map#getOrDefault for inspiration.
+ */
+ T readOrDefault();
+
+ /**
+ * Provides a default supplier to supply a default value if an issue occurs while reading.
+ *
+ * @param supplier to provide default value.
+ */
+ void setDefault(Supplier supplier);
+
+}
diff --git a/src/main/java/catbot/internal/Parser.java b/src/main/java/catbot/internal/Parser.java
new file mode 100644
index 0000000000..0350c19890
--- /dev/null
+++ b/src/main/java/catbot/internal/Parser.java
@@ -0,0 +1,103 @@
+package catbot.internal;
+
+import java.util.Arrays;
+
+/**
+ * Object that parses a String into a NamedParameterMap.
+ */
+public class Parser {
+
+ private final String delimiter;
+ private final boolean willKeepEmptyArgument;
+
+ //region Constructor
+
+ private Parser(String delimiter, boolean willKeepEmptyArgument) {
+ this.delimiter = delimiter;
+ this.willKeepEmptyArgument = willKeepEmptyArgument;
+ }
+
+ /**
+ * Returns a parser that uses the provided String as a delimiter.
+ *
+ * @param delimiter String to use as a trigger to start new commands.
+ * @return Parser constructed with the provided delimiter.
+ */
+ public static Parser with(String delimiter) {
+ return with(delimiter, false);
+ }
+
+ /**
+ * Returns a parser that uses the provided String as a delimiter.
+ *
+ * @param delimiter String to use as a trigger to start new commands.
+ * @param willKeepEmptyArgument true if the first parameter should have an empty name.
+ * @return Parser constructed with the provided delimiter.
+ */
+ public static Parser with(String delimiter, boolean willKeepEmptyArgument) {
+ if (delimiter == null || delimiter.isEmpty()) {
+ return new SingleParser();
+ } else {
+ return new Parser(delimiter, willKeepEmptyArgument);
+ }
+ }
+ //endregion
+
+ private static class SingleParser extends Parser {
+
+ private SingleParser() {
+ super(null, false);
+ }
+
+ @Override
+ public NamedParameterMap parse(String s) {
+ NamedParameterMap map = new NamedParameterMap();
+ parseCommandArgumentString(s, map);
+ return map;
+ }
+ }
+
+ /**
+ * Applies the parser to the given String, and return a representation of parameter-argument pairs.
+ *
+ * @param s String to parse.
+ * @return NamedParameterMap with commands as keys, and arguments as values.
+ */
+ public NamedParameterMap parse(String s) {
+ NamedParameterMap map = new NamedParameterMap();
+ String[] commandArgumentStrings = s.split(delimiter);
+
+ //first command potentially gets the empty argument treatment ("" -> value)
+ parseCommandArgumentString(commandArgumentStrings[0], map, this.willKeepEmptyArgument);
+ Arrays.stream(commandArgumentStrings).skip(1).forEach(segment -> parseCommandArgumentString(segment, map));
+ return map;
+ }
+
+ //region Internal Helpers
+
+ /**
+ * Splits string into one pair of command + argument based on the first whitespace.
+ *
+ * @param s string containing both command and argument, in that order
+ * @param map to store the mapping between command and argument
+ */
+ private static void parseCommandArgumentString(String s, NamedParameterMap map) {
+ String[] splitCommandArgument = s.split("\\s", 2);
+ if (splitCommandArgument.length == 2) {
+ map.addNamedParameter(splitCommandArgument[0].trim(), splitCommandArgument[1].trim());
+ } else {
+ map.addNamedParameter(splitCommandArgument[0].trim(), "");
+ }
+ }
+
+ private static void parseCommandArgumentString(String s, NamedParameterMap map, boolean willKeepEmptyArgument) {
+ if (willKeepEmptyArgument) {
+ map.addNamedParameter("", s.trim());
+ } else {
+ parseCommandArgumentString(s, map);
+ }
+ }
+
+ //endregion
+
+}
diff --git a/src/main/java/catbot/io/CatBotJavaFxApplication.java b/src/main/java/catbot/io/CatBotJavaFxApplication.java
new file mode 100644
index 0000000000..93c1e1bf07
--- /dev/null
+++ b/src/main/java/catbot/io/CatBotJavaFxApplication.java
@@ -0,0 +1,45 @@
+package catbot.io;
+
+import java.io.IOException;
+
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.layout.AnchorPane;
+import javafx.stage.Stage;
+
+/**
+ * Application that creates a JavaFX UI.
+ */
+public class CatBotJavaFxApplication extends Application {
+
+ private static CatbotJavaFxController lastCreatedController;
+
+ /**
+ * Retrieves the last created controller instance.
+ * Intended to be accessed after application launch.
+ *
+ * @return controller from the latest started CatBotJavaFxApplication object.
+ */
+ static CatbotJavaFxController getLastCreatedController() {
+ return lastCreatedController;
+ }
+
+ @Override
+ public void start(Stage stage) {
+ // https://se-education.org/guides/tutorials/javaFx.html
+ try {
+ FXMLLoader fxmlLoader =
+ new FXMLLoader(CatBotJavaFxApplication.class.getResource("/view/MainWindow.fxml"));
+ AnchorPane ap = fxmlLoader.load();
+ Scene scene = new Scene(ap);
+ stage.setScene(scene);
+ lastCreatedController = fxmlLoader.getController();
+ stage.show();
+ CatBotJavaFxIo.getLastApplicationLaunchPoint().initializeAfterFxml();
+ } catch (IOException ignored) { //noinspection UnnecessarySemicolon
+ ;
+ }
+ }
+
+}
diff --git a/src/main/java/catbot/io/CatBotJavaFxIo.java b/src/main/java/catbot/io/CatBotJavaFxIo.java
new file mode 100644
index 0000000000..43f8fcfb04
--- /dev/null
+++ b/src/main/java/catbot/io/CatBotJavaFxIo.java
@@ -0,0 +1,187 @@
+package catbot.io;
+
+import catbot.bot.Bot;
+import catbot.internal.Bounds;
+import catbot.internal.NamedParameterMap;
+import catbot.task.Task;
+import catbot.task.TaskList;
+import javafx.application.Application;
+
+/**
+ * UserIo that operates through JavaFX.
+ */
+public class CatBotJavaFxIo implements UserIo {
+
+ //region Constants
+
+ //http://www.patorjk.com/software/taag/#p=display&h=1&f=3D-ASCII&t=CAT%20BOT
+ //font: 3D-ASCII; Character Width: Fitted; Character Height: Default; Text: CAT BOT
+ public static final String NAME =
+ " ________ ________ _________ ________ ________ _________ \n"
+ + "|\\ ____\\ |\\ __ \\ |\\___ ___\\ |\\ __ \\ |\\ __ \\ |\\___ ___\\ \n"
+ + "\\ \\ \\___| \\ \\ \\|\\ \\\\|___ \\ \\_| \\ \\ \\|\\ /_\\ \\ \\|\\ \\\\|___ \\ \\_| \n"
+ + " \\ \\ \\ \\ \\ __ \\ \\ \\ \\ \\ \\ __ \\\\ \\ \\\\\\ \\ \\ \\ \\ \n"
+ + " \\ \\ \\____ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\|\\ \\\\ \\ \\\\\\ \\ \\ \\ \\ \n"
+ + " \\ \\_______\\\\ \\__\\ \\__\\ \\ \\__\\ \\ \\_______\\\\ \\_______\\ \\ \\__\\\n"
+ + " \\|_______| \\|__|\\|__| \\|__| \\|_______| \\|_______| \\|__|\n";
+
+ //endregion
+
+ //region Fields
+ private static CatBotJavaFxIo lastApplicationLaunchPoint;
+
+ private CatbotJavaFxController controller;
+ private volatile boolean isStillOpen = true;
+ private Bot bot;
+
+ //endregion
+
+ //region UserIo
+
+ @Override
+ public void initialize() {
+ //initialization impossible without FXML; handled in takeoverExecutionLogic() instead
+ }
+
+ void initializeAfterFxml() {
+ controller = CatBotJavaFxApplication.getLastCreatedController();
+ send("Hiya! I'm\n" + NAME + "\n");
+ controller.attachConsumerForParsedCommands(bot::run);
+ controller.sendAssistantDialogue();
+ }
+
+ @Override
+ public void cleanup() {
+ send("Bye!");
+ this.isStillOpen = false;
+ }
+
+ @Override
+ public boolean isStillOpen() {
+ return this.isStillOpen;
+ }
+
+ @Override
+ public void takeoverExecutionLogic(Bot bot) {
+ this.bot = bot;
+
+ lastApplicationLaunchPoint = this;
+ Application.launch(CatBotJavaFxApplication.class, "");
+ }
+
+ //endregion
+
+ //region ErrorIndicatorIo
+
+ @Override
+ public void indicateInvalidCommand(String attemptedCommand) {
+ warn("idgi ;-;");
+ }
+
+ @Override
+ public void indicateInvalidInteger(String attemptedInteger) {
+ warn("that doesn't look like a number... number pls");
+ }
+
+ @Override
+ public void indicateInvalidIndex(int attemptedIndex, Bounds bounds) {
+ warn("i expected a number from " + bounds.getLower() + " to " + bounds.getUpper() + "...");
+ }
+
+ @Override
+ public void indicateArgumentInvalid(InvalidArgumentState invalidState, NamedParameterMap namedParameterMap) {
+ switch (invalidState) {
+ case PARAMETER_EMPTY:
+ for (String arg : namedParameterMap.keySet()) {
+ warn(arg + " is empty");
+ }
+ send("please make sure these arguments are filled!");
+ break;
+ case PARAMETER_MISSING:
+ for (String arg : namedParameterMap.keySet()) {
+ warn(arg + " is missing");
+ }
+ send("please make sure to include them next time!");
+ break;
+ case NOT_A_DATE:
+ for (String arg : namedParameterMap.keySet()) {
+ warn(arg + " is set to \"" + namedParameterMap.get(arg) + "\", which is not a date!");
+ }
+ break;
+ default:
+ throw new RuntimeException();
+ }
+ }
+
+ //endregion
+
+ //region TaskAssistantIo
+
+ @Override
+ public void displayTaskList(TaskList taskList) {
+ if (taskList == null) {
+ return;
+ }
+ if (taskList.size() == 0) {
+ send("it's empty rn...");
+ return;
+ }
+
+ int i = 1;
+ int intlen = 0;
+ for (int len = taskList.size(); len > 0; intlen++) {
+ len /= 10;
+ }
+ for (String taskString : taskList.getTaskStrings()) {
+ send(String.format("%" + intlen + "d", i++) + ". " + taskString);
+ }
+ }
+
+ @Override
+ public void displayTaskListWithoutNumber(TaskList taskList) {
+ if (taskList == null) {
+ return;
+ }
+ if (taskList.size() == 0) {
+ send("there are no tasks to see...");
+ return;
+ }
+ for (String taskString : taskList.getTaskStrings()) {
+ send(String.format("- " + taskString));
+ }
+ }
+
+ @Override
+ public void displayTaskAdded(TaskList taskList) {
+ int index = taskList.size() - 1;
+ send("Added: " + (index + 1) + ". " + taskList.getTask(index));
+ }
+
+ @Override
+ public void displayTaskDeleted(Task deleted) {
+ send("Deleted: " + deleted);
+ }
+
+ @Override
+ public void displayTaskModified(TaskList taskList, int index) {
+ send((index + 1) + ". " + taskList.getTask(index));
+ }
+
+ //endregion
+
+ //region Internal Helper
+
+ static CatBotJavaFxIo getLastApplicationLaunchPoint() {
+ return lastApplicationLaunchPoint;
+ }
+
+ private void send(String s) {
+ controller.queueAssistantDialogue(s);
+ }
+
+ private void warn(String s) {
+ send("! " + s);
+ }
+
+ //endregion
+}
diff --git a/src/main/java/catbot/io/CatbotJavaFxController.java b/src/main/java/catbot/io/CatbotJavaFxController.java
new file mode 100644
index 0000000000..d7d4cef421
--- /dev/null
+++ b/src/main/java/catbot/io/CatbotJavaFxController.java
@@ -0,0 +1,91 @@
+package catbot.io;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import catbot.bot.CommandArgumentStruct;
+import catbot.internal.NamedParameterMap;
+import catbot.internal.Parser;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextField;
+import javafx.scene.image.Image;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.VBox;
+
+/**
+ * Controller for MainWindow. Provides the layout for the other controls.
+ */
+public class CatbotJavaFxController extends AnchorPane {
+ @FXML
+ private ScrollPane scrollPane;
+ @FXML
+ private VBox dialogContainer;
+ @FXML
+ private TextField userInput;
+ @FXML
+ private Button sendButton;
+ private final Parser parser = Parser.with(null);
+
+ private Consumer commandConsumer;
+ private StringBuilder queuedAssistantOutput = new StringBuilder();
+
+ private final Image userImage = new Image(Objects.requireNonNull(
+ this.getClass().getResourceAsStream("/images/DaUser.png")));
+ private final Image dukeImage = new Image(Objects.requireNonNull(
+ this.getClass().getResourceAsStream("/images/DaDuke.png")));
+
+ @FXML
+ public void initialize() {
+ scrollPane.vvalueProperty().bind(dialogContainer.heightProperty());
+ }
+
+ void attachConsumerForParsedCommands(Consumer consumer) {
+ this.commandConsumer = consumer;
+ }
+
+ @FXML
+ private void handleUserInput() {
+ if (commandConsumer == null) {
+ return;
+ }
+
+ String input = getUserInput();
+ addUserDialog(input);
+ CommandArgumentStruct command = parseStringToStruct(input);
+ commandConsumer.accept(command);
+ sendAssistantDialogue();
+ }
+
+ private String getUserInput() {
+ String input = userInput.getText();
+ userInput.clear();
+ return input;
+ }
+
+ private CommandArgumentStruct parseStringToStruct(String commandString) {
+ NamedParameterMap namedParameterMap = parser.parse(commandString);
+ assert namedParameterMap.keySet().size() == 1;
+ for (String command : namedParameterMap.keySet()) {
+ return new CommandArgumentStruct(command, namedParameterMap.get(command));
+ }
+ return null;
+ }
+
+ void addUserDialog(String text) {
+ dialogContainer.getChildren().add(DialogBox.getUserDialog(text, userImage));
+ }
+
+ void queueAssistantDialogue(String text) {
+ if (!queuedAssistantOutput.toString().isEmpty()) {
+ queuedAssistantOutput.append("\n");
+ }
+ queuedAssistantOutput.append(text);
+ }
+
+ void sendAssistantDialogue() {
+ dialogContainer.getChildren().add(DialogBox.getDukeDialog(queuedAssistantOutput.toString(), dukeImage));
+ queuedAssistantOutput = new StringBuilder();
+ }
+}
diff --git a/src/main/java/catbot/io/DialogBox.java b/src/main/java/catbot/io/DialogBox.java
new file mode 100644
index 0000000000..a435e35bdb
--- /dev/null
+++ b/src/main/java/catbot/io/DialogBox.java
@@ -0,0 +1,67 @@
+package catbot.io;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+
+/**
+ * An example of a custom control using FXML.
+ * This control represents a dialog box consisting of an ImageView to represent the speaker's face and a label
+ * containing text from the speaker.
+ */
+public class DialogBox extends HBox {
+ @FXML
+ private Label dialog;
+ @FXML
+ private ImageView displayPicture;
+
+ private DialogBox(String text, Image img) {
+ try {
+ FXMLLoader fxmlLoader =
+ new FXMLLoader(CatbotJavaFxController.class.getResource("/view/DialogBox.fxml"));
+ fxmlLoader.setController(this);
+ fxmlLoader.setRoot(this);
+ fxmlLoader.load();
+ } catch (IOException ignored) { //noinspection UnnecessarySemicolon
+ ;
+ }
+
+ dialog.setText(text);
+ displayPicture.setImage(img);
+ }
+
+ /**
+ * Flips the dialog box such that the ImageView is on the left and text on the right.
+ */
+ private void flip() {
+ ObservableList tmp = FXCollections.observableArrayList(this.getChildren());
+ Collections.reverse(tmp);
+ getChildren().setAll(tmp);
+ setAlignment(Pos.CENTER_LEFT);
+ }
+
+ public static DialogBox getUserDialog(String text, Image img) {
+ DialogBox db = new DialogBox(text, img);
+ db.setStyle(
+ db.getStyle().replaceFirst("-fx-background-color: \\w+;",
+ "-fx-background-color: lightsteelblue;")
+ );
+ return db;
+ }
+
+ public static DialogBox getDukeDialog(String text, Image img) {
+ DialogBox db = new DialogBox(text, img);
+ db.flip();
+ return db;
+ }
+}
diff --git a/src/main/java/catbot/io/ErrorIndicatorIo.java b/src/main/java/catbot/io/ErrorIndicatorIo.java
new file mode 100644
index 0000000000..042a151ba5
--- /dev/null
+++ b/src/main/java/catbot/io/ErrorIndicatorIo.java
@@ -0,0 +1,54 @@
+package catbot.io;
+
+import catbot.internal.Bounds;
+import catbot.internal.NamedParameterMap;
+
+/**
+ * An object that supports interacting with the user to communicate error states.
+ */
+public interface ErrorIndicatorIo {
+
+ /**
+ * Tells the user that the command they provided is not supported.
+ *
+ * @param attemptedCommand command that was attempted.
+ */
+ void indicateInvalidCommand(String attemptedCommand);
+
+ /**
+ * Tells the user that the string they provided is not an integer.
+ * Usually intended to also inform the user that an integer was expected instead.
+ *
+ * @param attemptedInteger string that could not be parsed as an integer.
+ */
+ void indicateInvalidInteger(String attemptedInteger);
+
+ /**
+ * Tells the user that the integer they provided could not be used as an index.
+ * Possibly also informs the user of the range of possible indices.
+ *
+ * @param attemptedIndex integer that was provided, that does not fall within expected bounds.
+ * @param bounds {@link Bounds Bounds} object signifying the minimum and maximum accepted indices.
+ */
+ void indicateInvalidIndex(int attemptedIndex, Bounds bounds);
+
+ /**
+ * Enum that identifies reason for the invalidity of arguments.
+ * Used as a default option when parameter-specific information is not required.
+ *
+ * @see ErrorIndicatorIo#indicateInvalidIndex example of invalid argument with parameter-specific information
+ */
+ enum InvalidArgumentState {
+ PARAMETER_EMPTY, PARAMETER_MISSING, NOT_A_DATE
+ }
+
+ /**
+ * Tells the user that the argument they provided is invalid.
+ * Handles all {@link InvalidArgumentState InvalidArgumentStates}.
+ *
+ * @param invalidState {@link InvalidArgumentState InvalidArgumentState} describing the reason for invalidity.
+ * @param namedParameterMap map with all invalid parameters as keys, and their respective arguments as values.
+ */
+ void indicateArgumentInvalid(InvalidArgumentState invalidState, NamedParameterMap namedParameterMap);
+
+}
diff --git a/src/main/java/catbot/io/TaskAssistantIo.java b/src/main/java/catbot/io/TaskAssistantIo.java
new file mode 100644
index 0000000000..335ea85984
--- /dev/null
+++ b/src/main/java/catbot/io/TaskAssistantIo.java
@@ -0,0 +1,48 @@
+package catbot.io;
+
+import catbot.task.Task;
+import catbot.task.TaskList;
+
+/**
+ * An object that supports interacting with the user to manage {@link Task Tasks} in a {@link TaskList TaskList}.
+ */
+public interface TaskAssistantIo {
+
+ /**
+ * Displays all Tasks in a TaskList with numbering.
+ *
+ * @param taskList TaskList containing Tasks to print.
+ */
+ void displayTaskList(TaskList taskList);
+
+ /**
+ * Displays all Tasks in a TaskList without numbering.
+ *
+ * @param taskList TaskList containing Tasks to print.
+ */
+ void displayTaskListWithoutNumber(TaskList taskList);
+
+ /**
+ * Displays that a Task was added to a list.
+ * Assumes the Task was added to the end of the TaskList.
+ *
+ * @param taskList TaskList to which the Task was added.
+ */
+ void displayTaskAdded(TaskList taskList);
+
+ /**
+ * Displays that a Task was deleted from its list.
+ *
+ * @param deleted Task that was deleted.
+ */
+ void displayTaskDeleted(Task deleted);
+
+ /**
+ * Displays that a Task was modified.
+ *
+ * @param taskList the list containing the edited Task.
+ * @param index the index of the modified task, in the given TaskList.
+ */
+ void displayTaskModified(TaskList taskList, int index);
+
+}
diff --git a/src/main/java/catbot/io/UserIo.java b/src/main/java/catbot/io/UserIo.java
new file mode 100644
index 0000000000..ef169a47c3
--- /dev/null
+++ b/src/main/java/catbot/io/UserIo.java
@@ -0,0 +1,36 @@
+package catbot.io;
+
+import catbot.bot.Bot;
+
+/**
+ * Object that implements the full expected functionality of IO expected of a CatBot assistant.
+ */
+public interface UserIo extends ErrorIndicatorIo, TaskAssistantIo {
+
+ /**
+ * Initializes IO channel.
+ * Intended to open resources, or send a welcome message.
+ * */
+ void initialize();
+
+ /**
+ * Cleans up the IO channel.
+ * Intended to close resources, or send a goodbye message.
+ */
+ void cleanup();
+
+ /**
+ * Returns a boolean describing whether the io channel is still open.
+ * Expected to be true after {@link #initialize() initialize}, and false after {@link #cleanup()}.
+ *
+ * @return true if the io can still be used to communicate to the user, and false otherwise.
+ */
+ boolean isStillOpen();
+
+ /**
+ * Allows the IO object to take over logic to respond to user input.
+ * A simple example is a while loop that uses a Scanner to wait for user interaction.
+ * Intended to also work for event handler designs.
+ */
+ void takeoverExecutionLogic(Bot bot);
+}
diff --git a/src/main/java/catbot/task/Deadline.java b/src/main/java/catbot/task/Deadline.java
new file mode 100644
index 0000000000..4c56891703
--- /dev/null
+++ b/src/main/java/catbot/task/Deadline.java
@@ -0,0 +1,83 @@
+package catbot.task;
+
+import java.time.LocalDate;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+
+import catbot.internal.NamedParameterMap;
+import catbot.io.ErrorIndicatorIo;
+
+/**
+ * Task with a due date.
+ */
+public class Deadline extends Task {
+
+
+ private static final String DESC_KEY = "";
+ private static final String DUE_DATE_KEY = "by";
+
+ private LocalDate dueDate;
+
+ private Deadline(String desc, LocalDate dateTime) {
+ setDescription(desc);
+ setDueDate(dateTime);
+ }
+
+ public void setDueDate(LocalDate dueDate) {
+ this.dueDate = dueDate;
+ }
+
+ /**
+ * Optionally creates a Deadline, if the given NamedParameterMap has valid arguments.
+ *
+ * @param map map of parameters and arguments to attempt to create a Deadline.
+ * @param invalidStateHandler consumer to accept information about the error in case of argument invalidity.
+ * @return an Optional Task if arguments are valid, otherwise an empty Optional.
+ */
+ public static Optional createIfValidElse(
+ NamedParameterMap map,
+ BiConsumer invalidStateHandler
+ ) {
+
+ Optional optionalInvalidParameterState =
+ Task.invalidStateIfTaskParametersMissingOrBlank(
+ map,
+ DESC_KEY, DUE_DATE_KEY
+ );
+ if (optionalInvalidParameterState.isPresent()) {
+ InvalidArgumentStruct invalidArgumentStruct = optionalInvalidParameterState.get();
+ invalidArgumentStruct.parameters.moveToNewKey(DESC_KEY, "description");
+ invalidArgumentStruct.parameters.moveToNewKey(DUE_DATE_KEY, "due date");
+ invalidStateHandler.accept(invalidArgumentStruct.state, invalidArgumentStruct.parameters);
+ return Optional.empty();
+ }
+
+ String description = map.get(DESC_KEY);
+ NamedParameterMap invalidArgs = new NamedParameterMap();
+ Optional optionalDueDate = Task.parseOptionalDateElseMap(map, invalidArgs, DUE_DATE_KEY);
+ if (optionalDueDate.isPresent()) {
+ return Optional.of(new Deadline(description, optionalDueDate.get()));
+ } else {
+ invalidArgs.moveToNewKey(DUE_DATE_KEY, "due date");
+ invalidStateHandler.accept(ErrorIndicatorIo.InvalidArgumentState.NOT_A_DATE, invalidArgs);
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " [due: " + Task.formatDate(this.dueDate) + "]";
+ }
+
+ @Override
+ public void edit(NamedParameterMap map) {
+ map.moveToNewKey("desc", "description");
+ if (map.containsKey("description")) {
+ this.setDescription(map.get("description"));
+ }
+ if (map.containsKey(DUE_DATE_KEY)) {
+ Optional optDate = Task.parseOptionalDateElseMap(map, null, DUE_DATE_KEY);
+ optDate.ifPresent(this::setDueDate);
+ }
+ }
+}
diff --git a/src/main/java/catbot/task/Event.java b/src/main/java/catbot/task/Event.java
new file mode 100644
index 0000000000..ebcb60afc6
--- /dev/null
+++ b/src/main/java/catbot/task/Event.java
@@ -0,0 +1,100 @@
+package catbot.task;
+
+import java.time.LocalDate;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+
+import catbot.internal.NamedParameterMap;
+import catbot.io.ErrorIndicatorIo;
+
+/**
+ * Task with start and end dates.
+ */
+public class Event extends Task {
+
+ private static final String DESC_KEY = "";
+ private static final String START_DATE_KEY = "from";
+ private static final String END_DATE_KEY = "to";
+
+
+ private LocalDate eventStart;
+ private LocalDate eventEnd;
+
+ private Event(String desc, LocalDate start, LocalDate end) {
+ setDescription(desc);
+ setEventStart(start);
+ setEventEnd(end);
+ }
+
+ public void setEventEnd(LocalDate eventEnd) {
+ this.eventEnd = eventEnd;
+ }
+
+ public void setEventStart(LocalDate eventStart) {
+ this.eventStart = eventStart;
+ }
+
+ /**
+ * Optionally creates a Deadline, if the given NamedParameterMap has valid arguments.
+ *
+ * @param map map of parameters and arguments to attempt to create a Deadline.
+ * @param invalidStateHandler consumer to accept information about the error in case of argument invalidity.
+ * @return an Optional Task if arguments are valid, otherwise an empty Optional.
+ */
+ public static Optional createIfValidElse(
+ NamedParameterMap map,
+ BiConsumer invalidStateHandler
+ ) {
+
+ Optional optionalInvalidParameterState =
+ Task.invalidStateIfTaskParametersMissingOrBlank(
+ map,
+ DESC_KEY, START_DATE_KEY, END_DATE_KEY
+ );
+ if (optionalInvalidParameterState.isPresent()) {
+ InvalidArgumentStruct invalidArgumentStruct = optionalInvalidParameterState.get();
+ invalidArgumentStruct.parameters.moveToNewKey(DESC_KEY, "description");
+ invalidArgumentStruct.parameters.moveToNewKey(START_DATE_KEY, "start date");
+ invalidArgumentStruct.parameters.moveToNewKey(END_DATE_KEY, "end date");
+ invalidStateHandler.accept(invalidArgumentStruct.state, invalidArgumentStruct.parameters);
+ return Optional.empty();
+ }
+
+ String description = map.get(DESC_KEY);
+ NamedParameterMap invalidArgs = new NamedParameterMap();
+ Optional start = Task.parseOptionalDateElseMap(map, invalidArgs, START_DATE_KEY);
+ Optional end = Task.parseOptionalDateElseMap(map, invalidArgs, END_DATE_KEY);
+ if (start.isEmpty() || end.isEmpty()) {
+ invalidArgs.moveToNewKey(DESC_KEY, "description");
+ invalidArgs.moveToNewKey(START_DATE_KEY, "start date");
+ invalidArgs.moveToNewKey(END_DATE_KEY, "end date");
+ invalidStateHandler.accept(ErrorIndicatorIo.InvalidArgumentState.NOT_A_DATE, invalidArgs);
+ return Optional.empty();
+ } else {
+ return Optional.of(new Event(description, start.get(), end.get()));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " [from: " + Task.formatDate(this.eventStart) + " | to: "
+ + Task.formatDate(this.eventEnd) + "]";
+ }
+
+ @Override
+ public void edit(NamedParameterMap map) {
+ map.moveToNewKey("desc", "description");
+ if (map.containsKey("description")) {
+ this.setDescription(map.get("description"));
+ }
+ if (map.containsKey(START_DATE_KEY)) {
+ Optional optDate = Task.parseOptionalDateElseMap(map, null, START_DATE_KEY);
+ optDate.ifPresent(this::setEventStart);
+ }
+ if (map.containsKey(START_DATE_KEY)) {
+ Optional optDate = Task.parseOptionalDateElseMap(map, null, END_DATE_KEY);
+ optDate.ifPresent(this::setEventEnd);
+ }
+
+ }
+}
diff --git a/src/main/java/catbot/task/Task.java b/src/main/java/catbot/task/Task.java
new file mode 100644
index 0000000000..73455b700a
--- /dev/null
+++ b/src/main/java/catbot/task/Task.java
@@ -0,0 +1,180 @@
+package catbot.task;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.time.Year;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Optional;
+
+import catbot.internal.NamedParameterMap;
+import catbot.io.ErrorIndicatorIo;
+
+/**
+ * Abstract object representing an entry in a TaskList.
+ * Intended to represent a single task that needs to be done by the user.
+ */
+public abstract class Task implements Serializable {
+
+ //region Fields
+
+ private String description;
+ private boolean isDone = false;
+
+ //endregion
+
+ //region Getter/setter
+
+ /**
+ * Checks if the task is marked as done.
+ *
+ * @return true if done, false otherwise.
+ */
+ public boolean isDone() {
+ return isDone;
+ }
+
+ /**
+ * Marks the task as done.
+ */
+ public void setDone() {
+ isDone = true;
+ }
+
+ /**
+ * Marks the task as not done.
+ */
+ public void setUndone() {
+ isDone = false;
+ }
+
+ /**
+ * Returns the description associated with the task.
+ *
+ * @return description of the task.
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Replaces the original description of the task.
+ *
+ * @param description String to replace the original description.
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * Replaces relevant information in the task with the new values provided by the NamedParameterMap.
+ *
+ * @param map map containing new arguments to override previous values.
+ */
+ public abstract void edit(NamedParameterMap map);
+
+ //endregion
+
+ //region Overrides
+
+ @Override
+ public String toString() {
+ return "[" + (isDone() ? "X" : " ") + "] " + getDescription();
+ }
+
+ //endregion
+
+ //region Internal Helpers
+
+ private static Optional mapIfDescriptionEmpty(NamedParameterMap map) {
+ String desc = map.get("");
+ if (desc == null || desc.isEmpty() || desc.isBlank()) {
+ NamedParameterMap newMap = new NamedParameterMap();
+ newMap.addNamedParameter("description", "");
+ return Optional.of(newMap);
+ } else {
+ return Optional.empty();
+ }
+ }
+ private static Optional mapIfArgumentsMissing(
+ NamedParameterMap map,
+ String... arguments
+ ) {
+ NamedParameterMap newMap = new NamedParameterMap();
+ boolean isArgumentMissing = false;
+ for (String arg : arguments) {
+ if (!map.containsKey(arg)) {
+ isArgumentMissing = true;
+ newMap.addNamedParameter(arg, "");
+ }
+ }
+ if (isArgumentMissing) {
+ return Optional.of(newMap);
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ protected static class InvalidArgumentStruct {
+ protected final ErrorIndicatorIo.InvalidArgumentState state;
+ protected final NamedParameterMap parameters;
+
+ private InvalidArgumentStruct(ErrorIndicatorIo.InvalidArgumentState state, NamedParameterMap map) {
+ this.state = state;
+ this.parameters = map;
+ }
+ }
+
+ protected static Optional invalidStateIfTaskParametersMissingOrBlank(
+ NamedParameterMap namedParameterMap, String... arguments
+ ) {
+ // parameters cannot be missing
+ Optional optionalNamedParameterMap =
+ Task.mapIfArgumentsMissing(namedParameterMap, arguments);
+ if (optionalNamedParameterMap.isPresent()) {
+ return Optional.of(new InvalidArgumentStruct(
+ ErrorIndicatorIo.InvalidArgumentState.PARAMETER_MISSING,
+ optionalNamedParameterMap.get())
+ );
+ }
+
+ // description cannot be empty
+ optionalNamedParameterMap = Task.mapIfDescriptionEmpty(namedParameterMap);
+ //noinspection OptionalIsPresent for readability, to distinguish default return value
+ if (optionalNamedParameterMap.isPresent()) {
+ return Optional.of(new InvalidArgumentStruct(
+ ErrorIndicatorIo.InvalidArgumentState.PARAMETER_EMPTY,
+ optionalNamedParameterMap.get())
+ );
+ }
+
+ // if all ok
+ return Optional.empty();
+ }
+
+ protected static Optional parseOptionalDateElseMap(
+ NamedParameterMap map, NamedParameterMap elseMap, String arg
+ ) {
+ String val = map.get(arg);
+ try {
+ return Optional.of(LocalDate.parse(val));
+ } catch (DateTimeParseException ignored) {
+ if (elseMap != null) {
+ elseMap.addNamedParameter(arg, val);
+ }
+ return Optional.empty();
+ }
+ }
+
+ protected static String formatDate(LocalDate date) {
+ return date.format(DateTimeFormatter.ofPattern(
+ date.getYear() == Year.now().getValue()
+ ? "MMM d"
+ : "MMM d yyyy"
+ )
+ );
+ }
+
+ //endregion
+
+}
diff --git a/src/main/java/catbot/task/TaskArrayListStorage.java b/src/main/java/catbot/task/TaskArrayListStorage.java
new file mode 100644
index 0000000000..59ef98d863
--- /dev/null
+++ b/src/main/java/catbot/task/TaskArrayListStorage.java
@@ -0,0 +1,62 @@
+package catbot.task;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.function.Supplier;
+
+import catbot.internal.ObjectStorage;
+
+/**
+ * Dedicated class to read and write ArrayLists of Tasks from storage.
+ */
+public class TaskArrayListStorage implements ObjectStorage> {
+
+ private final String path;
+ private Supplier> supplier;
+
+ /**
+ * Constructs a TaskArrayListStorage with a path to read from and write to.
+ *
+ * @param path String representing relative directory to read and write.
+ */
+ public TaskArrayListStorage(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public void write(ArrayList taskArrayList) {
+ try {
+ ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(path));
+ output.writeObject(taskArrayList);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public ArrayList readOrDefault() {
+ try {
+ ObjectInputStream input = new ObjectInputStream(new FileInputStream(path));
+ Object readObject = input.readObject();
+ @SuppressWarnings("unchecked")
+ ArrayList tasks = (ArrayList) readObject;
+ input.close();
+ return tasks;
+ } catch (IOException | ClassNotFoundException ignored) {
+ if (supplier != null) {
+ return supplier.get();
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public void setDefault(Supplier> supplier) {
+ this.supplier = supplier;
+ }
+}
diff --git a/src/main/java/catbot/task/TaskList.java b/src/main/java/catbot/task/TaskList.java
new file mode 100644
index 0000000000..13fe2790ec
--- /dev/null
+++ b/src/main/java/catbot/task/TaskList.java
@@ -0,0 +1,162 @@
+package catbot.task;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import catbot.internal.Bounds;
+import catbot.internal.NamedParameterMap;
+
+/**
+ * Object to manage a list of Tasks.
+ */
+public class TaskList {
+
+ private ArrayList tasks;
+ private final TaskArrayListStorage storage;
+
+ /**
+ * Constructs a TaskList with a path to read and write from, for storage.
+ *
+ * @param path relative directory to read from and write to.
+ */
+ public TaskList(String path) {
+ if (path != null) {
+ this.storage = new TaskArrayListStorage(path);
+ this.storage.setDefault(() -> new ArrayList<>());
+ this.tasks = storage.readOrDefault();
+ } else {
+ this.storage = null;
+ this.tasks = new ArrayList<>();
+ }
+ }
+
+ /**
+ * Adds a task to the list.
+ *
+ * @param task task to add to the list.
+ */
+ public void addTask(Task task) {
+ tasks.add(task);
+ update();
+ }
+
+ /**
+ * Removes the task at the specified index.
+ *
+ * @param index index of the task to remove.
+ * @return the removed task.
+ */
+ public Task removeTask(int index) {
+ Task removed = tasks.remove(index);
+ update();
+ return removed;
+ }
+
+ /**
+ * Checks if the provided integer is a valid index starting from 1.
+ * If so, converts to index starting from 0, and passes to the first consumer.
+ * Otherwise, passes the original integer to the second consumer.
+ *
+ * @param integer integer to check if is valid index based on {@link Bounds Bounds}
+ * @param ifValid consumer to accept (integer - 1) if it is a valid index
+ * @param otherwise consumer to accept (integer) if it is not a valid index
+ * @see java.util.Optional#ifPresentOrElse for inspiration.
+ */
+ public void ifValidIndexElse(int integer, Consumer ifValid, Consumer otherwise) {
+ Bounds bounds = getIndexBounds();
+ if (bounds.contains(integer)) {
+ ifValid.accept(integer - 1);
+ } else {
+ otherwise.accept(integer);
+ }
+ }
+
+ /**
+ * Retrieves a {@link Bounds bounds} object that represents valid indexes.
+ * @return Bounds object for relevant indexes.
+ */
+ public Bounds getIndexBounds() {
+ return new Bounds(1, tasks.size());
+ }
+
+ /**
+ * Marks the task at the given index as done.
+ *
+ * @param index index of the task to mark as done.
+ */
+ public void markTask(int index) {
+ tasks.get(index).setDone();
+ update();
+ }
+
+ /**
+ * Marks the task at the given index as undone.
+ *
+ * @param index index of the task to mark as undone.
+ */
+ @SuppressWarnings("SpellCheckingInspection")
+ public void unmarkTask(int index) {
+ tasks.get(index).setUndone();
+ update();
+ }
+
+ /**
+ * Edits the task at the given index based on the parameters and arguments provided through a NamedParameterMap.
+ *
+ * @param index the index of the task to edit.
+ * @param map map that contains new values for parameters.
+ * valid parameters result in their arguments replacing previous values in the task.
+ */
+ public void editTask(int index, NamedParameterMap map) {
+ tasks.get(index).edit(map);
+ update();
+ }
+
+ /**
+ * Retrieves the length of the list.
+ *
+ * @return number of tasks in the list.
+ */
+ public int size() {
+ return tasks.size();
+ }
+
+ /**
+ * Retrieves a task at a specified index.
+ *
+ * @param index index of the task to retrieve.
+ * @return task at the provided index.
+ */
+ public Task getTask(int index) {
+ return tasks.get(index);
+ }
+
+ /**
+ * Returns an ArrayList of the string representations of all tasks, in sequence.
+ *
+ * @return ArrayList of toStringed tasks.
+ */
+ public ArrayList getTaskStrings() {
+ return tasks.stream().map(task -> task.toString()).collect(Collectors.toCollection(ArrayList::new));
+ }
+
+ private void update() {
+ this.storage.write(this.tasks);
+ }
+
+ /**
+ * Returns a list of all tasks whose descriptions contain the search string.
+ *
+ * @param string text to search in descriptions.
+ * @return TaskList containing tasks whose descriptions contain the search text.
+ */
+ public TaskList findInDescriptions(String string) {
+ TaskList taskList = new TaskList(null);
+ // credit to IntelliJ for this line of code that I won't bother to understand
+ taskList.tasks = tasks.stream().filter(task -> task.getDescription().contains(string))
+ .collect(Collectors.toCollection(ArrayList::new));
+ return taskList;
+ }
+
+}
diff --git a/src/main/java/catbot/task/Todo.java b/src/main/java/catbot/task/Todo.java
new file mode 100644
index 0000000000..34f7a2b7e2
--- /dev/null
+++ b/src/main/java/catbot/task/Todo.java
@@ -0,0 +1,54 @@
+package catbot.task;
+
+import java.util.Optional;
+import java.util.function.BiConsumer;
+
+import catbot.internal.NamedParameterMap;
+import catbot.io.ErrorIndicatorIo;
+
+/**
+ * The most basic task.
+ */
+public class Todo extends Task {
+ private Todo(String desc) {
+ setDescription(desc);
+ }
+
+ /**
+ * Optionally creates a Deadline, if the given NamedParameterMap has valid arguments.
+ *
+ * @param map map of parameters and arguments to attempt to create a Deadline.
+ * @param invalidStateHandler consumer to accept information about the error in case of argument invalidity.
+ * @return an Optional Task if arguments are valid, otherwise an empty Optional.
+ */
+ public static Optional createIfValidElse(
+ NamedParameterMap map,
+ BiConsumer invalidStateHandler
+ ) {
+
+ Optional optionalInvalidParameterState =
+ Task.invalidStateIfTaskParametersMissingOrBlank(
+ map,
+ ""
+ );
+ if (optionalInvalidParameterState.isPresent()) {
+ InvalidArgumentStruct invalidArgumentStruct = optionalInvalidParameterState.get();
+ invalidArgumentStruct.parameters.moveToNewKey("", "description");
+ invalidStateHandler.accept(invalidArgumentStruct.state, invalidArgumentStruct.parameters);
+ return Optional.empty();
+ }
+
+ return Optional.of(new Todo(
+ map.get("")
+ ));
+ }
+
+ @Override
+ public void edit(NamedParameterMap map) {
+ map.moveToNewKey("desc", "description");
+ if (!map.containsKey("description")) {
+ return;
+ }
+ this.setDescription(map.get("description"));
+ }
+}
diff --git a/src/main/resources/images/DaDuke.png b/src/main/resources/images/DaDuke.png
new file mode 100644
index 0000000000..106a319a60
Binary files /dev/null and b/src/main/resources/images/DaDuke.png differ
diff --git a/src/main/resources/images/DaUser.jpg b/src/main/resources/images/DaUser.jpg
new file mode 100644
index 0000000000..e1e5a0f261
Binary files /dev/null and b/src/main/resources/images/DaUser.jpg differ
diff --git a/src/main/resources/images/DaUser.png b/src/main/resources/images/DaUser.png
new file mode 100644
index 0000000000..5f95c04f16
Binary files /dev/null and b/src/main/resources/images/DaUser.png differ
diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml
new file mode 100644
index 0000000000..1670759425
--- /dev/null
+++ b/src/main/resources/view/DialogBox.fxml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml
new file mode 100644
index 0000000000..3b26d5240b
--- /dev/null
+++ b/src/main/resources/view/MainWindow.fxml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/scrollpane.css b/src/main/resources/view/scrollpane.css
new file mode 100644
index 0000000000..d3d24d944a
--- /dev/null
+++ b/src/main/resources/view/scrollpane.css
@@ -0,0 +1,7 @@
+.scroll-pane .viewport {
+ -fx-background-color: darkgray;
+ }
+
+.scroll-pane {
+ -fx-background-color:transparent;
+}
\ No newline at end of file
diff --git a/src/test/java/catbot/CatBotCommandPatternsTest.java b/src/test/java/catbot/CatBotCommandPatternsTest.java
new file mode 100644
index 0000000000..50254cfa7e
--- /dev/null
+++ b/src/test/java/catbot/CatBotCommandPatternsTest.java
@@ -0,0 +1,89 @@
+package catbot;
+
+
+import catbot.bot.CatBotCommandPatterns;
+import catbot.internal.CommandPattern;
+import catbot.internal.CommandPatternGenerator;
+import catbot.internal.NamedParameterMap;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class CatBotCommandPatternsTest {
+
+ @Nested
+ class IntegerPatternTest {
+
+ @Test
+ public void integerPatternTest() {
+ CommandPatternGenerator generator = CatBotCommandPatterns.getIntegerPatternGenerator();
+ LambdaOutput defaultOutput = new LambdaOutput<>();
+ Consumer defaultConsumer = defaultOutput::setOutput;
+ CommandPattern integerPattern = generator.generateUsingDefault(defaultConsumer);
+
+ LambdaOutput output = new LambdaOutput<>();
+ integerPattern.ifParsableElseDefault("5", output::setOutput);
+ assertNull(defaultOutput.getOutput());
+ assertEquals(output.getOutput(), 5);
+
+ integerPattern.ifParsableElseDefault("100", output::setOutput);
+ assertNull(defaultOutput.getOutput());
+ assertEquals(output.getOutput(), 100);
+
+ output.setOutput(0);
+ integerPattern.ifParsableElseDefault("not integer", output::setOutput);
+ assertEquals(defaultOutput.getOutput(), "not integer");
+ assertEquals(output.getOutput(), 0);
+ }
+
+ }
+
+ @Nested
+ class SlashPatternTest {
+
+ @Test
+ public void slashPatternTest() {
+ CommandPatternGenerator generator = CatBotCommandPatterns.getSlashPatternGenerator();
+ LambdaOutput defaultOutput = new LambdaOutput<>();
+ Consumer defaultConsumer = defaultOutput::setOutput;
+ CommandPattern slashPattern = generator.generateUsingDefault(defaultConsumer);
+
+ LambdaOutput output = new LambdaOutput<>();
+
+ slashPattern.ifParsableElseDefault("placeholder text", output::setOutput);
+ NamedParameterMap expectedOutput = new NamedParameterMap();
+ expectedOutput.addNamedParameter("", "placeholder text");
+ assertNull(defaultOutput.getOutput());
+ assertEquals(output.getOutput(), expectedOutput);
+
+ slashPattern.ifParsableElseDefault("slash delimited /text test with /123 multiple slash",
+ output::setOutput);
+ expectedOutput = new NamedParameterMap()
+ .addNamedParameter("", "slash delimited")
+ .addNamedParameter("text", "test with")
+ .addNamedParameter("123", "multiple slash");
+ assertNull(defaultOutput.getOutput());
+ assertEquals(output.getOutput(), expectedOutput);
+
+ slashPattern.ifParsableElseDefault("overriding / slash /slash commands /slash test", output::setOutput);
+ expectedOutput = new NamedParameterMap()
+ .addNamedParameter("", "slash")
+ .addNamedParameter("slash", "test");
+ assertNull(defaultOutput.getOutput());
+ assertEquals(output.getOutput(), expectedOutput);
+
+ slashPattern.ifParsableElseDefault("whitespace /arg te st/text ", output::setOutput);
+ expectedOutput = new NamedParameterMap()
+ .addNamedParameter("", "whitespace")
+ .addNamedParameter("arg", "te st")
+ .addNamedParameter("text", "");
+ assertNull(defaultOutput.getOutput());
+ assertEquals(output.getOutput(), expectedOutput);
+ }
+
+ }
+}
diff --git a/src/test/java/catbot/LambdaOutput.java b/src/test/java/catbot/LambdaOutput.java
new file mode 100644
index 0000000000..feb9a1e04a
--- /dev/null
+++ b/src/test/java/catbot/LambdaOutput.java
@@ -0,0 +1,14 @@
+package catbot;
+
+public class LambdaOutput {
+
+ private T output;
+
+ public T getOutput() {
+ return output;
+ }
+
+ public void setOutput(T output) {
+ this.output = output;
+ }
+}
diff --git a/src/test/java/catbot/internal/CommandMapTest.java b/src/test/java/catbot/internal/CommandMapTest.java
new file mode 100644
index 0000000000..53169a8308
--- /dev/null
+++ b/src/test/java/catbot/internal/CommandMapTest.java
@@ -0,0 +1,53 @@
+package catbot.internal;
+
+import catbot.LambdaOutput;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class CommandMapTest {
+
+ @Test
+ public void run_validCommand_runCommand() {
+ LambdaOutput defaultOutput = new LambdaOutput<>();
+ LambdaOutput output = new LambdaOutput<>();
+
+ CommandMap map = new CommandMap()
+ .setDefaultCommand(defaultOutput::setOutput)
+ .addCommand("cmd", output::setOutput);
+
+ map.run("cmd", "args");
+
+ assertNull(defaultOutput.getOutput());
+ assertEquals(output.getOutput(), "args");
+ }
+
+ @Test
+ public void run_noCommands_runDefaultCommand() {
+ LambdaOutput defaultOutput = new LambdaOutput<>();
+
+ CommandMap map = new CommandMap()
+ .setDefaultCommand(defaultOutput::setOutput);
+
+ map.run("cmd", "args");
+
+ assertEquals(defaultOutput.getOutput(), "cmd");
+ }
+
+ @Test
+ public void run_invalidCommand_runDefaultCommand() {
+ LambdaOutput defaultOutput = new LambdaOutput<>();
+ LambdaOutput output = new LambdaOutput<>();
+
+ CommandMap map = new CommandMap()
+ .setDefaultCommand(defaultOutput::setOutput)
+ .addCommand("test", output::setOutput);
+
+ map.run("cmd", "args");
+
+ assertEquals(defaultOutput.getOutput(), "cmd");
+ assertNull(output.getOutput());
+ }
+
+}
diff --git a/src/test/java/catbot/task/TaskTest.java b/src/test/java/catbot/task/TaskTest.java
new file mode 100644
index 0000000000..c96fe8c965
--- /dev/null
+++ b/src/test/java/catbot/task/TaskTest.java
@@ -0,0 +1,31 @@
+package catbot.task;
+
+import catbot.internal.NamedParameterMap;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TaskTest {
+
+ @Test
+ public void DoneTest() {
+ NamedParameterMap namedParameterMap = new NamedParameterMap();
+ namedParameterMap.addNamedParameter("", "placeholder description");
+ Optional optionalTask = Todo.createIfValidElse(
+ namedParameterMap,
+ (invalidParameterState, map) -> {
+ throw new AssertionError("Should not be invalid");
+ });
+ assert optionalTask.isPresent();
+ Task task = optionalTask.get();
+ assert !task.isDone();
+ task.setDone();
+ assert task.isDone();
+ task.setDone();
+ assert task.isDone();
+ task.setUndone();
+ assert !task.isDone();
+ }
+}
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
index 657e74f6e7..38451d39a7 100644
--- a/text-ui-test/EXPECTED.TXT
+++ b/text-ui-test/EXPECTED.TXT
@@ -1,7 +1,31 @@
-Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
+───────────────────────────────────────────────────────────────────────────
+Hiya! I'm
+ ________ ________ _________ ________ ________ _________
+|\ ____\ |\ __ \ |\___ ___\ |\ __ \ |\ __ \ |\___ ___\
+\ \ \___| \ \ \|\ \\|___ \ \_| \ \ \|\ /_\ \ \|\ \\|___ \ \_|
+ \ \ \ \ \ __ \ \ \ \ \ \ __ \\ \ \\\ \ \ \ \
+ \ \ \____ \ \ \ \ \ \ \ \ \ \ \|\ \\ \ \\\ \ \ \ \
+ \ \_______\\ \__\ \__\ \ \__\ \ \_______\\ \_______\ \ \__\
+ \|_______| \|__|\|__| \|__| \|_______| \|_______| \|__|
+───────────────────────────────────────────────────────────────────────────
+ > I can't do that :(
+ > Added: 1. [ ] borrow book
+ > 1. [ ] borrow book
+ > Added: 2. [ ] return book [due: Sunday]
+ > Added: 3. [ ] project meeting [from: Mon 2pm | to: 4pm]
+ > Added: 4. [ ] do homework [due: no idea :-p]
+ > 1. [ ] borrow book
+ > 2. [ ] return book [due: Sunday]
+ > 3. [ ] project meeting [from: Mon 2pm | to: 4pm]
+ > 4. [ ] do homework [due: no idea :-p]
+ > 1. [ ] borrow book
+ > 2. [X] return book [due: Sunday]
+ > 3. [X] project meeting [from: Mon 2pm | to: 4pm]
+ > 4. [ ] do homework [due: no idea :-p]
+ > 1. [ ] borrow book
+ > 2. [ ] return book [due: Sunday]
+ > 3. [X] project meeting [from: Mon 2pm | to: 4pm]
+ > 4. [ ] do homework [due: no idea :-p]
+ > Bye!
+───────────────────────────────────────────────────────────────────────────
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
index e69de29bb2..314f397d2b 100644
--- a/text-ui-test/input.txt
+++ b/text-ui-test/input.txt
@@ -0,0 +1,13 @@
+philosophy is to keep as close to the official requirement as possible, so I get notified when it fails
+todo borrow book
+list
+deadline return book /by Sunday
+event project meeting /from Mon 2pm /to 4pm
+deadline do homework /by no idea :-p
+list
+mark 2
+mark 3
+list
+unmark 2
+list
+bye
diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat
index 0873744649..3953bf19f5 100644
--- a/text-ui-test/runtest.bat
+++ b/text-ui-test/runtest.bat
@@ -1,5 +1,7 @@
@ECHO OFF
+REM for my own use: set PATH=%PATH%;C:\Users\siazh\.jdks\corretto-11.0.20.1\bin
+
REM create bin directory if it doesn't exist
if not exist ..\bin mkdir ..\bin
@@ -15,7 +17,7 @@ IF ERRORLEVEL 1 (
REM no error here, errorlevel == 0
REM run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT
-java -classpath ..\bin Duke < input.txt > ACTUAL.TXT
+java -classpath ..\bin catbot.bot.CatBot < input.txt > ACTUAL.TXT
REM compare the output to the expected output
FC ACTUAL.TXT EXPECTED.TXT
diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh
old mode 100644
new mode 100755