diff --git a/.gitignore b/.gitignore
index 2873e189e1..03e335d3be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,8 @@ bin/
/text-ui-test/ACTUAL.TXT
text-ui-test/EXPECTED-UNIX.TXT
+
+*.class
+/.vscode/
+
+data/
\ No newline at end of file
diff --git a/AI.md b/AI.md
new file mode 100644
index 0000000000..7a11fc487d
--- /dev/null
+++ b/AI.md
@@ -0,0 +1,17 @@
+# AI Tool Used
+
+- GitHub Copilot
+
+# Usage
+
+- Used for JavaDoc documentation. Wrote two down manually, and used as example for Copilot to follow.
+- Used to generate commit messages, with manual editing where required for brevity.
+- Used to create basic assertions for `A-Assertions`.
+- Used to check for code quality issues, using the CS2103T website as a guide.
+- Used to merge JavaFX Tutorial, with manual coding first done to bridge the gap, and Copilot used to change variable names, function names etc.
+
+# Observations
+
+- Copilot can be very effective at widescale things, as long as given sufficient examples to work on; however without such examples, the format or style of the code may be undesired. Specifying the scope is also important, as without it, the AI may try to edit the whole code base vs just a certain subset of files.
+- Without sufficient context, Copilot will miss out on small details, e.g. when writing commits/PRs. Especially when there are large diffs, Copilot will try to summarize all the changes made, but may miss out on small things that are extremely key to the change being made.
+- Overall, a lot of time is saved from manually writing code/documentation. A decent amount of issues were also brought up regarding the structure, that I did not know about/would not have noticed.
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000000..abdf8fe416
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,68 @@
+plugins {
+ id 'java'
+ id 'application'
+ id 'checkstyle'
+ id 'com.github.johnrengelman.shadow' version '7.1.2'
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
+
+checkstyle {
+ toolVersion = '11.0.0'
+}
+
+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("amia.Launcher")
+}
+
+shadowJar {
+ archiveBaseName = "amia"
+ archiveClassifier = null
+}
+
+run{
+ standardInput = System.in
+ enableAssertions = true;
+}
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000000..d1399810b5
--- /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..dcaa1af3c3
--- /dev/null
+++ b/config/checkstyle/suppressions.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index 47b9f984f7..f6b238af35 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,30 +1,330 @@
-# Duke User Guide
+# Amia User Guide
-// Update the title above to match the actual product name
+
-// Product screenshot goes here
+Amia is a task management chatbot that helps you organize and track your tasks efficiently. Whether you need to manage simple to-dos, set deadlines, or schedule events, Amia is here to help!
-// Product intro goes here
+## Introduction
-## Adding deadlines
+Amia allows you to:
-// Describe the action and its outcome.
+- **Create tasks** in three formats: simple to-dos, tasks with deadlines, and scheduled events
+- **Organize your workflow** by marking tasks as complete
+- **Find tasks quickly** using keyword search
+- **Manage multiple tasks at once** with mass operations
+- **Persist your data** automatically (all tasks are saved!)
-// Give examples of usage
+Amia is available in both **command-line** and **GUI** versions, so choose whichever works best for you.
-Example: `keyword (optional arguments)`
+---
-// A description of the expected outcome goes here
+## Quick Start
+
+### Running Amia
+
+**GUI Version:** Double-click the Amia executable or run from your IDE
+**CLI Version:** Run `.\gradlew run` from the command line
+
+You'll see a welcome message from Amia, and you're ready to start!
+
+---
+
+## Features
+
+### 1. Adding a Simple To-Do Task
+
+Add a basic task without any specific deadline.
+
+**Format:** `todo `
+
+**Examples:**
+
+```
+todo buy groceries
+todo call mom
+todo finish homework
+```
+
+**Expected Output:**
+
+```
+I've added this task!
+ [T][ ] buy groceries
+You have 1 task.
+```
+
+---
+
+### 2. Adding a Task with a Deadline
+
+Add a task that needs to be completed by a specific date and time.
+
+**Format:** `deadline /by `
+
+**Date/Time Format:** `yyyy-MM-dd HHmm` (e.g., `2025-03-15 1400` for 2:00 PM on March 15, 2025)
+
+**Examples:**
+
+```
+deadline submit report /by 2025-02-28 2359
+deadline finish project /by 2025-03-10 1600
+deadline pay bills /by 2025-03-01 0900
+```
+
+**Expected Output:**
+
+```
+I've added this task!
+ [D][ ] submit report (by: Feb 28 2025, 11:59PM)
+You have 2 tasks.
+```
+
+---
+
+### 3. Adding an Event
+
+Add an event with a specific start and end time.
+
+**Format:** `event /from /to `
+
+**Date/Time Format:** `yyyy-MM-dd HHmm`
+
+**Examples:**
```
-expected output
+event team meeting /from 2025-03-05 1400 /to 2025-03-05 1530
+event summer vacation /from 2025-06-01 0000 /to 2025-08-31 2359
+event birthday party /from 2025-03-20 1800 /to 2025-03-20 2200
```
-## Feature ABC
+**Expected Output:**
+
+```
+I've added this task!
+ [E][ ] team meeting (from: Mar 05 2025, 2:00PM to: Mar 05 2025, 3:30PM)
+You have 3 tasks.
+```
+
+---
+
+### 4. Listing All Tasks
+
+View all your tasks in one place.
+
+**Format:** `list`
+
+**Example:**
+
+```
+list
+```
+
+**Expected Output:**
+
+```
+Here are your tasks:
+1. [T][ ] buy groceries
+2. [D][ ] submit report (by: Feb 28 2025, 11:59PM)
+3. [E][ ] team meeting (from: Mar 05 2025, 2:00PM to: Mar 05 2025, 3:30PM)
+```
+
+---
+
+### 5. Marking a Task as Done
+
+Mark one or more tasks as complete.
+
+**Format:** `mark [more numbers...]`
+
+**Examples:**
+
+```
+mark 1
+mark 2 3 5
+```
+
+**Expected Output (Single Task):**
+
+```
+I've marked the task as done!
+ [T][X] buy groceries
+```
+
+**Expected Output (Multiple Tasks):**
+
+```
+I've marked these tasks as done:
+ [D][X] submit report (by: Feb 28 2025, 11:59PM)
+ [E][X] team meeting (from: Mar 05 2025, 2:00PM to: Mar 05 2025, 3:30PM)
+ [T][X] call mom
+```
+
+---
+
+### 6. Unmarking a Task
+
+Mark a completed task as incomplete again.
+
+**Format:** `unmark [more numbers...]`
+
+**Examples:**
+
+```
+unmark 1
+unmark 2 3
+```
+
+**Expected Output:**
+
+```
+I've marked the task as not done yet.
+ [T][ ] buy groceries
+```
+
+---
+
+### 7. Deleting a Task
+
+Remove one or more tasks from your list.
+
+**Format:** `delete [more numbers...]`
+
+**Examples:**
+
+```
+delete 1
+delete 2 4 6
+```
+
+**Expected Output (Single Task):**
+
+```
+I've removed this task:
+ [T][ ] buy groceries
+You have 2 tasks.
+```
+
+**Expected Output (Multiple Tasks):**
+
+```
+I've removed these tasks:
+ [T][ ] read book
+ [D][ ] submit report (by: Feb 28 2025, 11:59PM)
+You have 1 task.
+```
+
+---
+
+### 8. Finding Tasks
+
+Search for tasks using keywords.
+
+**Format:** `find `
+
+**Examples:**
+
+```
+find meeting
+find buy
+find project
+```
+
+**Expected Output:**
+
+```
+Here are the matching tasks:
+1. [E][ ] team meeting (from: Mar 05 2025, 2:00PM to: Mar 05 2025, 3:30PM)
+2. [E][ ] emergency meeting (from: Mar 10 2025, 10:00AM to: Mar 10 2025, 11:00AM)
+```
+
+---
+
+### 9. Exiting Amia
+
+Exit the application gracefully.
+
+**Format:** `bye`
+
+**Example:**
+
+```
+bye
+```
+
+**Expected Output:**
+
+```
+Bye!
+```
+
+---
+
+## Task Format Legend
+
+When you see a task displayed, here's what the symbols mean:
+
+| Symbol | Meaning |
+| ------ | ------------------------- |
+| `[T]` | To-Do task |
+| `[D]` | Task with Deadline |
+| `[E]` | Event |
+| `[X]` | Task is completed ✓ |
+| `[ ]` | Task is not yet completed |
+
+**Example:** `[D][X] submit report (by: Feb 28 2025, 11:59PM)`
+
+- This is a completed deadline task
+- The task is to submit a report
+- The deadline is February 28, 2025 at 11:59 PM
+
+---
+
+## FAQ
+
+**Q: What happens if I enter an invalid command?**
+A: Amia will let you know with an error message. Just try again with the correct format!
+
+**Q: Can I edit an existing task?**
+A: Currently, you can't edit tasks directly. Delete the task with `delete` and create a new one instead.
+
+**Q: Is there a limit to how many tasks I can have?**
+A: Yes, the maximum is 100 tasks. If you reach this limit, you'll need to delete some tasks before adding more.
+
+**Q: Can I export my tasks?**
+A: Currently, Amia saves tasks in an internal format. You can view all your tasks anytime using the `list` command.
+
+---
+
+## Command Quick Reference
+
+| Command | Format | Example |
+| ------------- | ------------------------------------------------------- | --------------------------------------------------------- |
+| **To-Do** | `todo ` | `todo buy groceries` |
+| **Deadline** | `deadline /by ` | `deadline report /by 2025-03-15 1400` |
+| **Event** | `event /from /to ` | `event meeting /from 2025-03-15 1400 /to 2025-03-15 1500` |
+| **List** | `list` | `list` |
+| **Mark Done** | `mark [more...]` | `mark 1 2 3` |
+| **Unmark** | `unmark [more...]` | `unmark 1` |
+| **Delete** | `delete [more...]` | `delete 2 4` |
+| **Find** | `find ` | `find meeting` |
+| **Exit** | `bye` | `bye` |
+
+---
+
+## Troubleshooting
+
+**Problem:** Dates aren't being recognized
+
+- **Solution:** Make sure you're using the exact format `yyyy-MM-dd HHmm` (e.g., `2025-03-15 1430`)
+
+**Problem:** Tasks aren't being saved
+
+- **Solution:** Amia saves automatically after each command. Make sure the `data/` folder exists and Amia has write permissions to your system
-// Feature details
+**Problem:** Error message "Invalid task number"
+- **Solution:** Make sure the task number exists in your list. Use `list` to see all your tasks and their numbers
-## Feature XYZ
+**Problem:** Mass operations aren't working
-// Feature details
\ No newline at end of file
+- **Solution:** Separate task numbers with spaces. For example: `delete 1 2 3` (not `delete 1,2,3`)
diff --git a/docs/Ui.png b/docs/Ui.png
new file mode 100644
index 0000000000..a09527d60f
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/amia/Amia.java b/src/main/java/amia/Amia.java
new file mode 100644
index 0000000000..47c33eafbe
--- /dev/null
+++ b/src/main/java/amia/Amia.java
@@ -0,0 +1,166 @@
+package amia;
+
+import amia.command.Command;
+import amia.exception.AmiaException;
+import amia.parser.Parser;
+import amia.storage.Storage;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Main application class for the Amia task manager. Handles the main
+ * application loop and task management operations.
+ */
+public class Amia {
+ private TaskList tasks;
+ private Ui ui;
+ private Storage storage;
+ private boolean isExit = false;
+
+ /**
+ * Constructs an Amia instance with default file path. Initializes storage, UI,
+ * and loads saved tasks.
+ */
+ public Amia() {
+ this(null);
+ }
+
+ /**
+ * Constructs an Amia instance for testing with custom file path.
+ *
+ * @param filePath The path to the data file.
+ */
+ public Amia(String filePath) {
+ this.storage = new Storage(filePath);
+ this.ui = new Ui();
+ this.tasks = loadTasks();
+ assert this.storage != null && this.ui != null : "Storage and UI must be initialized";
+ assert this.tasks != null : "Tasks must be initialized";
+ }
+
+ /**
+ * Loads tasks from storage, returning an empty TaskList if loading fails.
+ *
+ * @return The loaded TaskList or a new empty TaskList.
+ */
+ private TaskList loadTasks() {
+ assert storage != null : "Storage must be initialized before loading tasks";
+ try {
+ return new TaskList(storage.load());
+ } catch (AmiaException e) {
+ return new TaskList();
+ }
+ }
+
+ /**
+ * Starts the application and runs the main loop.
+ *
+ * @param args Command line arguments (unused).
+ */
+ public static void main(String[] args) {
+ Amia amia = new Amia();
+ amia.start();
+ amia.loop();
+ amia.exit();
+ }
+
+ /**
+ * Generates a response for the user's input. Used by the GUI.
+ *
+ * @param input The user's command.
+ * @return The response string to display.
+ */
+ public String getResponse(String input) {
+ try {
+ return executeCommand(input);
+ } catch (AmiaException e) {
+ return e.getMessage();
+ }
+ }
+
+ /**
+ * Checks if the last command was an exit command.
+ *
+ * @return true if the application should exit, false otherwise.
+ */
+ public boolean shouldExit() {
+ return isExit;
+ }
+
+ /**
+ * Continuously reads user commands and processes them until the user exits.
+ * Used by the CLI.
+ */
+ public void loop() {
+ while (!shouldExit()) {
+ processOneCommand();
+ }
+ ui.close();
+ }
+
+ /**
+ * Processes a single command from the user in the CLI. Reads input, executes
+ * the command, and displays the result.
+ */
+ private void processOneCommand() {
+ try {
+ String input = ui.readCommand();
+ String response = getResponse(input);
+ displayResponse(response);
+ } catch (Exception e) {
+ displayError(e.getMessage());
+ }
+ }
+
+ /**
+ * Executes a parsed command and updates exit state.
+ *
+ * @param input The user's command string.
+ * @return The response string from command execution.
+ * @throws AmiaException If parsing or execution fails.
+ */
+ private String executeCommand(String input) throws AmiaException {
+ assert tasks != null && ui != null && storage != null : "Core components must be initialized";
+ Command command = Parser.parse(input);
+ assert command != null : "Parser must return a command";
+ String response = command.execute(tasks, ui, storage);
+ isExit = command.isExit();
+ return response;
+ }
+
+ /**
+ * Displays a response message with formatting lines.
+ *
+ * @param response The message to display.
+ */
+ private void displayResponse(String response) {
+ ui.showLine();
+ ui.showMessage(response);
+ ui.showLine();
+ }
+
+ /**
+ * Displays an error message with formatting lines.
+ *
+ * @param errorMessage The error message to display.
+ */
+ private void displayError(String errorMessage) {
+ ui.showLine();
+ ui.showMessage(errorMessage);
+ ui.showLine();
+ }
+
+ /**
+ * Initializes the application by displaying a welcome message.
+ */
+ public void start() {
+ ui.showWelcome();
+ }
+
+ /**
+ * Displays the goodbye message when the application exits.
+ */
+ public void exit() {
+ ui.showGoodbye();
+ }
+}
diff --git a/src/main/java/amia/DialogBox.java b/src/main/java/amia/DialogBox.java
new file mode 100644
index 0000000000..8ccea4650b
--- /dev/null
+++ b/src/main/java/amia/DialogBox.java
@@ -0,0 +1,61 @@
+package amia;
+
+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;
+
+/**
+ * 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(MainWindow.class.getResource("/view/DialogBox.fxml"));
+ fxmlLoader.setController(this);
+ fxmlLoader.setRoot(this);
+ fxmlLoader.load();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ 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.TOP_LEFT);
+ }
+
+ public static DialogBox getUserDialog(String text, Image img) {
+ return new DialogBox(text, img);
+ }
+
+ public static DialogBox getAmiaDialog(String text, Image img) {
+ var db = new DialogBox(text, img);
+ db.flip();
+ return db;
+ }
+}
diff --git a/src/main/java/amia/Launcher.java b/src/main/java/amia/Launcher.java
new file mode 100644
index 0000000000..f9f68dad42
--- /dev/null
+++ b/src/main/java/amia/Launcher.java
@@ -0,0 +1,12 @@
+package amia;
+
+import javafx.application.Application;
+
+/**
+ * A launcher class to workaround classpath issues.
+ */
+public class Launcher {
+ public static void main(String[] args) {
+ Application.launch(Main.class, args);
+ }
+}
diff --git a/src/main/java/amia/Main.java b/src/main/java/amia/Main.java
new file mode 100644
index 0000000000..ceced7cc93
--- /dev/null
+++ b/src/main/java/amia/Main.java
@@ -0,0 +1,32 @@
+package amia;
+
+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;
+
+/**
+ * A GUI for Amia using FXML.
+ */
+public class Main extends Application {
+
+ private Amia amia = new Amia();
+
+ @Override
+ public void start(Stage stage) {
+ try {
+ FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml"));
+ AnchorPane ap = fxmlLoader.load();
+ Scene scene = new Scene(ap);
+ stage.setScene(scene);
+ stage.setTitle("Amia");
+ fxmlLoader.getController().setAmia(amia); // inject the Amia instance
+ stage.show();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/amia/MainWindow.java b/src/main/java/amia/MainWindow.java
new file mode 100644
index 0000000000..f26f49258a
--- /dev/null
+++ b/src/main/java/amia/MainWindow.java
@@ -0,0 +1,71 @@
+package amia;
+
+import javafx.animation.PauseTransition;
+import javafx.application.Platform;
+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;
+import javafx.util.Duration;
+
+/**
+ * Controller for the main GUI.
+ */
+public class MainWindow extends AnchorPane {
+ @FXML
+ private ScrollPane scrollPane;
+ @FXML
+ private VBox dialogContainer;
+ @FXML
+ private TextField userInput;
+ @FXML
+ private Button sendButton;
+
+ private Amia amia;
+
+ private Image userImage = new Image(this.getClass().getResourceAsStream("/images/User.png"));
+ private Image amiaImage = new Image(this.getClass().getResourceAsStream("/images/Amia.jpg"));
+
+ @FXML
+ public void initialize() {
+ scrollPane.vvalueProperty().bind(dialogContainer.heightProperty());
+ }
+
+ /** Injects the Amia instance */
+ public void setAmia(Amia a) {
+ amia = a;
+ showWelcomeMessage();
+ }
+
+ /**
+ * Displays the welcome message when the GUI starts.
+ */
+ private void showWelcomeMessage() {
+ String welcomeMessage = "Hello! I'm Amia!\nWhat can I do for you?";
+ dialogContainer.getChildren().add(DialogBox.getAmiaDialog(welcomeMessage, amiaImage));
+ }
+
+ /**
+ * Creates two dialog boxes, one echoing user input and the other containing
+ * Amia's reply and then appends them to the dialog container. Clears the user
+ * input after processing.
+ */
+ @FXML
+ private void handleUserInput() {
+ String input = userInput.getText();
+ String response = amia.getResponse(input);
+ dialogContainer.getChildren().addAll(DialogBox.getUserDialog(input, userImage),
+ DialogBox.getAmiaDialog(response, amiaImage));
+ userInput.clear();
+
+ // Check if it's an exit command and close after delay
+ if (amia.shouldExit()) {
+ PauseTransition delay = new PauseTransition(Duration.seconds(2));
+ delay.setOnFinished(event -> Platform.exit());
+ delay.play();
+ }
+ }
+}
diff --git a/src/main/java/amia/command/AddCommand.java b/src/main/java/amia/command/AddCommand.java
new file mode 100644
index 0000000000..782a60159a
--- /dev/null
+++ b/src/main/java/amia/command/AddCommand.java
@@ -0,0 +1,52 @@
+package amia.command;
+
+import amia.exception.AmiaException;
+import amia.exception.ErrorMessages;
+import amia.storage.Storage;
+import amia.task.Task;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Abstract base class for commands that add tasks to the task list. Implements
+ * the template method pattern to eliminate code duplication.
+ */
+public abstract class AddCommand extends Command {
+ protected String commandText;
+
+ /**
+ * Constructs an AddCommand with the given command text.
+ *
+ * @param commandText The full command string.
+ */
+ public AddCommand(String commandText) {
+ this.commandText = commandText;
+ }
+
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ try {
+ if (tasks.size() >= TaskList.MAX_TASKS) {
+ throw new AmiaException(ErrorMessages.TASK_LIST_FULL);
+ }
+
+ Task task = createTask();
+ assert task != null : "createTask() must not return null";
+ tasks.add(task);
+ storage.save(tasks.toArrayList());
+ return ui.formatAddTaskMessage(task, tasks.size());
+ } catch (AmiaException e) {
+ return e.getMessage();
+ }
+ }
+
+ /**
+ * Creates the specific task type for this command. Subclasses must implement
+ * this method to define how to parse the command text and create the
+ * appropriate task.
+ *
+ * @return The created task.
+ * @throws AmiaException If task creation fails.
+ */
+ protected abstract Task createTask() throws AmiaException;
+}
diff --git a/src/main/java/amia/command/AddDeadlineCommand.java b/src/main/java/amia/command/AddDeadlineCommand.java
new file mode 100644
index 0000000000..ab613e36ac
--- /dev/null
+++ b/src/main/java/amia/command/AddDeadlineCommand.java
@@ -0,0 +1,27 @@
+package amia.command;
+
+import amia.exception.AmiaException;
+import amia.parser.Parser;
+import amia.task.Deadline;
+import amia.task.Task;
+
+/**
+ * Represents a command to add a Deadline task to the task list.
+ */
+public class AddDeadlineCommand extends AddCommand {
+
+ /**
+ * Constructs an AddDeadlineCommand with the given command text.
+ *
+ * @param commandText The full command string.
+ */
+ public AddDeadlineCommand(String commandText) {
+ super(commandText);
+ }
+
+ @Override
+ protected Task createTask() throws AmiaException {
+ Parser.DeadlineInfo info = Parser.parseDeadline(commandText);
+ return new Deadline(info.getDescription(), info.getDeadline());
+ }
+}
diff --git a/src/main/java/amia/command/AddEventCommand.java b/src/main/java/amia/command/AddEventCommand.java
new file mode 100644
index 0000000000..563390d848
--- /dev/null
+++ b/src/main/java/amia/command/AddEventCommand.java
@@ -0,0 +1,27 @@
+package amia.command;
+
+import amia.exception.AmiaException;
+import amia.parser.Parser;
+import amia.task.Event;
+import amia.task.Task;
+
+/**
+ * Represents a command to add an Event task to the task list.
+ */
+public class AddEventCommand extends AddCommand {
+
+ /**
+ * Constructs an AddEventCommand with the given command text.
+ *
+ * @param commandText The full command string.
+ */
+ public AddEventCommand(String commandText) {
+ super(commandText);
+ }
+
+ @Override
+ protected Task createTask() throws AmiaException {
+ Parser.EventInfo info = Parser.parseEvent(commandText);
+ return new Event(info.getDescription(), info.getFrom(), info.getTo());
+ }
+}
diff --git a/src/main/java/amia/command/AddTodoCommand.java b/src/main/java/amia/command/AddTodoCommand.java
new file mode 100644
index 0000000000..c160025cc1
--- /dev/null
+++ b/src/main/java/amia/command/AddTodoCommand.java
@@ -0,0 +1,27 @@
+package amia.command;
+
+import amia.exception.AmiaException;
+import amia.parser.Parser;
+import amia.task.Task;
+import amia.task.ToDo;
+
+/**
+ * Represents a command to add a ToDo task to the task list.
+ */
+public class AddTodoCommand extends AddCommand {
+
+ /**
+ * Constructs an AddTodoCommand with the given command text.
+ *
+ * @param commandText The full command string.
+ */
+ public AddTodoCommand(String commandText) {
+ super(commandText);
+ }
+
+ @Override
+ protected Task createTask() throws AmiaException {
+ String description = Parser.extractDescription(commandText, "todo");
+ return new ToDo(description);
+ }
+}
diff --git a/src/main/java/amia/command/Command.java b/src/main/java/amia/command/Command.java
new file mode 100644
index 0000000000..3876ba9a01
--- /dev/null
+++ b/src/main/java/amia/command/Command.java
@@ -0,0 +1,29 @@
+package amia.command;
+
+import amia.storage.Storage;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Represents an abstract command that can be executed.
+ */
+public abstract class Command {
+ /**
+ * Executes the command and returns the result message.
+ *
+ * @param tasks The task list to operate on.
+ * @param ui The UI for displaying messages (for CLI mode).
+ * @param storage The storage for saving tasks.
+ * @return The result message to display.
+ */
+ public abstract String execute(TaskList tasks, Ui ui, Storage storage);
+
+ /**
+ * Checks if this command causes the application to exit.
+ *
+ * @return True if this is an exit command, false otherwise.
+ */
+ public boolean isExit() {
+ return false;
+ }
+}
diff --git a/src/main/java/amia/command/DeleteCommand.java b/src/main/java/amia/command/DeleteCommand.java
new file mode 100644
index 0000000000..9dcad1ad6d
--- /dev/null
+++ b/src/main/java/amia/command/DeleteCommand.java
@@ -0,0 +1,49 @@
+package amia.command;
+
+import amia.exception.AmiaException;
+import amia.parser.Parser;
+import amia.storage.Storage;
+import amia.task.Task;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Represents a command to delete a task from the task list.
+ */
+public class DeleteCommand extends Command {
+ private String commandText;
+
+ /**
+ * Constructs a DeleteCommand with the given command text.
+ *
+ * @param commandText The full command string.
+ */
+ public DeleteCommand(String commandText) {
+ this.commandText = commandText;
+ }
+
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ try {
+ String arguments = Parser.extractIndexArgument(commandText, "delete");
+ int[] indices = Parser.parseIndices(arguments);
+
+ if (indices.length == 1) {
+ Task removedTask = tasks.remove(indices[0]);
+ storage.save(tasks.toArrayList());
+ return "I've removed this task:\n " + removedTask + "\n" + ui.formatTaskCountMessage(tasks.size());
+ } else {
+ StringBuilder response = new StringBuilder("I've removed these tasks:\n");
+ for (int index : indices) {
+ Task removedTask = tasks.remove(index);
+ response.append(" ").append(removedTask).append("\n");
+ }
+ storage.save(tasks.toArrayList());
+ response.append(ui.formatTaskCountMessage(tasks.size()));
+ return response.toString();
+ }
+ } catch (AmiaException e) {
+ return e.getMessage();
+ }
+ }
+}
diff --git a/src/main/java/amia/command/ExitCommand.java b/src/main/java/amia/command/ExitCommand.java
new file mode 100644
index 0000000000..772fab4edd
--- /dev/null
+++ b/src/main/java/amia/command/ExitCommand.java
@@ -0,0 +1,20 @@
+package amia.command;
+
+import amia.storage.Storage;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Represents a command to exit the application.
+ */
+public class ExitCommand extends Command {
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ return "Goodbye~";
+ }
+
+ @Override
+ public boolean isExit() {
+ return true;
+ }
+}
diff --git a/src/main/java/amia/command/FindCommand.java b/src/main/java/amia/command/FindCommand.java
new file mode 100644
index 0000000000..aa76280864
--- /dev/null
+++ b/src/main/java/amia/command/FindCommand.java
@@ -0,0 +1,46 @@
+package amia.command;
+
+import amia.exception.AmiaException;
+import amia.parser.Parser;
+import amia.storage.Storage;
+import amia.task.Task;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Represents a command to find tasks by keyword.
+ */
+public class FindCommand extends Command {
+ private String commandText;
+
+ /**
+ * Constructs a FindCommand with the given command text.
+ *
+ * @param commandText The full command string.
+ */
+ public FindCommand(String commandText) {
+ this.commandText = commandText;
+ }
+
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ try {
+ String keyword = Parser.extractDescription(commandText, "find");
+ StringBuilder result = new StringBuilder("Here are the matching tasks:\n");
+ boolean foundAny = false;
+ for (int i = 0; i < tasks.size(); i++) {
+ Task task = tasks.get(i);
+ if (task.matches(keyword)) {
+ result.append((i + 1)).append(". ").append(task).append("\n");
+ foundAny = true;
+ }
+ }
+ if (!foundAny) {
+ result.append("No matching tasks found.");
+ }
+ return result.toString().trim();
+ } catch (AmiaException e) {
+ return e.getMessage();
+ }
+ }
+}
diff --git a/src/main/java/amia/command/ListCommand.java b/src/main/java/amia/command/ListCommand.java
new file mode 100644
index 0000000000..c0c335492a
--- /dev/null
+++ b/src/main/java/amia/command/ListCommand.java
@@ -0,0 +1,27 @@
+package amia.command;
+
+import amia.exception.AmiaException;
+import amia.storage.Storage;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Represents a command to list all tasks.
+ */
+public class ListCommand extends Command {
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ try {
+ if (tasks.size() == 0) {
+ return "No tasks to list...";
+ }
+ StringBuilder result = new StringBuilder("Here are your tasks:\n");
+ for (int i = 0; i < tasks.size(); i++) {
+ result.append((i + 1)).append(". ").append(tasks.get(i)).append("\n");
+ }
+ return result.toString().trim();
+ } catch (AmiaException e) {
+ return e.getMessage();
+ }
+ }
+}
diff --git a/src/main/java/amia/command/MarkCommand.java b/src/main/java/amia/command/MarkCommand.java
new file mode 100644
index 0000000000..fec5015e38
--- /dev/null
+++ b/src/main/java/amia/command/MarkCommand.java
@@ -0,0 +1,47 @@
+package amia.command;
+
+import amia.exception.AmiaException;
+import amia.parser.Parser;
+import amia.storage.Storage;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Represents a command to mark a task as done.
+ */
+public class MarkCommand extends Command {
+ private String commandText;
+
+ /**
+ * Constructs a MarkCommand with the given command text.
+ *
+ * @param commandText The full command string.
+ */
+ public MarkCommand(String commandText) {
+ this.commandText = commandText;
+ }
+
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ try {
+ String arguments = Parser.extractIndexArgument(commandText, "mark");
+ int[] indices = Parser.parseIndices(arguments);
+
+ if (indices.length == 1) {
+ tasks.markDone(indices[0]);
+ storage.save(tasks.toArrayList());
+ return "I've marked the task as done!\n " + tasks.get(indices[0]);
+ } else {
+ StringBuilder response = new StringBuilder("I've marked these tasks as done:\n");
+ for (int index : indices) {
+ tasks.markDone(index);
+ response.append(" ").append(tasks.get(index)).append("\n");
+ }
+ storage.save(tasks.toArrayList());
+ return response.toString().trim();
+ }
+ } catch (AmiaException e) {
+ return e.getMessage();
+ }
+ }
+}
diff --git a/src/main/java/amia/command/UnknownCommand.java b/src/main/java/amia/command/UnknownCommand.java
new file mode 100644
index 0000000000..5aa2d7f659
--- /dev/null
+++ b/src/main/java/amia/command/UnknownCommand.java
@@ -0,0 +1,15 @@
+package amia.command;
+
+import amia.storage.Storage;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Represents an unknown or invalid command.
+ */
+public class UnknownCommand extends Command {
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ return "...?";
+ }
+}
diff --git a/src/main/java/amia/command/UnmarkCommand.java b/src/main/java/amia/command/UnmarkCommand.java
new file mode 100644
index 0000000000..1b6f926368
--- /dev/null
+++ b/src/main/java/amia/command/UnmarkCommand.java
@@ -0,0 +1,47 @@
+package amia.command;
+
+import amia.exception.AmiaException;
+import amia.parser.Parser;
+import amia.storage.Storage;
+import amia.task.TaskList;
+import amia.ui.Ui;
+
+/**
+ * Represents a command to unmark a task (mark as not done).
+ */
+public class UnmarkCommand extends Command {
+ private String commandText;
+
+ /**
+ * Constructs an UnmarkCommand with the given command text.
+ *
+ * @param commandText The full command string.
+ */
+ public UnmarkCommand(String commandText) {
+ this.commandText = commandText;
+ }
+
+ @Override
+ public String execute(TaskList tasks, Ui ui, Storage storage) {
+ try {
+ String arguments = Parser.extractIndexArgument(commandText, "unmark");
+ int[] indices = Parser.parseIndices(arguments);
+
+ if (indices.length == 1) {
+ tasks.markUndone(indices[0]);
+ storage.save(tasks.toArrayList());
+ return "I've marked the task as not done yet.\n " + tasks.get(indices[0]);
+ } else {
+ StringBuilder response = new StringBuilder("I've marked these tasks as not done yet:\n");
+ for (int index : indices) {
+ tasks.markUndone(index);
+ response.append(" ").append(tasks.get(index)).append("\n");
+ }
+ storage.save(tasks.toArrayList());
+ return response.toString().trim();
+ }
+ } catch (AmiaException e) {
+ return e.getMessage();
+ }
+ }
+}
diff --git a/src/main/java/amia/exception/AmiaException.java b/src/main/java/amia/exception/AmiaException.java
new file mode 100644
index 0000000000..c549e276b9
--- /dev/null
+++ b/src/main/java/amia/exception/AmiaException.java
@@ -0,0 +1,15 @@
+package amia.exception;
+
+/**
+ * Custom exception class for the Amia application.
+ */
+public class AmiaException extends Exception {
+ /**
+ * Constructs an AmiaException with the given message.
+ *
+ * @param message The detail message.
+ */
+ public AmiaException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/amia/exception/ErrorMessages.java b/src/main/java/amia/exception/ErrorMessages.java
new file mode 100644
index 0000000000..213ff8f917
--- /dev/null
+++ b/src/main/java/amia/exception/ErrorMessages.java
@@ -0,0 +1,46 @@
+package amia.exception;
+
+/**
+ * Centralized error messages for consistent formatting across the application.
+ */
+public class ErrorMessages {
+ // General errors
+ public static final String EMPTY_INPUT = "Please provide a command.";
+ public static final String UNKNOWN_COMMAND = "I don't understand that command.";
+
+ // Task description errors
+ public static final String EMPTY_DESCRIPTION = "The description of a task cannot be empty.";
+
+ // Task list errors
+ public static final String INVALID_TASK_NUMBER = "Invalid task number.";
+ public static final String TASK_LIST_FULL = "The task list is full (maximum 100 tasks).";
+ public static final String NULL_TASK = "Cannot add null task.";
+
+ // Deadline errors
+ public static final String EMPTY_DEADLINE = "The deadline cannot be empty.";
+ public static final String INVALID_DEADLINE_FORMAT =
+ "Invalid format. Use: deadline /by ";
+
+ // Event errors
+ public static final String EMPTY_START_TIME = "The start time cannot be empty.";
+ public static final String EMPTY_END_TIME = "The end time cannot be empty.";
+ public static final String INVALID_EVENT_FORMAT =
+ "Invalid format. Use: event /from /to ";
+
+ // Date/time errors
+ public static final String INVALID_DATETIME_FORMAT = "Invalid date/time format. Use: yyyy-MM-dd HHmm";
+
+ // Storage errors
+ public static final String CANNOT_LOAD_TASKS = "Unable to load saved tasks from file.";
+ public static final String CANNOT_SAVE_TASKS = "Unable to save tasks to file.";
+
+ /**
+ * Returns a formatted error message for invalid command format.
+ *
+ * @param keyword The command keyword (e.g., "mark", "delete").
+ * @return Formatted error message.
+ */
+ public static String invalidIndexFormat(String keyword) {
+ return "Invalid format. Use: " + keyword + " [more numbers...]";
+ }
+}
diff --git a/src/main/java/amia/parser/CommandType.java b/src/main/java/amia/parser/CommandType.java
new file mode 100644
index 0000000000..2f411086c8
--- /dev/null
+++ b/src/main/java/amia/parser/CommandType.java
@@ -0,0 +1,34 @@
+package amia.parser;
+
+/**
+ * Enum representing different types of commands that can be parsed.
+ */
+public enum CommandType {
+ TODO("todo"), DEADLINE("deadline"), EVENT("event"), MARK("mark"), UNMARK("unmark"), DELETE("delete"), LIST("list"),
+ FIND("find"), BYE("bye"), UNKNOWN("unknown");
+
+ private final String value;
+
+ CommandType(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Converts a string to the corresponding CommandType.
+ *
+ * @param input The input string to convert.
+ * @return The CommandType that matches the input, or UNKNOWN if no match.
+ */
+ public static CommandType fromString(String input) {
+ // Extract the first word from the input to avoid false matches
+ String[] parts = input.split("\\s+", 2);
+ String command = parts[0].toLowerCase();
+
+ for (CommandType cmd : CommandType.values()) {
+ if (command.equals(cmd.value)) {
+ return cmd;
+ }
+ }
+ return UNKNOWN;
+ }
+}
diff --git a/src/main/java/amia/parser/Parser.java b/src/main/java/amia/parser/Parser.java
new file mode 100644
index 0000000000..feba0942e5
--- /dev/null
+++ b/src/main/java/amia/parser/Parser.java
@@ -0,0 +1,271 @@
+package amia.parser;
+
+import amia.command.AddDeadlineCommand;
+import amia.command.AddEventCommand;
+import amia.command.AddTodoCommand;
+import amia.command.Command;
+import amia.command.DeleteCommand;
+import amia.command.ExitCommand;
+import amia.command.FindCommand;
+import amia.command.ListCommand;
+import amia.command.MarkCommand;
+import amia.command.UnknownCommand;
+import amia.command.UnmarkCommand;
+import amia.exception.AmiaException;
+import amia.exception.ErrorMessages;
+
+/**
+ * Parses user commands and extracts relevant information.
+ */
+public class Parser {
+
+ /**
+ * Parses a command string and returns the appropriate Command object.
+ *
+ * @param input The user's input string.
+ * @return The Command object to execute.
+ * @throws AmiaException If parsing fails.
+ */
+ public static Command parse(String input) throws AmiaException {
+ CommandType commandType = parseCommandType(input);
+ assert commandType != null : "Command type must be resolved";
+
+ switch (commandType) {
+ case TODO:
+ return new AddTodoCommand(input);
+ case DEADLINE:
+ return new AddDeadlineCommand(input);
+ case EVENT:
+ return new AddEventCommand(input);
+ case MARK:
+ return new MarkCommand(input);
+ case UNMARK:
+ return new UnmarkCommand(input);
+ case DELETE:
+ return new DeleteCommand(input);
+ case LIST:
+ return new ListCommand();
+ case FIND:
+ return new FindCommand(input);
+ case BYE:
+ return new ExitCommand();
+ case UNKNOWN:
+ default:
+ return new UnknownCommand();
+ }
+ }
+
+ /**
+ * Parses a command string and returns the corresponding CommandType.
+ *
+ * @param input The command string to parse.
+ * @return The CommandType corresponding to the input.
+ * @throws AmiaException If the input is null, empty, or unknown.
+ */
+ public static CommandType parseCommandType(String input) throws AmiaException {
+ if (input == null || input.trim().isEmpty()) {
+ throw new AmiaException(ErrorMessages.EMPTY_INPUT);
+ }
+ CommandType commandType = CommandType.fromString(input.trim().toLowerCase());
+ return commandType;
+ }
+
+ /**
+ * Extracts the description from a command by removing the keyword prefix.
+ *
+ * @param command The full command string.
+ * @param keyword The keyword prefix to remove (e.g., "todo", "deadline").
+ * @return The extracted description.
+ * @throws AmiaException If the description is empty.
+ */
+ public static String extractDescription(String command, String keyword) throws AmiaException {
+ assert command != null && keyword != null : "Command and keyword must be non-null";
+ String description = command.substring(keyword.length()).trim();
+ if (description.isEmpty()) {
+ throw new AmiaException(ErrorMessages.EMPTY_DESCRIPTION);
+ }
+ return description;
+ }
+
+ /**
+ * Extracts the index argument from a command by removing the keyword prefix.
+ *
+ * @param command The full command string.
+ * @param keyword The keyword prefix to remove (e.g., "mark", "delete").
+ * @return The extracted index argument as a string.
+ * @throws AmiaException If the argument is empty.
+ */
+ public static String extractIndexArgument(String command, String keyword) throws AmiaException {
+ assert command != null && keyword != null : "Command and keyword must be non-null";
+ String arguments = command.substring(keyword.length()).trim();
+ if (arguments.isEmpty()) {
+ throw new AmiaException(ErrorMessages.invalidIndexFormat(keyword));
+ }
+ return arguments;
+ }
+
+ /**
+ * Parses a string index and converts it to a zero-based integer.
+ *
+ * @param indexStr The index string to parse.
+ * @return The zero-based index as an integer.
+ * @throws AmiaException If the index string is not a valid number.
+ */
+ public static int parseIndex(String indexStr) throws AmiaException {
+ assert indexStr != null : "Index string must be non-null";
+ try {
+ return Integer.parseInt(indexStr) - 1;
+ } catch (NumberFormatException e) {
+ throw new AmiaException(ErrorMessages.INVALID_TASK_NUMBER);
+ }
+ }
+
+ /**
+ * Parses multiple space-separated indices and converts them to zero-based integers.
+ * Returns indices sorted in descending order for safe deletion operations.
+ *
+ * @param indicesStr The space-separated indices string to parse.
+ * @return An array of zero-based indices sorted in descending order.
+ * @throws AmiaException If any index string is not a valid number.
+ */
+ public static int[] parseIndices(String indicesStr) throws AmiaException {
+ assert indicesStr != null : "Indices string must be non-null";
+ String[] parts = indicesStr.trim().split("\\s+");
+ int[] indices = new int[parts.length];
+
+ for (int i = 0; i < parts.length; i++) {
+ try {
+ indices[i] = Integer.parseInt(parts[i]) - 1;
+ } catch (NumberFormatException e) {
+ throw new AmiaException(ErrorMessages.INVALID_TASK_NUMBER);
+ }
+ }
+
+ // Sort in descending order for safe deletion
+ java.util.Arrays.sort(indices);
+ for (int i = 0; i < indices.length / 2; i++) {
+ int temp = indices[i];
+ indices[i] = indices[indices.length - 1 - i];
+ indices[indices.length - 1 - i] = temp;
+ }
+
+ return indices;
+ }
+
+ /**
+ * Parses a deadline command and extracts description and deadline date.
+ *
+ * @param command The deadline command string.
+ * @return A DeadlineInfo object containing the description and deadline.
+ * @throws AmiaException If the format is invalid or required fields are empty.
+ */
+ public static DeadlineInfo parseDeadline(String command) throws AmiaException {
+ assert command != null : "Command must be non-null";
+ String arguments = command.substring(8).trim();
+ int deadlineIndex = arguments.lastIndexOf("/by");
+ if (deadlineIndex == -1 || arguments.isEmpty()) {
+ throw new AmiaException(ErrorMessages.INVALID_DEADLINE_FORMAT);
+ }
+ String description = arguments.substring(0, deadlineIndex).trim();
+ String deadline = arguments.substring(deadlineIndex + 3).trim();
+ if (description.isEmpty()) {
+ throw new AmiaException(ErrorMessages.EMPTY_DESCRIPTION);
+ }
+ if (deadline.isEmpty()) {
+ throw new AmiaException(ErrorMessages.EMPTY_DEADLINE);
+ }
+ return new DeadlineInfo(description, deadline);
+ }
+
+ /**
+ * Parses an event command and extracts description, start time, and end time.
+ *
+ * @param command The event command string.
+ * @return An EventInfo object containing the description, start time, and end
+ * time.
+ * @throws AmiaException If the format is invalid or required fields are empty.
+ */
+ public static EventInfo parseEvent(String command) throws AmiaException {
+ assert command != null : "Command must be non-null";
+ String arguments = command.substring(5).trim();
+ int fromIndex = arguments.lastIndexOf("/from");
+ int toIndex = arguments.lastIndexOf("/to");
+ if (fromIndex == -1 || toIndex == -1 || arguments.isEmpty()) {
+ throw new AmiaException(ErrorMessages.INVALID_EVENT_FORMAT);
+ }
+ String description = arguments.substring(0, fromIndex).trim();
+ String from = arguments.substring(fromIndex + 5, toIndex).trim();
+ String to = arguments.substring(toIndex + 3).trim();
+ if (description.isEmpty()) {
+ throw new AmiaException(ErrorMessages.EMPTY_DESCRIPTION);
+ }
+ if (from.isEmpty()) {
+ throw new AmiaException(ErrorMessages.EMPTY_START_TIME);
+ }
+ if (to.isEmpty()) {
+ throw new AmiaException(ErrorMessages.EMPTY_END_TIME);
+ }
+ return new EventInfo(description, from, to);
+ }
+
+ /**
+ * Inner class to hold deadline information.
+ */
+ public static class DeadlineInfo {
+ private String description;
+ private String deadline;
+
+ /**
+ * Constructs a DeadlineInfo with the given description and deadline.
+ *
+ * @param description The task description.
+ * @param deadline The deadline date/time.
+ */
+ public DeadlineInfo(String description, String deadline) {
+ this.description = description;
+ this.deadline = deadline;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getDeadline() {
+ return deadline;
+ }
+ }
+
+ /**
+ * Inner class to hold event information.
+ */
+ public static class EventInfo {
+ private String description;
+ private String from;
+ private String to;
+
+ /**
+ * Constructs an EventInfo with the given description and time range.
+ *
+ * @param description The task description.
+ * @param from The event start time.
+ * @param to The event end time.
+ */
+ public EventInfo(String description, String from, String to) {
+ this.description = description;
+ this.from = from;
+ this.to = to;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public String getTo() {
+ return to;
+ }
+ }
+}
diff --git a/src/main/java/amia/storage/Storage.java b/src/main/java/amia/storage/Storage.java
new file mode 100644
index 0000000000..65273c7be1
--- /dev/null
+++ b/src/main/java/amia/storage/Storage.java
@@ -0,0 +1,160 @@
+package amia.storage;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+
+import amia.exception.AmiaException;
+import amia.exception.ErrorMessages;
+import amia.task.Deadline;
+import amia.task.Event;
+import amia.task.Task;
+import amia.task.ToDo;
+
+/**
+ * Handles loading and saving tasks to a file.
+ */
+public class Storage {
+ private static final DateTimeFormatter INPUT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm");
+ private static final DateTimeFormatter FILE_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+ private final String filePath;
+
+ /**
+ * Constructs a Storage object with the given file path. Uses a default path if
+ * the given path is null or empty. The default path is relative to the JAR file
+ * location.
+ *
+ * @param filePath The path to the file where tasks will be saved.
+ */
+ public Storage(String filePath) {
+ this.filePath = filePath == null || filePath.isEmpty() ? getDefaultFilePath() : filePath;
+ }
+
+ /**
+ * Gets the default file path relative to the JAR file location.
+ *
+ * @return The file path for storing tasks.
+ */
+ private static String getDefaultFilePath() {
+ try {
+ // Get the location of the JAR file
+ File jarFile = new File(Storage.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ // Get the directory containing the JAR
+ File jarDir = jarFile.isDirectory() ? jarFile : jarFile.getParentFile();
+ // Create the data file path relative to the JAR directory
+ return new File(jarDir, "data/amia.txt").getPath();
+ } catch (URISyntaxException e) {
+ // Fall back to current directory if unable to determine JAR location
+ return "./data/amia.txt";
+ }
+ }
+
+ /**
+ * Loads tasks from the storage file. Returns an empty list if the file does not
+ * exist.
+ *
+ * @return An ArrayList of tasks loaded from the file.
+ * @throws AmiaException If an error occurs while reading the file.
+ */
+ public ArrayList load() throws AmiaException {
+ ArrayList tasks = new ArrayList<>();
+
+ File file = new File(filePath);
+ if (!file.exists()) {
+ return tasks;
+ }
+
+ try (BufferedReader br = new BufferedReader(new FileReader(file))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ Task task = parseTaskFromLine(line);
+ if (task != null) {
+ tasks.add(task);
+ }
+ }
+ } catch (IOException e) {
+ throw new AmiaException(ErrorMessages.CANNOT_LOAD_TASKS);
+ }
+
+ return tasks;
+ }
+
+ /**
+ * Parses a single line from the storage file into a Task. Returns null when the
+ * line is malformed or unrecognized.
+ *
+ * @param line The raw line from the storage file.
+ * @return The parsed Task, or null if the line should be skipped.
+ * @throws AmiaException If a valid line contains an invalid date/time.
+ */
+ private Task parseTaskFromLine(String line) throws AmiaException {
+ assert line != null : "Storage line must be non-null";
+ String[] parts = line.split(" \\| ");
+ if (parts.length < 3) {
+ return null;
+ }
+
+ String type = parts[0];
+ boolean isDone = parts[1].equals("1");
+
+ Task task;
+ switch (type) {
+ case "T":
+ task = new ToDo(parts[2]);
+ break;
+ case "D":
+ if (parts.length < 4) {
+ return null;
+ }
+ task = new Deadline(parts[2], LocalDateTime.parse(parts[3], FILE_FORMAT).format(INPUT_FORMAT));
+ break;
+ case "E":
+ if (parts.length < 5) {
+ return null;
+ }
+ task = new Event(parts[2], LocalDateTime.parse(parts[3], FILE_FORMAT).format(INPUT_FORMAT),
+ LocalDateTime.parse(parts[4], FILE_FORMAT).format(INPUT_FORMAT));
+ break;
+ default:
+ return null;
+ }
+
+ if (isDone) {
+ task.markDone();
+ }
+
+ return task;
+ }
+
+ /**
+ * Saves tasks to the storage file. Creates the directory if it does not exist.
+ *
+ * @param tasks The ArrayList of tasks to save.
+ * @throws AmiaException If an error occurs while writing to the file.
+ */
+ public void save(ArrayList tasks) throws AmiaException {
+ assert tasks != null : "Task list must be non-null";
+ try {
+ File dir = new File(new File(filePath).getParent());
+ if (dir != null && !dir.exists()) {
+ dir.mkdirs();
+ }
+
+ try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath))) {
+ for (Task task : tasks) {
+ bw.write(task.toFileString());
+ bw.newLine();
+ }
+ }
+ } catch (IOException e) {
+ throw new AmiaException(ErrorMessages.CANNOT_SAVE_TASKS);
+ }
+ }
+}
diff --git a/src/main/java/amia/task/Deadline.java b/src/main/java/amia/task/Deadline.java
new file mode 100644
index 0000000000..89821e474c
--- /dev/null
+++ b/src/main/java/amia/task/Deadline.java
@@ -0,0 +1,45 @@
+package amia.task;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+import amia.exception.AmiaException;
+import amia.exception.ErrorMessages;
+
+/**
+ * Represents a task with a deadline.
+ */
+public class Deadline extends Task {
+ private static final DateTimeFormatter INPUT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm");
+ private static final DateTimeFormatter OUTPUT_FORMAT = DateTimeFormatter.ofPattern("MMM dd yyyy, h:mma");
+ private static final DateTimeFormatter FILE_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+
+ private LocalDateTime by;
+
+ /**
+ * Constructs a Deadline task with the given description and deadline.
+ *
+ * @param description The description of the task.
+ * @param byStr The deadline in the format "yyyy-MM-dd HHmm".
+ * @throws AmiaException If the date/time format is invalid.
+ */
+ public Deadline(String description, String byStr) throws AmiaException {
+ super(description);
+ try {
+ this.by = LocalDateTime.parse(byStr, INPUT_FORMAT);
+ } catch (DateTimeParseException e) {
+ throw new AmiaException(ErrorMessages.INVALID_DATETIME_FORMAT);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "[D]" + super.toString() + " (by: " + by.format(OUTPUT_FORMAT) + ")";
+ }
+
+ @Override
+ public String toFileString() {
+ return "D | " + (isDone ? "1" : "0") + " | " + description + " | " + by.format(FILE_FORMAT);
+ }
+}
diff --git a/src/main/java/amia/task/Event.java b/src/main/java/amia/task/Event.java
new file mode 100644
index 0000000000..23ae736172
--- /dev/null
+++ b/src/main/java/amia/task/Event.java
@@ -0,0 +1,50 @@
+package amia.task;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+import amia.exception.AmiaException;
+import amia.exception.ErrorMessages;
+
+/**
+ * Represents an event task with a start and end time.
+ */
+public class Event extends Task {
+ private static final DateTimeFormatter INPUT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm");
+ private static final DateTimeFormatter OUTPUT_FORMAT = DateTimeFormatter.ofPattern("MMM dd yyyy, h:mma");
+ private static final DateTimeFormatter FILE_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+
+ private LocalDateTime from;
+ private LocalDateTime to;
+
+ /**
+ * Constructs an Event task with the given description and time range.
+ *
+ * @param description The description of the event.
+ * @param fromStr The start time in the format "yyyy-MM-dd HHmm".
+ * @param toStr The end time in the format "yyyy-MM-dd HHmm".
+ * @throws AmiaException If the date/time format is invalid.
+ */
+ public Event(String description, String fromStr, String toStr) throws AmiaException {
+ super(description);
+ try {
+ this.from = LocalDateTime.parse(fromStr, INPUT_FORMAT);
+ this.to = LocalDateTime.parse(toStr, INPUT_FORMAT);
+ } catch (DateTimeParseException e) {
+ throw new AmiaException(ErrorMessages.INVALID_DATETIME_FORMAT);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "[E]" + super.toString() + " (from: " + from.format(OUTPUT_FORMAT) + " to: " + to.format(OUTPUT_FORMAT)
+ + ")";
+ }
+
+ @Override
+ public String toFileString() {
+ return "E | " + (isDone ? "1" : "0") + " | " + description + " | " + from.format(FILE_FORMAT) + " | "
+ + to.format(FILE_FORMAT);
+ }
+}
diff --git a/src/main/java/amia/task/Task.java b/src/main/java/amia/task/Task.java
new file mode 100644
index 0000000000..0ec0be4657
--- /dev/null
+++ b/src/main/java/amia/task/Task.java
@@ -0,0 +1,76 @@
+package amia.task;
+
+/**
+ * Abstract base class representing a task. Provides the common functionality
+ * for all task types.
+ */
+public abstract class Task {
+ protected String description;
+ protected boolean isDone;
+
+ /**
+ * Constructs a Task with the given description.
+ *
+ * @param description The description of the task.
+ */
+ public Task(String description) {
+ this.description = description;
+ this.isDone = false;
+ }
+
+ /**
+ * Returns the status icon for this task.
+ *
+ * @return "X" if the task is done, " " otherwise.
+ */
+ public String getStatusIcon() {
+ return isDone ? "X" : " ";
+ }
+
+ /**
+ * Marks this task as done.
+ */
+ public void markDone() {
+ this.isDone = true;
+ }
+
+ /**
+ * Marks this task as not done.
+ */
+ public void markUndone() {
+ this.isDone = false;
+ }
+
+ /**
+ * Returns the string representation of this task with status icon and
+ * description.
+ *
+ * @return A string in the format "[status] description".
+ */
+ @Override
+ public String toString() {
+ return "[" + getStatusIcon() + "] " + description;
+ }
+
+ /**
+ * Checks if this task matches the given keyword.
+ *
+ * @param keyword The keyword to match against.
+ * @return True if the keyword is found in the task description, false
+ * otherwise.
+ */
+ public boolean matches(String keyword) {
+ if (keyword == null) {
+ return false;
+ }
+ String needle = keyword.trim().toLowerCase();
+ return !needle.isEmpty() && description.toLowerCase().contains(needle);
+ }
+
+ /**
+ * Returns the file representation of this task for storage purposes.
+ *
+ * @return A string representation suitable for file storage.
+ */
+ public abstract String toFileString();
+}
diff --git a/src/main/java/amia/task/TaskList.java b/src/main/java/amia/task/TaskList.java
new file mode 100644
index 0000000000..0be09653a3
--- /dev/null
+++ b/src/main/java/amia/task/TaskList.java
@@ -0,0 +1,121 @@
+package amia.task;
+
+import java.util.ArrayList;
+
+import amia.exception.AmiaException;
+import amia.exception.ErrorMessages;
+
+/**
+ * Manages a list of tasks. Provides methods to add, remove, retrieve, and mark
+ * tasks.
+ */
+public class TaskList {
+ public static final int MAX_TASKS = 100;
+ private ArrayList tasks;
+
+ /**
+ * Constructs an empty TaskList.
+ */
+ public TaskList() {
+ this.tasks = new ArrayList<>();
+ }
+
+ /**
+ * Constructs a TaskList with the given list of tasks.
+ *
+ * @param tasks The initial list of tasks.
+ */
+ public TaskList(ArrayList tasks) {
+ assert tasks != null : "Task list must be non-null";
+ this.tasks = tasks;
+ }
+
+ /**
+ * Validates that the given index is within the bounds of the task list.
+ *
+ * @param index The index to validate.
+ * @throws AmiaException If the index is out of bounds.
+ */
+ public void validateIndex(int index) throws AmiaException {
+ if (index < 0 || index >= tasks.size()) {
+ throw new AmiaException(ErrorMessages.INVALID_TASK_NUMBER);
+ }
+ }
+
+ /**
+ * Adds a task to the task list.
+ *
+ * @param task The task to add.
+ * @throws AmiaException If the task is null.
+ */
+ public void add(Task task) throws AmiaException {
+ if (task == null) {
+ throw new AmiaException(ErrorMessages.NULL_TASK);
+ }
+ tasks.add(task);
+ }
+
+ /**
+ * Removes and returns the task at the given index.
+ *
+ * @param index The index of the task to remove.
+ * @return The removed task.
+ * @throws AmiaException If the index is out of bounds.
+ */
+ public Task remove(int index) throws AmiaException {
+ validateIndex(index);
+ return tasks.remove(index);
+ }
+
+ /**
+ * Retrieves the task at the given index.
+ *
+ * @param index The index of the task to retrieve.
+ * @return The task at the specified index.
+ * @throws AmiaException If the index is out of bounds.
+ */
+ public Task get(int index) throws AmiaException {
+ validateIndex(index);
+ return tasks.get(index);
+ }
+
+ /**
+ * Marks the task at the given index as done.
+ *
+ * @param index The index of the task to mark as done.
+ * @throws AmiaException If the index is out of bounds.
+ */
+ public void markDone(int index) throws AmiaException {
+ validateIndex(index);
+ tasks.get(index).markDone();
+ }
+
+ /**
+ * Marks the task at the given index as not done.
+ *
+ * @param index The index of the task to mark as not done.
+ * @throws AmiaException If the index is out of bounds.
+ */
+ public void markUndone(int index) throws AmiaException {
+ validateIndex(index);
+ tasks.get(index).markUndone();
+ }
+
+ /**
+ * Returns the number of tasks in the task list.
+ *
+ * @return The size of the task list.
+ */
+ public int size() {
+ return tasks.size();
+ }
+
+ /**
+ * Returns a defensive copy of the underlying ArrayList of tasks.
+ *
+ * @return A new ArrayList containing all tasks.
+ */
+ public ArrayList toArrayList() {
+ return new ArrayList<>(tasks);
+ }
+}
diff --git a/src/main/java/amia/task/ToDo.java b/src/main/java/amia/task/ToDo.java
new file mode 100644
index 0000000000..e7b2407a8e
--- /dev/null
+++ b/src/main/java/amia/task/ToDo.java
@@ -0,0 +1,20 @@
+package amia.task;
+
+/**
+ * Represents a simple to-do task.
+ */
+public class ToDo extends Task {
+ public ToDo(String decsription) {
+ super(decsription);
+ }
+
+ @Override
+ public String toString() {
+ return "[T]" + super.toString();
+ }
+
+ @Override
+ public String toFileString() {
+ return "T | " + (isDone ? "1" : "0") + " | " + description;
+ }
+}
diff --git a/src/main/java/amia/ui/Ui.java b/src/main/java/amia/ui/Ui.java
new file mode 100644
index 0000000000..5c98db4a3b
--- /dev/null
+++ b/src/main/java/amia/ui/Ui.java
@@ -0,0 +1,93 @@
+package amia.ui;
+
+import java.util.Scanner;
+
+import amia.task.Task;
+
+/**
+ * Handles the user interface and user input/output.
+ */
+public class Ui {
+ private Scanner scanner;
+
+ /**
+ * Constructs a Ui object and initializes the scanner for reading user input.
+ */
+ public Ui() {
+ scanner = new Scanner(System.in);
+ }
+
+ /**
+ * Reads a command line from the user.
+ *
+ * @return The user's command with leading and trailing whitespace removed.
+ */
+ public String readCommand() {
+ return scanner.nextLine().trim();
+ }
+
+ /**
+ * Closes the scanner resource.
+ */
+ public void close() {
+ scanner.close();
+ }
+
+ /**
+ * Displays a message to the user with indentation.
+ *
+ * @param s The message to display.
+ */
+ public void showMessage(String s) {
+ assert s != null : "Message must be non-null";
+ System.out.println("\t" + s);
+ }
+
+ /**
+ * Displays a line separator.
+ */
+ public void showLine() {
+ System.out.println("-----------------------------------");
+ }
+
+ /**
+ * Displays the welcome message.
+ */
+ public void showWelcome() {
+ showLine();
+ showMessage("Hello! I'm Amia!");
+ showMessage("What can I do for you?");
+ showLine();
+ }
+
+ /**
+ * Displays the goodbye message.
+ */
+ public void showGoodbye() {
+ showLine();
+ showMessage("Bye!");
+ showLine();
+ }
+
+ /**
+ * Formats the task count message.
+ *
+ * @param count The number of tasks.
+ * @return The formatted task count message.
+ */
+ public String formatTaskCountMessage(int count) {
+ return "You have " + count + " task" + (count == 1 ? "" : "s") + ".";
+ }
+
+ /**
+ * Formats the message shown after adding a task.
+ *
+ * @param task The task that was added.
+ * @param taskCount The number of tasks after the add.
+ * @return The formatted message to display.
+ */
+ public String formatAddTaskMessage(Task task, int taskCount) {
+ assert task != null : "Added task must be non-null";
+ return "I've added this task!\n " + task + "\n" + formatTaskCountMessage(taskCount);
+ }
+}
diff --git a/src/main/resources/images/Amia.jpg b/src/main/resources/images/Amia.jpg
new file mode 100644
index 0000000000..a4895b95c3
Binary files /dev/null and b/src/main/resources/images/Amia.jpg differ
diff --git a/src/main/resources/images/User.png b/src/main/resources/images/User.png
new file mode 100644
index 0000000000..73c7f7592f
Binary files /dev/null and b/src/main/resources/images/User.png differ
diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml
new file mode 100644
index 0000000000..525b7250cb
--- /dev/null
+++ b/src/main/resources/view/DialogBox.fxml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml
new file mode 100644
index 0000000000..c825cf33c0
--- /dev/null
+++ b/src/main/resources/view/MainWindow.fxml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/amia/parser/ParserTest.java b/src/test/java/amia/parser/ParserTest.java
new file mode 100644
index 0000000000..5f685c5590
--- /dev/null
+++ b/src/test/java/amia/parser/ParserTest.java
@@ -0,0 +1,43 @@
+package amia.parser;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import amia.exception.AmiaException;
+
+public class ParserTest {
+
+ @Test
+ public void testParseCommandType() throws AmiaException {
+ CommandType type1 = Parser.parseCommandType("todo read book");
+ Assertions.assertEquals(CommandType.TODO, type1);
+
+ CommandType type2 = Parser.parseCommandType("list");
+ Assertions.assertEquals(CommandType.LIST, type2);
+
+ CommandType type3 = Parser.parseCommandType("bye");
+ Assertions.assertEquals(CommandType.BYE, type3);
+ }
+
+ @Test
+ public void testExtractDescription() throws AmiaException {
+ String desc = Parser.extractDescription("todo read a book", "todo");
+ Assertions.assertEquals("read a book", desc);
+
+ String desc2 = Parser.extractDescription("deadline submit project /by 2024-02-15", "deadline");
+ Assertions.assertEquals("submit project /by 2024-02-15", desc2);
+ }
+
+ @Test
+ public void testParseIndex() throws AmiaException {
+ int idx1 = Parser.parseIndex("1");
+ Assertions.assertEquals(0, idx1);
+
+ int idx2 = Parser.parseIndex("5");
+ Assertions.assertEquals(4, idx2);
+
+ Assertions.assertThrows(AmiaException.class, () -> {
+ Parser.parseIndex("abc");
+ });
+ }
+}
diff --git a/src/test/java/amia/task/TaskListTest.java b/src/test/java/amia/task/TaskListTest.java
new file mode 100644
index 0000000000..b8bc99b43f
--- /dev/null
+++ b/src/test/java/amia/task/TaskListTest.java
@@ -0,0 +1,48 @@
+package amia.task;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import amia.exception.AmiaException;
+
+public class TaskListTest {
+ private TaskList taskList;
+
+ @BeforeEach
+ public void setUp() {
+ taskList = new TaskList();
+ }
+
+ @Test
+ public void testAddValidTask() throws AmiaException {
+ Task task = new ToDo("Read book");
+ taskList.add(task);
+ Assertions.assertEquals(1, taskList.size());
+ Assertions.assertEquals(task, taskList.get(0));
+ }
+
+ @Test
+ public void testRemoveValidTask() throws AmiaException {
+ Task task1 = new ToDo("Task 1");
+ Task task2 = new ToDo("Task 2");
+ taskList.add(task1);
+ taskList.add(task2);
+
+ Task removed = taskList.remove(0);
+ Assertions.assertEquals(1, taskList.size());
+ Assertions.assertEquals(task2, taskList.get(0));
+ }
+
+ @Test
+ public void testMarkAndUnmarkTask() throws AmiaException {
+ Task task = new ToDo("Task 1");
+ taskList.add(task);
+
+ Assertions.assertTrue(taskList.get(0).toString().contains("[ ]"));
+ taskList.markDone(0);
+ Assertions.assertTrue(taskList.get(0).toString().contains("[X]"));
+ taskList.markUndone(0);
+ Assertions.assertTrue(taskList.get(0).toString().contains("[ ]"));
+ }
+}
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
index 657e74f6e7..ab620abdc5 100644
--- a/text-ui-test/EXPECTED.TXT
+++ b/text-ui-test/EXPECTED.TXT
@@ -1,7 +1,124 @@
-Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
-
+ -----------------------------------
+ Hello! I'm Amia!
+ What can I do for you?
+ -----------------------------------
+ -----------------------------------
+ I've added this task!
+ [T][ ] read book
+ You have 1 task.
+ -----------------------------------
+ -----------------------------------
+ I've added this task!
+ [D][ ] return book (by: June 6th)
+ You have 2 tasks.
+ -----------------------------------
+ -----------------------------------
+ I've added this task!
+ [E][ ] project meeting (from: Aug 6th 2pm to: 4pm)
+ You have 3 tasks.
+ -----------------------------------
+ -----------------------------------
+ I've added this task!
+ [T][ ] join sports club
+ You have 4 tasks.
+ -----------------------------------
+ -----------------------------------
+ I've marked the task as done!
+ [T][X] read book
+ -----------------------------------
+ -----------------------------------
+ I've marked the task as done!
+ [T][X] join sports club
+ -----------------------------------
+ -----------------------------------
+ I've added this task!
+ [T][ ] borrow book
+ You have 5 tasks.
+ -----------------------------------
+ -----------------------------------
+ Here are your tasks:
+ 1. [T][X] read book
+ 2. [D][ ] return book (by: June 6th)
+ 3. [E][ ] project meeting (from: Aug 6th 2pm to: 4pm)
+ 4. [T][X] join sports club
+ 5. [T][ ] borrow book
+ -----------------------------------
+ -----------------------------------
+ I've added this task!
+ [D][ ] return book (by: Sunday)
+ You have 6 tasks.
+ -----------------------------------
+ -----------------------------------
+ I've added this task!
+ [E][ ] project meeting (from: Mon 2pm to: 4pm)
+ You have 7 tasks.
+ -----------------------------------
+ -----------------------------------
+ ... The description of a task cannot be empty...
+ -----------------------------------
+ -----------------------------------
+ Invalid format... Use: deadline /by
+ -----------------------------------
+ -----------------------------------
+ ... The description of a task can't be empty...
+ -----------------------------------
+ -----------------------------------
+ ... The deadline can't be empty...
+ -----------------------------------
+ -----------------------------------
+ Invalid format... Use: event /from /to
+ -----------------------------------
+ -----------------------------------
+ ...The description of a task can't be empty...
+ -----------------------------------
+ -----------------------------------
+ ... The end time can't be empty...
+ -----------------------------------
+ -----------------------------------
+ ... The start time can't be empty...
+ -----------------------------------
+ -----------------------------------
+ ...The description of a task can't be empty...
+ -----------------------------------
+ -----------------------------------
+ ... Invalid format... Use: mark
+ -----------------------------------
+ -----------------------------------
+ ... Invalid task number...
+ -----------------------------------
+ -----------------------------------
+ ... Invalid format... Use: unmark
+ -----------------------------------
+ -----------------------------------
+ ... Invalid task number...
+ -----------------------------------
+ -----------------------------------
+ Here are your tasks:
+ 1. [T][X] read book
+ 2. [D][ ] return book (by: June 6th)
+ 3. [E][ ] project meeting (from: Aug 6th 2pm to: 4pm)
+ 4. [T][X] join sports club
+ 5. [T][ ] borrow book
+ 6. [D][ ] return book (by: Sunday)
+ 7. [E][ ] project meeting (from: Mon 2pm to: 4pm)
+ -----------------------------------
+ -----------------------------------
+ ... Invalid format... Use: delete
+ -----------------------------------
+ -----------------------------------
+ I've removed this task:
+ [T][X] read book
+ You have 6 tasks.
+ -----------------------------------
+ -----------------------------------
+ Here are your tasks:
+ 1. [D][ ] return book (by: June 6th)
+ 2. [E][ ] project meeting (from: Aug 6th 2pm to: 4pm)
+ 3. [T][X] join sports club
+ 4. [T][ ] borrow book
+ 5. [D][ ] return book (by: Sunday)
+ 6. [E][ ] project meeting (from: Mon 2pm to: 4pm)
+ -----------------------------------
+ -----------------------------------
+ Bye!
+ -----------------------------------
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
index e69de29bb2..4fac0852d8 100644
--- a/text-ui-test/input.txt
+++ b/text-ui-test/input.txt
@@ -0,0 +1,28 @@
+todo read book
+deadline return book /by June 6th
+event project meeting /from Aug 6th 2pm /to 4pm
+todo join sports club
+mark 1
+mark 4
+todo borrow book
+list
+deadline return book /by Sunday
+event project meeting /from Mon 2pm /to 4pm
+todo
+deadline
+deadline /by 1
+deadline a /by
+event
+event /from /to
+event a /from 1 /to
+event a /from /to 2
+event /from 1 /to 2
+mark
+mark 999
+unmark
+unmark 999
+list
+delete
+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..2107b1410f 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 Amia < 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