diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
new file mode 100644
index 0000000000..ddcdab8e18
--- /dev/null
+++ b/.github/workflows/gradle.yml
@@ -0,0 +1,51 @@
+name: Java CI
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ platform: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.platform }}
+
+ steps:
+ - name: Set up repository
+ uses: actions/checkout@master
+
+ - name: Set up repository
+ uses: actions/checkout@master
+ with:
+ ref: master
+
+ - name: Merge to master
+ run: git checkout --progress --force ${{ github.sha }}
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: Setup JDK 17
+ uses: actions/setup-java@v1
+ with:
+ java-version: '17'
+ java-package: jdk+fx
+
+ - name: Build and check with Gradle
+ run: ./gradlew check
+
+# NO NEED FOR TEXT UI TESTING ANYMORE
+# - name: Perform IO redirection test (*NIX)
+# if: runner.os == 'Linux'
+# working-directory: ${{ github.workspace }}/text-ui-test
+# run: ./runtest.sh
+#
+# - name: Perform IO redirection test (MacOS)
+# if: always() && runner.os == 'macOS'
+# working-directory: ${{ github.workspace }}/text-ui-test
+# run: ./runtest.sh
+#
+# - name: Perform IO redirection test (Windows)
+# if: always() && runner.os == 'Windows'
+# working-directory: ${{ github.workspace }}/text-ui-test
+# shell: cmd
+# run: runtest.bat
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2873e189e1..8d164427f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@ bin/
/text-ui-test/ACTUAL.TXT
text-ui-test/EXPECTED-UNIX.TXT
+jar-test/Nether.jar-all.jar
+jar-test/Instructions
diff --git a/README.md b/README.md
deleted file mode 100644
index 90aa7f092a..0000000000
--- a/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Duke project template
-
-This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it.
-
-## Setting up in Intellij
-
-Prerequisites: JDK 17, update Intellij to the most recent version.
-
-1. Open Intellij (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project first)
-1. Open the project into Intellij as follows:
- 1. Click `Open`.
- 1. Select the project directory, and click `OK`.
- 1. If there are any further prompts, accept the defaults.
-1. Configure the project to use **JDK 17** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
- In the same dialog, set the **Project language level** field to the `SDK default` option.
-3. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output:
- ```
- Hello from
- ____ _
- | _ \ _ _| | _____
- | | | | | | | |/ / _ \
- | |_| | |_| | < __/
- |____/ \__,_|_|\_\___|
- ```
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000000..2982ecde10
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,81 @@
+plugins {
+ id 'checkstyle'
+ id 'java'
+ id 'application'
+ id 'com.github.johnrengelman.shadow' version '7.1.2'
+}
+
+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 'com.google.guava:guava:33.0-jre'
+ 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()
+ jvmArgs '-ea'
+
+ testLogging {
+ events "passed", "skipped", "failed"
+
+ showExceptions true
+ exceptionFormat "full"
+ showCauses true
+ showStackTraces true
+ showStandardStreams = false
+ }
+}
+
+checkstyle {
+ toolVersion = '10.12.7'
+ configDirectory = file("$rootDir/config/checkstyle")
+}
+
+configurations.checkstyle {
+ resolutionStrategy.capabilitiesResolution.withCapability("com.google.collections:google-collections") {
+ select("com.google.guava:guava:0")
+ }
+}
+
+tasks.withType(Checkstyle).configureEach {
+ reports {
+ xml.required = true
+ html.required = true // Enable the HTML report
+ }
+}
+
+application {
+ mainClass.set("Launcher")
+}
+
+shadowJar {
+ archiveBaseName = "Nether.jar"
+// archiveClassifier = null
+}
+
+run{
+ jvmArgs '-ea'
+ standardInput = System.in
+ jvmArgs = [
+ '--module-path', classpath.asPath,
+ '--add-modules', 'javafx.controls,javafx.fxml'
+ ]
+}
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000000..acac1a8e28
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,434 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
new file mode 100644
index 0000000000..135ea49ee0
--- /dev/null
+++ b/config/checkstyle/suppressions.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/nether.txt b/data/nether.txt
new file mode 100644
index 0000000000..b262d5194f
--- /dev/null
+++ b/data/nether.txt
@@ -0,0 +1,4 @@
+T| |Chores|Buy groceries
+D|X|School|Submit CS2103T assignment|2024-09-19 2359
+E| |School|Festival|2024-09-01 0700|2024-09-30 2300
+T| ||Read Book
diff --git a/docs/README.md b/docs/README.md
index 47b9f984f7..9eb317dab5 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,30 +1,215 @@
-# Duke User Guide
+
Nether User Guide
-// Update the title above to match the actual product name
+___
-// Product screenshot goes here
+
-// Product intro goes here
+**Welcome to Nether!**
-## Adding deadlines
+Meet your personal aide, Nether, a chatbot designed to make your life easier by keep track of all your tasks
+systematically. Whether you're juggling deadlines, attending events, or simply trying to keep your daily tasks in order,
+Nether is here to help.
-// Describe the action and its outcome.
+With Nether, you can organize your tasks through simple chat commands. Nether offers a streamlined, conversational
+experience that feels natural and straightforward.
-// Give examples of usage
+## Table of Contents
+- [Nether User Guide](#nether-user-guide)
+- [Features](#features)
+ - [Add a ToDo Task: `todo`](#add-a-todo-task-todo)
+ - [Add a Deadline Task: `deadline`](#add-a-deadline-task-deadline)
+ - [Add an Event Task: `event`](#add-an-event-task-event)
+ - [Mark a Task: `mark`, `unmark`](#mark-a-task-mark-unmark)
+ - [List Out Tasks: `list`](#list-out-tasks-list)
+ - [Find a Task: `find`](#find-a-task-find)
+ - [Delete a Task: `delete`](#delete-a-task-delete)
+ - [Tagging: `#`](#tagging-)
+ - [Miscellaneous Commands: `nether`, `bye`](#miscellaneous-commands-nether-bye)
+- [Known Issues](#known-issues)
+- [Command Summary](#command-summary)
+- [Acknowledgements](#acknowledgements)
-Example: `keyword (optional arguments)`
+
Features
-// A description of the expected outcome goes here
+_____
+## Add a ToDo Task: `todo`
+Adds a `todo` task to the task list.
+
+Format: `todo (description) [#tag]`
+
+> [!NOTE]
+> `#tag` is an optional part of the command. Learn more about how to add tags to your tasks [here](#tagging-)
+
+Example input:
+`todo Read Book`
+
+Expected output:
+```
+Got it. I've added this task:
+ [T][ ] Read Book
+```
+
+## Add a Deadline Task: `deadline`
+
+Adds a `deadline` task to the task list.
+
+Format: `deadline (description) [#tag] /by (time)`
+
+> [!IMPORTANT]
+> The only acceptable time format is `yyyy-MM-dd HHmm`
+> e.g. `2024-09-19-2359`
+
+Example input: `deadline Submit CS2103T Assignment /by 2024-09-20 2359`
+
+Expected output:
```
-expected output
+Got it. I've added this task:
+ [D][ ] Submit CS2103T Assignment (by: Sep 20 2024, 11:59pm)
+```
+
+## Add an Event Task: `event`
+
+Adds an `event` task to the task list.
+
+Format: `event (description) [#tag] /from (time) /to (time)`
+
+Example input: `event Festival /from 2024-09-01 0700 /to 2024-09-03 1900`
+
+Expected output:
```
+Got it. I've added this task:
+ [E][ ] Festival (from: Sep 1 2024, 7:00am to: Sep 03 2024, 7:00pm)
+```
+
+## Mark a Task: `mark`, `unmark`
+
+Mark your task as done or not done using `mark` and `unmark` respectively.
+
+### Mark a task as done: `mark`
+Format: `mark (task number)`
+
+Example input: `mark 3`
+
+Expected output:
+```
+Well done! I've marked this task as done:
+ [E][X] Festival (from: Sep 1 2024, 7:00am to: Sep 03 2024, 7:00pm)
+```
+
+### Mark a task as not done: `unmark`
+Format `unmark (task number)`
+
+Example input: `unmark 3`
+
+Expected output:
+```
+Understood, I've marked this task as not done:
+ [E][ ] Festival (from: Sep 1 2024, 7:00am to: Sep 03 2024, 7:00pm)
+```
+
+## List Out Tasks: `list`
+List out all the tasks you have in your task list.
+
+Format: `list`
+
+Example input: `list`
+
+Expected output:
+```
+Here are the tasks in your list:
+1. [T][ ] Read Book
+2. [D][ ] Submit CS2103T Assignment (by: Sep 20 2024, 11:59pm)
+3. [E][ ] Festival (from: Sep 1 2024, 7:00am to: Sep 03 2024, 7:00pm)
+```
+
+## Find a Task: `find`
+Find all tasks that contain the input search keyword (not case-sensitive).
+
+Format: `find (keyword)`
+
+Example input: `find book`
+
+Expected output:
+```
+Here are the tasks that match your search in your list:
+1. [T][ ] Read Book
+```
+
+## Delete a Task: `delete`
+Delete a task from your task list.
+
+Format: `delete (task number)`
+
+Example input: `delete 1`
+
+Expected output:
+```
+Noted, I've removed this task from the list:
+ [T][ ] Read Book
+Now you have 2 tasks in the list.
+```
+
+## Tagging: `#`
+Tag or find your tasks using `#`.
+
+> [!IMPORTANT]
+> Tags may not contain any whitespace.
+
+### Add tasks with a tag
+
+Format: `(type) (description) [#tag] [time for deadline or event task]`
+
+Example input: `deadline Do Laundry #Chores /by 2024-09-20 0600`
+
+Expected output:
+```
+Got it. I've added this task:
+ [D][ ] Do Laundry (by: Sep 20 2024, 6:00am)
+```
+
+### Find tasks with a tag
+
+List out all the tasks that contain the searched tag.
+
+Format: `find (tag)`
+
+Example input: `find #chores`
+
+Expected output:
+```
+Here are the tasks that match your search in your list:
+1. [D][ ] Do Laundry (by: Sep 20 2024, 6:00am)
+```
+
+## Miscellaneous Commands: `nether`, `bye`
+
+`nether` prompts nether to respond to you in a not so interesting way.
+
+`bye` stops nether from running and closes the application after a short delay.
+
+___
+
+ * The {@code ExitCommand} class handles the termination of the application by printing an exit message
+ * and signaling that the application should close.
+ *
+ */
+public class ExitCommand extends Command {
+ /**
+ * Executes the exit command by displaying an exit message to the user.
+ *
+ * This method interacts with the user interface to print a goodbye message and does not modify the task list
+ * or storage.
+ *
+ *
+ * @param tasks The {@code TaskList} instance (unused in this command).
+ * @param ui The {@code Ui} instance used to interact with the user.
+ * @param storage The {@code Storage} instance (unused in this command).
+ */
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ return ui.printExitMessage();
+ }
+
+ /**
+ * Returns {@code true} to indicate that this command represents an exit command.
+ *
+ * @return {@code true} to indicate that the application should terminate.
+ */
+ @Override
+ public boolean isExit() {
+ return true;
+ }
+}
diff --git a/src/main/java/nether/command/FindCommand.java b/src/main/java/nether/command/FindCommand.java
new file mode 100644
index 0000000000..d8a741aaa3
--- /dev/null
+++ b/src/main/java/nether/command/FindCommand.java
@@ -0,0 +1,30 @@
+package nether.command;
+
+import nether.Ui;
+import nether.storage.Storage;
+import nether.task.TaskList;
+
+/**
+ * A command that searches for tasks in the task list that match the user's input string.
+ * The search is case-insensitive and returns a list of all tasks that contain the input string.
+ */
+public class FindCommand extends Command {
+ private final String searchString;
+
+ public FindCommand(String searchString) {
+ this.searchString = searchString.toLowerCase();
+ }
+
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ TaskList searchResult;
+ if (searchString.startsWith("#")) {
+ searchResult = tasks.searchTag(searchString.substring(1).toLowerCase().trim());
+ // search from 2nd character onwards
+ } else {
+ searchResult = tasks.searchTask(searchString);
+ }
+ return ui.printMatchingTasks(searchResult);
+ }
+
+}
diff --git a/src/main/java/nether/command/ListCommand.java b/src/main/java/nether/command/ListCommand.java
new file mode 100644
index 0000000000..804767ad83
--- /dev/null
+++ b/src/main/java/nether/command/ListCommand.java
@@ -0,0 +1,30 @@
+package nether.command;
+
+import nether.Ui;
+import nether.storage.Storage;
+import nether.task.TaskList;
+
+/**
+ * Represents a command to list all tasks in the task list.
+ *
+ * The {@code ListCommand} class extends {@code Command} and provides the implementation for
+ * displaying the list of tasks to the user.
+ *
+ */
+public class ListCommand extends Command {
+ /**
+ * Executes the command to list all tasks.
+ *
+ * This method calls the {@code printList} method of the {@code TaskList} class to display
+ * the current list of tasks to the user.
+ *
+ *
+ * @param tasks The {@code TaskList} object that contains all tasks.
+ * @param ui The {@code Ui} object used for printing the task list to the user.
+ * @param storage The {@code Storage} object (not used in this method).
+ */
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ return tasks.printList();
+ }
+}
diff --git a/src/main/java/nether/command/MarkCommand.java b/src/main/java/nether/command/MarkCommand.java
new file mode 100644
index 0000000000..6104f9fe11
--- /dev/null
+++ b/src/main/java/nether/command/MarkCommand.java
@@ -0,0 +1,73 @@
+package nether.command;
+
+import nether.NetherException;
+import nether.Ui;
+import nether.storage.Storage;
+import nether.task.Task;
+import nether.task.TaskList;
+
+/**
+ * Represents a command that marks a task as done or not done.
+ *
+ * The {@code MarkCommand} class is an abstract class that defines the common functionality for all commands.
+ *
+ */
+public abstract class MarkCommand extends Command {
+ protected int taskIndex;
+
+ /**
+ * Constructs a {@code MarkCommand} with the specified task number.
+ *
+ * @param taskNumber The index of the task to be marked.
+ */
+ public MarkCommand(int taskNumber) {
+ this.taskIndex = taskNumber;
+ }
+
+ /**
+ * Executes the mark command by marking the specified task and updating the task list, user interface,
+ * and storage.
+ *
+ * This method validates the task number, retrieves the task, marks it accordingly, prints a message
+ * to the user, and saves the updated task list.
+ *
+ *
+ * @param tasks The {@code TaskList} instance containing the tasks.
+ * @param ui The {@code Ui} instance used to interact with the user.
+ * @param storage The {@code Storage} instance used to save the updated tasks.
+ * @throws NetherException If the task number is invalid.
+ */
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) throws NetherException {
+ if (taskIndex > tasks.getSize() || taskIndex < 1) {
+ throw new NetherException("invalid task number!");
+ }
+
+ Task taskToMark = tasks.getTask(taskIndex - 1);
+ // taskNumber needs to be decremented since list index starts from 0
+ markTask(taskToMark);
+
+ storage.saveTasks(tasks.getTasks());
+ return ui.printMarkMessage(taskToMark, getMarkMessage());
+ }
+
+ /**
+ * Marks the specified task as done or not done.
+ *
+ * This method must be implemented by subclasses to define specific marking behavior.
+ *
+ *
+ * @param taskToMark The task to be marked.
+ */
+ public abstract void markTask(Task taskToMark);
+
+ /**
+ * Returns the message to be displayed to the user after marking a task.
+ *
+ * This method must be implemented by subclasses to provide the appropriate marking message.
+ *
+ *
+ * @return The message indicating the result of the marking operation.
+ */
+ public abstract String getMarkMessage();
+}
diff --git a/src/main/java/nether/command/MarkDoneCommand.java b/src/main/java/nether/command/MarkDoneCommand.java
new file mode 100644
index 0000000000..59049a0e3b
--- /dev/null
+++ b/src/main/java/nether/command/MarkDoneCommand.java
@@ -0,0 +1,47 @@
+package nether.command;
+
+import nether.task.Task;
+
+/**
+ * Represents a command to mark a task as done.
+ *
+ * The {@code MarkDoneCommand} class extends {@code MarkCommand} and provides the specific implementation
+ * for marking a task as completed.
+ *
+ */
+public class MarkDoneCommand extends MarkCommand {
+ /**
+ * Constructs a {@code MarkDoneCommand} with the specified task number.
+ *
+ * @param taskNumber The index of the task to be marked as done.
+ */
+ public MarkDoneCommand(int taskNumber) {
+ super(taskNumber);
+ }
+
+ /**
+ * Marks the specified task as done.
+ *
+ * This method sets the status of the task to done by calling the {@code markAsDone} method on the task.
+ *
+ *
+ * @param taskToMark The task to be marked as done.
+ */
+ @Override
+ public void markTask(Task taskToMark) {
+ taskToMark.markAsDone();
+ }
+
+ /**
+ * Returns the message to be displayed to the user after marking a task as done.
+ *
+ * This method provides a message indicating that the task has been successfully marked as done.
+ *
+ *
+ * @return The message "Well done! I've marked this task as done:".
+ */
+ @Override
+ public String getMarkMessage() {
+ return "Well done! I've marked this task as done:";
+ }
+}
diff --git a/src/main/java/nether/command/MarkNotDoneCommand.java b/src/main/java/nether/command/MarkNotDoneCommand.java
new file mode 100644
index 0000000000..283d31d54b
--- /dev/null
+++ b/src/main/java/nether/command/MarkNotDoneCommand.java
@@ -0,0 +1,48 @@
+package nether.command;
+
+import nether.task.Task;
+
+/**
+ * Represents a command to mark a task as not done.
+ *
+ * The {@code MarkNotDoneCommand} class is a subclass of {@code MarkCommand} and provides the implementation
+ * for marking a task as incomplete or not done.
+ *
+ */
+public class MarkNotDoneCommand extends MarkCommand {
+
+ /**
+ * Constructs a {@code MarkNotDoneCommand} with the specified task number.
+ *
+ * @param taskNumber The index of the task to be marked as not done.
+ */
+ public MarkNotDoneCommand(int taskNumber) {
+ super(taskNumber);
+ }
+
+ /**
+ * Marks the specified task as not done.
+ *
+ * This method sets the status of the task to not done by calling the {@code markAsNotDone} method on the task.
+ *
+ *
+ * @param taskToMark The task to be marked as not done.
+ */
+ @Override
+ public void markTask(Task taskToMark) {
+ taskToMark.markAsNotDone();
+ }
+
+ /**
+ * Returns the message to be displayed to the user after marking a task as not done.
+ *
+ * This method provides a message indicating that the task has been successfully marked as not done.
+ *
+ *
+ * @return The message "Understood, I've marked this task as not done:".
+ */
+ @Override
+ public String getMarkMessage() {
+ return "Understood, I've marked this task as not done:";
+ }
+}
diff --git a/src/main/java/nether/command/NetherCommand.java b/src/main/java/nether/command/NetherCommand.java
new file mode 100644
index 0000000000..d3d82c2ffb
--- /dev/null
+++ b/src/main/java/nether/command/NetherCommand.java
@@ -0,0 +1,18 @@
+package nether.command;
+
+import nether.NetherException;
+import nether.Ui;
+import nether.storage.Storage;
+import nether.task.TaskList;
+
+/**
+ * Represents a command to let Nether express its personality to the users.
+ * The {@code NetherCommand} class returns a response String that lets the users know more about Nether's behaviour.
+ *
+ */
+public class NetherCommand extends Command {
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) throws NetherException {
+ return ui.printSelf();
+ }
+}
diff --git a/src/main/java/nether/parser/Parser.java b/src/main/java/nether/parser/Parser.java
new file mode 100644
index 0000000000..cdc3d0fcb5
--- /dev/null
+++ b/src/main/java/nether/parser/Parser.java
@@ -0,0 +1,224 @@
+package nether.parser;
+
+import java.util.Objects;
+
+import nether.NetherException;
+import nether.command.AddCommand;
+import nether.command.Command;
+import nether.command.DeleteCommand;
+import nether.command.ExitCommand;
+import nether.command.FindCommand;
+import nether.command.ListCommand;
+import nether.command.MarkDoneCommand;
+import nether.command.MarkNotDoneCommand;
+import nether.command.NetherCommand;
+import nether.task.DeadlineTask;
+import nether.task.EventTask;
+import nether.task.TodoTask;
+
+
+/**
+ * Handles the parsing of user input into commands and arguments that the program can understand.
+ * The Parser class provides methods to interpret different types of tasks and extract relevant details.
+ */
+
+public class Parser {
+ /**
+ * Parses the user input to identify the {@code Command} and extracts details relevant to the {@code Command}.
+ *
+ * @param userInput The full input string provided by the user (without trailing or leading whitespaces).
+ * @return An array of strings containing the parts of the user input necessary to create tasks.
+ * @throws NetherException If the command is not recognized or the input format is incorrect.
+ */
+ public Command parse(String userInput) throws NetherException {
+ assert !Objects.equals(userInput, "") : "Your input cannot be empty!";
+ String[] processedInput;
+ String commandWord = extractCommandWord(userInput);
+
+ switch (commandWord) {
+ case "list":
+ return new ListCommand();
+ case "todo":
+ processedInput = extractInputDetails(userInput, "todo");
+ return new AddCommand(new TodoTask(processedInput[0], processedInput[1]));
+ case "deadline":
+ processedInput = extractInputDetails(userInput, "deadline");
+ return new AddCommand(new DeadlineTask(processedInput[0], processedInput[1], processedInput[2]));
+ case "event":
+ processedInput = extractInputDetails(userInput, "event");
+ return new AddCommand(new EventTask(processedInput[0], processedInput[1], processedInput[2],
+ processedInput[3]));
+ case "mark":
+ return new MarkDoneCommand(extractTaskNumber(userInput));
+ case "unmark":
+ return new MarkNotDoneCommand(extractTaskNumber(userInput));
+ case "delete":
+ return new DeleteCommand(extractTaskNumber(userInput));
+ case "find":
+ processedInput = extractInputDetails(userInput, "find");
+ return new FindCommand(processedInput[0]);
+ case "nether":
+ return new NetherCommand();
+ case "bye":
+ return new ExitCommand();
+ default:
+ throw new NetherException("the command: '" + userInput + "' is not in our database.");
+ }
+
+ }
+
+ /**
+ * Extracts the {@code Command} from the user's input string. The command is assumed to be the
+ * first word of the input.
+ *
+ * @param userInput The full input string provided by the user.
+ * @return The command in lowercase (e.g., "todo", "deadline", or "event").
+ */
+ public String extractCommandWord(String userInput) {
+ return userInput.split(" ", 2)[0].toLowerCase();
+ }
+
+ /**
+ * Processes the user input into parts, making it easier to instantiate the respective {@code Task} objects.
+ * Splits the input based on the command and uses regex to identify {@code Task} details.
+ *
+ * @param userInput The full input string provided by the user (without leading or trailing whitespaces).
+ * @param taskType The type of task ("todo", "deadline", or "event").
+ * @return A string array containing the task details to be instantiated by Nether class.
+ * @throws NetherException If the input does not follow the expected format or required details are missing.
+ */
+
+ public String[] extractInputDetails(String userInput, String taskType) throws NetherException {
+ switch (taskType) {
+ case "todo":
+ return handleTodoDetails(userInput);
+ case "deadline":
+ return handleDeadlineDetails(userInput);
+ case "event":
+ return handleEventDetails(userInput);
+ case "find":
+ return handleFindDetails(userInput);
+ default:
+ throw new NetherException("the command: '" + userInput + "' is not in our database");
+ }
+ }
+
+ /**
+ * Parses the user input for "todo" commands.
+ *
+ * @param userInput The full input string provided by the user.
+ * @return An array containing the details of the todo task.
+ * @throws NetherException If the todo task's description is empty.
+ */
+ private static String[] handleTodoDetails(String userInput) {
+ String[] todoDetails = userInput.split("(?i)todo ", 2);
+ if (todoDetails.length < 2 || todoDetails[1].trim().isEmpty()) {
+ throw new NetherException("the description of a todo cannot be empty.");
+ }
+ String[] splitByTag = todoDetails[1].split("#", 2);
+ String description = splitByTag[0].trim();
+ String tag = splitByTag.length > 1 ? splitByTag[1].trim() : "";
+ return new String[]{description, tag};
+ }
+
+ /**
+ * Parses the user input for "deadline" commands.
+ *
+ * @param userInput The full input string provided by the user.
+ * @return An array containing the description and due date of the deadline task.
+ * @throws NetherException If either the description or the due date is empty.
+ */
+ private static String[] handleDeadlineDetails(String userInput) {
+ String[] deadlineDetails = userInput.split("(?i)deadline ", 2);
+ if (deadlineDetails.length < 2 || deadlineDetails[1].trim().isEmpty()) {
+ throw new NetherException("the description of a deadline cannot be empty.");
+ }
+ String[] splitByTag = deadlineDetails[1].split("#", 2);
+ String description = "";
+ String tag = "";
+ String time = "";
+ if (splitByTag.length > 1) {
+ String[] splitByTime = splitByTag[1].split("/by ", 2);
+ description = splitByTag[0].trim();
+ tag = splitByTime[0].trim();
+ time = splitByTime[1].trim();
+ } else { // goes into this branch if the command does not have a tag
+ String[] splitByTime = deadlineDetails[1].split("/by ", 2);
+ if (splitByTime.length < 2 || splitByTime[0].trim().isEmpty() || splitByTime[1].trim().isEmpty()) {
+ throw new NetherException("the description or date/time of a deadline cannot be empty.");
+ }
+ description = splitByTime[0].trim();
+ time = splitByTime[1].trim();
+ }
+ return new String[]{description, tag, time};
+ }
+
+ /**
+ * Parses the user input for "event" commands.
+ *
+ * @param userInput The full input string provided by the user.
+ * @return An array containing the description, start time, and end time of event.
+ * @throws NetherException If the description, start time, or end time is empty.
+ */
+ private static String[] handleEventDetails(String userInput) {
+ String[] eventDetails = userInput.split("(?i)event ", 2);
+ if (eventDetails.length < 2 || eventDetails[1].trim().isEmpty()) {
+ throw new NetherException("the description of an event cannot be empty.");
+ }
+ String description = "";
+ String tag = "";
+ String timeStart = "";
+ String timeEnd = "";
+ String[] splitByTag = eventDetails[1].split("#", 2);
+ if (splitByTag.length > 1) {
+ String[] splitByTime = splitByTag[1].split("/from |/to ", 3);
+ description = splitByTag[0].trim();
+ tag = splitByTime[0].trim();
+ timeStart = splitByTime[1].trim();
+ timeEnd = splitByTime[2].trim();
+ } else {
+ String[] splitByTime = eventDetails[1].split("/from |/to ", 3);
+ if (splitByTime.length < 3 || splitByTime[0].trim().isEmpty() || splitByTime[1].trim().isEmpty()
+ || splitByTime[2].trim().isEmpty()) {
+ throw new NetherException(
+ "the description, start time, or end time of an event cannot be empty.");
+ }
+ description = splitByTime[0].trim();
+ timeStart = splitByTime[1].trim();
+ timeEnd = splitByTime[2].trim();
+ }
+ return new String[]{description, tag, timeStart, timeEnd};
+ }
+
+ /**
+ * Parses the user input for "find" commands.
+ *
+ * @param userInput The full input string provided by the user.
+ * @return An array containing the search keyword
+ * @throws NetherException If the keyword for searching is empty.
+ */
+ private static String[] handleFindDetails(String userInput) {
+ String[] findDetails = userInput.split("(?i)find", 2);
+ if (findDetails.length < 2 || findDetails[1].trim().isEmpty()) {
+ throw new NetherException("please enter a keyword for me to search.");
+ }
+ return new String[]{findDetails[1].trim()};
+ }
+
+ /**
+ * Returns the index/number of the {@code Task} stated in the user input.
+ * Useful for commands like {@code mark} or {@code unmark}.
+ *
+ * @param userInput The input string provided by the user.
+ * @return The task number (index + 1) to be marked/unmarked if successfully parsed; -1 otherwise.
+ */
+
+ public int extractTaskNumber(String userInput) {
+ try {
+ String[] parts = userInput.split(" ");
+ return Integer.parseInt(parts[1]);
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+}
diff --git a/src/main/java/nether/storage/Storage.java b/src/main/java/nether/storage/Storage.java
new file mode 100644
index 0000000000..3fc1dcdf93
--- /dev/null
+++ b/src/main/java/nether/storage/Storage.java
@@ -0,0 +1,113 @@
+package nether.storage;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+import nether.NetherException;
+import nether.task.DeadlineTask;
+import nether.task.EventTask;
+import nether.task.Task;
+import nether.task.TodoTask;
+
+
+/**
+ * Handles the storage of tasks to and from a file.
+ * The {@code Storage} class provides functionality to save tasks to a file and load tasks from a file.
+ */
+public class Storage {
+ private final String filePath;
+
+ /**
+ * Constructs a {@code Storage} object with the specified file path.
+ *
+ * @param filePath The path to the file where tasks will be saved or loaded from.
+ */
+ public Storage(String filePath) {
+ this.filePath = filePath;
+ }
+
+ /**
+ * Saves the specified list of tasks to the data file.
+ * Each task is saved in a format defined by {@code Task.toSaveFormat()}.
+ *
+ * @param tasks The list of tasks to be saved in the data file.
+ */
+ public void saveTasks(List tasks) {
+ File file = new File(filePath);
+ file.getParentFile().mkdirs(); // create the parent directory just in case it doesn't exist yet
+
+ try (FileWriter writer = new FileWriter(file)) {
+ for (Task task : tasks) {
+ writer.write(task.toSaveFormat() + System.lineSeparator());
+ }
+ } catch (IOException e) {
+ System.out.println("Error saving tasks: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Loads the tasks present in the data file.
+ * If the file does not exist, an empty list is returned.
+ *
+ * @return A list of tasks loaded from the data file.
+ */
+ public List loadTasks() {
+ List tasks = new ArrayList<>();
+ File file = new File(filePath);
+
+ if (!file.exists()) {
+ return tasks; // return an empty arraylist of tasks if the file doesn't exist yet
+ }
+
+ try (Scanner scanner = new Scanner(file)) {
+ while (scanner.hasNextLine()) {
+ String taskLine = scanner.nextLine();
+ Task task = parseTask(taskLine);
+
+ if (task != null) {
+ tasks.add(task);
+ }
+
+ }
+ } catch (Exception e) {
+ System.out.println("Error loading tasks: " + e.getMessage());
+ }
+
+ return tasks;
+
+ }
+
+ /**
+ * Creates a {@code Task} object based on the data in a line from the task data file.
+ *
+ * @param taskLine A line from the task data file in the format used by {@code Task.toSaveFormat()}.
+ * @return A {@code Task} object corresponding to the line of data.
+ */
+ private static Task parseTask(String taskLine) {
+ String[] taskParts = taskLine.split("\\|");
+ Task task = null;
+
+ switch (taskParts[0]) {
+ case "T":
+ task = new TodoTask(taskParts[3], taskParts[2]);
+ break;
+ case "D":
+ task = new DeadlineTask(taskParts[3], taskParts[2], taskParts[4]);
+ break;
+ case "E":
+ task = new EventTask(taskParts[3], taskParts[2], taskParts[4], taskParts[5]);
+ break;
+ default:
+ throw new NetherException("this should have never happened. What is going on..");
+ }
+
+ if (taskParts[1].equals("X")) {
+ task.markAsDone();
+ }
+ return task;
+ }
+}
diff --git a/src/main/java/nether/task/DeadlineTask.java b/src/main/java/nether/task/DeadlineTask.java
new file mode 100644
index 0000000000..85afd02ef0
--- /dev/null
+++ b/src/main/java/nether/task/DeadlineTask.java
@@ -0,0 +1,74 @@
+package nether.task;
+
+import java.time.DateTimeException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+import nether.NetherException;
+
+
+/**
+ * Represents a task with a deadline, which includes a description and a specific date/time by when it should be
+ * completed.
+ * The {@code DeadlineTask} class is a subclass of the {@link Task} class and adds a deadline component to the task.
+ */
+public class DeadlineTask extends Task {
+ private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HHmm";
+ private static final String DISPLAY_DATE_FORMAT = "MMM dd yyyy, h:mma";
+ protected LocalDateTime by;
+
+ /**
+ * Constructs a {@code DeadlineTask} object with the specified description and deadline date/time.
+ *
+ * @param description The description of the deadline task.
+ * @param by The deadline date and time in the format {@code yyyy-MM-dd HHmm}.
+ * @throws NetherException If the date/time format is invalid.
+ */
+ public DeadlineTask(String description, String tag, String by) {
+ super(description, tag);
+ assert by.matches("\\d{4}-\\d{2}-\\d{2} \\d{4}") : "Date format must be YYYY-MM-DD HHmm";
+ this.by = parseDateTime(by);
+ }
+
+ /**
+ * Returns a parsed date and time of the deadline.
+ * @param dateTimeStr The input date and time in the format {@code yyyy-MM-dd HHmm}.
+ * @return Parsed date and time of the deadline.
+ * @throws NetherException If the input date/time does not follow the accepted format.
+ */
+ private static LocalDateTime parseDateTime(String dateTimeStr) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT);
+ try {
+ return LocalDateTime.parse(dateTimeStr, formatter);
+ } catch (DateTimeException e) {
+ throw new NetherException("the date/time format for the deadline is invalid. Please use "
+ + "the format: " + DATE_TIME_FORMAT + ".");
+ }
+ }
+
+ /**
+ * Returns the string representation of the {@code DeadlineTask} in the format desired for saving into a data file.
+ * The format is: {@code D|status|description|yyyy-MM-dd HHmm}, where {@code D} indicates a deadline task.
+ *
+ * @return A string in the format {@code D|status|description|deadline}.
+ */
+ @Override
+ public String toSaveFormat() {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT);
+ return "D|" + getStatusIcon() + "|" + this.getTag() + "|" + this.getDescription() + "|"
+ + this.by.format(formatter);
+ }
+
+ /**
+ * Returns the string representation of the {@code DeadlineTask}.
+ * The format is: {@code [D][status] description (by: MMM dd yyyy, h:mma)}, where {@code [D]} indicates a
+ * deadline task.
+ *
+ * @return A string representation of the {@code DeadlineTask}.
+ */
+ @Override
+ public String toString() {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DISPLAY_DATE_FORMAT);
+ return "[D]" + super.toString() + " (by: " + this.by.format(formatter) + ")";
+ }
+}
diff --git a/src/main/java/nether/task/EventTask.java b/src/main/java/nether/task/EventTask.java
new file mode 100644
index 0000000000..be32dbafeb
--- /dev/null
+++ b/src/main/java/nether/task/EventTask.java
@@ -0,0 +1,79 @@
+package nether.task;
+
+import java.time.DateTimeException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+import nether.NetherException;
+
+
+/**
+ * Represents an event task that includes a description, a start date/time, and an end date/time.
+ * The {@code EventTask} class inherits from the {@link Task} class and adds specific start and end timings to the task.
+ */
+public class EventTask extends Task {
+ protected LocalDateTime startTime;
+ protected LocalDateTime endTime;
+
+ /**
+ * Constructs an {@code EventTask} object with the specified description, start, and end date/times.
+ *
+ * @param description The description of the event task.
+ * @param startTime The start date and time of the event in the format {@code yyyy-MM-dd HHmm}.
+ * @param endTime The end date and time of the event in the format {@code yyyy-MM-dd HHmm}.
+ * @throws NetherException If the date/time format for the start or end timings is invalid.
+ */
+
+ public EventTask(String description, String tag, String startTime, String endTime) {
+ super(description, tag);
+ assert startTime.matches("\\d{4}-\\d{2}-\\d{2} \\d{4}") : "Date format must be YYYY-MM-DD HHmm";
+ assert endTime.matches("\\d{4}-\\d{2}-\\d{2} \\d{4}") : "Date format must be YYYY-MM-DD HHmm";
+ this.startTime = getDateTime(startTime);
+ this.endTime = getDateTime(endTime);
+ }
+
+ /**
+ * Returns a parsed date and time of the event.
+ * @param timeStr The input date and time in the format {@code yyyy-MM-dd HHmm}.
+ * @return Parsed date and time for the event.
+ * @throws NetherException If the input date/time does not follow the accepted format.
+ */
+ private static LocalDateTime getDateTime(String timeStr) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm");
+
+ // Validate the input date/time and then assign them
+ try {
+ return LocalDateTime.parse(timeStr.trim(), formatter);
+ } catch (DateTimeException e) {
+ throw new NetherException("the date/time format for the event timing is invalid. Please use "
+ + "the format: yyyy-MM-dd HHmm.");
+ }
+ }
+
+ /**
+ * Returns the string representation of the {@code EventTask} in the format desired for saving into a data file.
+ * The format is: {@code E|status|description|start|end}, where {@code E} indicates an event task.
+ *
+ * @return A string in the format {@code E|status|description|start|end}.
+ */
+ @Override
+ public String toSaveFormat() {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm");
+ return "E|" + this.getStatusIcon() + "|" + this.getTag() + "|" + this.getDescription() + "|"
+ + this.startTime.format(formatter)
+ + "|" + this.endTime.format(formatter);
+ }
+
+ /**
+ * Returns the string representation of the {@code EventTask}.
+ * The format is: {@code [E][status] description (from: start to: end)}, where {@code [E]} indicates an event task.
+ *
+ * @return A string representation of the {@code EventTask}.
+ */
+ @Override
+ public String toString() {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd yyyy, h:mma");
+ return "[E]" + super.toString() + " (from: " + this.startTime.format(formatter)
+ + " to: " + this.endTime.format(formatter) + ")";
+ }
+}
diff --git a/src/main/java/nether/task/Task.java b/src/main/java/nether/task/Task.java
new file mode 100644
index 0000000000..e1e7a5398a
--- /dev/null
+++ b/src/main/java/nether/task/Task.java
@@ -0,0 +1,104 @@
+package nether.task;
+
+import java.util.Objects;
+
+import nether.NetherException;
+
+/**
+ * Represents a task object model with a description and status.
+ * The {@code Task} class serves as an abstract base class for different types of tasks, providing methods to manage the
+ * task's status and description.
+ */
+
+public abstract class Task {
+ protected String description;
+ protected boolean isDone;
+ protected String tag;
+
+ /**
+ * Constructs (by default) an incomplete {@code Task} object with the specified description.
+ *
+ * @param description The description of the task.
+ */
+ public Task(String description, String tag) {
+ this.description = description;
+ this.tag = tag;
+ this.isDone = false;
+ if (tag.contains(" ")) {
+ throw new NetherException("a tag should not have any space!");
+ }
+ }
+
+ /**
+ * Returns the status icon of the task.
+ * The status icon is represented as "X" if the task is marked as done, or a space if not.
+ *
+ * @return A string representing the task's status icon.
+ */
+ public String getStatusIcon() {
+ return (this.isDone ? "X" : " ");
+ }
+
+ /**
+ * Returns the description of the task.
+ *
+ * @return A string representing the task's description.
+ */
+ public String getDescription() {
+ return this.description;
+ }
+
+ /**
+ * Returns the tag of the task.
+ *
+ * @return A string representing the task's tag.
+ */
+ public String getTag() {
+ return this.tag;
+ }
+
+ /**
+ * Marks the task as done.
+ */
+ public void markAsDone() {
+ this.isDone = true;
+ }
+
+ /**
+ * Marks the task as not done.
+ */
+ public void markAsNotDone() {
+ this.isDone = false;
+ }
+
+ /**
+ * Returns a string representation of the task in a format suitable for saving.
+ * This method must be implemented by subclasses to provide a format specific to the type of task.
+ *
+ * @return A string in the format suitable for saving the task.
+ */
+ public abstract String toSaveFormat();
+
+ /**
+ * Returns whether the task is marked as done.
+ *
+ * @return {@code true} if the task is done; {@code false} otherwise.
+ */
+ public boolean isDone() {
+ return this.isDone;
+ }
+
+ /**
+ * Returns a string representation of the task.
+ * The format is: {@code [statusIcon] description}, where {@code statusIcon} is "X" if the task is done.
+ *
+ * @return A string representation of the task.
+ */
+ @Override
+ public String toString() {
+ return Objects.equals(this.getTag(), "")
+ ? String.format("[%s] %s", this.getStatusIcon(), this.getDescription())
+ : String.format("[%s] %s %s", this.getStatusIcon(), "<" + this.getTag() + ">", this.getDescription());
+ }
+
+}
diff --git a/src/main/java/nether/task/TaskList.java b/src/main/java/nether/task/TaskList.java
new file mode 100644
index 0000000000..e133dbf5cb
--- /dev/null
+++ b/src/main/java/nether/task/TaskList.java
@@ -0,0 +1,119 @@
+package nether.task;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Represents a list of tasks.
+ * The {@code TaskList} class provides methods to manage a collection of {@code Task} objects,
+ * such as adding, deleting, and retrieving tasks.
+ */
+public class TaskList {
+ private List tasks;
+
+ /**
+ * Constructs an empty {@code TaskList}.
+ */
+ public TaskList() {
+ this.tasks = new ArrayList<>();
+ }
+
+ /**
+ * Constructs a {@code TaskList} with an initial list of tasks.
+ * This method is used when we are loading tasks from data file.
+ *
+ * @param tasks The initial list of tasks.
+ */
+ public TaskList(List tasks) {
+ this.tasks = tasks;
+ }
+
+ /**
+ * Adds a {@code Task} to the task list.
+ *
+ * @param task The task to be added to the list.
+ */
+ public void addTask(Task task) {
+ tasks.add(task);
+ }
+
+ /**
+ * Deletes a {@code Task} from the task list by its index.
+ *
+ * @param index The index of the task to be removed.
+ * @throws IndexOutOfBoundsException if the index is out of range.
+ */
+ public void deleteTask(int index) {
+ tasks.remove(index);
+ }
+
+ /**
+ * Retrieves a {@code Task} from the task list by its index.
+ *
+ * @param index The index of the task to be retrieved.
+ * @return The task with the specified index.
+ * @throws IndexOutOfBoundsException if the index is out of range.
+ */
+ public Task getTask(int index) {
+ return tasks.get(index);
+ }
+
+ /**
+ * Returns the number of tasks in the task list.
+ *
+ * @return The number of tasks in the list.
+ */
+ public int getSize() {
+ return tasks.size();
+ }
+
+ /**
+ * Returns the list of tasks.
+ *
+ * @return A {@code List} of {@code Task} objects in the task list.
+ */
+ public List getTasks() {
+ return tasks;
+ }
+
+ /**
+ * Prints out the task list along with its status (done or not done).
+ */
+ public String printList() {
+ StringBuilder response = new StringBuilder();
+ if (tasks.isEmpty()) { // guard case for 0 length list
+ response.append("There are no tasks in your list");
+ return response.toString();
+ }
+ response.append("Here are the tasks in your list:\n");
+ for (int i = 0; i < getSize(); i++) {
+ response.append((i + 1)).append(". ").append(tasks.get(i).toString()).append("\n");
+ }
+ return response.toString();
+ }
+
+ /**
+ * Finds the tasks whose description matches the input string.
+ * @param searchString The input string given by user.
+ * @return A list of tasks whose descriptions match the input string.
+ */
+ public TaskList searchTask(String searchString) {
+ List matchingTasks = tasks.stream()
+ .filter(task -> task.getDescription().toLowerCase().contains(searchString))
+ .collect(Collectors.toList());
+ return new TaskList(matchingTasks);
+ }
+
+ /**
+ * Finds the tasks whose tag matches the input string.
+ * @param searchString The input string given by user.
+ * @return A list of tasks whose tag match the input string.
+ */
+ public TaskList searchTag(String searchString) {
+ List matchingTasks = tasks.stream()
+ .filter(task -> task.getTag().toLowerCase().contains(searchString))
+ .collect(Collectors.toList());
+ return new TaskList(matchingTasks);
+ }
+}
diff --git a/src/main/java/nether/task/TodoTask.java b/src/main/java/nether/task/TodoTask.java
new file mode 100644
index 0000000000..dcab83dab9
--- /dev/null
+++ b/src/main/java/nether/task/TodoTask.java
@@ -0,0 +1,39 @@
+package nether.task;
+
+/**
+ * Represents a TodoTask, a type of Task that only has a description and status. No timestamps.
+ * The TodoTask class inherits the Task class
+ */
+public class TodoTask extends Task {
+
+ /**
+ * Constructs a TodoTask object.
+ *
+ * @param description The description of the task.
+ */
+ public TodoTask(String description, String tag) {
+ super(description, tag);
+ }
+
+ /**
+ * Returns the string representation of the TodoTask in the format desired to save into a data file.
+ * The format is: T|status|description, where T indicates a TodoTask.
+ *
+ * @return A string in the format "T|status|description".
+ */
+ @Override
+ public String toSaveFormat() {
+ return "T|" + this.getStatusIcon() + "|" + this.getTag() + "|" + this.getDescription();
+ }
+
+ /**
+ * Returns the string representation of the TodoTask.
+ * The format is: [T][status] description, where [T] indicates a TodoTask.
+ *
+ * @return A string representation of the TodoTask.
+ */
+ @Override
+ public String toString() {
+ return "[T]" + super.toString();
+ }
+}
diff --git a/src/main/resources/css/dialog-box.css b/src/main/resources/css/dialog-box.css
new file mode 100644
index 0000000000..151f17587b
--- /dev/null
+++ b/src/main/resources/css/dialog-box.css
@@ -0,0 +1,40 @@
+.label {
+ background: #fad9a7;
+ -fx-background-color: background;
+ -fx-border-color: black;
+ -fx-border-width: 1px;
+ -fx-background-radius: 1em 1em 0 1em;
+ -fx-border-radius: 1em 1em 0 1em;
+ -fx-padding: 6px;
+ -fx-border-insets: 0px 7px 0px 7px;
+ -fx-background-insets: 0px 7px 0px 7px;
+ -fx-text-fill: ladder(background, white 49%, black 50%);
+}
+
+.reply-label {
+ -fx-background-radius: 1em 1em 1em 0;
+ -fx-border-radius: 1em 1em 1em 0;
+}
+
+.error-label {
+ -fx-background-color: #a52732; /* Light red background */
+ -fx-border-color: #721c24; /* Light red border */
+ -fx-text-fill: white; /* Dark red text for high contrast */
+ -fx-background-radius: 1em 1em 0 1em;
+ -fx-border-radius: 1em 1em 0 1em;
+ -fx-padding: 6px;
+ -fx-border-insets: 0px 7px 0px 7px;
+ -fx-background-insets: 0px 7px 0px 7px;
+}
+
+#displayPicture {
+ /* Shadow effect on image. */
+ -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 10, 0.5, 5, 5);
+
+ /* Change size of image. */
+ -fx-scale-x: 1;
+ -fx-scale-y: 1;
+
+ /* Rotate image clockwise by degrees. */
+ -fx-rotate: 0;
+}
diff --git a/src/main/resources/css/main.css b/src/main/resources/css/main.css
new file mode 100644
index 0000000000..cc4c9a02f3
--- /dev/null
+++ b/src/main/resources/css/main.css
@@ -0,0 +1,49 @@
+.root {
+ main-color: rgb(255, 246, 185); /* Create a looked-up color called "main-color" within root. */
+ -fx-background-color: main-color;
+}
+
+.text-field {
+ -fx-background-color: orange;
+ -fx-font: 16px "Arial";
+ -fx-prompt-text-fill: #5c5c5c;
+ -fx-background-radius: 0;
+ -fx-border-radius: 0;
+}
+
+.button {
+ -fx-background-color: darkorange;
+ -fx-font: italic bold 16px "Arial";
+ -fx-background-radius: 0;
+ -fx-border-radius: 0;
+}
+
+.button:hover {
+ -fx-background-color: #ffb258;
+}
+
+.button:pressed {
+ -fx-background-color: #f8c48a;
+}
+
+.scroll-pane,
+.scroll-pane .viewport {
+ -fx-background-color: transparent;
+}
+
+.scroll-bar {
+ -fx-font-size: 10px; /* Change width of scroll bar. */
+ -fx-background-color: main-color;
+}
+
+.scroll-bar .thumb {
+ -fx-background-color: #ffd19c;
+ -fx-background-radius: 1em;
+}
+
+/* Hides the increment and decrement buttons. */
+.scroll-bar .increment-button,
+.scroll-bar .decrement-button {
+ -fx-pref-height: 0;
+ -fx-opacity: 0;
+}
diff --git a/src/main/resources/images/Nether.jpeg b/src/main/resources/images/Nether.jpeg
new file mode 100644
index 0000000000..664a695831
Binary files /dev/null and b/src/main/resources/images/Nether.jpeg differ
diff --git a/src/main/resources/images/UserIcon.png b/src/main/resources/images/UserIcon.png
new file mode 100644
index 0000000000..07b82ccf35
Binary files /dev/null and b/src/main/resources/images/UserIcon.png differ
diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml
new file mode 100644
index 0000000000..86dad28c23
--- /dev/null
+++ b/src/main/resources/view/DialogBox.fxml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml
new file mode 100644
index 0000000000..d8e6864d07
--- /dev/null
+++ b/src/main/resources/view/MainWindow.fxml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/nether/NetherTest.java b/src/test/java/nether/NetherTest.java
new file mode 100644
index 0000000000..40c525596f
--- /dev/null
+++ b/src/test/java/nether/NetherTest.java
@@ -0,0 +1,18 @@
+package nether;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+
+public class NetherTest {
+ @Test
+ public void dummyTest() {
+ assertEquals(2, 2);
+ }
+
+ @Test
+ public void anotherDummyTest() {
+ assertEquals(4, 4);
+ }
+}
diff --git a/src/test/java/nether/parser/ParserTest.java b/src/test/java/nether/parser/ParserTest.java
new file mode 100644
index 0000000000..04f6888570
--- /dev/null
+++ b/src/test/java/nether/parser/ParserTest.java
@@ -0,0 +1,118 @@
+package nether.parser;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+import nether.NetherException;
+import nether.command.AddCommand;
+import nether.command.Command;
+import nether.command.MarkDoneCommand;
+
+
+
+public class ParserTest {
+ private final Parser parser = new Parser();
+
+ @Test
+ public void parse_validCommand_returnsCorrectCommand() throws NetherException {
+ Command command = parser.parse("todo Read book");
+ assertInstanceOf(AddCommand.class, command);
+
+ command = parser.parse("mark 1");
+ assertInstanceOf(MarkDoneCommand.class, command);
+ }
+
+ @Test
+ public void parse_invalidCommand_throwsNetherException() {
+ assertThrows(NetherException.class, () -> parser.parse("blah"));
+ }
+
+ @Test
+ public void extractCommandWord_commandWords_returnsCorrectWOrds() {
+ assertEquals("todo", parser.extractCommandWord("todo read book"));
+ assertEquals("deadline", parser.extractCommandWord("deadline return book /by 2024-08-29 1500"));
+ assertEquals("event", parser.extractCommandWord("event this /from 2024-08-29 1500 /to 2024-09-02 2359"));
+ }
+
+ @Test
+ public void extractInputDetails_validTodoInput_returnsCorrectDetails() throws NetherException {
+ String[] result = parser.extractInputDetails("todo Read book", "todo");
+ assertArrayEquals(new String[]{"Read book", ""}, result);
+ }
+
+ @Test
+ public void extractInputDetails_emptyTodoDescription_throwsNetherException() {
+ NetherException exception = assertThrows(NetherException.class, () -> {
+ parser.extractInputDetails("todo ", "todo");
+ });
+ assertEquals("the description of a todo cannot be empty.", exception.getMessage());
+ }
+
+ @Test
+ public void extractInputDetails_validDeadlineInput_returnsCorrectDetails() throws NetherException {
+ String[] result = parser.extractInputDetails("deadline Submit assignment /by 2024-09-01", "deadline");
+ assertArrayEquals(new String[]{"Submit assignment", "", "2024-09-01"}, result);
+ }
+
+ @Test
+ public void extractInputDetails_missingDeadlineDescription_throwsNetherException() {
+ NetherException exception = assertThrows(NetherException.class, () -> {
+ parser.extractInputDetails("deadline /by 2024-09-01", "deadline");
+ });
+ assertEquals("the description or date/time of a deadline cannot be empty.", exception.getMessage());
+ }
+
+ @Test
+ void extractInputDetails_missingDeadlineDate_throwsNetherException() {
+ NetherException exception = assertThrows(NetherException.class, () -> {
+ parser.extractInputDetails("deadline Submit assignment /by ", "deadline");
+ });
+ assertEquals("the description or date/time of a deadline cannot be empty.", exception.getMessage());
+ }
+
+ @Test
+ void extractInputDetails_validEventInput_returnsCorrectDetails() throws NetherException {
+ String[] result = parser.extractInputDetails("event Project meeting /from 2024-09-01 /to 2024-09-02", "event");
+ assertArrayEquals(new String[]{"Project meeting", "", "2024-09-01", "2024-09-02"}, result);
+ }
+
+ @Test
+ void extractInputDetails_missingEventDescription_throwsNetherException() {
+ NetherException exception = assertThrows(NetherException.class, () -> {
+ parser.extractInputDetails("event /from 2024-09-01 /to 2024-09-02", "event");
+ });
+ assertEquals("the description, start time, or end time of an event cannot be empty.", exception.getMessage());
+ }
+
+ @Test
+ void extractInputDetails_missingEventDates_throwsNetherException() {
+ NetherException exception = assertThrows(NetherException.class, () -> {
+ parser.extractInputDetails("event Project meeting /from /to", "event");
+ });
+ assertEquals("the description, start time, or end time of an event cannot be empty.", exception.getMessage());
+ }
+
+ // Tests for the extractTaskNumber method
+
+ @Test
+ void extractTaskNumber_validInput_returnsCorrectTaskNumber() {
+ int result = parser.extractTaskNumber("mark 3");
+ assertEquals(3, result);
+ }
+
+ @Test
+ void extractTaskNumber_invalidNumberFormat_returnsMinusOne() {
+ int result = parser.extractTaskNumber("mark one");
+ assertEquals(-1, result);
+ }
+
+ @Test
+ void extractTaskNumber_missingTaskNumber_returnsMinusOne() {
+ int result = parser.extractTaskNumber("mark");
+ assertEquals(-1, result);
+ }
+}
diff --git a/src/test/java/nether/task/TaskListTest.java b/src/test/java/nether/task/TaskListTest.java
new file mode 100644
index 0000000000..8ef31279cf
--- /dev/null
+++ b/src/test/java/nether/task/TaskListTest.java
@@ -0,0 +1,66 @@
+package nether.task;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+public class TaskListTest {
+ private final TaskList taskList = new TaskList();
+
+ @Test
+ public void addTask_validTask_taskAddedSuccessfully() {
+ TodoTask task = new TodoTask("Read book", "");
+
+ taskList.addTask(task);
+
+ assertEquals(1, taskList.getSize());
+ assertEquals(task, taskList.getTask(0));
+ }
+
+ @Test
+ public void deleteTask_invalidIndexUnder_throwsIndexOutOfBoundsException() {
+ assertThrows(IndexOutOfBoundsException.class, () -> taskList.deleteTask(0));
+ }
+
+ @Test
+ public void deleteTask_invalidIndexUpper_throwsIndexOutOfBoundsException() {
+ assertThrows(IndexOutOfBoundsException.class, () -> taskList.deleteTask(taskList.getSize() + 1));
+ }
+
+ // Tests for getSize()
+ @Test
+ void getSize_withEmptyTaskList_returnsZero() {
+ assertEquals(0, taskList.getSize());
+ }
+
+ @Test
+ void getSize_withMultipleTasks_returnsCorrectSize() {
+ TaskList taskListTest = new TaskList();
+ taskListTest.addTask(new TodoTask("Read a book", ""));
+ taskListTest.addTask(new DeadlineTask("Submit assignment", "", "2024-09-01 2359"));
+ assertEquals(2, taskListTest.getSize());
+ }
+
+ // Tests for getTask()
+ @Test
+ void getTask_withValidIndex_returnsCorrectTask() {
+ Task task = new TodoTask("Go jogging", "Exercise");
+ TaskList taskListTest = new TaskList();
+ taskListTest.addTask(task);
+ assertEquals(task, taskListTest.getTask(0));
+ }
+
+ // Tests for getTasks()
+ @Test
+ void getTasks_returnsListOfAllTasks() {
+ List taskListTest = new ArrayList<>();
+ taskListTest.add(new TodoTask("Finish project", "Work"));
+ taskListTest.add(new EventTask("Team meeting", "Work", "2024-09-01 2100", "2024-09-02 2300"));
+ TaskList taskList = new TaskList(taskListTest);
+ assertEquals(taskListTest, taskList.getTasks());
+ }
+}
diff --git a/src/test/java/nether/task/ToDoTaskTest.java b/src/test/java/nether/task/ToDoTaskTest.java
new file mode 100644
index 0000000000..b295bfc9c2
--- /dev/null
+++ b/src/test/java/nether/task/ToDoTaskTest.java
@@ -0,0 +1,50 @@
+package nether.task;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.junit.jupiter.api.Test;
+
+
+public class ToDoTaskTest {
+ // Test the constructor and inherited methods
+ @Test
+ void constructor_withValidDescription_createsTodoTask() {
+ TodoTask task = new TodoTask("Complete assignment", "School");
+ assertEquals("Complete assignment", task.getDescription(), "Description should match the "
+ + "input provided.");
+ assertFalse(task.isDone(), "Newly created TodoTask should not be marked as done.");
+ }
+
+ // Test the toSaveFormat() method
+ @Test
+ void toSaveFormat_withUndoneTask_returnsCorrectFormat() {
+ TodoTask task = new TodoTask("Buy groceries", "Chores");
+ assertEquals("T| |Chores|Buy groceries", task.toSaveFormat(), "toSaveFormat should return correct "
+ + "save string for an undone task.");
+ }
+
+ @Test
+ void toSaveFormat_withDoneTask_returnsCorrectFormat() {
+ TodoTask task = new TodoTask("Read a book", "");
+ task.markAsDone();
+ assertEquals("T|X||Read a book", task.toSaveFormat(), "toSaveFormat should return correct save "
+ + "string for a done task.");
+ }
+
+ // Test the toString() method
+ @Test
+ void toString_withUndoneTask_returnsCorrectString() {
+ TodoTask task = new TodoTask("Go jogging", "Exercise");
+ assertEquals("[T][ ] Go jogging", task.toString(), "toString should match the expected format "
+ + "for an undone task.");
+ }
+
+ @Test
+ void toString_withDoneTask_returnsCorrectString() {
+ TodoTask task = new TodoTask("Do the laundry", "Chores");
+ task.markAsDone();
+ assertEquals("[T][X] Do the laundry", task.toString(), "toString should match the expected "
+ + "format for a done task.");
+ }
+}
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
index 657e74f6e7..549a254ce7 100644
--- a/text-ui-test/EXPECTED.TXT
+++ b/text-ui-test/EXPECTED.TXT
@@ -1,7 +1,82 @@
Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
+ _ _ _ _
+| \ | | ___| |_| |__ ___ _ __
+| \| |/ _ \ __| '_ \/ _ \ '__|
+| |\ | __/ |_| | | ||__/ |
+|_| \_|\___|\__|_| |_\___|_|
+____________________________________________________________
+Hello sir! I'm Nether
+What can I do for you today?
+____________________________________________________________
+____________________________________________________________
+Got it. I've added this task:
+ [T][ ] borrow book
+Now you have 1 task in the list.
+____________________________________________________________
+____________________________________________________________
+Got it. I've added this task:
+ [D][ ] return book (by: Sunday)
+Now you have 2 tasks in the list.
+____________________________________________________________
+____________________________________________________________
+Got it. I've added this task:
+ [E][ ] project meeting (from: Mon 2pm to: 4pm)
+Now you have 3 tasks in the list.
+____________________________________________________________
+____________________________________________________________
+Here are the tasks in your list:
+1. [T][ ] borrow book
+2. [D][ ] return book (by: Sunday)
+3. [E][ ] project meeting (from: Mon 2pm to: 4pm)
+____________________________________________________________
+____________________________________________________________
+Sir, the command: 'blah' is not in our database
+____________________________________________________________
+____________________________________________________________
+Sir, The description of a todo cannot be empty.
+____________________________________________________________
+____________________________________________________________
+Sir, The description of a deadline cannot be empty.
+____________________________________________________________
+____________________________________________________________
+Sir, The description or date/time of a deadline cannot be empty.
+____________________________________________________________
+____________________________________________________________
+Got it. I've added this task:
+ [T][ ] join sports club
+Now you have 4 tasks in the list.
+____________________________________________________________
+____________________________________________________________
+Well done! I've marked this task as done:
+ [D][X] return book (by: Sunday)
+____________________________________________________________
+____________________________________________________________
+Well done! I've marked this task as done:
+ [E][X] project meeting (from: Mon 2pm to: 4pm)
+____________________________________________________________
+____________________________________________________________
+Here are the tasks in your list:
+1. [T][ ] borrow book
+2. [D][X] return book (by: Sunday)
+3. [E][X] project meeting (from: Mon 2pm to: 4pm)
+4. [T][ ] join sports club
+____________________________________________________________
+____________________________________________________________
+Understood, I've marked this task as not done:
+ [D][ ] return book (by: Sunday)
+____________________________________________________________
+____________________________________________________________
+Noted, I've removed this task from the list:
+ [T][ ] borrow book
+Now you have 3 tasks in the list.
+____________________________________________________________
+____________________________________________________________
+Here are the tasks in your list:
+1. [D][ ] return book (by: Sunday)
+2. [E][X] project meeting (from: Mon 2pm to: 4pm)
+3. [T][ ] join sports club
+____________________________________________________________
+____________________________________________________________
+Bye. If you need any more help in the future, feel free to ask me. Enjoy your day!
+____________________________________________________________
\ No newline at end of file
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
index e69de29bb2..fd0495e389 100644
--- a/text-ui-test/input.txt
+++ b/text-ui-test/input.txt
@@ -0,0 +1,16 @@
+todo borrow book
+deadline return book /by Sunday
+event project meeting /from Mon 2pm /to 4pm
+list
+blah
+todo
+deadline
+deadline submit essay
+Todo join sports club
+mark 2
+mark 3
+list
+unmark 2
+delete 1
+list
+bye
\ No newline at end of file
diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat
index 0873744649..911cc71a0d 100644
--- a/text-ui-test/runtest.bat
+++ b/text-ui-test/runtest.bat
@@ -15,7 +15,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 Nether < input.txt > ACTUAL.TXT
REM compare the output to the expected output
-FC ACTUAL.TXT EXPECTED.TXT
+FC /W ACTUAL.TXT EXPECTED.TXT
diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh
old mode 100644
new mode 100755