about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--COPYING675
-rw-r--r--Cargo.lock1104
-rw-r--r--Cargo.toml22
-rw-r--r--build-aux/cargo.sh24
-rwxr-xr-xbuild-aux/meson/postinstall.py21
-rw-r--r--com.lakoliu.Furtherance.json52
-rw-r--r--data/com.lakoliu.Furtherance.appdata.xml.in9
-rw-r--r--data/com.lakoliu.Furtherance.desktop.in11
-rw-r--r--data/com.lakoliu.Furtherance.gschema.xml5
-rw-r--r--data/icons/hicolor/scalable/apps/com.lakoliu.Furtherance.svg50
-rw-r--r--data/icons/hicolor/symbolic/apps/com.lakoliu.Furtherance-symbolic.svg50
-rw-r--r--data/icons/meson.build13
-rw-r--r--data/meson.build43
-rw-r--r--meson.build32
-rw-r--r--po/LINGUAS0
-rw-r--r--po/POTFILES4
-rw-r--r--po/meson.build1
-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
38 files changed, 4364 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1cea60c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+com.lakoliu.Furtherance.json~
+src/config.rs
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..10926e8
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,675 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..c977cd5
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1104 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "cairo-rs"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b869e97a87170f96762f9f178eae8c461147e722ba21dd8814105bf5716bf14a"
+dependencies = [
+ "bitflags",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "thiserror",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-expr"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "295b6eb918a60a25fec0b23a5e633e74fddbaf7bb04411e65a10c366aca4b5cd"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "time",
+ "winapi",
+]
+
+[[package]]
+name = "clap"
+version = "2.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "dbus"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0a745c25b32caa56b82a3950f5fec7893a960f4c10ca3b02060b0c38d8c2ce"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "winapi",
+]
+
+[[package]]
+name = "dbus-codegen"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a76dc35ce83e4e9fa089b4fabe66c757b27bd504dc2179c97a01b36d3e874fb0"
+dependencies = [
+ "clap",
+ "dbus",
+ "xml-rs",
+]
+
+[[package]]
+name = "directories"
+version = "4.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "field-offset"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "furtherance"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "dbus",
+ "dbus-codegen",
+ "directories",
+ "gettext-rs",
+ "gtk4",
+ "libadwaita",
+ "once_cell",
+ "rusqlite",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
+
+[[package]]
+name = "futures-task"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
+
+[[package]]
+name = "futures-util"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73aa2f5de1b45710da90a55863276667dc3a3264aaf6a2aeace62bb015244d49"
+dependencies = [
+ "bitflags",
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk4"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9df40006277ff44538fe758400fc671146f6f2665978b6b57d2408db3c2becf"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk4-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk4-sys"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48a39e34abe35ee2cf54a1e29dd983accecd113ad30bdead5050418fa92f2a1b"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gettext-rs"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364"
+dependencies = [
+ "gettext-sys",
+ "locale_config",
+]
+
+[[package]]
+name = "gettext-sys"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afa9e06ab9e7514cc9ae668ea3b71ea1536259d767dff0289ac23ad134f99929"
+dependencies = [
+ "cc",
+ "temp-dir",
+]
+
+[[package]]
+name = "gio"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59105fa464928adf56b159c8d980cc11fbfbe414befb904caac5163d383049bf"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f0bc4cfc9ebcdd05cc5057bc51b99c32f8f9bf246274f6a556ffd27279f8fe3"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41dcfbdb6cc6c02aee163339465d8a40d6f3f64c3a43f729a4195f0e153338b7"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1"
+dependencies = [
+ "anyhow",
+ "heck",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa1d4e1a63d8574541e5b92931e4e669ddc87ffa85d58e84e631dba13ad2e10c"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "graphene-rs"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c54f9fbbeefdb62c99f892dfca35f83991e2cb5b46a8dc2a715e58612f85570"
+dependencies = [
+ "glib",
+ "graphene-sys",
+ "libc",
+]
+
+[[package]]
+name = "graphene-sys"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03f311acb023cf7af5537f35de028e03706136eead7f25a31e8fd26f5011e0b3"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gsk4"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf63d454e2f75abd92ee6de0ac9fc5aaf1018cd9c458aaf9de296c5cbab6bb9"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "gdk4",
+ "glib",
+ "graphene-rs",
+ "gsk4-sys",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gsk4-sys"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31d21d7ce02ba261bb24c50c4ab238a10b41a2c97c32afffae29471b7cca69b"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk4-sys",
+ "glib-sys",
+ "gobject-sys",
+ "graphene-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk4"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e841556e3fe55d8a43ada76b7b08a5f65570bbdfe3b8f72c333053b8832c626"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk-pixbuf",
+ "gdk4",
+ "gio",
+ "glib",
+ "graphene-rs",
+ "gsk4",
+ "gtk4-macros",
+ "gtk4-sys",
+ "libc",
+ "once_cell",
+ "pango",
+]
+
+[[package]]
+name = "gtk4-macros"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "573db42bb64973a4d5f718b73caa7204285a1a665308a23b11723d0ee56ec305"
+dependencies = [
+ "anyhow",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "gtk4-sys"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c47c075e8f795c38f6e9a47b51a73eab77b325f83c0154979ed4d4245c36490d"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk4-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "graphene-sys",
+ "gsk4-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libadwaita"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4b1d54d907dfa5d6663fdf4bdbe46c34747258b85c787adbf66187ccbaac81"
+dependencies = [
+ "gdk-pixbuf",
+ "gdk4",
+ "gio",
+ "glib",
+ "gtk4",
+ "libadwaita-sys",
+ "libc",
+ "once_cell",
+ "pango",
+]
+
+[[package]]
+name = "libadwaita-sys"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f18b6ac4cadd252a89f5cba0a5a4e99836131795d6fad37b859ac79e8cb7d2c8"
+dependencies = [
+ "gdk4-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk4-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58"
+dependencies = [
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "locale_config"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934"
+dependencies = [
+ "lazy_static",
+ "objc",
+ "objc-foundation",
+ "regex",
+ "winapi",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "objc-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
+dependencies = [
+ "block",
+ "objc",
+ "objc_id",
+]
+
+[[package]]
+name = "objc_id"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
+dependencies = [
+ "objc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
+
+[[package]]
+name = "pango"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79211eff430c29cc38c69e0ab54bc78fa1568121ca9737707eee7f92a8417a94"
+dependencies = [
+ "bitflags",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "pest"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dada8c9981fcf32929c3c0f0cd796a9284aca335565227ed88c83babb1d43dc"
+dependencies = [
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "rusqlite"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7"
+dependencies = [
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "memchr",
+ "smallvec",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+
+[[package]]
+name = "slab"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "syn"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "temp-dir"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab"
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version-compare"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "xml-rs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..0283d90
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "furtherance"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+gettext-rs = { version = "0.7", features = ["gettext-system"] }
+rusqlite = "0.26.3"
+chrono = "0.4"
+directories = "4.0"
+once_cell = "1.9.0"
+dbus = "0.9.5"
+dbus-codegen = "0.10.0"
+
+[dependencies.gtk]
+package = "gtk4"
+version = "0.4"
+
+[dependencies.adw]
+package = "libadwaita"
+version = "0.1"
+
diff --git a/build-aux/cargo.sh b/build-aux/cargo.sh
new file mode 100644
index 0000000..3c37396
--- /dev/null
+++ b/build-aux/cargo.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+export MESON_BUILD_ROOT="$1"
+export MESON_SOURCE_ROOT="$2"
+export CARGO_TARGET_DIR="$MESON_BUILD_ROOT"/target
+export CARGO_HOME="$MESON_BUILD_ROOT"/cargo-home
+export OUTPUT="$3"
+export BUILDTYPE="$4"
+export APP_BIN="$5"
+
+
+if [ $BUILDTYPE = "release" ]
+then
+    echo "RELEASE MODE"
+    cargo build --manifest-path \
+        "$MESON_SOURCE_ROOT"/Cargo.toml --release && \
+        cp "$CARGO_TARGET_DIR"/release/"$APP_BIN" "$OUTPUT"
+else
+    echo "DEBUG MODE"
+    cargo build --manifest-path \
+        "$MESON_SOURCE_ROOT"/Cargo.toml && \
+        cp "$CARGO_TARGET_DIR"/debug/"$APP_BIN" "$OUTPUT"
+fi
+
diff --git a/build-aux/meson/postinstall.py b/build-aux/meson/postinstall.py
new file mode 100755
index 0000000..6a3ea97
--- /dev/null
+++ b/build-aux/meson/postinstall.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+
+from os import environ, path
+from subprocess import call
+
+prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
+datadir = path.join(prefix, 'share')
+destdir = environ.get('DESTDIR', '')
+
+# Package managers set this so we don't need to run
+if not destdir:
+    print('Updating icon cache...')
+    call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
+
+    print('Updating desktop database...')
+    call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
+
+    print('Compiling GSettings schemas...')
+    call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])
+
+
diff --git a/com.lakoliu.Furtherance.json b/com.lakoliu.Furtherance.json
new file mode 100644
index 0000000..65283ad
--- /dev/null
+++ b/com.lakoliu.Furtherance.json
@@ -0,0 +1,52 @@
+{
+    "app-id" : "com.lakoliu.Furtherance",
+    "runtime" : "org.gnome.Platform",
+    "runtime-version" : "master",
+    "sdk" : "org.gnome.Sdk",
+    "sdk-extensions" : [
+        "org.freedesktop.Sdk.Extension.rust-stable"
+    ],
+    "command" : "furtherance",
+    "finish-args" : [
+        "--share=network",
+        "--share=ipc",
+        "--socket=fallback-x11",
+        "--device=dri",
+        "--socket=wayland",
+        "--socket=session-bus"
+    ],
+    "build-options" : {
+        "append-path" : "/usr/lib/sdk/rust-stable/bin",
+        "build-args" : [
+            "--share=network"
+        ],
+        "env" : {
+            "RUST_BACKTRACE" : "1",
+            "RUST_LOG" : "furtherance=debug"
+        }
+    },
+    "cleanup" : [
+        "/include",
+        "/lib/pkgconfig",
+        "/man",
+        "/share/doc",
+        "/share/gtk-doc",
+        "/share/man",
+        "/share/pkgconfig",
+        "*.la",
+        "*.a"
+    ],
+    "modules" : [
+        {
+            "name" : "furtherance",
+            "builddir" : true,
+            "buildsystem" : "meson",
+            "sources" : [
+                {
+                    "type" : "git",
+                    "url" : "file:///home/ricky/Documents/SoftwareDev/Rust/furtherance"
+                }
+            ]
+        }
+    ]
+}
diff --git a/data/com.lakoliu.Furtherance.appdata.xml.in b/data/com.lakoliu.Furtherance.appdata.xml.in
new file mode 100644
index 0000000..fac43de
--- /dev/null
+++ b/data/com.lakoliu.Furtherance.appdata.xml.in
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop">
+	<id>com.lakoliu.Furtherance.desktop</id>
+	<metadata_license>CC0-1.0</metadata_license>
+	<project_license>GPL-3.0-or-later</project_license>
+	<description>
+	  <p>Simple yet powerful time tracking app.</p>
+	</description>
+</component>
diff --git a/data/com.lakoliu.Furtherance.desktop.in b/data/com.lakoliu.Furtherance.desktop.in
new file mode 100644
index 0000000..ee5cde6
--- /dev/null
+++ b/data/com.lakoliu.Furtherance.desktop.in
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Name=Furtherance
+GenericName=Time Tracker
+Comment=Track your time without being tracked
+Exec=furtherance
+Icon=com.lakoliu.Furtherance
+Terminal=false
+Type=Application
+Categories=GTK;Utility;
+StartupNotify=true
+Keywords=timer;tracker;clock;tasks;productivity
diff --git a/data/com.lakoliu.Furtherance.gschema.xml b/data/com.lakoliu.Furtherance.gschema.xml
new file mode 100644
index 0000000..cf2db5d
--- /dev/null
+++ b/data/com.lakoliu.Furtherance.gschema.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist gettext-domain="furtherance">
+	<schema id="com.lakoliu.Furtherance" path="/com/lakoliu/Furtherance/">
+	</schema>
+</schemalist>
diff --git a/data/icons/hicolor/scalable/apps/com.lakoliu.Furtherance.svg b/data/icons/hicolor/scalable/apps/com.lakoliu.Furtherance.svg
new file mode 100644
index 0000000..f0eca7c
--- /dev/null
+++ b/data/icons/hicolor/scalable/apps/com.lakoliu.Furtherance.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="161mm"
+   height="161mm"
+   viewBox="0 0 161 161"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+   sodipodi:docname="furtherance_logo_square.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     inkscape:zoom="0.65477694"
+     inkscape:cx="298.57496"
+     inkscape:cy="310.02924"
+     inkscape:window-width="1920"
+     inkscape:window-height="1131"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs2" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-43.512234,-66.387247)">
+    <path
+       id="text1508"
+       style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:105.833px;line-height:1.25;font-family:Dosis;-inkscape-font-specification:'Dosis Ultra-Bold';fill:#ffd42a;fill-opacity:1;stroke:none;stroke-width:0.566104"
+       d="M 129.43493,67.567547 A 80.04071,80.04071 0 0 0 61.234416,131.11444 l 58.978804,93.48427 a 80.04071,80.04071 0 0 0 29.62945,1.74318 80.04071,80.04071 0 0 0 24.48666,-7.30544 c 0.10719,-0.25431 0.19856,-0.5101 0.25695,-0.76765 0.42413,-1.87402 -0.12902,-4.02385 -1.65951,-6.44968 l -23.80256,-37.72776 33.32376,-21.02368 c 2.42582,-1.53045 3.82701,-3.12854 4.20403,-4.79433 0.42413,-1.87402 -0.12902,-4.02383 -1.6595,-6.44968 l -14.49842,-22.98132 c -1.53042,-2.42585 -3.1927,-3.78704 -4.98616,-4.08351 -1.74635,-0.50469 -3.83213,0.006 -6.25797,1.53787 l -33.32376,21.02368 -12.56515,-19.91783 48.8354,-30.809699 c 2.42585,-1.530419 3.82813,-3.128664 4.20515,-4.794456 0.42413,-1.874019 -0.12902,-4.023834 -1.6595,-6.449679 l -3.43132,-5.438243 a 80.04071,80.04071 0 0 0 -31.87584,-2.342936 z m 10.2176,6.206941 1.20815,1.914842 -59.177825,37.33497 34.554845,54.77287 43.66523,-27.54774 1.20818,1.91485 -43.66524,27.54773 28.27283,44.81387 -1.91484,1.20817 -65.244149,-103.41753 z"
+       inkscape:export-filename="/home/ricky/Pictures/Furtherance/Furtherance-logo-256.png"
+       inkscape:export-xdpi="40.620335"
+       inkscape:export-ydpi="40.620335" />
+  </g>
+</svg>
diff --git a/data/icons/hicolor/symbolic/apps/com.lakoliu.Furtherance-symbolic.svg b/data/icons/hicolor/symbolic/apps/com.lakoliu.Furtherance-symbolic.svg
new file mode 100644
index 0000000..4b00158
--- /dev/null
+++ b/data/icons/hicolor/symbolic/apps/com.lakoliu.Furtherance-symbolic.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="161mm"
+   height="161mm"
+   viewBox="0 0 161 161"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   sodipodi:docname="com.lakoliu.Furtherance.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     inkscape:zoom="0.65477694"
+     inkscape:cx="298.57496"
+     inkscape:cy="310.02924"
+     inkscape:window-width="1920"
+     inkscape:window-height="1131"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs2" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-43.512234,-66.387247)">
+    <path
+       id="text1508"
+       style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:105.833px;line-height:1.25;font-family:Dosis;-inkscape-font-specification:'Dosis Ultra-Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.566104"
+       d="M 129.43493,67.567547 A 80.04071,80.04071 0 0 0 61.234416,131.11444 l 58.978804,93.48427 a 80.04071,80.04071 0 0 0 29.62945,1.74318 80.04071,80.04071 0 0 0 24.48666,-7.30544 c 0.10719,-0.25431 0.19856,-0.5101 0.25695,-0.76765 0.42413,-1.87402 -0.12902,-4.02385 -1.65951,-6.44968 l -23.80256,-37.72776 33.32376,-21.02368 c 2.42582,-1.53045 3.82701,-3.12854 4.20403,-4.79433 0.42413,-1.87402 -0.12902,-4.02383 -1.6595,-6.44968 l -14.49842,-22.98132 c -1.53042,-2.42585 -3.1927,-3.78704 -4.98616,-4.08351 -1.74635,-0.50469 -3.83213,0.006 -6.25797,1.53787 l -33.32376,21.02368 -12.56515,-19.91783 48.8354,-30.809699 c 2.42585,-1.530419 3.82813,-3.128664 4.20515,-4.794456 0.42413,-1.874019 -0.12902,-4.023834 -1.6595,-6.449679 l -3.43132,-5.438243 a 80.04071,80.04071 0 0 0 -31.87584,-2.342936 z m 10.2176,6.206941 1.20815,1.914842 -59.177825,37.33497 34.554845,54.77287 43.66523,-27.54774 1.20818,1.91485 -43.66524,27.54773 28.27283,44.81387 -1.91484,1.20817 -65.244149,-103.41753 z"
+       inkscape:export-filename="/home/ricky/Pictures/Furtherance/Furtherance-logo-256.png"
+       inkscape:export-xdpi="40.620335"
+       inkscape:export-ydpi="40.620335" />
+  </g>
+</svg>
diff --git a/data/icons/meson.build b/data/icons/meson.build
new file mode 100644
index 0000000..7ffa296
--- /dev/null
+++ b/data/icons/meson.build
@@ -0,0 +1,13 @@
+application_id = 'com.lakoliu.Furtherance'
+
+scalable_dir = join_paths('hicolor', 'scalable', 'apps')
+install_data(
+  join_paths(scalable_dir, ('@0@.svg').format(application_id)),
+  install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir)
+)
+
+symbolic_dir = join_paths('hicolor', 'symbolic', 'apps')
+install_data(
+  join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)),
+  install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir)
+)
diff --git a/data/meson.build b/data/meson.build
new file mode 100644
index 0000000..63e3267
--- /dev/null
+++ b/data/meson.build
@@ -0,0 +1,43 @@
+desktop_file = i18n.merge_file(
+  input: 'com.lakoliu.Furtherance.desktop.in',
+  output: 'com.lakoliu.Furtherance.desktop',
+  type: 'desktop',
+  po_dir: '../po',
+  install: true,
+  install_dir: join_paths(get_option('datadir'), 'applications')
+)
+
+desktop_utils = find_program('desktop-file-validate', required: false)
+if desktop_utils.found()
+  test('Validate desktop file', desktop_utils,
+    args: [desktop_file]
+  )
+endif
+
+appstream_file = i18n.merge_file(
+  input: 'com.lakoliu.Furtherance.appdata.xml.in',
+  output: 'com.lakoliu.Furtherance.appdata.xml',
+  po_dir: '../po',
+  install: true,
+  install_dir: join_paths(get_option('datadir'), 'appdata')
+)
+
+appstream_util = find_program('appstream-util', required: false)
+if appstream_util.found()
+  test('Validate appstream file', appstream_util,
+    args: ['validate', appstream_file]
+  )
+endif
+
+install_data('com.lakoliu.Furtherance.gschema.xml',
+  install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
+)
+
+compile_schemas = find_program('glib-compile-schemas', required: false)
+if compile_schemas.found()
+  test('Validate schema file', compile_schemas,
+    args: ['--strict', '--dry-run', meson.current_source_dir()]
+  )
+endif
+
+subdir('icons')
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..3d0b4df
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,32 @@
+project('furtherance', 'rust',
+          version: '1.0.0',
+    meson_version: '>= 0.50.0',
+  default_options: [ 'warning_level=2',
+                   ],
+)
+
+# Dependencies
+dependency('sqlite3', version: '>= 3.20')
+dependency('dbus-1')
+
+dependency('glib-2.0', version: '>= 2.66')
+dependency('gio-2.0', version: '>= 2.66')
+dependency('gtk4', version: '>= 4.0.0')
+dependency('libadwaita-1', version: '>=1.0.0')
+
+name       = 'Furtherance'
+app_id     = 'com.lakoliu.Furtherance'
+
+i18n = import('i18n')
+
+
+cargo_sources = files(
+  'Cargo.toml',
+  'Cargo.lock',
+)
+
+subdir('data')
+subdir('src')
+subdir('po')
+
+meson.add_install_script('build-aux/meson/postinstall.py')
diff --git a/po/LINGUAS b/po/LINGUAS
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/po/LINGUAS
diff --git a/po/POTFILES b/po/POTFILES
new file mode 100644
index 0000000..1dc8b86
--- /dev/null
+++ b/po/POTFILES
@@ -0,0 +1,4 @@
+data/com.lakoliu.Furtherance.desktop.in
+data/com.lakoliu.Furtherance.appdata.xml.in
+data/com.lakoliu.Furtherance.gschema.xml
+src/window.ui
diff --git a/po/meson.build b/po/meson.build
new file mode 100644
index 0000000..9c83879
--- /dev/null
+++ b/po/meson.build
@@ -0,0 +1 @@
+i18n.gettext('furtherance', preset: 'glib')
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()
+    }
+}