about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorRicky Kresslein <ricky@kressle.in>2022-02-18 16:14:09 +0100
committerRicky Kresslein <ricky@kressle.in>2022-02-18 16:14:09 +0100
commitac99aca2510eebb83ac9a112849d8788ef67db3d (patch)
tree57bbddbf10a83f8abd5e5020b897ed80dc7d7e44 /src
downloadFurtherance-ac99aca2510eebb83ac9a112849d8788ef67db3d.tar.zst
- Moved to com.lakoliu.Furtherance
- Removed development flag for release
- Created nicer about dialog
- Added description to data file
- Improved desktop file
- Changed database directory
- Delete All no longer enabled if no tasks
- Added GPL to top of every file
Diffstat (limited to 'src')
-rw-r--r--src/application.rs183
-rw-r--r--src/config.rs.in21
-rw-r--r--src/database.rs183
-rw-r--r--src/furtherance.gresource.xml12
-rw-r--r--src/gtk/history_box.ui87
-rw-r--r--src/gtk/style.css21
-rw-r--r--src/gtk/task_details.ui138
-rw-r--r--src/gtk/task_row.ui39
-rw-r--r--src/gtk/tasks_group.ui10
-rw-r--r--src/gtk/tasks_page.ui5
-rw-r--r--src/gtk/window.ui95
-rw-r--r--src/main.rs58
-rw-r--r--src/meson.build67
-rw-r--r--src/ui.rs29
-rw-r--r--src/ui/history_box.rs138
-rw-r--r--src/ui/task_details.rs441
-rw-r--r--src/ui/task_row.rs130
-rw-r--r--src/ui/tasks_group.rs112
-rw-r--r--src/ui/tasks_page.rs147
-rw-r--r--src/ui/window.rs330
20 files changed, 2246 insertions, 0 deletions
diff --git a/src/application.rs b/src/application.rs
new file mode 100644
index 0000000..1f5d345
--- /dev/null
+++ b/src/application.rs
@@ -0,0 +1,183 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use glib::clone;
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{gdk, gio, glib};
+
+use crate::config;
+use crate::ui::FurtheranceWindow;
+use crate::database;
+
+mod imp {
+    use super::*;
+
+    #[derive(Debug, Default)]
+    pub struct FurtheranceApplication {}
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurtheranceApplication {
+        const NAME: &'static str = "FurtheranceApplication";
+        type Type = super::FurtheranceApplication;
+        type ParentType = gtk::Application;
+    }
+
+    impl ObjectImpl for FurtheranceApplication {
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            obj.setup_gactions();
+            obj.set_accels_for_action("app.quit", &["<primary>Q", "<primary>W"]);
+        }
+    }
+
+    impl ApplicationImpl for FurtheranceApplication {
+        // We connect to the activate callback to create a window when the application
+        // has been launched. Additionally, this callback notifies us when the user
+        // tries to launch a "second instance" of the application. When they try
+        // to do that, we'll just present any existing window.
+        fn activate(&self, application: &Self::Type) {
+            // Initialize the database
+            let _ = database::db_init();
+
+            // Get the current window or create one if necessary
+            let window = if let Some(window) = application.active_window() {
+                window
+            } else {
+                let window = FurtheranceWindow::new(application);
+                window.set_default_size(400, 600);
+                window.set_title(Some("Furtherance"));
+                window.upcast()
+            };
+
+            // Load style.css
+            let css_file = gtk::CssProvider::new();
+            gtk::CssProvider::load_from_resource(&css_file, "/com/lakoliu/Furtherance/gtk/style.css");
+            gtk::StyleContext::add_provider_for_display(&gdk::Display::default().unwrap(), &css_file, 500);
+
+            // Ask the window manager/compositor to present the window
+            window.present();
+        }
+    }
+
+    impl GtkApplicationImpl for FurtheranceApplication {}
+}
+
+glib::wrapper! {
+    pub struct FurtheranceApplication(ObjectSubclass<imp::FurtheranceApplication>)
+        @extends gio::Application, gtk::Application,
+        @implements gio::ActionGroup, gio::ActionMap;
+}
+
+impl FurtheranceApplication {
+    pub fn new(application_id: &str, flags: &gio::ApplicationFlags) -> Self {
+        glib::Object::new(&[("application-id", &application_id), ("flags", flags)])
+            .expect("Failed to create FurtheranceApplication")
+    }
+
+    fn setup_gactions(&self) {
+        let quit_action = gio::SimpleAction::new("quit", None);
+        quit_action.connect_activate(clone!(@weak self as app => move |_, _| {
+            app.quit();
+        }));
+        self.add_action(&quit_action);
+
+        let about_action = gio::SimpleAction::new("about", None);
+        about_action.connect_activate(clone!(@weak self as app => move |_, _| {
+            app.show_about();
+        }));
+        self.add_action(&about_action);
+    }
+
+    fn show_about(&self) {
+        let window = self.active_window().unwrap();
+        let dialog = gtk::AboutDialog::builder()
+            .transient_for(&window)
+            .modal(true)
+            .program_name("Furtherance")
+            .logo_icon_name(config::APP_ID)
+            .version(config::VERSION)
+            .comments("Track your time without being tracked.")
+            .copyright("© 2022 Ricky Kresslein")
+            .authors(vec!["Ricky Kresslein".into()])
+            // .website("https://furtherance.app")
+            .license_type(gtk::License::Gpl30)
+            .build();
+
+        dialog.present();
+    }
+
+    fn delete_history(&self) {
+        // Show dialog to delete all history
+        let window = FurtheranceWindow::default();
+        let dialog = gtk::MessageDialog::with_markup(
+            Some(&window),
+            gtk::DialogFlags::MODAL,
+            gtk::MessageType::Question,
+            gtk::ButtonsType::None,
+            Some("<span size='x-large' weight='bold'>Delete history?</span>"),
+        );
+        dialog.add_buttons(&[
+            ("Cancel", gtk::ResponseType::Reject),
+            ("Delete", gtk::ResponseType::Accept)
+        ]);
+
+        let message_area = dialog.message_area().downcast::<gtk::Box>().unwrap();
+        let explanation = gtk::Label::new(Some("This will delete ALL of your task history."));
+        let instructions = gtk::Label::new(Some(
+            "Type DELETE in the box below then click Delete to proceed."));
+        let delete_entry = gtk::Entry::new();
+        message_area.append(&explanation);
+        message_area.append(&instructions);
+        message_area.append(&delete_entry);
+
+        dialog.connect_response(clone!(@weak dialog = > move |_, resp| {
+            if resp == gtk::ResponseType::Accept {
+                if delete_entry.text().to_uppercase() == "DELETE" {
+                    let _ = database::delete_all();
+                    window.reset_history_box();
+                    dialog.close();
+                }
+            } else {
+                dialog.close();
+            }
+        }));
+
+        dialog.show();
+    }
+
+    pub fn delete_enabled(&self, enabled: bool) {
+        if enabled {
+            let delete_history_action = gio::SimpleAction::new("delete-history", None);
+            delete_history_action.connect_activate(clone!(@weak self as app => move |_, _| {
+                app.delete_history();
+            }));
+            self.add_action(&delete_history_action);
+        } else {
+            self.remove_action("delete-history");
+        }
+    }
+}
+
+impl Default for FurtheranceApplication {
+    fn default() -> Self {
+        gio::Application::default()
+            .expect("Could not get default GApplication")
+            .downcast()
+            .unwrap()
+    }
+}
diff --git a/src/config.rs.in b/src/config.rs.in
new file mode 100644
index 0000000..4005031
--- /dev/null
+++ b/src/config.rs.in
@@ -0,0 +1,21 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+pub static VERSION: &str = @VERSION@;
+pub static GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@;
+pub static LOCALEDIR: &str = @LOCALEDIR@;
+pub static PKGDATADIR: &str = @PKGDATADIR@;
+pub static APP_ID: &str = @APP_ID@;
diff --git a/src/database.rs b/src/database.rs
new file mode 100644
index 0000000..70e6043
--- /dev/null
+++ b/src/database.rs
@@ -0,0 +1,183 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use rusqlite::{Connection, Result};
+use chrono::{DateTime, Local};
+use directories::ProjectDirs;
+use std::path::PathBuf;
+use std::fs::create_dir_all;
+
+#[derive(Clone, Debug)]
+pub struct Task {
+    pub id: i32,
+    pub task_name: String,
+    pub start_time: String,
+    pub stop_time: String,
+}
+
+pub fn get_directory() -> PathBuf {
+    if let Some(proj_dirs) = ProjectDirs::from("com", "lakoliu",  "Furtherance") {
+        let mut path = PathBuf::from(proj_dirs.data_dir());
+        create_dir_all(path.clone()).expect("Unable to create database directory");
+        path.extend(&["furtherance.db"]);
+        return path
+    }
+    PathBuf::new()
+}
+
+pub fn db_init() -> Result<()> {
+    let conn = Connection::open(get_directory())?;
+    conn.execute(
+        "CREATE TABLE tasks (
+                    id integer primary key,
+                    task_name text,
+                    start_time timestamp,
+                    stop_time timestamp)",
+        [],
+    )?;
+
+    Ok(())
+}
+
+
+pub fn db_write(task_name: &str, start_time: DateTime<Local>, stop_time: DateTime<Local>) -> Result<()> {
+    // Write data into database
+    let conn = Connection::open(get_directory())?;
+
+    conn.execute(
+        "INSERT INTO tasks (task_name, start_time, stop_time) values (?1, ?2, ?3)",
+        &[&task_name.to_string(), &start_time.to_rfc3339(), &stop_time.to_rfc3339()],
+    )?;
+
+    Ok(())
+}
+
+pub fn retrieve() -> Result<Vec<Task>, rusqlite::Error> {
+    // Retrieve all tasks from the database
+    let conn = Connection::open(get_directory())?;
+
+    let mut query = conn.prepare("SELECT * FROM tasks ORDER BY start_time")?;
+    let task_iter = query.query_map([], |row| {
+        Ok(Task {
+            id: row.get(0)?,
+            task_name: row.get(1)?,
+            start_time: row.get(2)?,
+            stop_time: row.get(3)?,
+        })
+    })?;
+
+    let mut tasks_vec: Vec<Task> = Vec::new();
+    for task_item in task_iter {
+        tasks_vec.push(task_item.unwrap());
+    }
+
+    Ok(tasks_vec)
+
+}
+
+pub fn update_start_time(id: i32, start_time: String) -> Result<()> {
+    let conn = Connection::open(get_directory())?;
+
+    conn.execute(
+        "UPDATE tasks SET start_time = (?1) WHERE id = (?2)",
+        &[&start_time, &id.to_string()]
+    )?;
+
+    Ok(())
+}
+
+pub fn update_stop_time(id: i32, stop_time: String) -> Result<()> {
+    let conn = Connection::open(get_directory())?;
+
+    conn.execute(
+        "UPDATE tasks SET stop_time = (?1) WHERE id = (?2)",
+        &[&stop_time, &id.to_string()]
+    )?;
+
+    Ok(())
+}
+
+pub fn update_task_name(id: i32, task_name: String) -> Result<()> {
+    let conn = Connection::open(get_directory())?;
+
+    conn.execute(
+        "UPDATE tasks SET task_name = (?1) WHERE id = (?2)",
+        &[&task_name, &id.to_string()]
+    )?;
+
+    Ok(())
+}
+
+pub fn get_list_by_id(id_list: Vec<i32>) -> Result<Vec<Task>, rusqlite::Error> {
+    let conn = Connection::open(get_directory())?;
+    let mut tasks_vec: Vec<Task> = Vec::new();
+
+    for id in id_list {
+        let mut query = conn.prepare(
+            "SELECT * FROM tasks WHERE id = :id;")?;
+        let task_iter = query.query_map(&[(":id", &id.to_string())], |row| {
+            Ok(Task {
+                id: row.get(0)?,
+                task_name: row.get(1)?,
+                start_time: row.get(2)?,
+                stop_time: row.get(3)?,
+            })
+        })?;
+
+        for task_item in task_iter {
+            tasks_vec.push(task_item.unwrap());
+        }
+    }
+
+    Ok(tasks_vec)
+}
+
+pub fn check_for_tasks() -> Result<String> {
+    let conn = Connection::open(get_directory())?;
+
+    conn.query_row(
+        "SELECT task_name FROM tasks WHERE id='1'",
+        [],
+        |row| row.get(0),
+    )
+}
+
+pub fn delete_by_ids(id_list: Vec<i32>) -> Result<()> {
+    let conn = Connection::open(get_directory())?;
+
+    for id in id_list {
+        conn.execute("delete FROM tasks WHERE id = (?1)", &[&id.to_string()])?;
+    }
+
+    Ok(())
+}
+
+pub fn delete_by_id(id: i32) -> Result<()> {
+    let conn = Connection::open(get_directory())?;
+
+    conn.execute("delete FROM tasks WHERE id = (?1)", &[&id.to_string()])?;
+
+    Ok(())
+}
+
+pub fn delete_all() -> Result<()> {
+    // Delete everything from the database
+    let conn = Connection::open(get_directory())?;
+
+    conn.execute("delete from tasks",[],)?;
+
+    Ok(())
+}
diff --git a/src/furtherance.gresource.xml b/src/furtherance.gresource.xml
new file mode 100644
index 0000000..0116668
--- /dev/null
+++ b/src/furtherance.gresource.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/com/lakoliu/Furtherance">
+    <file>gtk/window.ui</file>
+    <file>gtk/history_box.ui</file>
+    <file>gtk/style.css</file>
+    <file>gtk/tasks_page.ui</file>
+    <file>gtk/tasks_group.ui</file>
+    <file>gtk/task_row.ui</file>
+    <file>gtk/task_details.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/gtk/history_box.ui b/src/gtk/history_box.ui
new file mode 100644
index 0000000..b761624
--- /dev/null
+++ b/src/gtk/history_box.ui
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FurHistoryBox" parent="GtkBox">
+    <child>
+      <object class="GtkStack" id="stack">
+        <property name="transition_type">crossfade</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">loading</property>
+            <property name="child">
+              <object class="GtkSpinner" id="spinner">
+                <property name="halign">center</property>
+                <property name="width-request">32</property>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">empty</property>
+            <property name="child">
+              <object class="AdwStatusPage" id="welcome_page">
+                <property name="title" translatable="yes">Start Tracking</property>
+                <property name="icon_name">com.lakoliu.Furtherance</property>
+                <property name="child">
+                  <object class="GtkGrid">
+                    <property name="halign">center</property>
+                    <property name="row_spacing">12</property>
+                    <property name="column_spacing">12</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="icon_name">list-add-symbolic</property>
+                        <layout>
+                          <property name="column">0</property>
+                          <property name="row">0</property>
+                        </layout>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="halign">start</property>
+                        <property name="label" translatable="yes">Type your task and press start</property>
+                        <layout>
+                          <property name="column">1</property>
+                          <property name="row">0</property>
+                        </layout>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="icon_name">window-new-symbolic</property>
+                        <layout>
+                          <property name="column">0</property>
+                          <property name="row">1</property>
+                        </layout>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="halign">start</property>
+                        <property name="label" translatable="yes">Prior tasks will show up here</property>
+                        <layout>
+                          <property name="column">1</property>
+                          <property name="row">1</property>
+                        </layout>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">tasks</property>
+            <property name="child">
+              <object class="FurTasksPage" id="tasks_page"/>
+            </property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/gtk/style.css b/src/gtk/style.css
new file mode 100644
index 0000000..5681ce3
--- /dev/null
+++ b/src/gtk/style.css
@@ -0,0 +1,21 @@
+.welcome-icon {
+  transform: scale(2.5);
+}
+
+.task-details headerbar {
+  transition: 200ms ease-out;
+  transition-property: box-shadow, background-color;
+}
+
+.task-details headerbar.hidden {
+  background-color: transparent;
+  box-shadow: inset 0 -1px transparent;
+}
+
+.inactive-button, .inactive-button:hover {
+  background-color: #f3f3f3;
+}
+
+.error_message {
+  color: red;
+}
diff --git a/src/gtk/task_details.ui b/src/gtk/task_details.ui
new file mode 100644
index 0000000..ccbfc1e
--- /dev/null
+++ b/src/gtk/task_details.ui
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FurTaskDetails" parent="AdwWindow">
+    <property name="width-request">350</property>
+    <property name="height-request">400</property>
+    <property name="default-width">350</property>
+    <property name="default-height">550</property>
+    <property name="title" translatable="yes">Task Details</property>
+    <property name="modal">True</property>
+    <style>
+      <class name="task-details"/>
+    </style>
+    <child>
+      <object class="GtkOverlay">
+        <child type="overlay">
+          <object class="GtkHeaderBar" id="headerbar">
+            <property name="valign">start</property>
+            <property name="title-widget">
+              <object class="AdwWindowTitle" id="dialog_title">
+                <property name="visible">False</property>
+              </object>
+            </property>
+            <style>
+              <class name="hidden"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow" id="scrolled_window">
+            <property name="vexpand">True</property>
+            <property name="hscrollbar-policy">never</property>
+            <style>
+              <class name="flat-headerbar"/>
+            </style>
+            <child>
+              <object class="AdwClamp">
+                <property name="margin-start">12</property>
+                <property name="margin-end">12</property>
+                <property name="margin-top">24</property>
+                <property name="margin-bottom">24</property>
+                <property name="maximum-size">500</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">18</property>
+                    <child>
+                      <object class="AdwClamp">
+                        <property name="maximum-size">400</property>
+                        <property name="margin-top">48</property>
+                        <property name="tightening-threshold">200</property>
+                        <child>
+                          <object class="GtkLabel" id="task_name_label">
+                            <property name="wrap">True</property>
+                            <property name="justify">center</property>
+                            <property name="ellipsize">end</property>
+                            <property name="lines">3</property>
+                            <property name="wrap-mode">word-char</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="main_box">
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">8</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="hexpand">True</property>
+                            <property name="homogeneous">True</property>
+                            <property name="spacing">5</property>
+                            <child>
+                              <object class="GtkLabel">
+                                <property name="label">Start</property>
+                                <style>
+                                  <class name="title-2"/>
+                                </style>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkLabel">
+                                <property name="label">Stop</property>
+                                <style>
+                                  <class name="title-2"/>
+                                </style>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkLabel">
+                                <property name="label">Total</property>
+                                <style>
+                                  <class name="title-2"/>
+                                </style>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="overlay">
+          <object class="GtkBox">
+            <property name="spacing">12</property>
+            <property name="margin-end">12</property>
+            <property name="margin-bottom">12</property>
+            <property name="orientation">vertical</property>
+            <property name="halign">end</property>
+            <property name="valign">end</property>
+            <child>
+              <object class="GtkButton" id="delete_all_btn">
+                <property name="icon-name">user-trash-symbolic</property>
+                <property name="tooltip-text" translatable="yes">Delete all</property>
+                <style>
+                  <class name="delete-all-button"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkShortcutController">
+        <property name="scope">local</property>
+        <child>
+          <object class="GtkShortcut">
+            <property name="trigger">Escape</property>
+            <property name="action">action(window.close)</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/gtk/task_row.ui b/src/gtk/task_row.ui
new file mode 100644
index 0000000..edd2899
--- /dev/null
+++ b/src/gtk/task_row.ui
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FurTaskRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="margin_top">10</property>
+        <property name="margin_bottom">10</property>
+        <property name="margin_end">12</property>
+        <property name="margin_start">12</property>
+        <property name="hexpand">True</property>
+        <property name="spacing">3</property>
+        <property name="valign">center</property>
+        <property name="homogeneous">True</property>
+        <child>
+          <object class="GtkLabel" id="task_name_label">
+            <property name="halign">start</property>
+            <property name="label" translatable="yes">Task</property>
+            <property name="ellipsize">end</property>
+            <property name="single_line_mode">True</property>
+            <style>
+              <class name="heading"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="total_time_label">
+            <property name="halign">end</property>
+            <property name="label" translatable="yes">Time</property>
+            <property name="single_line_mode">True</property>
+            <style>
+              <class name="numeric"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/gtk/tasks_group.ui b/src/gtk/tasks_group.ui
new file mode 100644
index 0000000..be43050
--- /dev/null
+++ b/src/gtk/tasks_group.ui
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FurTasksGroup" parent="AdwPreferencesGroup">
+    <child>
+      <object class="GtkBox" id="listbox_box">
+        <property name="orientation">vertical</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/gtk/tasks_page.ui b/src/gtk/tasks_page.ui
new file mode 100644
index 0000000..dcd6513
--- /dev/null
+++ b/src/gtk/tasks_page.ui
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FurTasksPage" parent="AdwPreferencesPage">
+  </template>
+</interface>
diff --git a/src/gtk/window.ui b/src/gtk/window.ui
new file mode 100644
index 0000000..d6c5311
--- /dev/null
+++ b/src/gtk/window.ui
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <template class="FurtheranceWindow" parent="AdwApplicationWindow">
+    <property name="title">Furtherance</property>
+    <property name="content">
+      <object class="AdwToastOverlay" id="toast_overlay">
+        <property name="child">
+          <object class="GtkBox" id="main_box">
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="AdwHeaderBar" id="header_bar">
+                <property name="title-widget">
+                  <object class="AdwWindowTitle" id="window_title">
+                    <property name="title">Furtherance</property>
+                  </object>
+                </property>
+                <child type="end">
+                  <object class="GtkMenuButton" id="app_menu_button">
+                    <property name="tooltip_text" translatable="yes">Main Menu</property>
+                    <property name="icon_name">open-menu-symbolic</property>
+                    <property name="menu_model">primary_menu</property>
+                  </object>
+                </child>
+                <style>
+                  <class name="titlebar"/>
+                </style>
+              </object>
+            </child>
+            <child>
+            <object class="GtkBox" id="win_box">
+              <property name="orientation">vertical</property>
+              <property name="spacing">10</property>
+              <property name="margin_bottom">18</property>
+              <property name="halign">center</property>
+              <property name="width_request">400</property>
+              <property name="vexpand">True</property>
+              <child>
+                <object class="GtkLabel" id="watch">
+                  <property name="label">00:00:00</property>
+                  <attributes>
+                    <attribute name="weight" value="bold"/>
+                    <attribute name="scale" value="5"/>
+                  </attributes>
+                  <style>
+                    <class name="numeric"/>
+                  </style>
+                </object>
+              </child>
+              <child>
+                <object class="GtkBox">
+                  <property name="spacing">5</property>
+                  <property name="margin_start">12</property>
+                  <property name="margin_end">8</property>
+                  <child>
+                    <object class="GtkEntry" id="task_input">
+                      <property name="placeholder-text" translatable="yes">Task Name</property>
+                      <property name="hexpand">True</property>
+                      <property name="hexpand-set">True</property>
+                    </object>
+                  </child>
+                  <child>
+                    <object class="GtkButton" id="start_button">
+                      <property name="icon-name">media-playback-start-symbolic</property>
+                    </object>
+                  </child>
+                </object>
+              </child>
+              <child>
+                <object class="FurHistoryBox" id="history_box" />
+              </child>
+            </object>
+          </child>
+          </object>
+        </property>
+      </object>
+    </property>
+  </template>
+  <menu id="primary_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Preferences</attribute>
+        <attribute name="action">app.preferences</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_About Furtherance</attribute>
+        <attribute name="action">app.about</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Delete history</attribute>
+        <attribute name="action">app.delete-history</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..541e42c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,58 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+mod application;
+mod config;
+mod ui;
+mod database;
+
+use self::application::FurtheranceApplication;
+
+use config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR};
+use gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain};
+use gtk::{gio, glib};
+use gtk::prelude::*;
+
+fn main() {
+    // Initialize GTK
+    gtk::init().expect("Failed to initialize GTK.");
+    // Initialize libadwaita
+    adw::init();
+
+    // Set up gettext translations
+    bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain");
+    bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8")
+        .expect("Unable to set the text domain encoding");
+    textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain");
+
+    // Load resources
+    let resources = gio::Resource::load(PKGDATADIR.to_owned() + "/furtherance.gresource")
+        .expect("Could not load resources");
+    gio::resources_register(&resources);
+
+    // Create a new GtkApplication. The application manages our main loop,
+    // application windows, integration with the window manager/compositor, and
+    // desktop features such as file opening and single-instance applications.
+    let app = FurtheranceApplication::new("com.lakoliu.Furtherance", &gio::ApplicationFlags::empty());
+
+    glib::set_application_name("Furtherance");
+
+    // Run the application. This function will block until the application
+    // exits. Upon return, we have our exit code to return to the shell. (This
+    // is the code you see when you do `echo $?` after running a command in a
+    // terminal.
+    std::process::exit(app.run());
+}
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..7833e8f
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,67 @@
+pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
+gnome = import('gnome')
+
+gnome.compile_resources('furtherance',
+  'furtherance.gresource.xml',
+  gresource_bundle: true,
+  install: true,
+  install_dir: pkgdatadir,
+)
+
+conf = configuration_data()
+conf.set_quoted('VERSION', meson.project_version())
+conf.set_quoted('GETTEXT_PACKAGE', 'furtherance')
+conf.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir')))
+conf.set_quoted('PKGDATADIR', pkgdatadir)
+conf.set_quoted('APP_ID', app_id)
+
+configure_file(
+    input: 'config.rs.in',
+    output: 'config.rs',
+    configuration: conf
+)
+
+# Copy the config.rs output to the source directory.
+run_command(
+  'cp',
+  join_paths(meson.build_root(), 'src', 'config.rs'),
+  join_paths(meson.source_root(), 'src', 'config.rs'),
+  check: true
+)
+
+rust_sources = files(
+  'ui.rs',
+  'ui/task_details.rs',
+  'ui/task_row.rs',
+  'ui/tasks_group.rs',
+  'ui/tasks_page.rs',
+  'ui/history_box.rs',
+  'ui/window.rs',
+
+  'application.rs',
+  'config.rs',
+  'main.rs',
+
+  'database.rs',
+)
+
+sources = [cargo_sources, rust_sources]
+
+cargo_script = find_program(join_paths(meson.source_root(), 'build-aux/cargo.sh'))
+cargo_release = custom_target(
+  'cargo-build',
+  build_by_default: true,
+  input: sources,
+  output: meson.project_name(),
+  console: true,
+  install: true,
+  install_dir: get_option('bindir'),
+  command: [
+    cargo_script,
+    meson.build_root(),
+    meson.source_root(),
+    '@OUTPUT@',
+    get_option('buildtype'),
+    meson.project_name(),
+  ]
+)
diff --git a/src/ui.rs b/src/ui.rs
new file mode 100644
index 0000000..e1cf7fe
--- /dev/null
+++ b/src/ui.rs
@@ -0,0 +1,29 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+pub mod window;
+mod history_box;
+mod tasks_page;
+mod tasks_group;
+mod task_row;
+mod task_details;
+
+pub use window::FurtheranceWindow;
+pub use history_box::FurHistoryBox;
+pub use tasks_page::FurTasksPage;
+pub use tasks_group::FurTasksGroup;
+pub use task_row::FurTaskRow;
+pub use task_details::FurTaskDetails;
diff --git a/src/ui/history_box.rs b/src/ui/history_box.rs
new file mode 100644
index 0000000..4de6dce
--- /dev/null
+++ b/src/ui/history_box.rs
@@ -0,0 +1,138 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{glib, CompositeTemplate};
+use glib::subclass;
+
+use crate::ui::FurTasksPage;
+use crate::FurtheranceApplication;
+use crate::database;
+
+enum View {
+    Loading,
+    Empty,
+    Tasks,
+}
+
+mod imp {
+    use super::*;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/history_box.ui")]
+    pub struct FurHistoryBox {
+        // Template widgets
+        #[template_child]
+        pub stack: TemplateChild<gtk::Stack>,
+        #[template_child]
+        pub spinner: TemplateChild<gtk::Spinner>,
+        #[template_child]
+        pub welcome_page: TemplateChild<adw::StatusPage>,
+        #[template_child]
+        pub tasks_page: TemplateChild<FurTasksPage>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurHistoryBox {
+        const NAME: &'static str = "FurHistoryBox";
+        type Type = super::FurHistoryBox;
+        type ParentType = gtk::Box;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+
+    }
+
+    impl ObjectImpl for FurHistoryBox {
+        fn constructed(&self, obj: &Self::Type) {
+            obj.setup_widgets();
+            self.parent_constructed(obj);
+        }
+
+    }
+    impl WidgetImpl for FurHistoryBox {}
+    impl BoxImpl for FurHistoryBox {}
+}
+
+glib::wrapper! {
+    pub struct FurHistoryBox(
+        ObjectSubclass<imp::FurHistoryBox>)
+        @extends gtk::Widget, gtk::Box;
+}
+
+
+impl FurHistoryBox {
+    fn setup_widgets(&self) {
+        self.set_view(View::Loading);
+        let is_saved_task: bool = match database::check_for_tasks() {
+            Ok(_) => true,
+            Err(_) => false,
+        };
+        if is_saved_task {
+            self.set_view(View::Tasks);
+        } else {
+            self.set_view(View::Empty);
+        }
+    }
+
+    fn set_view(&self, view: View) {
+        let imp = imp::FurHistoryBox::from_instance(self);
+        let app = FurtheranceApplication::default();
+        app.delete_enabled(false);
+        imp.spinner.set_spinning(false);
+
+        let name = match view {
+            View::Loading => {
+                imp.spinner.set_spinning(true);
+                "loading"
+            }
+            View::Empty => "empty",
+            View::Tasks => {
+                app.delete_enabled(true);
+                "tasks"
+            }
+        };
+
+        imp.stack.set_visible_child_name(name);
+    }
+
+    pub fn create_tasks_page(&self) {
+        let imp = imp::FurHistoryBox::from_instance(self);
+        imp.tasks_page.clear_task_list();
+        let is_saved_task: bool = match database::check_for_tasks() {
+            Ok(_) => true,
+            Err(_) => false,
+        };
+        if is_saved_task {
+            self.set_view(View::Loading);
+            imp.tasks_page.build_task_list();
+            self.set_view(View::Tasks);
+        } else {
+            self.set_view(View::Empty);
+        }
+    }
+
+    pub fn empty_view(&self) {
+        self.set_view(View::Empty);
+    }
+
+}
diff --git a/src/ui/task_details.rs b/src/ui/task_details.rs
new file mode 100644
index 0000000..69c3494
--- /dev/null
+++ b/src/ui/task_details.rs
@@ -0,0 +1,441 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use adw::subclass::prelude::*;
+use glib::clone;
+use gtk::subclass::prelude::*;
+use gtk::{glib, prelude::*, CompositeTemplate};
+use chrono::{DateTime, NaiveDateTime, Local, offset::TimeZone};
+
+use crate::FurtheranceApplication;
+use crate::ui::FurtheranceWindow;
+use crate::database;
+
+mod imp {
+    use super::*;
+    use glib::subclass;
+    use std::cell::RefCell;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/task_details.ui")]
+    pub struct FurTaskDetails {
+        #[template_child]
+        pub headerbar: TemplateChild<gtk::HeaderBar>,
+        #[template_child]
+        pub dialog_title: TemplateChild<adw::WindowTitle>,
+        #[template_child]
+        pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
+
+        #[template_child]
+        pub task_name_label: TemplateChild<gtk::Label>,
+
+        #[template_child]
+        pub main_box: TemplateChild<gtk::Box>,
+
+        #[template_child]
+        pub delete_all_btn: TemplateChild<gtk::Button>,
+
+        pub all_boxes: RefCell<Vec<gtk::Box>>,
+        pub all_task_ids: RefCell<Vec<i32>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurTaskDetails {
+        const NAME: &'static str = "FurTaskDetails";
+        type ParentType = adw::Window;
+        type Type = super::FurTaskDetails;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FurTaskDetails {
+
+        fn constructed(&self, obj: &Self::Type) {
+            obj.setup_signals();
+            obj.setup_delete_all();
+            self.parent_constructed(obj);
+        }
+    }
+
+    impl WidgetImpl for FurTaskDetails {}
+
+    impl WindowImpl for FurTaskDetails {}
+
+    impl AdwWindowImpl for FurTaskDetails {}
+}
+
+glib::wrapper! {
+    pub struct FurTaskDetails(ObjectSubclass<imp::FurTaskDetails>)
+        @extends gtk::Widget, gtk::Window, adw::Window;
+}
+
+impl FurTaskDetails {
+    pub fn new() -> Self {
+        let dialog: Self = glib::Object::new(&[]).unwrap();
+
+        let window = FurtheranceWindow::default();
+        dialog.set_transient_for(Some(&window));
+
+        let app = FurtheranceApplication::default();
+        app.add_window(&window);
+
+        dialog
+    }
+
+    pub fn setup_widgets(&self, mut task_group: Vec<database::Task>) {
+        let imp = imp::FurTaskDetails::from_instance(self);
+
+        imp.task_name_label.set_text(&task_group[0].task_name);
+
+        for task in task_group.clone() {
+            imp.all_task_ids.borrow_mut().push(task.id);
+        }
+
+        let task_group_len = task_group.len();
+        task_group.reverse();
+        for task in task_group {
+            let task_box = gtk::Box::new(gtk::Orientation::Horizontal, 5);
+            task_box.set_homogeneous(true);
+
+            let start_time = DateTime::parse_from_rfc3339(&task.start_time).unwrap();
+            let start_time_str = start_time.format("%H:%M:%S").to_string();
+            let start = gtk::Button::new();
+            start.set_label(&start_time_str);
+            task_box.append(&start);
+
+            let stop_time = DateTime::parse_from_rfc3339(&task.stop_time).unwrap();
+            let stop_time_str = stop_time.format("%H:%M:%S").to_string();
+            let stop = gtk::Button::new();
+            stop.set_label(&stop_time_str);
+            task_box.append(&stop);
+
+            let total_time = stop_time - start_time;
+            let total_time = total_time.num_seconds();
+            let h = total_time / 60 / 60;
+            let m = (total_time / 60) - (h * 60);
+            let s = total_time - (m * 60);
+            let total_time_str = format!("{:02}:{:02}:{:02}", h, m, s);
+            let total = gtk::Button::new();
+            total.set_label(&total_time_str);
+            total.add_css_class("inactive-button");
+            total.set_hexpand(false);
+            task_box.append(&total);
+
+            imp.main_box.append(&task_box);
+            imp.all_boxes.borrow_mut().push(task_box);
+
+            start.connect_clicked(clone!(@weak self as this => move |_|{
+                let window = FurtheranceWindow::default();
+                let dialog = gtk::MessageDialog::new(
+                    Some(&window),
+                    gtk::DialogFlags::MODAL,
+                    gtk::MessageType::Question,
+                    gtk::ButtonsType::OkCancel,
+                    "<span size='x-large' weight='bold'>Edit Task</span>",
+                );
+                dialog.set_use_markup(true);
+
+                let message_area = dialog.message_area().downcast::<gtk::Box>().unwrap();
+                let vert_box = gtk::Box::new(gtk::Orientation::Vertical, 5);
+                let task_name_edit = gtk::Entry::new();
+                task_name_edit.set_text(&task.task_name);
+                let labels_box = gtk::Box::new(gtk::Orientation::Horizontal, 5);
+                labels_box.set_homogeneous(true);
+                let start_label = gtk::Label::new(Some("Start"));
+                start_label.add_css_class("title-4");
+                let stop_label = gtk::Label::new(Some("Stop"));
+                stop_label.add_css_class("title-4");
+                let times_box = gtk::Box::new(gtk::Orientation::Horizontal, 5);
+                times_box.set_homogeneous(true);
+
+                let start_time_w_year = start_time.format("%h %e %Y %H:%M:%S").to_string();
+                let stop_time_w_year = stop_time.format("%h %e %Y %H:%M:%S").to_string();
+                let start_time_edit = gtk::Entry::new();
+                start_time_edit.set_text(&start_time_w_year);
+                let stop_time_edit = gtk::Entry::new();
+                stop_time_edit.set_text(&stop_time_w_year);
+
+                let instructions = gtk::Label::new(Some(
+                    "*Use the format MMM DD YYYY HH:MM:SS"));
+                instructions.set_visible(false);
+                instructions.add_css_class("error_message");
+
+                let time_error = gtk::Label::new(Some(
+                    "*Start time cannot be later than stop time."));
+                time_error.set_visible(false);
+                time_error.add_css_class("error_message");
+
+                let future_error = gtk::Label::new(Some(
+                    "*Time cannot be in the future."));
+                future_error.set_visible(false);
+                future_error.add_css_class("error_message");
+
+                let delete_task_btn = gtk::Button::new();
+                delete_task_btn.set_icon_name("user-trash-symbolic");
+                delete_task_btn.set_tooltip_text(Some("Delete task"));
+                delete_task_btn.set_hexpand(false);
+                delete_task_btn.set_vexpand(false);
+                delete_task_btn.set_halign(gtk::Align::End);
+
+                vert_box.append(&task_name_edit);
+                labels_box.append(&start_label);
+                labels_box.append(&stop_label);
+                times_box.append(&start_time_edit);
+                times_box.append(&stop_time_edit);
+                vert_box.append(&labels_box);
+                vert_box.append(&times_box);
+                vert_box.append(&instructions);
+                vert_box.append(&time_error);
+                vert_box.append(&future_error);
+                message_area.append(&delete_task_btn);
+                message_area.append(&vert_box);
+
+                delete_task_btn.connect_clicked(clone!(
+                    @strong task, @strong dialog, @weak this => move |_| {
+
+                    let delete_confirmation = gtk::MessageDialog::with_markup(
+                        Some(&window),
+                        gtk::DialogFlags::MODAL,
+                        gtk::MessageType::Question,
+                        gtk::ButtonsType::OkCancel,
+                        Some("<span size='x-large' weight='bold'>Delete task?</span>"),
+                    );
+
+                    delete_confirmation.connect_response(clone!(
+                        @strong dialog,
+                        @strong delete_confirmation => move |_, resp| {
+                        if resp == gtk::ResponseType::Ok {
+                            let _ = database::delete_by_id(task.id);
+                            if task_group_len == 1 {
+                                delete_confirmation.close();
+                                dialog.close();
+                                this.close();
+                                let window = FurtheranceWindow::default();
+                                window.reset_history_box();
+                            } else {
+                                delete_confirmation.close();
+                                this.clear_task_list();
+                                dialog.close();
+                            }
+                        } else {
+                            delete_confirmation.close();
+                        }
+                    }));
+
+                    delete_confirmation.show();
+                }));
+
+
+                dialog.connect_response(
+                    clone!(@strong dialog,
+                        @strong task.task_name as name
+                        @strong task.start_time as start_time
+                        @strong task.stop_time as stop_time => move |_ , resp| {
+                        if resp == gtk::ResponseType::Ok {
+                            instructions.set_visible(false);
+                            time_error.set_visible(false);
+                            future_error.set_visible(false);
+                            let mut start_successful = false;
+                            let mut stop_successful = false;
+                            let mut do_not_close = false;
+                            let mut new_start_time_edited: String = "".to_string();
+                            let mut new_start_time_local = Local::now();
+                            let new_stop_time_edited: String;
+                            if start_time_edit.text() != start_time_w_year {
+                                let new_start_time_str = start_time_edit.text();
+                                let new_start_time = NaiveDateTime::parse_from_str(
+                                                        &new_start_time_str,
+                                                        "%h %e %Y %H:%M:%S");
+                                if let Err(_) = new_start_time {
+                                    instructions.set_visible(true);
+                                    do_not_close = true;
+                                } else {
+                                    new_start_time_local = Local.from_local_datetime(&new_start_time.unwrap()).unwrap();
+                                    new_start_time_edited = new_start_time_local.to_rfc3339();
+                                    start_successful = true;
+                                }
+                            }
+                            if stop_time_edit.text() != stop_time_w_year {
+                                let new_stop_time_str = stop_time_edit.text();
+                                let new_stop_time = NaiveDateTime::parse_from_str(
+                                                        &new_stop_time_str,
+                                                        "%h %e %Y %H:%M:%S");
+                                if let Err(_) = new_stop_time {
+                                    instructions.set_visible(true);
+                                    do_not_close = true;
+                                } else {
+                                    let new_stop_time = Local.from_local_datetime(&new_stop_time.unwrap()).unwrap();
+                                    new_stop_time_edited = new_stop_time.to_rfc3339();
+                                    if start_successful {
+                                        if (new_stop_time - new_start_time_local).num_seconds() >= 0 {
+                                            database::update_stop_time(task.id, new_stop_time_edited.clone())
+                                                .expect("Failed to update stop time.");
+                                            database::update_start_time(task.id, new_start_time_edited.clone())
+                                                .expect("Failed to update stop time.");
+                                        }
+                                    } else {
+                                        let old_start_time = DateTime::parse_from_rfc3339(&start_time);
+                                        let old_start_time = old_start_time.unwrap().with_timezone(&Local);
+                                        if (Local::now() - new_stop_time).num_seconds() < 0 {
+                                            future_error.set_visible(true);
+                                            do_not_close = true;
+                                        } else if (new_stop_time - old_start_time).num_seconds() >= 0 {
+                                            database::update_stop_time(task.id, new_stop_time_edited)
+                                                .expect("Failed to update stop time.");
+                                        } else {
+                                            time_error.set_visible(true);
+                                            do_not_close = true;
+                                        }
+                                    }
+                                    stop_successful = true;
+                                }
+                            }
+                            if task_name_edit.text() != name {
+                                database::update_task_name(task.id, task_name_edit.text().to_string())
+                                    .expect("Failed to update start time.");
+                            }
+
+                            if start_successful && !stop_successful {
+                                let old_stop_time = DateTime::parse_from_rfc3339(&stop_time);
+                                let old_stop_time = old_stop_time.unwrap().with_timezone(&Local);
+                                if (old_stop_time - new_start_time_local).num_seconds() >= 0 {
+                                    database::update_start_time(task.id, new_start_time_edited)
+                                        .expect("Failed to update start time.");
+                                } else {
+                                    time_error.set_visible(true);
+                                    do_not_close = true;
+                                }
+
+                            }
+
+                            if !do_not_close {
+                                this.clear_task_list();
+                                dialog.close();
+                            }
+
+
+                        } else {
+                            // If Cancel, close dialog and do nothing.
+                            dialog.close();
+                        }
+                    }),
+                );
+
+
+                dialog.show();
+            }));
+
+            stop.connect_clicked(move |_|{
+                start.emit_clicked();
+            });
+        }
+    }
+
+    fn clear_task_list(&self) {
+        let imp = imp::FurTaskDetails::from_instance(&self);
+
+        for task_box in &*imp.all_boxes.borrow() {
+            imp.main_box.remove(task_box);
+        }
+
+        imp.all_boxes.borrow_mut().clear();
+        // Get list from database by a vec of IDs
+        let updated_list = database::get_list_by_id(imp.all_task_ids.clone().borrow().to_vec());
+        imp.all_task_ids.borrow_mut().clear();
+        let window = FurtheranceWindow::default();
+        window.reset_history_box();
+        self.setup_widgets(updated_list.unwrap());
+    }
+
+    fn setup_signals(&self) {
+        let imp = imp::FurTaskDetails::from_instance(self);
+
+        // Add headerbar to dialog when scrolled far
+        imp.scrolled_window.vadjustment().connect_value_notify(
+            clone!(@weak self as this => move |adj|{
+                let imp = imp::FurTaskDetails::from_instance(&this);
+                if adj.value() < 120.0 {
+                    imp.headerbar.add_css_class("hidden");
+                    imp.dialog_title.set_visible(false);
+                }else {
+                    imp.headerbar.remove_css_class("hidden");
+                    imp.dialog_title.set_visible(true);
+                }
+            }),
+        );
+
+        // Make dialog header smaller if the name is long
+        imp.task_name_label.connect_label_notify(|label| {
+            let large_title = !(label.text().len() > 25);
+
+            if large_title {
+                label.remove_css_class("title-2");
+                label.add_css_class("title-1");
+            } else {
+                label.remove_css_class("title-1");
+                label.add_css_class("title-2");
+            }
+        });
+    }
+
+    fn setup_delete_all(&self) {
+        let imp = imp::FurTaskDetails::from_instance(self);
+        let window = FurtheranceWindow::default();
+
+        imp.delete_all_btn.connect_clicked(clone!(@weak self as this => move |_|{
+            let dialog = gtk::MessageDialog::with_markup(
+                Some(&window),
+                gtk::DialogFlags::MODAL,
+                gtk::MessageType::Warning,
+                gtk::ButtonsType::None,
+                Some("<span size='large'>Delete All?</span>"),
+            );
+            dialog.set_secondary_text(Some("This will delete all occurrences of this task on this day."));
+            dialog.add_buttons(&[
+                ("Delete", gtk::ResponseType::Accept),
+                ("Cancel", gtk::ResponseType::Reject)
+            ]);
+            dialog.show();
+
+            dialog.connect_response(clone!(@strong dialog => move |_,resp|{
+                if resp == gtk::ResponseType::Accept {
+                    this.delete_all();
+                    dialog.close();
+                    this.close();
+                    let window = FurtheranceWindow::default();
+                    window.reset_history_box();
+                } else {
+                    dialog.close();
+                }
+            }));
+
+        }));
+
+    }
+
+    fn delete_all(&self) {
+        let imp = imp::FurTaskDetails::from_instance(self);
+        let _ = database::delete_by_ids(imp.all_task_ids.borrow().to_vec());
+    }
+
+}
+
diff --git a/src/ui/task_row.rs b/src/ui/task_row.rs
new file mode 100644
index 0000000..d17f263
--- /dev/null
+++ b/src/ui/task_row.rs
@@ -0,0 +1,130 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use gtk::subclass::prelude::*;
+use gtk::{glib, gio, prelude::*, CompositeTemplate};
+use chrono::DateTime;
+use std::sync::Mutex;
+use once_cell::sync::Lazy;
+use glib::clone;
+
+use crate::database::Task;
+use crate::ui::FurTaskDetails;
+
+
+mod imp {
+    use super::*;
+    use glib::subclass;
+
+    #[derive(Debug, CompositeTemplate, Default)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/task_row.ui")]
+    pub struct FurTaskRow {
+        #[template_child]
+        pub task_name_label: TemplateChild<gtk::Label>,
+        #[template_child]
+        pub total_time_label: TemplateChild<gtk::Label>,
+        // pub tasks: Vec<database::Task>,
+        pub tasks: Lazy<Mutex<Vec<Task>>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurTaskRow {
+        const NAME: &'static str = "FurTaskRow";
+        type ParentType = gtk::ListBoxRow;
+        type Type = super::FurTaskRow;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FurTaskRow {
+        fn constructed(&self, obj: &Self::Type) {
+            obj.setup_signals();
+            self.parent_constructed(obj);
+        }
+    }
+
+    impl WidgetImpl for FurTaskRow {}
+
+    impl ListBoxRowImpl for FurTaskRow {}
+}
+
+glib::wrapper! {
+    pub struct FurTaskRow(
+        ObjectSubclass<imp::FurTaskRow>)
+        @extends gtk::Widget, gtk::ListBoxRow;
+}
+
+impl FurTaskRow {
+    pub fn new() -> Self {
+        /* This should take a model, object, or vec filled with the description
+        details and fill out the labels based on those. */
+        glib::Object::new(&[]).expect("Failed to create `FurTaskRow`.")
+    }
+
+    fn setup_signals(&self) {
+        let open_details_action = gio::SimpleAction::new("open-details", None);
+
+        open_details_action.connect_activate(clone!(@strong self as this => move |_, _| {
+            let dialog = FurTaskDetails::new();
+            dialog.setup_widgets(this.get_tasks());
+            dialog.show();
+        }));
+
+        let actions = gio::SimpleActionGroup::new();
+        self.insert_action_group("task-row", Some(&actions));
+        actions.add_action(&open_details_action);
+    }
+
+    pub fn set_row_labels(&self, task_list: Vec<Task>) {
+        let imp = imp::FurTaskRow::from_instance(&self);
+        for task in task_list.clone() {
+            imp.tasks.lock().unwrap().push(task);
+        }
+        imp.task_name_label.set_text(&imp.tasks.lock().unwrap()[0].task_name);
+
+        // Add up all durations for task of said name to create total_time
+        let mut total_time: i64 = 0;
+        for task in &task_list {
+            if task.task_name == task.task_name {
+                let start_time = DateTime::parse_from_rfc3339(&task.start_time).unwrap();
+                let stop_time = DateTime::parse_from_rfc3339(&task.stop_time).unwrap();
+
+                let duration = stop_time - start_time;
+                total_time += duration.num_seconds();
+            }
+        }
+        // Format total time to readable string
+        let h = total_time / 60 / 60;
+        let m = (total_time / 60) - (h * 60);
+        let s = total_time - (m * 60);
+
+        let total_time_str = format!("{:02}:{:02}:{:02}", h, m, s);
+
+        imp.total_time_label.set_text(&total_time_str);
+    }
+
+    pub fn get_tasks(&self) -> Vec<Task> {
+        let imp = imp::FurTaskRow::from_instance(&self);
+        imp.tasks.lock().unwrap().to_vec()
+    }
+}
+
diff --git a/src/ui/tasks_group.rs b/src/ui/tasks_group.rs
new file mode 100644
index 0000000..86c20dc
--- /dev/null
+++ b/src/ui/tasks_group.rs
@@ -0,0 +1,112 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use adw::subclass::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{glib, prelude::*};
+
+use crate::ui::FurTaskRow;
+use crate::database;
+
+mod imp {
+    use super::*;
+    use glib::subclass;
+    use gtk::CompositeTemplate;
+
+    use std::cell::RefCell;
+
+    #[derive(Default, Debug, CompositeTemplate)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/tasks_group.ui")]
+    pub struct FurTasksGroup {
+        #[template_child]
+        pub listbox_box: TemplateChild<gtk::Box>,
+        pub models: RefCell<Vec<gtk::SortListModel>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurTasksGroup {
+        const NAME: &'static str = "FurTasksGroup";
+        type ParentType = adw::PreferencesGroup;
+        type Type = super::FurTasksGroup;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FurTasksGroup {}
+    impl WidgetImpl for FurTasksGroup {}
+    impl PreferencesGroupImpl for FurTasksGroup {}
+}
+
+glib::wrapper! {
+    pub struct FurTasksGroup(
+        ObjectSubclass<imp::FurTasksGroup>)
+        @extends gtk::Widget, adw::PreferencesGroup;
+
+}
+
+impl FurTasksGroup {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create `FurTaskGroup`.")
+    }
+
+    pub fn add_task_model(&self, tasks: Vec<database::Task>) {
+        let imp = imp::FurTasksGroup::from_instance(&self);
+
+        let listbox = gtk::ListBox::new();
+        listbox.add_css_class("content");
+        listbox.set_selection_mode(gtk::SelectionMode::None);
+        imp.listbox_box.append(&listbox);
+
+        // Check if tasks have the same name. If they do, make one listbox row for all of them.
+        // If they don't, move on.
+        let mut tasks_by_name: Vec<Vec<database::Task>> = Vec::new();
+        let mut unique: bool;
+
+        for task in &tasks {
+            unique = true;
+            for i in 0..tasks_by_name.len() {
+                if tasks_by_name[i][0].task_name == task.task_name {
+                    tasks_by_name[i].push(task.clone());
+                    unique = false;
+                }
+            }
+            if unique {
+                // Add unique task to list for group name
+                let mut new_name_list: Vec<database::Task> = Vec::new();
+                new_name_list.push(task.clone());
+                tasks_by_name.push(new_name_list);
+            }
+        }
+
+        for same_name in tasks_by_name {
+            let listbox_row = FurTaskRow::new();
+            listbox_row.set_row_labels(same_name);
+            listbox.append(&listbox_row);
+        }
+
+        listbox.connect_row_activated(move |_, row| {
+            row.activate_action("task-row.open-details", None).unwrap();
+        });
+
+    }
+}
+
diff --git a/src/ui/tasks_page.rs b/src/ui/tasks_page.rs
new file mode 100644
index 0000000..fbbaf0c
--- /dev/null
+++ b/src/ui/tasks_page.rs
@@ -0,0 +1,147 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use adw::subclass::prelude::*;
+use adw::prelude::{PreferencesPageExt, PreferencesGroupExt};
+use gtk::subclass::prelude::*;
+use gtk::{glib, prelude::*};
+use chrono::{DateTime, Local, Duration};
+
+use crate::ui::FurTasksGroup;
+use crate::database;
+
+mod imp {
+    use super::*;
+    use glib::subclass;
+    use gtk::CompositeTemplate;
+    use std::cell::RefCell;
+
+    #[derive(Default, Debug, CompositeTemplate)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/tasks_page.ui")]
+    pub struct FurTasksPage {
+        pub all_groups: RefCell<Vec<FurTasksGroup>>,
+    }
+
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurTasksPage {
+        const NAME: &'static str = "FurTasksPage";
+        type ParentType = adw::PreferencesPage;
+        type Type = super::FurTasksPage;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FurTasksPage {
+        fn constructed(&self, obj: &Self::Type) {
+            obj.setup_widgets();
+            self.parent_constructed(obj);
+        }
+    }
+
+    impl WidgetImpl for FurTasksPage {}
+    impl PreferencesPageImpl for FurTasksPage {}
+}
+
+glib::wrapper! {
+    pub struct FurTasksPage(
+        ObjectSubclass<imp::FurTasksPage>)
+        @extends gtk::Widget, adw::PreferencesPage;
+}
+
+impl FurTasksPage {
+    fn setup_widgets(&self) {
+        self.build_task_list();
+    }
+
+    pub fn clear_task_list(&self) {
+        let imp = imp::FurTasksPage::from_instance(&self);
+
+        for group in &*imp.all_groups.borrow() {
+            self.remove(group);
+        }
+
+        imp.all_groups.borrow_mut().clear();
+    }
+
+    pub fn build_task_list(&self) {
+        let imp = imp::FurTasksPage::from_instance(&self);
+
+        let mut tasks_list = database::retrieve().unwrap();
+
+        // Reversing chronological order of tasks_list
+        tasks_list.reverse();
+        let mut uniq_date_list: Vec<String> = Vec::new();
+        let mut same_date_list: Vec<database::Task> = Vec::new();
+        let mut tasks_sorted_by_day: Vec<Vec<database::Task>> = Vec::new();
+
+        // Go through tasks list and look at all dates
+        let mut i: u32 = 1;
+        let len = tasks_list.len() as u32;
+        for task in tasks_list {
+            let task_clone = task.clone();
+            let date = DateTime::parse_from_rfc3339(&task.start_time).unwrap();
+            let date = date.format("%h %e").to_string();
+            if !uniq_date_list.contains(&date) {
+                // if same_date_list is empty, push "date" to it
+                // if it is not empty, push it to a vec of vecs, and then clear it
+                // and push "date" to it
+                if same_date_list.is_empty() {
+                    same_date_list.push(task_clone);
+                } else {
+                    tasks_sorted_by_day.push(same_date_list.clone());
+                    same_date_list.clear();
+                    same_date_list.push(task_clone);
+                }
+                uniq_date_list.push(date);
+            } else {
+                // otherwise push the task to the list of others with the same date
+                same_date_list.push(task_clone);
+            }
+            // If this is the last iteration, push the list of objects to sorted_by_day
+            if i == len {
+                tasks_sorted_by_day.push(same_date_list.clone());
+            }
+            i += 1;
+        }
+
+        // Create FurTasksGroups for all unique days
+        let now = Local::now();
+        let yesterday = now - Duration::days(1);
+        let yesterday = yesterday.format("%h %e").to_string();
+        let today = now.format("%h %e").to_string();
+        for i in 0..uniq_date_list.len() {
+            let group = FurTasksGroup::new();
+            if uniq_date_list[i] == today {
+                group.set_title("Today")
+            } else if uniq_date_list[i] == yesterday{
+                group.set_title("Yesterday")
+            } else {
+                group.set_title(&uniq_date_list[i])
+            }
+            self.add(&group);
+            group.add_task_model(tasks_sorted_by_day[i].clone());
+            imp.all_groups.borrow_mut().push(group);
+        }
+    }
+}
+
diff --git a/src/ui/window.rs b/src/ui/window.rs
new file mode 100644
index 0000000..965f0f3
--- /dev/null
+++ b/src/ui/window.rs
@@ -0,0 +1,330 @@
+// Furtherance - Track your time without being tracked
+// Copyright (C) 2022  Ricky Kresslein <rk@lakoliu.com>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use adw::subclass::prelude::AdwApplicationWindowImpl;
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{gio, glib, CompositeTemplate};
+use glib::{clone, timeout_add_local};
+use std::time::Duration;
+use std::sync::{Arc, Mutex};
+use std::rc::Rc;
+use std::cell::RefCell;
+use chrono::{DateTime, Local, Duration as ChronDur};
+use dbus::blocking::Connection;
+use once_cell::unsync::OnceCell;
+
+use crate::ui::FurHistoryBox;
+use crate::FurtheranceApplication;
+use crate::database;
+
+mod imp {
+    use super::*;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/com/lakoliu/Furtherance/gtk/window.ui")]
+    pub struct FurtheranceWindow {
+        // Template widgets
+        #[template_child]
+        pub header_bar: TemplateChild<gtk::HeaderBar>,
+        #[template_child]
+        pub watch: TemplateChild<gtk::Label>,
+        #[template_child]
+        pub task_input: TemplateChild<gtk::Entry>,
+        #[template_child]
+        pub start_button: TemplateChild<gtk::Button>,
+        #[template_child]
+        pub history_box: TemplateChild<FurHistoryBox>,
+        #[template_child]
+        pub toast_overlay: TemplateChild<adw::ToastOverlay>,
+
+        pub notify_of_idle: OnceCell<u64>,
+        pub stored_idle: Mutex<u64>,
+        pub idle_notified: Mutex<bool>,
+        pub idle_time_reached: Mutex<bool>,
+        pub subtract_idle: Mutex<bool>,
+        pub idle_start_time: Mutex<String>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FurtheranceWindow {
+        const NAME: &'static str = "FurtheranceWindow";
+        type Type = super::FurtheranceWindow;
+        type ParentType = adw::ApplicationWindow;
+
+        fn class_init(klass: &mut Self::Class) {
+            FurHistoryBox::static_type();
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FurtheranceWindow {
+        fn constructed(&self, obj: &Self::Type) {
+            obj.setup_signals();
+            obj.setup_settings();
+            self.parent_constructed(obj);
+        }
+    }
+    impl WidgetImpl for FurtheranceWindow {}
+    impl WindowImpl for FurtheranceWindow {}
+    impl ApplicationWindowImpl for FurtheranceWindow {}
+    impl AdwApplicationWindowImpl for FurtheranceWindow {}
+}
+
+glib::wrapper! {
+    pub struct FurtheranceWindow(ObjectSubclass<imp::FurtheranceWindow>)
+        @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow,
+        @implements gio::ActionGroup, gio::ActionMap;
+}
+
+impl FurtheranceWindow {
+    pub fn new<P: glib::IsA<gtk::Application>>(application: &P) -> Self {
+        glib::Object::new(&[("application", application)])
+            .expect("Failed to create FurtheranceWindow")
+    }
+
+    pub fn inapp_notification(&self, text: &str) {
+        // Display in-app notifications
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        let toast = adw::Toast::new(text);
+        imp.toast_overlay.add_toast(&toast);
+    }
+
+    fn set_watch_time(&self, text: &str) {
+        // Update watch time while timer is running
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        imp.watch.set_text(text);
+        self.check_user_idle();
+    }
+
+    fn activate_task_input(&self, sensitive: bool) {
+        // Deactivate task_input while timer is running
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        imp.task_input.set_sensitive(sensitive);
+    }
+
+    fn get_task_text(&self) -> String {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        imp.task_input.text().to_string()
+    }
+
+    fn save_task(&self, start_time: DateTime<Local>, mut stop_time: DateTime<Local>) {
+        // Save the most recent task to the database and clear the task_input field
+        let imp = imp::FurtheranceWindow::from_instance(self);
+
+        if *imp.subtract_idle.lock().unwrap() {
+            let idle_start = DateTime::parse_from_rfc3339(&imp.idle_start_time.lock().unwrap()).unwrap();
+            stop_time = idle_start.with_timezone(&Local);
+            *imp.subtract_idle.lock().unwrap() = false
+        }
+
+        let _ = database::db_write(&imp.task_input.text().trim(), start_time, stop_time);
+        imp.task_input.set_text("");
+        imp.history_box.create_tasks_page();
+    }
+
+    pub fn reset_history_box(&self) {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        imp.history_box.create_tasks_page();
+    }
+
+    fn setup_signals(&self) {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        let running = Arc::new(Mutex::new(false));
+        let start_time = Rc::new(RefCell::new(Local::now()));
+        let stop_time = Rc::new(RefCell::new(Local::now()));
+
+        // Development mode
+        // self.add_css_class("devel");
+
+        imp.start_button.connect_clicked(clone!(
+            @weak self as this,
+            @strong running => move |button| {
+            if this.get_task_text().trim().is_empty() {
+                let dialog = gtk::MessageDialog::with_markup(
+                    Some(&this),
+                    gtk::DialogFlags::MODAL,
+                    gtk::MessageType::Error,
+                    gtk::ButtonsType::Ok,
+                    Some("<span size='large'>No Task Name</span>"),
+                );
+                dialog.set_secondary_text(Some("Enter a task name to start the timer."));
+                dialog.show();
+
+                dialog.connect_response(clone!(@strong dialog => move |_,_|{
+                    dialog.close();
+                }));
+
+            } else {
+                if !*running.lock().unwrap() {
+                    let mut secs: u32 = 0;
+                    let mut mins: u32 = 0;
+                    let mut hrs: u32 = 0;
+
+                    *running.lock().unwrap() = true;
+                    *start_time.borrow_mut() = Local::now();
+                    this.activate_task_input(false);
+                    let duration = Duration::new(1,0);
+                    timeout_add_local(duration, clone!(@strong running as running_clone => move || {
+                        if *running_clone.lock().unwrap() {
+                            secs += 1;
+                            if secs > 59 {
+                                secs = 0;
+                                mins += 1;
+                                if mins > 59 {
+                                    mins = 0;
+                                    hrs += 1;
+                                }
+                            }
+                            let watch_text: &str = &format!("{:02}:{:02}:{:02}", hrs, mins, secs).to_string();
+                            this.set_watch_time(watch_text);
+                        }
+                        Continue(*running_clone.lock().unwrap())
+                    }));
+                    button.set_icon_name("media-playback-stop-symbolic");
+                } else {
+                    *stop_time.borrow_mut() = Local::now();
+                    *running.lock().unwrap() = false;
+                    button.set_icon_name("media-playback-start-symbolic");
+                    this.set_watch_time("00:00:00");
+                    this.activate_task_input(true);
+                    this.save_task(*start_time.borrow(), *stop_time.borrow());
+                }
+            }
+        }));
+    }
+
+    fn setup_settings(&self) {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        imp.notify_of_idle.set(300).expect("Failed to set notify_of_idle");
+        self.reset_vars();
+
+        // Enter starts timer
+        let start = imp.start_button.clone();
+        self.set_default_widget(Some(&start));
+        imp.task_input.set_activates_default(true);
+    }
+
+    fn get_idle_time(&self) -> Result<u64, Box<dyn std::error::Error>> {
+        let c = Connection::new_session()?;
+
+        let p = c.with_proxy("org.gnome.Mutter.IdleMonitor",
+            "/org/gnome/Mutter/IdleMonitor/Core",
+            Duration::from_millis(5000)
+        );
+        let (idle_time,): (u64,) = p.method_call("org.gnome.Mutter.IdleMonitor", "GetIdletime", ())?;
+
+        Ok(idle_time / 1000)
+    }
+
+    fn check_user_idle(&self) {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        // Check for user idle
+        let idle_time = self.get_idle_time().unwrap();
+
+        // If user was idle and has now returned...
+        if idle_time < *imp.notify_of_idle.get().unwrap()
+            && *imp.idle_time_reached.lock().unwrap()
+            && !*imp.idle_notified.lock().unwrap() {
+
+                *imp.idle_notified.lock().unwrap() = true;
+                self.resume_from_idle();
+        }
+        *imp.stored_idle.lock().unwrap() = idle_time;
+
+        // If user is idle but has not returned...
+        if *imp.stored_idle.lock().unwrap() >= *imp.notify_of_idle.get().unwrap()
+            && !*imp.idle_time_reached.lock().unwrap() {
+
+            *imp.idle_time_reached.lock().unwrap() = true;
+            let true_idle_start_time = Local::now() -
+                ChronDur::seconds(*imp.notify_of_idle.get().unwrap() as i64);
+            *imp.idle_start_time.lock().unwrap() = true_idle_start_time.to_rfc3339();
+        }
+    }
+
+    fn resume_from_idle(&self) {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+
+        let resume_time = Local::now();
+        let idle_start = DateTime::parse_from_rfc3339(&imp.idle_start_time.lock().unwrap()).unwrap();
+        let idle_start = idle_start.with_timezone(&Local);
+        let idle_time = resume_time - idle_start;
+        let idle_time = idle_time.num_seconds();
+        let h = idle_time / 60 / 60;
+        let m = (idle_time / 60) - (h * 60);
+        let s = idle_time - (m * 60);
+        let idle_time_str = format!(
+            "You have been idle for {:02}:{:02}:{:02}.\nWould you like to discard that time, or continue the clock?",
+            h, m, s);
+
+        let dialog = gtk::MessageDialog::with_markup(
+            Some(self),
+            gtk::DialogFlags::MODAL,
+            gtk::MessageType::Warning,
+            gtk::ButtonsType::None,
+            Some("<span size='x-large' weight='bold'>Edit Task</span>"),
+        );
+        dialog.add_buttons(&[
+            ("Discard", gtk::ResponseType::Reject),
+            ("Continue", gtk::ResponseType::Accept)
+        ]);
+        dialog.set_secondary_text(Some(&idle_time_str));
+
+        dialog.connect_response(clone!(
+            @weak self as this,
+            @strong dialog,
+            @strong imp.start_button as start_button => move |_, resp| {
+            if resp == gtk::ResponseType::Reject {
+                this.set_subtract_idle(true);
+                start_button.emit_clicked();
+                dialog.close();
+            } else {
+                this.reset_vars();
+                dialog.close();
+            }
+        }));
+
+        dialog.show()
+    }
+
+    fn reset_vars(&self) {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        *imp.stored_idle.lock().unwrap() = 0;
+        *imp.idle_notified.lock().unwrap() = false;
+        *imp.idle_time_reached.lock().unwrap() = false;
+        *imp.subtract_idle.lock().unwrap() = false;
+    }
+
+    fn set_subtract_idle(&self, val: bool) {
+        let imp = imp::FurtheranceWindow::from_instance(self);
+        *imp.subtract_idle.lock().unwrap() = val;
+    }
+}
+
+impl Default for FurtheranceWindow {
+    fn default() -> Self {
+        FurtheranceApplication::default()
+            .active_window()
+            .unwrap()
+            .downcast()
+            .unwrap()
+    }
+}