1
0
Fork 0

Merge ../kassandra

This commit is contained in:
Malte 2023-01-16 00:33:37 +01:00
commit 91949d9ca6
78 changed files with 13665 additions and 0 deletions

View file

@ -0,0 +1,2 @@
# DO NOT HAND-EDIT THIS FILE
import (import ./thunk.nix)

View file

@ -0,0 +1,8 @@
{
"owner": "obsidiansystems",
"repo": "obelisk",
"branch": "release/0.9.0.1",
"private": false,
"rev": "11beb6e8cd2419b2429925b76a98f24035e40985",
"sha256": "0b4m33b7yyzsbkvfz2kwg4v9hlnvbjlmjikbvwd7pg52vy84and0"
}

9
.obelisk/impl/thunk.nix Normal file
View file

@ -0,0 +1,9 @@
# DO NOT HAND-EDIT THIS FILE
let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
if !fetchSubmodules && !private then builtins.fetchTarball {
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
} else (import <nixpkgs> {}).fetchFromGitHub {
inherit owner repo rev sha256 fetchSubmodules private;
};
json = builtins.fromJSON (builtins.readFile ./github.json);
in fetch json

14
apps/kassandra/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
.envrc
.direnv
.ghc.environment.x86_64-linux-8.6.5
dist-newstyle
dist
result
result-*
result-android
result-ios
result-exe
.attr-cache
ghcid-output.txt
profile
hie-output

3086
apps/kassandra/.hlint.yaml Normal file

File diff suppressed because it is too large Load diff

37
apps/kassandra/README.md Normal file
View file

@ -0,0 +1,37 @@
# kassandra
*Kassandra from Greek mythology tells the Trojan _warriors_ in the Illiad not to take in the horse offer by the Greek. They dont listen to her.*
This is a taskwarrior frontend build with Haskell and reflex-frp.
**WARNING: This will eat all of your tasks! This app is underdocumented and not intended for use by anyone else, yet.**
## State
* This project is primarily for my personal use. I share it in the spirit of free software but it is not primarily intendend to be usable for anyone else, at least at the moment.
* This app provides a GUI to view and edit tasks.
* You can compile the standalone:kassandra2 target with ghc to get a webkit-gtk-app or you can compile with obelisk to get a webserver. (Which needs some authorization right now.)
* Right now there are only a few custom lists to see tasks. The most useful one under the button "List" are lists based on any tag.
* It is supposed to be completely reactive, so everything it shows should be up-to-date. It relies right now on two netcat hooks to get updated from taskwarrior:
```shell
cat ~/.task/hooks/on-add.kassandra-notification
#!/bin/bash
tee > (nc 127.0.0.1 6545)
cat ~/.task/hooks/on-modify.kassandra-notification
#!/bin/bash
tail -n 1 | tee >(nc 127.0.0.1 6545)
```
(In the future these hooks might be generated by the program.)
* Right now the app shows tasks in a tree. Tasks tagged with +root are roots. Tasks with partof:<uuid-of-parent> are children. If you dont use this feature you can still see a plain list of tasks and edit them.
* You can sort tasks by dragging and dropping them either in a taglist or as children of a common parent. The sortposition is saved in custom uda attributes.
## Plans
* The default UI should use some reasonable search dialog which should more or less fit normal taskwarrior use.
* Some calendar integration to sort tasks by time.
* UI improvements und documentation. The current UI is not self explanatory and has some ugly quirks.
* Custom UDA based features should be deconfigurable.
* There is supposed to be a solid auth system so you can use any client: Android/webkit/web to connect to any server.

View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
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 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 work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
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 AGPL, see
<https://www.gnu.org/licenses/>.

View file

@ -0,0 +1,106 @@
cabal-version: 2.4
name: backend
version: 0.1
-- BEGIN dhall generated common configuration
license-file: LICENSE
author: Malte Brandy
maintainer: malte.brandy@maralorn.de
build-type: Simple
extra-source-files: CHANGELOG.md
common common-config
default-extensions:
AllowAmbiguousTypes
ConstraintKinds
DataKinds
DeriveAnyClass
DeriveGeneric
DuplicateRecordFields
EmptyCase
FlexibleContexts
FlexibleInstances
GADTs
LambdaCase
MultiParamTypeClasses
NamedFieldPuns
OverloadedLabels
OverloadedStrings
PartialTypeSignatures
PatternGuards
PatternSynonyms
QuasiQuotes
RankNTypes
RecursiveDo
ScopedTypeVariables
StandaloneDeriving
StrictData
TemplateHaskell
TupleSections
TypeApplications
TypeFamilies
UndecidableInstances
ViewPatterns
ghc-options:
-Weverything -Wno-unsafe -Wno-safe -Wno-all-missed-specialisations
-Wno-implicit-prelude -Wno-missed-specialisations
-Wno-monomorphism-restriction -Wno-partial-fields
-Wno-missing-import-lists -Wno-orphans -Wcompat
mixins: base hiding (Prelude)
default-language: Haskell2010
-- END dhall generated common configuration
library
import: common-config
hs-source-dirs: src
if impl(ghcjs -any)
buildable: False
build-depends:
, aeson
, async
, base
, containers
, dhall
, data-default-class
, frontend
, say
, kassandra
, network-simple
, obelisk-backend
, obelisk-route
, password >=2.0.1.0
, relude
, snap-core
, standalone
, stm
, taskwarrior
, uuid
, websockets
, websockets-snap
exposed-modules:
Backend
Backend.Config
executable backend
import: common-config
main-is: main.hs
hs-source-dirs: src-bin
if impl(ghcjs -any)
buildable: False
build-depends:
, backend
, base
, frontend
, kassandra
, obelisk-backend
ghc-options: -threaded

View file

@ -0,0 +1 @@
../frontend-js/bin/frontend.jsexe

View file

@ -0,0 +1 @@
../../frontend-js/bin/frontend.jsexe

View file

@ -0,0 +1,2 @@
cradle:
none:

View file

@ -0,0 +1,8 @@
module Main (main) where
import Backend
import Frontend
import Obelisk.Backend
main :: IO ()
main = runBackend backend frontend

View file

@ -0,0 +1,87 @@
module Backend (
backend,
) where
import Backend.Config (BackendConfig, readConfig, users)
import Control.Concurrent.STM.TQueue (TQueue, newTQueueIO, writeTQueue)
import Control.Exception (try)
import qualified Data.Aeson as Aeson
import Data.Map (lookup)
import Data.Password.Argon2
import Frontend.Route (BackendRoute (..), FrontendRoute, fullRouteEncoder)
import Kassandra.Config (AccountConfig (..))
import Kassandra.LocalBackend (LocalBackendRequest (..))
import Kassandra.Standalone.State (localBackendProvider)
import Network.WebSockets (ConnectionException, ServerApp, acceptRequest, forkPingThread, receiveData, rejectRequest, sendTextData)
import Network.WebSockets.Snap (runWebSocketsSnap)
import Obelisk.Backend (Backend (..))
import Obelisk.Route (R, pattern (:/))
import Say (say)
import Snap.Core (MonadSnap, Snap)
backend :: Backend BackendRoute FrontendRoute
backend =
Backend
{ _backend_run = serveSnaplet
, _backend_routeEncoder = fullRouteEncoder
}
serveSnaplet :: ((R BackendRoute -> Snap ()) -> IO b) -> IO ()
serveSnaplet serve = do
config <- readConfig Nothing
backendRequestQueue <- newTQueueIO
let backendSnaplet :: MonadSnap m => R BackendRoute -> m ()
backendSnaplet = \case
BackendRouteSocket :/ (_, params) -> serveWebsocket config backendRequestQueue params
BackendRouteMissing :/ () -> pass
concurrently_
(localBackendProvider backendRequestQueue)
(serve backendSnaplet)
serveWebsocket ::
MonadSnap m =>
BackendConfig ->
TQueue LocalBackendRequest ->
Map Text (Maybe Text) ->
m ()
serveWebsocket config backendRequestQueue params =
let mayUsername = join (params ^. at "username")
mayPassword = join (params ^. at "password")
mayCreds = liftA2 (,) mayUsername mayPassword
action (Just (username, password))
| Just userConfig <- lookup username (users config)
, PasswordCheckSuccess <- checkPassword (mkPassword password) (passwordHash userConfig) =
acceptSocket backendRequestQueue username userConfig
action (Just (username, _))
| Just _ <- lookup username (users config) =
\connection -> do
say [i|Rejecting Websocket request for #{username :: Text}, wrong password.|]
rejectRequest connection "No valid 'username' and 'password' provided."
action _ = \connection -> do
say [i|Rejecting Websocket request #{show mayUsername :: Text}. No matching user found.|]
rejectRequest connection "No valid 'username' and 'password' provided."
in runWebSocketsSnap . action $ mayCreds
acceptSocket :: TQueue LocalBackendRequest -> Text -> AccountConfig -> ServerApp
acceptSocket backendRequestQueue username accountConfig pendingConnection = do
say [i|Websocket Client by user #{username} connected!|]
connection <- acceptRequest pendingConnection
forkPingThread connection 30
let responseCallback = sendTextData connection . Aeson.encode
alive <- newTVarIO True
requestQueue <- newTQueueIO
atomically . writeTQueue backendRequestQueue $
LocalBackendRequest
{ userConfig = accountConfig ^. #userConfig
, alive
, responseCallback
, requestQueue
}
let go =
(try (receiveData connection) :: IO (Either ConnectionException LByteString)) >>= \case
Left err -> do
say [i|Socket for #{username} closed. With error #{err}|]
atomically (writeTVar alive False)
Right (Aeson.decode -> msg) ->
concurrently_ go . whenJust msg $ atomically . writeTQueue requestQueue
go

View file

@ -0,0 +1,31 @@
module Backend.Config (
readConfig,
BackendConfig (..),
) where
import Dhall (FromDhall)
import Kassandra.Config (
AccountConfig,
Dict,
)
import Kassandra.Config.Dhall (
DhallLoadConfig (
DhallLoadConfig
),
defaultConfig,
defaultFile,
envName,
loadDhallConfig,
)
import Kassandra.Standalone.Config (
BackendConfig (..),
)
readConfig :: Maybe Text -> IO BackendConfig
readConfig =
loadDhallConfig
DhallLoadConfig
{ envName = "KASSANDRA_BACKEND_CONFIG"
, defaultFile = "~/.config/kassandra/backend.dhall"
, defaultConfig = "{ = }"
}

View file

@ -0,0 +1 @@
../static

View file

@ -0,0 +1,3 @@
packages:
kassandra/
standalone/

View file

@ -0,0 +1,2 @@
package *
ghc-options: -fwrite-ide-info

1
apps/kassandra/code.nix Normal file
View file

@ -0,0 +1 @@
"16"

View file

@ -0,0 +1,71 @@
let Prelude = https://prelude.dhall-lang.org/v16.0.0/package.dhall
let extensions =
[ "AllowAmbiguousTypes"
, "BlockArguments"
, "ConstraintKinds"
, "DataKinds"
, "DeriveAnyClass"
, "DeriveGeneric"
, "DerivingStrategies"
, "DuplicateRecordFields"
, "EmptyCase"
, "FlexibleContexts"
, "FlexibleInstances"
, "GADTs"
, "LambdaCase"
, "MultiParamTypeClasses"
, "NamedFieldPuns"
, "OverloadedLabels"
, "OverloadedStrings"
, "PartialTypeSignatures"
, "PatternGuards"
, "PatternSynonyms"
, "QuasiQuotes"
, "RankNTypes"
, "RecursiveDo"
, "ScopedTypeVariables"
, "StandaloneDeriving"
, "StrictData"
, "TemplateHaskell"
, "TupleSections"
, "TypeApplications"
, "TypeFamilies"
, "UndecidableInstances"
, "ViewPatterns"
]
let ghc-options =
[ "-Wall"
, "-Wcompat"
, "-Wno-orphans"
, "-Wincomplete-uni-patterns"
, "-Wincomplete-record-updates"
, "-Wmissing-export-lists"
, "-Widentities"
, "-Wredundant-constraints"
, "-Wmissing-home-modules"
]
let multiLineList =
Prelude.Text.concatMapSep "\n" Text (λ(x : Text) → " ${x}")
in ''
-- BEGIN dhall generated common configuration
-- generate with: dhall text --file common-config.dhall
license-file: LICENSE
author: Malte Brandy
maintainer: malte.brandy@maralorn.de
build-type: Simple
extra-source-files: CHANGELOG.md
common common-config
default-extensions:
${multiLineList extensions}
ghc-options:
${multiLineList ghc-options}
mixins: base hiding (Prelude)
default-language: Haskell2010
-- END dhall generated common configuration
''

View file

@ -0,0 +1 @@
This string comes from config/common/example

View file

@ -0,0 +1 @@
http://localhost:8000

View file

@ -0,0 +1,9 @@
### Config
Obelisk projects should contain a config folder with the following subfolders: common, frontend, and backend.
Things that should never be transmitted to the frontend belong in backend/ (e.g., email credentials)
Frontend-only configuration belongs in frontend/.
Shared configuration files (e.g., the route config) belong in common/

147
apps/kassandra/default.nix Normal file
View file

@ -0,0 +1,147 @@
{ obelisk ? import ./.obelisk/impl {
system = builtins.currentSystem;
config.android_sdk.accept_license = true;
}
}:
with obelisk;
project ./. (
{ pkgs, ... }:
let
inherit (pkgs.haskell.lib)
markUnbroken dontCheck addBuildDepend doJailbreak
overrideCabal
;
in
{
android = {
applicationId = "de.maralorn.kassandra";
displayName = "Kassandra";
releaseKey = null;
isRelease = true;
version = {
code = import ./code.nix;
name = "0.1.0";
};
};
overrides = self: super: {
kassandra = overrideCabal super.kassandra { doHaddock = false; };
backend = addBuildDepend super.backend pkgs.taskwarrior;
clay = markUnbroken (dontCheck super.clay);
haskeline = dontCheck (self.callHackage "haskeline" "0.8.0.1" { });
repline = doJailbreak (self.callHackage "repline" "0.4.0.0" { });
dhall = dontCheck (self.callHackage "dhall" "1.35.0" { });
relude = dontCheck super.relude;
stm-containers = markUnbroken super.stm-containers;
stm-hamt = markUnbroken (doJailbreak super.stm-hamt);
streamly-bytestring = self.callHackageDirect
{
pkg = "streamly-bytestring";
ver = "0.1.2";
sha256 = "08xhp8zgf5n1j4v1br1dz9ih8j05vk92swp3nz9in5xajllkc7qv";
}
{ };
streamly = self.callHackageDirect
{
pkg = "streamly";
ver = "0.7.0";
sha256 = "0hr2cz14w6nnbvhnq1fvr8v4rzyqcj3b9khf2rszyji00fmp27l1";
}
{ };
nonempty-vector = self.callHackageDirect
{
pkg = "nonempty-vector";
ver = "0.1.0.0";
sha256 = "06abdmdy9z0w6ishiibir3qfjpqxmb4mrkhgyc4j58hd14s8rj0x";
}
{ };
nonempty-containers = self.callHackageDirect
{
pkg = "nonempty-containers";
ver = "0.3.4.1";
sha256 = "0nbnr0az201lv09dwcxcppkfc9b05kyw4la990z5asn9737pvpr2";
}
{ };
iCalendar = overrideCabal (doJailbreak (markUnbroken super.iCalendar)) {
preConfigure = ''substituteInPlace iCalendar.cabal --replace "network >=2.6 && <2.7" "network -any"'';
};
prettyprinter = self.callHackageDirect
{
pkg = "prettyprinter";
ver = "1.5.1";
sha256 = "0wx01rvgwnnmg10sh9x2whif5z12058w5djh7m5swz94wvkg5cg3";
}
{ };
cborg-json = self.callHackageDirect
{
pkg = "cborg-json";
ver = "0.2.2.0";
sha256 = "1s7pv3jz8s1qb0ydcc5nra9f63jp4ay4d0vncv919bakf8snj4vw";
}
{ };
generic-random = self.callHackageDirect
{
pkg = "generic-random";
ver = "1.3.0.0";
sha256 = "0m7lb40wgmyszv8l6qmarkfgs8r0idgl9agwsi72236hpvp353ad";
}
{ };
atomic-write = self.callHackageDirect
{
pkg = "atomic-write";
ver = "0.2.0.7";
sha256 = "1r9ckwljdbw3mi8rmzmsnh89z8nhw2qnds9n271gkjgavb6hxxf3";
}
{ };
taskwarrior = self.callHackageDirect
{
pkg = "taskwarrior";
ver = "0.5.0.0";
sha256 = "sha256-elDUtz0NSG4WHxkyCQ1CunYXWIVRj6EqkKSchPy+c3E=";
}
{ };
base64 = self.callHackageDirect
{
pkg = "base64";
ver = "0.4.1";
sha256 = "1pz9s8bmnkrrr3v5mhkwv8vaf251vmxs87zzc5nsjsa027j9lr22";
}
{ };
password = self.callHackageDirect
{
pkg = "password";
ver = "2.0.1.0";
sha256 = "1q99v7w6bdfpnw245aa3zaj3x7mhl9i2y7f2rzlc30g066p9jhaz";
}
{ };
indexed-profunctors = self.callHackageDirect
{
pkg = "indexed-profunctors";
ver = "0.1";
sha256 = "0vpgbymfhnvip90jwvyniqi34lhz5n3ni1f21g81n5rap0q140za";
}
{ };
generic-lens-core = self.callHackageDirect
{
pkg = "generic-lens-core";
ver = "2.0.0.0";
sha256 = "07parw0frqxxkjbbas9m9xb3pmpqrx9wz63m35wa6xqng9vlcscm";
}
{ };
generic-optics = self.callHackageDirect
{
pkg = "generic-optics";
ver = "2.0.0.0";
sha256 = "0xy5k5b35w1i1zxy0dv5fk1b3zrd3hx3v5kh593k2la7ri880wmq";
}
{ };
optics-core = self.callHackage "optics-core" "0.3.0.1" { };
optics-th = self.callHackage "optics-th" "0.3.0.2" { };
optics-extra = self.callHackage "optics-extra" "0.3" { };
optics = self.callHackage "optics" "0.3" { };
};
packages = {
kassandra = ./kassandra;
standalone = ./standalone;
};
}
)

View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
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 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 work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
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 AGPL, see
<https://www.gnu.org/licenses/>.

View file

@ -0,0 +1,104 @@
cabal-version: 2.4
name: frontend
version: 0.1
-- BEGIN dhall generated common configuration
license-file: LICENSE
author: Malte Brandy
maintainer: malte.brandy@maralorn.de
build-type: Simple
extra-source-files: CHANGELOG.md
common common-config
default-extensions:
AllowAmbiguousTypes
ConstraintKinds
DataKinds
DeriveAnyClass
DeriveGeneric
DuplicateRecordFields
EmptyCase
FlexibleContexts
FlexibleInstances
GADTs
LambdaCase
MultiParamTypeClasses
NamedFieldPuns
OverloadedLabels
OverloadedStrings
PartialTypeSignatures
PatternGuards
QuasiQuotes
RankNTypes
RecursiveDo
ScopedTypeVariables
StandaloneDeriving
StrictData
TemplateHaskell
TupleSections
TypeApplications
TypeFamilies
UndecidableInstances
ViewPatterns
ghc-options:
-Weverything -Wno-unsafe -Wno-safe -Wno-all-missed-specialisations
-Wno-implicit-prelude -Wno-missed-specialisations
-Wno-monomorphism-restriction -Wno-partial-fields
-Wno-missing-import-lists -Wno-orphans -Wcompat
mixins: base hiding (Prelude)
default-language: Haskell2010
-- END dhall generated common configuration
library
import: common-config
hs-source-dirs: src
build-depends:
, aeson
, base
, dependent-map
, dependent-sum-template
, extra
, generic-optics
, jsaddle
, kassandra
, mtl
, obelisk-executable-config-lookup
, obelisk-frontend
, obelisk-generated-static
, obelisk-route
, optics
, optics-th
, reflex
, reflex-dom
, scientific
, taskwarrior
, text
, these
, time
, unordered-containers
, uuid
, relude
exposed-modules:
Frontend
Frontend.Route
executable frontend
import: common-config
main-is: main.hs
hs-source-dirs: src-bin
build-depends:
, base
, frontend
, kassandra
, obelisk-frontend
, obelisk-generated-static
, obelisk-route
, reflex-dom
ghc-options: -threaded
mixins: base hiding (Prelude)

View file

@ -0,0 +1,2 @@
cradle:
none:

View file

@ -0,0 +1,12 @@
module Main (main) where
import Frontend
import Frontend.Route
import Obelisk.Frontend
import Obelisk.Route.Frontend
import Reflex.Dom
main :: IO ()
main = do
let Right validFullEncoder = checkEncoder fullRouteEncoder
run $ runFrontend validFullEncoder frontend

View file

@ -0,0 +1,39 @@
module Frontend (
frontend,
) where
import Obelisk.Frontend
import Obelisk.Generated.Static
import Obelisk.Route
import Frontend.Route (FrontendRoute)
import Kassandra.Css (cssAsText)
import Kassandra.MainWidget (mainWidget)
import Kassandra.RemoteBackendWidget (
CloseEvent (..),
remoteBackendWidget,
)
import Kassandra.Types
import qualified Reflex.Dom as D
import Relude.Extra.Newtype
-- This runs in a monad that can be run on the client or the server.
-- To run code in a pure client or pure server context, use one of the
-- `prerender` functions.
frontend :: Frontend (R FrontendRoute)
frontend =
Frontend
{ _frontend_head = frontendHead
, _frontend_body = void $ D.prerender pass frontendBody
}
frontendBody :: WidgetJSM t m => m ()
frontendBody = D.dyn_ . fmap (maybe pass mainWidget)
=<< remoteBackendWidget (wrap D.never) Nothing
css = cssAsText (static @"MaterialIcons-Regular-Outlined.otf")
frontendHead :: ObeliskWidget js t route m => m ()
frontendHead = do
D.el "title" $ D.text "Kassandra 2 Webversion"
D.elAttr "style" mempty . D.text $ css

View file

@ -0,0 +1,55 @@
module Frontend.Route (
BackendRoute (..),
FrontendRoute (..),
fullRouteEncoder,
) where
{- -- You will probably want these imports for composing Encoders.
import Prelude hiding (id, (.))
import Control.Category
-}
import qualified Control.Category
import Data.Text (Text)
--import Data.Functor.Identity
import Obelisk.Route
import Obelisk.Route.TH
data BackendRoute :: Type -> Type where
-- | Used to handle unparseable routes.
BackendRouteMissing :: BackendRoute ()
BackendRouteSocket :: BackendRoute PageName
-- You can define any routes that will be handled specially by the backend here.
-- i.e. These do not serve the frontend, but do something different, such as serving static files.
data FrontendRoute :: Type -> Type where
FrontendRouteMain :: FrontendRoute ()
-- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
fullRouteEncoder ::
Encoder
(Either Text)
Identity
(R (FullRoute BackendRoute FrontendRoute))
PageName
fullRouteEncoder =
mkFullRouteEncoder
(FullRoute_Backend BackendRouteMissing :/ ())
( \case
BackendRouteMissing -> PathSegment "missing" $ unitEncoder mempty
BackendRouteSocket -> PathSegment "socket" $ Control.Category.id
)
( \case
FrontendRouteMain -> PathEnd $ unitEncoder mempty
)
concat
<$> mapM
deriveRouteComponent
[ ''BackendRoute
, ''FrontendRoute
]

2
apps/kassandra/hie.yaml Normal file
View file

@ -0,0 +1,2 @@
cradle:
cabal:

View file

@ -0,0 +1,5 @@
# Revision history for kassandra
## 0.1.0.0 -- YYYY-mm-dd
* First version. Released on an unsuspecting world.

View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
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 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 work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
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 AGPL, see
<https://www.gnu.org/licenses/>.

View file

@ -0,0 +1,3 @@
import Distribution.Simple
main = defaultMain

View file

@ -0,0 +1,124 @@
cabal-version: 2.4
name: kassandra
version: 0.1.0.0
-- BEGIN dhall generated common configuration
-- generate with: dhall text --file common-config.dhall
license-file: LICENSE
author: Malte Brandy
maintainer: malte.brandy@maralorn.de
build-type: Simple
extra-source-files: CHANGELOG.md
common common-config
default-extensions:
AllowAmbiguousTypes
ConstraintKinds
DataKinds
DeriveAnyClass
DeriveGeneric
DerivingStrategies
DuplicateRecordFields
EmptyCase
FlexibleContexts
FlexibleInstances
GADTs
GeneralizedNewtypeDeriving
LambdaCase
MultiParamTypeClasses
NamedFieldPuns
OverloadedLabels
OverloadedStrings
PartialTypeSignatures
PatternGuards
PatternSynonyms
QuasiQuotes
RankNTypes
RecursiveDo
ScopedTypeVariables
StandaloneDeriving
StrictData
TemplateHaskell
TupleSections
TypeApplications
TypeFamilies
UndecidableInstances
ViewPatterns
ghc-options:
-Wall -Wcompat -Wno-orphans -Wincomplete-uni-patterns
-Wincomplete-record-updates -Wmissing-export-lists -Widentities
-Wredundant-constraints -Wmissing-home-modules
mixins: base hiding (Prelude)
default-language: Haskell2010
-- END dhall generated common configuration
library
import: common-config
exposed-modules:
Kassandra.Api
Kassandra.Calendar
Kassandra.Config
Kassandra.Css
Kassandra.Debug
Kassandra.LocalBackend
Kassandra.LocalBackendWidget
Kassandra.MainWidget
Kassandra.RemoteBackendWidget
Kassandra.SelectorWidget
Kassandra.State
Kassandra.Types
Kassandra.Util
Prelude
other-modules:
Kassandra.AgendaWidget
Kassandra.BaseWidgets
Kassandra.DragAndDrop
Kassandra.ListElementWidget
Kassandra.ListWidget
Kassandra.LogWidget
Kassandra.ReflexUtil
Kassandra.Sorting
Kassandra.TaskWidget
Kassandra.TextEditWidget
Kassandra.TimeWidgets
build-depends:
, aeson
, ansi-terminal
, async
, base
, clay
, containers
, data-default-class
, extra
, generic-optics
, jsaddle
, jsaddle-dom
, nonempty-containers
, optics
, optics-th
, password
, patch
, process
, reflex
, reflex-dom
, relude
, say
, scientific
, stm
, string-interpolate
, taskwarrior
, template-haskell
, text
, these
, time
, unordered-containers
, uuid
, witherable
hs-source-dirs: src

View file

@ -0,0 +1,98 @@
{-# LANGUAGE BlockArguments #-}
module Kassandra.AgendaWidget (agendaWidget) where
import qualified Data.Sequence as Seq
import qualified Data.Sequence.NonEmpty as NESeq
import Kassandra.BaseWidgets (br, button, icon)
import Kassandra.Calendar (
CalendarEvent (
CalendarEvent,
calendarName,
comment,
description,
location,
time,
todoList,
uid
),
CalendarList (entries),
EventTime (AllDayEvent, SimpleEvent),
switchToCurrentZone,
)
import Kassandra.Config (DefinitionElement (..), ListItem (..))
import Kassandra.DragAndDrop (insertArea)
import Kassandra.ListElementWidget (AdhocContext (..), definitionElementWidget, tellList)
import Kassandra.ReflexUtil (listWithGaps)
import Kassandra.TextEditWidget (createTextWidget)
import Kassandra.Types (StandardWidget, Widget, getAppState)
import qualified Reflex as R
import qualified Reflex.Dom as D
agendaWidget :: StandardWidget t m r e => m ()
agendaWidget = do
appState <- getAppState
let calendarEvents = appState ^. #calendarEvents
void $
D.dyn_ $
calendarEvents <&> mapM_ \CalendarEvent{description, time, calendarName, location, comment, uid, todoList} ->
D.divClass "event" $ do
icon "" "event"
D.text description
whenJust location \l -> do
br
icon "" "room"
D.text l
whenJust comment \c -> do
br
icon "" "comment"
D.text c
br
icon "" "schedule"
printEventTime time
br
icon "" "list"
D.text calendarName
br
calendarListWidget uid todoList
calendarListWidget :: StandardWidget t m r e => Text -> CalendarList -> m ()
calendarListWidget uid calendarList = do
listWithGaps widget gapWidget (pure (entries calendarList))
newTaskEvent <- createTextWidget (button "selector" $ D.text "New Task")
tellList uid $ newTaskEvent <&> \content -> (#entries %~ (Seq.|> ListElement (AdHocTask content))) calendarList
where
widget definitionElement = D.divClass "definitionElement" do
D.divClass "definitionUI" $ do
delete <- button "" (D.text "x")
tellList uid $ delete $> (#entries %~ Seq.filter (definitionElement /=)) calendarList
D.divClass "element" $ definitionElementWidget (AgendaEvent uid calendarList) definitionElement
gapWidget around = do
evs <- insertArea (pure mempty) $ icon "dropHere above" "forward"
tellList uid $ R.attachWith (flip insertedCalendarList) (R.current around) evs
insertedCalendarList toInsert = \case
(Nothing, Nothing) -> updateOnList (const (toSeq toInsert))
(Just _, Nothing) -> updateOnList (<> toSeq toInsert)
(Nothing, Just _) -> updateOnList (toSeq toInsert <>)
(Just _, Just after) -> updateOnList ((\(a, b) -> a <> toSeq toInsert <> b) . Seq.breakl (== after))
where
updateOnList upd = (#entries %~ upd . Seq.filter (isNothing . flip NESeq.elemIndexL toInsert)) calendarList
printEventTime :: Widget t m => EventTime -> m ()
printEventTime (SimpleEvent start end) = do
showstart <- switchToCurrentZone (start ^. #time)
showend <- switchToCurrentZone (end ^. #time)
let printFullTime = toText . formatTime defaultTimeLocale "%a %Y-%m-%d %H:%M"
let printEndTime = toText . formatTime defaultTimeLocale "%H:%M"
let zone = start ^. #zone
D.text (printFullTime showstart)
icon "" "arrow_right_alt"
D.text (printEndTime showend)
D.text [i|(Defined in #{zone})|]
printEventTime (AllDayEvent startDay endDay) = do
let printFullTime = toText . formatTime defaultTimeLocale "%a %Y-%m-%d"
D.text (printFullTime startDay)
when (startDay /= endDay) do
icon "" "arrow_right_alt"
D.text (printFullTime endDay)
printEventTime _ = pass

View file

@ -0,0 +1,29 @@
module Kassandra.Api (
SocketMessage (..),
SocketRequest (..),
) where
import Kassandra.Calendar
import Kassandra.Config (UIConfig)
type SocketError = Text
data SocketMessage
= TaskUpdates (NESeq Task)
| CalendarEvents (Seq CalendarEvent)
| UIConfigResponse UIConfig
| SocketError SocketError
| ConnectionEstablished
deriving stock (Show, Read, Generic)
deriving anyclass (ToJSON, FromJSON)
makePrismLabels ''SocketMessage
data SocketRequest
= UIConfigRequest
| AllTasks
| CalenderRequest
| ChangeTasks (NESeq Task)
| SetCalendarList Text CalendarList
deriving stock (Show, Read, Eq, Generic)
deriving anyclass (ToJSON, FromJSON)
makePrismLabels ''SocketRequest

View file

@ -0,0 +1,32 @@
module Kassandra.BaseWidgets (
br,
icon,
button,
stateWidget,
) where
import Kassandra.Types (Widget)
import qualified Reflex as R
import qualified Reflex.Dom as D
import Relude.Extra.Bifunctor (secondF)
br :: D.DomBuilder t m => m ()
br = D.el "br" pass
stateWidget ::
Widget t m =>
state ->
(state -> m (R.Event t a, R.Event t state)) ->
m (R.Event t a)
stateWidget initialState widget = do
eventsEvent <- D.workflowView $ stateToWorkflow initialState
R.switchHold R.never eventsEvent
where
stateToWorkflow = D.Workflow . secondF (fmap stateToWorkflow) . widget
icon :: Widget t m => Text -> Text -> m ()
icon cssClass = D.elClass "i" ("material-icons icon " <> cssClass) . D.text
button :: Widget t m => Text -> m () -> m (R.Event t ())
button cssClass =
fmap (D.domEvent D.Click . fst) . D.elClass' "span" ("button " <> cssClass)

View file

@ -0,0 +1,76 @@
module Kassandra.Calendar (
CalendarEvent (..),
CalendarList (..),
EventTime (..),
TZTime (..),
sortEvents,
tzTimeToUTC,
zonedDay,
switchToCurrentZone,
) where
import Data.Time
import Kassandra.Config
data TZTime = TZTime
{ time :: ZonedTime
, zone :: Text
}
deriving stock (Show, Read, Generic)
deriving anyclass (ToJSON, FromJSON)
makeLabels ''TZTime
data EventTime
= SimpleEvent {start :: TZTime, end :: TZTime}
| AllDayEvent {startDay :: Day, endDay :: Day}
| RecurringEvent
deriving stock (Show, Read, Generic)
deriving anyclass (ToJSON, FromJSON)
makeLabels ''EventTime
data CalendarList = CalendarList
{ entries :: Seq DefinitionElement
, completed :: Set Text
}
deriving stock (Eq, Show, Read, Generic)
deriving anyclass (ToJSON, FromJSON)
makeLabels ''CalendarList
data CalendarEvent = CalendarEvent
{ uid :: Text
, time :: EventTime
, description :: Text
, location :: Maybe Text
, comment :: Maybe Text
, todoList :: CalendarList
, calendarName :: Text
}
deriving stock (Show, Read, Generic)
deriving anyclass (ToJSON, FromJSON)
makeLabels ''CalendarEvent
sortEvents :: CalendarEvent -> CalendarEvent -> Ordering
sortEvents = sortEventTimes `on` (^. #time)
sortEventTimes :: EventTime -> EventTime -> Ordering
sortEventTimes lhs rhs = case (lhs, rhs) of
(SimpleEvent startTimeLhs _, SimpleEvent startTimeRhs _) -> compare (tzTimeToUTC startTimeLhs) (tzTimeToUTC startTimeRhs)
(AllDayEvent startDayLhs _, AllDayEvent startDayRhs _) -> compare startDayLhs startDayRhs
(AllDayEvent startDayLhs _, SimpleEvent startTimeRhs _) -> case compare startDayLhs (tzTimeDay startTimeRhs) of EQ -> LT; a -> a
(SimpleEvent startTimeLhs _, AllDayEvent startDayRhs _) -> case compare (tzTimeDay startTimeLhs) startDayRhs of EQ -> GT; a -> a
(_, _) -> EQ
switchToCurrentZone :: MonadIO m => ZonedTime -> m ZonedTime
switchToCurrentZone time = do
let inUtc = zonedTimeToUTC time
zone <- liftIO $ getTimeZone inUtc
pure $ utcToZonedTime zone inUtc
tzTimeToUTC :: TZTime -> UTCTime
tzTimeToUTC = zonedTimeToUTC . (^. #time)
tzTimeDay :: TZTime -> Day
tzTimeDay = localDay . zonedTimeToLocalTime . (^. #time)
zonedDay :: ZonedTime -> Day
zonedDay = localDay . zonedTimeToLocalTime

View file

@ -0,0 +1,216 @@
module Kassandra.Config (
AccountConfig (..),
RemoteBackend (..),
NamedBackend (..),
UserConfig (..),
LocalBackend (..),
Dict,
UIConfig (..),
PortConfig (..),
Widget (..),
TreeOption (..),
ListItem (..),
HabiticaTask (..),
HabiticaList (..),
DefinitionElement (..),
ListQuery,
Query,
QueryFilter (..),
TaskProperty (..),
UIFeatures (..),
PasswordConfig (..),
NamedListQuery (..),
TaskwarriorOption (..),
) where
import Data.Default.Class ( Default(..) )
import Data.Password.Argon2 (
Argon2,
PasswordHash,
)
type Dict = Map Text
data UIFeatures = UIFeatures
{ sortInTag :: Bool
, treeOption :: TreeOption
}
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
data TreeOption = NoTree | PartOfTree | DependsTree
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
data HabiticaTask = HabiticaDaily | HabiticaTodo
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
data ListItem
= TaskwarriorTask {uuid :: UUID}
| AdHocTask {description :: Text}
| HabiticaTask {task :: HabiticaTask}
| Mail {id :: Text}
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
makePrismLabels ''ListItem
data HabiticaList = HabiticaDailys | HabiticaTodos
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
data TaskProperty
= DescriptionMatches {filter :: Text}
| ParentBlocked
| Blocked
| Waiting
| Pending
| Completed
| Deleted
| IsParent
| OnList
| HasTag {tag :: Text}
| HasParent
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
data TaskwarriorOption = TaskwarriorOption
{ name :: Text
, value :: Text
}
deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (Hashable)
data QueryFilter
= HasProperty {property :: TaskProperty}
| HasntProperty {property :: TaskProperty}
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
type Query = Seq QueryFilter
data DefinitionElement
= ConfigList {name :: Text, limit :: Maybe Natural}
| ListElement {item :: ListItem}
| QueryList {query :: Query}
| TagList {name :: Text}
| ChildrenList {uuid :: UUID}
| DependenciesList {uuid :: UUID}
| HabiticaList {list :: HabiticaList}
| Mails
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
makePrismLabels ''DefinitionElement
data Widget
= SearchWidget
| DefinitionElementWidget {name :: Text, definitionElement :: DefinitionElement}
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
type ListQuery = Seq DefinitionElement
data NamedListQuery = NamedListQuery
{ name :: Text
, list :: ListQuery
}
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
data UIConfig = UIConfig
{ viewList :: Seq Widget
, configuredLists :: Seq NamedListQuery
, uiFeatures :: UIFeatures
}
deriving stock (Show, Eq, Ord, Generic, Read)
deriving anyclass (ToJSON, FromJSON)
makeLabels ''UIConfig
instance Default UIConfig where
def =
UIConfig
{ viewList = mempty
, configuredLists = mempty
, uiFeatures = UIFeatures True PartOfTree
}
data PortConfig = Port {port :: Word16} | PortRange {min :: Word16, max :: Word16}
deriving stock (Show, Eq, Ord, Generic)
data PasswordConfig = Prompt | Password {plaintext :: Text} | PasswordCommand {command :: Text}
deriving stock (Show, Eq, Ord, Generic)
data LocalBackend
= TaskwarriorBackend
{ -- | Set config file
taskRcPath :: Maybe Text
, -- | Set task data directory
taskDataPath :: Maybe Text
, -- | Override config variables
taskConfig :: Seq TaskwarriorOption
, -- | Path to taskwarrior binary. Nothing => Lookup "task" from PATH
taskBin :: Maybe Text
, -- | Use the first free port from the given range for the taskwarrior hook listener.
hookListenPort :: PortConfig
, -- | Created hooks are called ".on-add.<suffix>.<port>" and ".on-remove.<suffix>.<port>"
hookSuffix :: Text
, -- | Ensure existence of taskwarrior hook on every start
createHooksOnStart :: Bool
, -- | Remove hook on exit.
removeHooksOnExit :: Bool
}
| GitBackend
{ directoryPath :: Text
, commit :: Bool
, configureMerge :: Bool
, createIfMissing :: Bool
, origin :: Maybe Text
, pushOnWrite :: Bool
, watchFiles :: Bool
, pullTimerSeconds :: Maybe Natural
}
deriving stock (Show, Eq, Ord, Generic)
instance Default LocalBackend where
def =
TaskwarriorBackend
{ taskRcPath = Nothing
, taskDataPath = Nothing
, taskConfig = mempty
, taskBin = Nothing
, hookListenPort = Port 6545
, hookSuffix = "kassandra"
, createHooksOnStart = True
, removeHooksOnExit = True
}
data RemoteBackend a = RemoteBackend
{ url :: Text
, user :: Text
, password :: a
}
deriving stock (Show, Eq, Ord, Generic)
data NamedBackend b = NamedBackend
{ name :: Text
, backend :: b
}
deriving stock (Show, Eq, Ord, Generic)
data UserConfig = UserConfig
{ localBackend :: LocalBackend
, uiConfig :: UIConfig
}
deriving stock (Show, Eq, Ord, Generic)
instance Default UserConfig where
def = UserConfig def def
data AccountConfig = AccountConfig
{ passwordHash :: PasswordHash Argon2
, userConfig :: UserConfig
, filterTag :: Text
}
deriving stock (Show, Eq, Ord, Generic)
makeLabels ''AccountConfig

View file

@ -0,0 +1,166 @@
module Kassandra.Css (
cssAsBS,
cssAsText,
) where
import Clay
import Prelude hiding (
i,
(&),
(|>),
)
cssToBS :: Css -> ByteString
cssToBS = encodeUtf8 . cssToText
cssToText :: Css -> Text
cssToText = toStrict . render
cssAsBS :: ByteString
cssAsBS = $$([||cssToBS (css (Just ""))||])
cssAsText :: Text -> Text
cssAsText fontPath = cssToText (css (Just fontPath))
fontName :: Text
fontName = "Material Icons"
css :: Maybe Text -> Css
css fontPath = do
whenJust fontPath $ \fontSrc -> do
fontFace $ do
fontFamily [fontName] []
fontFaceSrc [FontFaceSrcUrl fontSrc (Just OpenType)]
let --darkBlue = rgb 0 0 33
lightBlue = rgb 200 200 255
noMargin = margin (px 0) (px 0) (px 0) (px 0)
noPadding = padding (px 0) (px 0) (px 0) (px 0)
star ? do
fontFamily ["B612"] []
noMargin
noPadding
body ? do
background white
color black
minHeight (pct 100)
".definitionElement" ? do
clear clearLeft
".definitionUI" ? do
float floatLeft
".remoteBackend" ? do
textAlign (alignSide sideRight)
".loginDialog" ? do
textAlign (alignSide sideCenter)
padding (em 5) (em 5) (em 5) (em 5)
".header" ? do
padding (em 0.2) (em 0.2) (em 0.2) (em 0.2)
background lightBlue
fontWeight bold
fontSize (em 1.5)
".content" ? do
paddingBottom (em 4)
".footer" ? do
position fixed
bottom (px 0)
width (pct 100)
padding (em 0.22) (em 0.2) (em 0.2) (em 0.2)
background (grayish 200)
color black
".container" ? do
display flex
minHeight (pct 100)
".dropHere" ? do
position absolute
background white
color black
border (em 0.1) solid black
let offset = 2
".plusOne" ? marginLeft (em offset)
".plusTwo" ? marginLeft (em (offset * 2))
".above" ? marginTop (em (-0.8))
".pane" ? width (pct 100)
let buttonPadding =
padding (em 0.3) (em 0.5) (em 0.3) (em 0.5)
buttonCss = do
display inlineBlock
margin (px 1) (px 1) (px 1) (px 1)
buttonPadding
border (em 0.1) solid black
active & do
background black
color white
".button" ? buttonCss
".selector" ? buttonCss
--".tag" ? ".icon" ? do
--position absolute
--borderRadius tagRadius tagRadius tagRadius tagRadius
--background lightBlue
--marginLeft (em (-1.1))
--marginTop (em 0.70)
--fontSize (em 0.85)
".material-icons" ? do
fontFamily [fontName] []
fontWeight normal
fontStyle normal
fontSize (em 1)
display inlineBlock
lineHeight (em 1)
width (em 1)
textTransform none
letterSpacing normal
wordWrap normal
whiteSpace nowrap
direction ltr
"-webkit-font-smoothing" -: "antialiased"
".path" ? do
color grey
fontSize (em 0.8)
let radius = em 0.3
leftBarWidth = em 1.8
".activeEdit" ? buttonPadding
".event" ? do
border (px 1) solid black
".task" ? do
color (rgb 0 0 33)
border (px 1) solid black
background white
".task" ? do
noMargin
".parentPath" ? display none
".righttask" ? do
width (pct 100)
".statusWrapper" ? do
background black
width leftBarWidth
minWidth leftBarWidth
".uppertask" ? do
display flex
".edit" ? visibility visible
i ? cursor cursorDefault
".icon" ? padding radius radius radius radius
".children" ? do
padding (px 0) (px 0) (px 0) leftBarWidth
background black
--".slimButton" ? do
--marginRight (px (-5))
--marginLeft (px (-5))
let blockSize = do
width (em 1)
height (em 1)
bg = grayish 255
".checkbox" ? do
marginTop (em 0.28)
marginBottom (em 0.28)
marginLeft (em 0.25)
display inlineBlock
fontSize (em 1.2)
blockSize
borderRadius (em 0.2) (em 0.2) (em 0.2) (em 0.2)
background bg
i ? do
".hide" & color bg
".grey" & color (grayish 160)
".show" & color black
".showable" & display none
active & i ? do
background black

View file

@ -0,0 +1,164 @@
module Kassandra.Debug (
log,
logShow,
logR,
logRShow,
setLogLevel,
Severity (..),
) where
import Control.Concurrent (
ThreadId,
modifyMVar,
myThreadId,
)
import GHC.Stack (
SrcLoc (SrcLoc),
srcLocModule,
srcLocStartLine,
)
import Reflex as R
import System.IO.Unsafe (unsafePerformIO)
import qualified Debug.Trace as Trace
import System.Console.ANSI (
Color (..),
ColorIntensity (Vivid),
ConsoleLayer (Foreground),
SGR (..),
setSGRCode,
)
import Relude.Extra.Bifunctor
import Relude.Extra.Enum
import Say
data Severity = Debug | Info | Warning | Error deriving stock (Show, Read, Eq, Ord)
class ReflexLoggable l where
useLogString :: (Text -> a -> Text) -> l a -> l a
instance R.Reflex t => ReflexLoggable (R.Dynamic t) where
useLogString f d =
let e' = traceEventWith (toString . f "updated Dynamic") $ updated d
getV0 = do
x <- sample $ current d
Trace.trace (toString $ f "initialized Dynamic" x) $ return x
in unsafeBuildDynamic getV0 e'
instance R.Reflex t => ReflexLoggable (R.Event t) where
useLogString f e = traceEventWith (toString . f "triggered Event") e
logR ::
(HasCallStack, MonadIO m, ReflexLoggable l) =>
Severity ->
(a -> Text) ->
l a ->
m (l a)
logR severity decorate loggable = do
isSevere <- severeEnough severity
if isSevere
then do
myId <- liftIO $ modifyMVar traceID $ \a -> pure (next a, a)
withFrozenCallStack $ log Debug ("Registering eventTrace " <> show myId)
let f comment value =
formatMessage
Message
{ msgSeverity = severity
, msgCallStack = callStack
, msgThreadId = unsafePerformIO myThreadId
, msgTime = unsafePerformIO getZonedTime
, msgComment = Just (comment <> " " <> show myId)
, msgContent = decorate value
}
pure $ useLogString f loggable
else pure loggable
logRShow ::
(HasCallStack, MonadIO m, ReflexLoggable l, Show a) =>
Severity ->
l a ->
m (l a)
logRShow a = withFrozenCallStack (logR a show)
logShow :: (HasCallStack, Show a, MonadIO m) => Severity -> a -> m ()
logShow s = withFrozenCallStack (log s . show)
log :: (HasCallStack, MonadIO m) => Severity -> Text -> m ()
log severity text = do
thread <- liftIO myThreadId
time <- liftIO getZonedTime
whenM (severeEnough severity) . say . formatMessage $
Message
{ msgSeverity = severity
, msgCallStack = callStack
, msgThreadId = thread
, msgTime = time
, msgComment = Nothing
, msgContent = text
}
{-# NOINLINE logLevel #-}
logLevel :: MVar (Maybe Severity)
logLevel = unsafePerformIO . newMVar $ Just Warning
{-# NOINLINE traceID #-}
traceID :: MVar Int
traceID = unsafePerformIO . newMVar $ 0
setLogLevel :: Maybe Severity -> IO ()
setLogLevel = void . swapMVar logLevel
severeEnough :: MonadIO m => Severity -> m Bool
severeEnough severity = severeEnough' <$> readMVar logLevel
where
severeEnough' (Just minSeverity) | minSeverity <= severity = True
severeEnough' _ = False
data Message = Message
{ msgSeverity :: !Severity
, msgCallStack :: !CallStack
, msgThreadId :: !ThreadId
, msgTime :: !ZonedTime
, msgComment :: !(Maybe Text)
, msgContent :: !Text
}
formatMessage :: Message -> Text
formatMessage Message{msgSeverity, msgTime, msgCallStack, msgThreadId, msgComment, msgContent} =
showSeverity msgSeverity
<> showTime msgTime
<> showSourceLoc msgCallStack
<> square (show msgThreadId)
<> maybe "" square msgComment
<> msgContent
square :: Text -> Text
square t = "[" <> t <> "] "
-- [30 May 2020 16:44:03.534 +00:00]
showTime :: ZonedTime -> Text
showTime = square . toText . formatTime defaultTimeLocale "%F %T%3Q %z"
-- | Formats severity in different colours with alignment.
showSeverity :: Severity -> Text
showSeverity = \case
Debug -> color Green "[Debug] "
Info -> color Blue "[Info] "
Warning -> color Yellow "[Warning] "
Error -> color Red "[Error] "
where
color :: Color -> Text -> Text
color c txt =
toText (setSGRCode [SetColor Foreground Vivid c]) <> txt
<> toText
(setSGRCode [Reset])
showSourceLoc :: CallStack -> Text
showSourceLoc = square . showCallStack . firstF toText . getCallStack
where
showCallStack = \case
[] -> "<unknown loc>"
[(name, SrcLoc{srcLocModule, srcLocStartLine})] ->
name <> "@" <> toText srcLocModule <> "#" <> show srcLocStartLine
(_, SrcLoc{srcLocModule, srcLocStartLine}) : (callerName, _) : _ ->
toText srcLocModule <> "." <> callerName <> "#" <> show srcLocStartLine

View file

@ -0,0 +1,83 @@
module Kassandra.DragAndDrop (
childDropArea,
taskDropArea,
tellSelected,
insertArea,
) where
import Kassandra.Config (DefinitionElement)
import Kassandra.Debug (
Severity (..),
logRShow,
)
import Kassandra.Sorting (
SortPosition,
saveSorting,
)
import Kassandra.Types (
AppStateChange,
DataChange,
SelectState,
StandardWidget,
TaskInfos,
WriteApp,
getSelectState,
getTasks,
)
import Kassandra.Util (
lookupTask,
tellSingleton,
)
import qualified Reflex as R
import qualified Reflex.Dom as D
tellSelected :: (MonadIO m, WriteApp t m e) => R.Event t (Seq DefinitionElement) -> m ()
tellSelected = tellSingleton . fmap (_Typed @AppStateChange % _Typed @SelectState #) <=< logRShow Info
insertArea :: StandardWidget t m r e => R.Dynamic t (Seq DefinitionElement) -> m () -> m (R.Event t (NESeq DefinitionElement))
insertArea blacklistD areaW = do
selectStateD <- getSelectState
let dropActive = do
selectState <- selectStateD
blacklist <- blacklistD
pure $ if all (`notElem` blacklist) selectState then nonEmptySeq selectState else Nothing
evEv <-
D.dyn $
dropActive <&> \case
Just entry -> do
dropEl <- fmap fst <$> D.element "span" D.def $ areaW
let event = D.domEvent D.Click dropEl
tellSelected (mempty <$ event)
pure $ entry <$ event
Nothing -> pure R.never
R.switchHold R.never evEv
taskDropArea ::
StandardWidget t m r e =>
R.Dynamic t (Seq UUID) ->
m () ->
(R.Event t (NESeq TaskInfos) -> R.Event t (NESeq Task)) ->
m ()
taskDropArea blacklistD areaW handler = do
tasksD <- getTasks
let blackListDefinitionElements = (#_ListElement % #_TaskwarriorTask #) <<$>> blacklistD
insertEvent <- insertArea blackListDefinitionElements areaW
let droppedTaskEvent =
R.attachWithMaybe
(\tasks -> nonEmptySeq . mapMaybe (lookupTask tasks <=< (^? #_ListElement % #_TaskwarriorTask)) . toSeq)
(R.current tasksD)
insertEvent
R.tellEvent $
fmap (_Typed @AppStateChange % _Typed @DataChange % #_ChangeTask #)
<$> handler droppedTaskEvent
childDropArea ::
StandardWidget t m r e =>
SortPosition t ->
R.Dynamic t (Seq UUID) ->
m () ->
m ()
childDropArea pos blacklistD areaW =
taskDropArea blacklistD areaW $
saveSorting (pos ^. #mode) (pos ^. #list)
. R.attachWith (\u t -> ((^. #task) <$> t, u)) (pos ^. #before)

View file

@ -0,0 +1,128 @@
{-# LANGUAGE BlockArguments #-}
module Kassandra.ListElementWidget (
definitionElementWidget,
configListWidget,
AdhocContext (..),
tellList,
queryWidget,
selectWidget,
) where
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import Kassandra.BaseWidgets (br, button, icon)
import Kassandra.Calendar (CalendarList, completed)
import Kassandra.Config (
DefinitionElement (
ChildrenList,
ConfigList,
DependenciesList,
HabiticaList,
ListElement,
Mails,
QueryList,
TagList
),
ListItem (AdHocTask, HabiticaTask, Mail, TaskwarriorTask),
ListQuery,
NamedListQuery (NamedListQuery),
)
import Kassandra.DragAndDrop (tellSelected)
import Kassandra.ReflexUtil (smartSimpleList)
import Kassandra.Sorting (SortMode (SortModeTag), sortTasks)
import Kassandra.TaskWidget (
taskList,
taskTreeWidget,
uuidWidget,
)
import Kassandra.TextEditWidget (createTextWidget)
import Kassandra.Types (AppStateChange, DataChange (SetEventList), StandardWidget, TaskInfos, TaskState, getAppState, getSelectState, getTasks)
import Kassandra.Util (tellNewTask, tellSingleton)
import qualified Reflex as R
import qualified Reflex.Dom as D
data AdhocContext = NoContext | AgendaEvent Text CalendarList | AgendaList Text (Set Text)
selectWidget :: StandardWidget t m r e => DefinitionElement -> m ()
selectWidget definitionElement = do
(dragEl, _) <- D.elClass' "span" "button" $ icon "" "filter_list"
selectStateB <- toggleContainElement definitionElement <<$>> R.current <$> getSelectState
tellSelected $ R.tag selectStateB (D.domEvent D.Click dragEl)
where
toggleContainElement :: DefinitionElement -> Seq DefinitionElement -> Seq DefinitionElement
toggleContainElement entry selectedTasks =
Seq.findIndexL (== entry) selectedTasks & maybe (selectedTasks |> entry) (`Seq.deleteAt` selectedTasks)
listElementWidget :: StandardWidget t m r e => AdhocContext -> ListItem -> m ()
listElementWidget context = \case
TaskwarriorTask uuid -> uuidWidget taskTreeWidget (pure uuid)
AdHocTask t -> adhocTaskWidget t context
HabiticaTask _ -> error "HabiticaTasks are not yet supported"
Mail _ -> error "Mails are not yet supported"
configListWidget :: forall t m r e. StandardWidget t m r e => AdhocContext -> Text -> Maybe Natural -> m ()
configListWidget context name limit = do
D.text name >> br
namedListQueries <- getAppState ^. mapping (#uiConfig % mapping #configuredLists)
D.dyn_ $ namedListQueries <&> maybe (D.text [i|No list with name "#{name}" configured.|]) (queryWidget context) . getWidget
where
getWidget = Seq.lookup 0 . mapMaybe f
f (NamedListQuery x query) | x == name = Just query
f _ = Nothing
queryWidget :: StandardWidget t m r e => AdhocContext -> ListQuery -> m ()
queryWidget context els = smartSimpleList ((>> br) . definitionElementWidget context) (pure els)
tasksToShow :: Text -> TaskState -> Seq TaskInfos
tasksToShow tag = filter inList . fromList . HashMap.elems
where
inList :: TaskInfos -> Bool
inList ((^. #task) -> task) = tag `Set.member` (task ^. #tags) && has (#status % #_Pending) task
definitionElementWidget :: StandardWidget t m r e => AdhocContext -> DefinitionElement -> m ()
definitionElementWidget context el = do
selectWidget el
el & \case
ConfigList name limit -> configListWidget context name limit
ListElement el' -> listElementWidget context el'
QueryList query -> D.text "QueryLists not implemented"
(TagList tag) ->
do
D.text tag
tasks <- getTasks
let showTasks = tasksToShow tag <$> tasks
let sortMode = SortModeTag tag
taskList
(R.constant sortMode)
(sortTasks sortMode <$> showTasks)
(R.constDyn IsEmpty)
taskTreeWidget
tellNewTask . fmap (,#tags %~ Set.insert tag)
=<< createTextWidget
(button "selector" $ D.text "Add task to list")
(ChildrenList uuid) -> D.text "ChildrenList not implemented"
(DependenciesList uuid) -> D.text "DependenciesList not implemented"
(HabiticaList list) -> D.text "HabiticaList not implemented"
Mails -> D.text "Mails not implemented"
adhocTaskWidget :: StandardWidget t m r e => Text -> AdhocContext -> m ()
adhocTaskWidget description = \case
AgendaEvent uid calendarList -> do
changeDoneStatus <- checkBox (completed calendarList)
D.text [i| #{description}|]
tellList uid $
changeDoneStatus <&> \case
True -> (#completed %~ Set.insert description) calendarList
False -> (#completed %~ Set.delete description) calendarList
AgendaList _ completed -> do
checkBox completed >> text
NoContext ->
checkBox mempty >> text
where
checkBox completed = if description `Set.member` completed then (False <$) <$> button "" (D.text "[x]") else (True <$) <$> button "" (D.text "[ ]")
text = D.text [i| #{description}|]
tellList :: StandardWidget t m r e => Text -> D.Event t CalendarList -> m ()
tellList uid listEvent = tellSingleton $ (_Typed @AppStateChange % _Typed @DataChange #) . SetEventList uid <$> listEvent

View file

@ -0,0 +1,52 @@
module Kassandra.ListWidget (
listsWidget,
listWidget,
) where
import qualified Data.HashMap.Strict as HashMap
import Kassandra.Config (DefinitionElement (TagList))
import Kassandra.ListElementWidget (AdhocContext (NoContext), definitionElementWidget)
import Kassandra.Types (
StandardWidget,
TaskState,
Widget,
getTasks,
)
import qualified Reflex as R
import qualified Reflex.Dom as D
listsWidget :: (StandardWidget t m r e) => m ()
listsWidget = do
taskState <- getTasks
D.text "Select a list"
list <- listSelector (getLists <$> taskState)
maybeList <- R.maybeDyn list
D.dyn_ $ maybeList <&> maybe (D.text "Select a list") listWidget
where
getLists :: TaskState -> Seq Text
getLists =
fromList
. toList
. foldMap (^. #tags)
. filter (has $ #status % #_Pending)
. (^. mapping #task)
. HashMap.elems
listSelector ::
(Widget t m) => R.Dynamic t (Seq Text) -> m (R.Dynamic t (Maybe Text))
listSelector lists = D.el "div" $ do
buttons <- D.dyn $ mapM listButton <$> lists
buttonSum <- R.switchHold R.never $ R.leftmost . toList <$> buttons
R.holdDyn Nothing (Just <$> buttonSum)
listButton :: Widget t m => Text -> m (R.Event t Text)
listButton tag =
fmap ((tag <$) . D.domEvent D.Click . fst)
. D.elClass' "a" "selector"
. D.text
$ tag
listWidget ::
forall t m r e. StandardWidget t m r e => R.Dynamic t Text -> m ()
listWidget list = D.dyn_ (innerRenderList <$> list)
where
innerRenderList :: Text -> m ()
innerRenderList tag = definitionElementWidget NoContext (TagList tag)

View file

@ -0,0 +1,41 @@
module Kassandra.LocalBackend (
localClientSocket,
LocalBackendRequest (LocalBackendRequest, userConfig, alive, responseCallback, requestQueue),
BackendError (..),
) where
import Control.Concurrent.STM (TQueue, newTQueueIO, writeTQueue)
import Kassandra.Api (SocketMessage, SocketRequest)
import Kassandra.Config (UserConfig)
import Kassandra.State (ClientSocket)
import Kassandra.Types (WidgetIO)
import qualified Reflex as R
data LocalSocketState = LocalError Text | SettingUp deriving (Show)
makePrismLabels ''LocalSocketState
newtype BackendError = BackendError Text
data LocalBackendRequest = LocalBackendRequest
{ userConfig :: UserConfig
, alive :: TVar Bool
, responseCallback :: SocketMessage -> IO ()
, requestQueue :: TQueue SocketRequest
}
makeLabels ''LocalBackendRequest
localClientSocket ::
WidgetIO t m =>
TQueue LocalBackendRequest ->
UserConfig ->
m (ClientSocket t m)
localClientSocket requestsQueue userConfig = do
requestQueue <- liftIO newTQueueIO
(socketMessageEvent, responseCallback) <- R.newTriggerEvent
alive <- newTVarIO True
let backendRequest = LocalBackendRequest{userConfig, alive, responseCallback, requestQueue}
clientSocket socketRequestEvent = do
R.performEvent_ $ atomically . mapM_ (writeTQueue requestQueue) <$> socketRequestEvent
pure (pure socketMessageEvent)
atomically $ writeTQueue requestsQueue backendRequest
pure clientSocket

View file

@ -0,0 +1,22 @@
module Kassandra.LocalBackendWidget (
localBackendWidget,
) where
import Control.Concurrent.STM (TQueue)
import Kassandra.Config (
NamedBackend (NamedBackend, backend),
UserConfig,
)
import Kassandra.LocalBackend (LocalBackendRequest, localClientSocket)
import Kassandra.State (StateProvider, makeStateProvider)
import Kassandra.Types (WidgetIO)
import qualified Reflex as R
localBackendWidget ::
WidgetIO t m =>
TQueue LocalBackendRequest ->
NamedBackend UserConfig ->
m (R.Dynamic t (Maybe (StateProvider t m)))
localBackendWidget requestsQueue NamedBackend{backend} =
-- TODO: Use the uiConfig
pure . pure . makeStateProvider <$> localClientSocket requestsQueue backend

View file

@ -0,0 +1,4 @@
module Kassandra.LogWidget (logWidget) where
logWidget :: Monad m => m ()
logWidget = pass

View file

@ -0,0 +1,200 @@
{-# LANGUAGE PatternSynonyms #-}
module Kassandra.MainWidget (
mainWidget,
) where
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Sequence as Seq
import qualified Data.Sequence.NonEmpty as NESeq
import qualified Data.Set as Set
import Kassandra.AgendaWidget (agendaWidget)
import Kassandra.BaseWidgets (button, br)
import Kassandra.Calendar (CalendarEvent)
import Kassandra.Config (DefinitionElement, Widget (DefinitionElementWidget, SearchWidget))
import Kassandra.Debug (
Severity (..),
log,
logR,
setLogLevel,
)
import Kassandra.ListElementWidget (AdhocContext (NoContext), definitionElementWidget)
import Kassandra.ListWidget (listsWidget)
import Kassandra.LogWidget (logWidget)
import Kassandra.State (StateProvider)
import Kassandra.TaskWidget (taskTreeWidget)
import Kassandra.TextEditWidget (createTextWidget)
import Kassandra.Types (
AppState (AppState),
AppStateChange,
StandardWidget,
TaskInfos,
TaskState,
WidgetIO,
getAppState,
getSelectState,
getTasks,
getTime,
)
import Kassandra.Util (lookupTasks, stillTodo, tellNewTask)
import qualified Reflex as R
import qualified Reflex.Dom as D
mainWidget :: WidgetIO t m => StateProvider t m -> m ()
mainWidget stateProvider = do
-- TODO: Use ui Config
liftIO $ setLogLevel $ Just Info
log Info "Loaded Mainwidget"
time <- liftIO getZonedTime
timeDyn <-
fmap (utcToZonedTime (zonedTimeZone time) . (^. lensVL R.tickInfo_lastUTC))
<$> R.clockLossy 1 (zonedTimeToUTC time)
rec let (appChangeEvents, dataChangeEvents) =
R.fanThese $ partitionEithersNESeq <$> stateChanges
appData <- stateProvider dataChangeEvents
selectedDyn <- R.holdDyn mempty $ NESeq.last <$> appChangeEvents
uiConfig <- R.holdUniqDyn $ appData ^. mapping #uiConfig
(_, stateChanges' :: R.Event t (NESeq AppStateChange)) <-
R.runEventWriterT $
runReaderT
( do
taskDiagnosticsWidget
D.divClass "content" widgetSwitcher
infoFooter
)
( AppState
(appData ^. mapping #taskState)
timeDyn
selectedDyn
(appData ^. mapping #calendarData)
uiConfig
)
stateChanges <- logR Info (\a -> [i|StateChange: #{a}|]) stateChanges'
pass
infoFooter :: StandardWidget t m r e => m ()
infoFooter = D.divClass "footer" $ do
selectedState <- getSelectState
D.dyn_ $
selectedState <&> \a -> do
whenJust (nonEmptySeq a) $ \(selectedTasks :: NESeq DefinitionElement) -> do
forM_ selectedTasks $ \t -> D.divClass "selectedTask" (definitionElementWidget NoContext t)
tellNewTask . fmap (,id)
=<< createTextWidget
(button "selector" $ D.text "New Task")
tasks <- getTasks
D.el "p" $
D.dynText $
tasks <&> \taskMap ->
let taskList = HashMap.elems taskMap
countTasks a = length . filter (has (#task % #status % a))
pending = countTasks #_Pending taskList
completed = countTasks #_Completed taskList
in [i|#{pending} pending and #{completed} completed tasks. Kassandra-ToDo-Management|]
taskDiagnosticsWidget :: StandardWidget t m r e => m ()
taskDiagnosticsWidget = do
tasks <- getTasks
D.dynText $ do
tasksMap <- tasks
let uuids = HashMap.keys tasksMap
hasLoop :: Seq UUID -> UUID -> Maybe UUID
hasLoop seen new
| new `elem` seen = Just new
| otherwise = Seq.lookup 0 (mapMaybe (hasLoop (new <| seen)) nexts)
where
nexts = maybe mempty (^. #children) $ HashMap.lookup new tasksMap
pure $
firstJust (hasLoop mempty) uuids & \case
Just uuid -> "Found a loop for uuid " <> show uuid
Nothing -> "" -- everything fine
widgets :: StandardWidget t m r e => Seq (Text, m ())
widgets =
fromList
[ ("Agenda", agendaWidget)
, ("Sort", nextWidget)
, ("Tag Lists", listsWidget)
, ("Logs", logWidget)
]
widgetSwitcher :: forall t m r e. StandardWidget t m r e => m ()
widgetSwitcher = do
uiConfigD <- getAppState ^. mapping #uiConfig
D.el "div" . D.dyn_ $ uiConfigD <&> withUIConfig
where
withUIConfig uiConfig = do
let userWidgets = mkWidget <$> uiConfig ^. #viewList
buttons <- forM (widgets <> userWidgets) selectButton
listName <- R.holdDyn (fromMaybe ("No list", pass) (Seq.lookup 0 widgets)) (R.leftmost (toList buttons))
D.el "div" $ D.dyn_ (snd <$> listName)
selectButton label =
(label <$) . D.domEvent D.Click . fst
<$> D.elClass'
"a"
"selector"
(D.text $ fst label)
mkWidget :: Widget -> (Text, m ())
mkWidget SearchWidget = ("Search", D.text "Not implemented")
mkWidget (DefinitionElementWidget name definitionElement) = (name, definitionElementWidget NoContext definitionElement)
filterInbox :: UTCTime -> TaskState -> Seq CalendarEvent -> Seq TaskInfos
filterInbox now tasks events =
Seq.reverse . Seq.sortOn (^. #modified) . fromList . toListOf (folded % filtered inInbox) $ tasks
where
scheduledEvents :: Set UUID
scheduledEvents = fromList $ toList $ mapMaybe (^? #_ListElement % #_TaskwarriorTask) <$> (^. #todoList % #entries) =<< events
inInbox :: TaskInfos -> Bool
inInbox taskInfos =
has (#tags % _Empty) taskInfos
&& maybe True (now >=) (taskInfos ^. #wait)
&& has (#status % #_Pending) taskInfos
&& (not . any stillTodo . lookupTasks tasks) (taskInfos ^. #children)
&& ( not
. any (`notElem` ["kategorie", "root"])
. Set.unions
. view (mapping #tags)
$ lookupTasks tasks (taskInfos ^. #parents)
)
&& not (taskInfos ^. #blocked)
&& not ((taskInfos ^. #uuid) `Set.member` scheduledEvents)
getInboxTasks :: StandardWidget t m r e => m (D.Dynamic t (Seq TaskInfos))
getInboxTasks = do
appState <- getAppState
timeDyn <- zonedTimeToUTC <<$>> getTime
let calendarEvents = appState ^. #calendarEvents
tasks <- getTasks
R.holdUniqDyn $ filterInbox <$> timeDyn <*> tasks <*> calendarEvents
nextWidget :: StandardWidget t m r e => m ()
nextWidget = do
inboxTasks <- getInboxTasks
unsortedTasks <-
fmap
( filter
( \task ->
not (Set.member "root" (task ^. #tags))
&& has (#partof % _Nothing) task
&& has (#status % #_Pending) task
)
. HashMap.elems
)
<$> getTasks
D.dynText $
(\x y -> if length x + length y > 0 then [i|There are #{length x} tasks in the inbox and #{length y} tasks unsorted.|] else "Nothing to do.")
<$> inboxTasks <*> unsortedTasks
inboxTaskDyn <- R.maybeDyn $ Seq.lookup 0 <$> inboxTasks
let decorateSortTask x = D.el "p" (D.text "Sort this task into the task tree:") *> taskTreeWidget x
decorateInboxTask x = D.el "p" $ do
D.text "Process this task from the inbox:" *> br
D.text "1. Does it need to be done?" *> br
D.text "2. Can you do it in under 2 minutes?" *> br
D.text "3. Should someone else do this?" *> br
D.text "4. Should you split this task into sub tasks?" *> br
D.text "5. On which tag list does it belong or when do you want to do it?" *> br
*> taskTreeWidget x
sortTask = do
taskDyn <- R.maybeDyn $ viaNonEmpty head <$> unsortedTasks
D.dyn_ (maybe pass decorateSortTask <$> taskDyn)
D.dyn_ (maybe sortTask decorateInboxTask <$> inboxTaskDyn)

View file

@ -0,0 +1,84 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Kassandra.ReflexUtil (
smartSimpleList,
listWithGaps,
keyDynamic,
) where
import qualified Data.Map as Map
import qualified Data.Patch.Map as Patch
--import qualified Data.Patch.MapWithMove as Patch
import qualified Data.Sequence as Seq
import qualified Reflex as R
import qualified Reflex.Dom as D
{- | Renders a list of widgets depending on a Dynamic list of inputs. This will
call the widget constructor once per value in the list.
When the list changes, the widget will move and reuse all values that it can
so that it only needs to call the constructor again, when a new value (or a
second copy of a same value) appears in the list.
-}
smartSimpleList ::
forall t m v.
(R.Adjustable t m, R.PostBuild t m, Ord v, R.MonadHold t m, MonadFix m, D.NotReady t m) =>
(v -> m ()) ->
R.Dynamic t (Seq v) ->
m ()
smartSimpleList widget listElements = do
void $ R.simpleList (toList <$> listElements) \vDyn -> do
u <- R.holdUniqDyn vDyn
D.dyn_ . fmap widget $ u
--postBuild <- R.getPostBuild
--keyMap <- R.holdUniqDyn $ Seq.foldMapWithIndex (curry one) <$> listElements
--let keyMapChange =
--R.attachWith
--((Newtype.under @(Map Int (Patch.NodeInfo Int v)) fixPatchMap .) . Patch.patchThatChangesMap)
--(R.current keyMap)
--(R.updated keyMap)
--initialKeyMap = Patch.patchMapWithMoveInsertAll <$> R.tag (R.current keyMap) postBuild
--keyMapEvents = keyMapChange <> initialKeyMap
--void $ R.mapMapWithAdjustWithMove (const widget) mempty keyMapEvents
-- | A workaround for a bug in patchThatChangesMap in patch 0.0.3.2.
--fixPatchMap :: Map Int (Patch.NodeInfo Int v) -> Map Int (Patch.NodeInfo Int v)
--fixPatchMap inputMap = appEndo setMoves . fmap (Patch.nodeInfoSetTo Nothing) $ inputMap
-- where
-- setMoves = Map.foldMapWithKey f inputMap
-- f to' (Patch.NodeInfo (Patch.From_Move from) _) = Endo $ Map.adjust (Patch.nodeInfoSetTo (Just to')) from
-- f _ _ = mempty
listWithGaps ::
(R.Adjustable t m, R.PostBuild t m, R.MonadHold t m, MonadFix m, Ord v, D.NotReady t m) =>
(v -> m ()) ->
(R.Dynamic t (Maybe v, Maybe v) -> m ()) ->
R.Dynamic t (Seq v) ->
m ()
listWithGaps widget gapWidget listD = do
smartSimpleList elementWidget listD
lastElementD <- R.holdUniqDyn $ (,Nothing) . lastOf folded <$> listD
gapWidget lastElementD
where
elementWidget currentElement = do
elementPair <- R.holdUniqDyn $ (,Just currentElement) . Map.lookup currentElement <$> prevElementsD
gapWidget elementPair
widget currentElement
prevElementsD = (\xs -> Map.unions . fmap one $ Seq.zip (Seq.drop 1 xs) xs) <$> listD
keyDynamic ::
forall t k v.
(R.Reflex t, Ord k) =>
R.Incremental t (Patch.PatchMap k v) ->
k ->
R.Dynamic t (Maybe v)
keyDynamic incremental key =
R.incrementalToDynamic
. R.unsafeMapIncremental mapMap mapPatchMap
$ incremental
where
mapMap :: Map k v -> Maybe v
mapMap = Map.lookup key
mapPatchMap :: Patch.PatchMap k v -> Identity (Maybe v)
mapPatchMap = Identity . join . Map.lookup key . Patch.unPatchMap

View file

@ -0,0 +1,167 @@
module Kassandra.RemoteBackendWidget (
remoteBackendWidget,
webClientSocket,
CloseEvent (..),
) where
import Data.Aeson (decode, encode)
import Data.Map (elems, insert)
import Data.Text (stripPrefix)
import JSDOM (currentWindowUnchecked)
import JSDOM.Custom.Window (getLocalStorage)
import JSDOM.Generated.Storage (Storage, getItem, setItem)
import Kassandra.Api (
SocketMessage,
SocketRequest (AllTasks),
)
import Kassandra.BaseWidgets (button, stateWidget)
import Kassandra.Config (
PasswordConfig (..),
RemoteBackend (..),
)
import Kassandra.State (
ClientSocket,
StateProvider,
makeStateProvider,
)
import Kassandra.Types (WidgetJSM)
import Language.Javascript.JSaddle (liftJSM)
import qualified Reflex as R
import qualified Reflex.Dom as D
import Relude.Extra.Newtype (un, wrap)
import System.Process (readCreateProcess, shell)
data Connecting = LoggedOut | LoggedIn deriving (Show, Read)
newtype CloseEvent t = CloseEvent (R.Event t ()) deriving newtype (Semigroup, Monoid)
urlKey, userKey, passwordKey, loginStateKey :: String
urlKey = "Url"
userKey = "User"
passwordKey = "Password"
loginStateKey = "LoginState"
remoteBackendWidget ::
forall t m.
WidgetJSM t m =>
CloseEvent t ->
Maybe (RemoteBackend PasswordConfig) ->
m (R.Dynamic t (Maybe (StateProvider t m)))
remoteBackendWidget closeEvent mayBackend = D.divClass "remoteBackend" $ do
backendDyn <- maybe inputBackend getPassword mayBackend
responseEvent <-
D.dyn
(withBackend (closeEvent <> wrap (() <$ R.updated backendDyn)) <$> backendDyn)
D.holdDyn Nothing responseEvent
where
getPassword :: RemoteBackend PasswordConfig -> m (R.Dynamic t (Maybe (RemoteBackend Text)))
getPassword RemoteBackend{url, user, password} = do
fmap (pure . RemoteBackend url user) <$> case password of
Password plain -> pure (pure plain)
PasswordCommand command -> pure . Text <$> liftIO (readCreateProcess (shell $ toString command) "")
Prompt -> do
D.text [i|Enter password for #{user} on #{url}:|]
storage <- getStorage
initialPassword <- fromMaybe "" <$> getItem storage passwordKey
passwordInput <- textInput True initialPassword
let sendEvent = R.tag (inputValue passwordInput) (D.keypress D.Enter passwordInput)
R.performEvent_ $ sendEvent <&> setItem storage ([i|PasswordFor#{user}On#{url}|] :: String) . toString
R.holdDyn "" sendEvent
inputBackend :: m (R.Dynamic t (Maybe (RemoteBackend Text)))
inputBackend = do
protocol <- D.getLocationProtocol
host <- D.getLocationHost
let defaultUrl = protocol <> "//" <> host
defaultUser = ""
defaultPassword = ""
storage <- getStorage
initialUrl <- fromMaybe defaultUrl <$> getItem storage urlKey
initialUser <- fromMaybe defaultUser <$> getItem storage userKey
initialPassword <- fromMaybe defaultPassword <$> getItem storage passwordKey
initialState <- fromMaybe LoggedOut . (readMaybe =<<) <$> getItem storage loginStateKey
stateEvent <- stateWidget (initialState, initialUrl, initialUser, initialPassword) stateTransition
R.performEvent_ $
stateEvent <&> \(loginState, url, user, password) -> do
setItem storage loginStateKey (show loginState :: String)
setItem storage urlKey url
setItem storage userKey user
setItem storage passwordKey password
fmap backendFromState <$> R.holdDyn (initialState, initialUrl, initialUser, initialPassword) stateEvent
stateTransition (loginState, url, user, password) = case loginState of
LoggedOut -> D.divClass "loginDialog" $ do
D.text "Host:"
urlInput <- textInput False url
D.el "br" pass
D.text "User:"
userNameInput <- textInput False user
D.el "br" pass
D.text "Password:"
passwordInput <- textInput True password
D.el "br" pass
saveButton <- button "selector" $ D.text "Login"
let saveEvent =
liftA3
(LoggedIn,,,)
(inputValue urlInput)
(inputValue userNameInput)
(inputValue passwordInput)
R.<@ fold (saveButton : (D.keypress D.Enter <$> [urlInput, userNameInput, passwordInput]))
pure (saveEvent, saveEvent)
LoggedIn -> do
D.text [i|#{user} @ #{url}|]
logoutEvent <-
((LoggedOut, url, user, password) <$)
<$> button "selector" (D.text "Logout")
pure (logoutEvent, logoutEvent)
backendFromState (LoggedIn, url, user, password) = Just $ RemoteBackend url user password
backendFromState (LoggedOut, _, _, _) = Nothing
withBackend :: CloseEvent t -> Maybe (RemoteBackend Text) -> m (Maybe (StateProvider t m))
withBackend innerCloseEvent = traverse (fmap makeStateProvider . webClientSocket innerCloseEvent)
-- TODO: Get UI Config from Server
textInput hidden defaultValue =
D.inputElement $
D.def
& lensVL D.inputElementConfig_initialValue
.~ defaultValue
& lensVL (D.inputElementConfig_elementConfig . D.elementConfig_initialAttributes)
.~ if hidden then "type" D.=: "password" else mempty
inputValue = R.current . D._inputElement_value
data WebSocketState = WebSocketError Text | Connecting deriving stock (Show)
getStorage :: WidgetJSM t m => m Storage
getStorage = getLocalStorage =<< currentWindowUnchecked
webClientSocket ::
WidgetJSM t m => CloseEvent t -> RemoteBackend Text -> m (ClientSocket t m)
webClientSocket closeEvent backend@RemoteBackend{url, user, password} = do
refreshEvent <- button "selector" $ D.text "Refresh Tasks"
let wsUrl = maybe "ws://localhost:8000" ("ws" <>) $ stripPrefix "http" url -- TODO: Warn user about missing http
socketString = [i|#{wsUrl}/socket?username=#{user}&password=#{password}|]
clientSocket socketRequestEvent = do
let socketConfig =
D.def
& (lensVL D.webSocketConfig_send .~ (toList <$> (socketRequestEvent <> (one AllTasks <$ refreshEvent))))
. (lensVL D.webSocketConfig_reconnect .~ True)
. (lensVL D.webSocketConfig_close .~ ((3000, "Client unloaded websocket.") <$ un closeEvent))
storage <- getStorage
socket <- D.jsonWebSocket @SocketRequest @SocketMessage socketString socketConfig
let messages = R.fmapMaybe id $ socket ^. lensVL D.webSocket_recv
err = [i|Connection to kassandra server not possible. Check network connection, server url and credentials. #{backend}|]
close = Just err <$ socket ^. lensVL D.webSocket_close
open = Nothing <$ socket ^. lensVL D.webSocket_open
messageParseFail = maybe (Just "Failed to parse SocketMessage JSON") (const Nothing) <$> socket ^. lensVL D.webSocket_recv
nextStateEvent = R.leftmost [messageParseFail, close, open]
D.dynText . fmap (fromMaybe "") =<< R.holdDyn (Just "Websocket Connecting ...") nextStateEvent
let taskUpdates = (^? #_TaskUpdates) <$?> messages
mapKey = [i|TaskMap#{wsUrl}#{user}|] :: String
foldTasksToMap tasks currentMap = foldr (\task theMap -> insert (task ^. #uuid) task theMap) currentMap tasks
asyncSaveStorage = void . liftJSM . D.forkJSM . setItem storage mapKey . decodeUtf8 @Text . encode
taskMap :: Map UUID Task <- maybeToMonoid . (decode . encodeUtf8 @Text =<<) <$> getItem storage mapKey
tasksToSave <- R.foldDyn foldTasksToMap taskMap taskUpdates
-- TODO: Saving the whole Map everytime is probably quite inefficient, we should try to reduce writes by a smarter saving scheme
void . R.performEventAsync $ const . asyncSaveStorage <$> R.updated tasksToSave
ev <- R.getPostBuild
let cachedTasks = R.fmapMaybe (nonEmptySeq . fromList) $ elems taskMap <$ ev
pure . pure $ R.leftmost [messages, (#_TaskUpdates #) <$> cachedTasks]
pure clientSocket

View file

@ -0,0 +1,18 @@
module Kassandra.SelectorWidget (
backendSelector,
) where
import Kassandra.Config (NamedBackend (..))
import Kassandra.Types (Widget)
import qualified Reflex as R
import qualified Reflex.Dom as D
backendSelector ::
Widget t m => NonEmpty (NamedBackend a) -> m (R.Dynamic t (NamedBackend a))
backendSelector backends = D.el "div" $ do
buttons <- forM backends $ \backend -> do
fmap ((backend <$) . D.domEvent D.Click . fst)
. D.elClass' "a" "selector"
. D.text
$ name backend
R.holdDyn (head backends) $ R.leftmost (toList buttons)

View file

@ -0,0 +1,182 @@
module Kassandra.Sorting (
sortTasks,
saveSorting,
SortPosition (SortPosition),
SortMode (SortModePartof, SortModeTag),
) where
import qualified Data.Aeson as Aeson
import Data.Scientific (toRealFloat)
import Data.Set (member)
import Kassandra.Types (TaskInfos)
import qualified Reflex as R
import Relude.Extra.Foldable1 (maximum1)
import qualified Taskwarrior.Task as Task
import qualified Data.Sequence as Seq
data SortMode = SortModePartof UUID | SortModeTag Task.Tag
deriving stock (Show, Eq, Ord, Generic)
makePrismLabels ''SortMode
data SortState = HasSortPos Double | WillWrite {iprev :: Double, dprev :: Int, inext :: Double, dnext :: Int}
deriving stock (Eq, Show, Ord, Read, Generic)
makePrismLabels ''SortState
declareFieldLabels
[d|
data SortPosition t = SortPosition
{ mode :: R.Behavior t SortMode
, list :: R.Behavior t (Seq Task)
, before :: R.Behavior t (Maybe UUID)
}
|]
sortTasks :: SortMode -> Seq TaskInfos -> Seq TaskInfos
sortTasks mode = Seq.sortOn (getSortOrder mode . (^. #task))
getSortOrder :: SortMode -> Task -> Maybe Double
getSortOrder mode = valToNumber <=< (^. #uda % at (sortFieldName mode))
valToNumber :: Aeson.Value -> Maybe Double
valToNumber = \case
Aeson.Number a -> _Just # toRealFloat a
Aeson.String a -> readMaybe $ a ^. unpacked
_ -> Nothing
sortFieldName :: SortMode -> Text
sortFieldName = \case
SortModeTag tag -> "kassandra_tag_pos_" <> tag
SortModePartof _ -> "kassandra_partof_pos"
setSortOrder :: SortMode -> Double -> Task -> Task
setSortOrder mode val = #uda %~ at (sortFieldName mode) ?~ Aeson.toJSON val
taskInList :: SortMode -> Task -> Bool
taskInList (SortModePartof uuid) = (Just uuid ==) . (^. #partof)
taskInList (SortModeTag tag) = member tag . Task.tags
insertInList :: SortMode -> Task -> Task
insertInList (SortModePartof uuid) = #partof ?~ uuid
insertInList (SortModeTag tag) = #tags %~ addTag
where
addTag tags
| tag `member` tags = tags
| otherwise = tags <> one tag
unSetSortOrder :: SortMode -> Task -> Task
unSetSortOrder mode = #uda %~ sans (sortFieldName mode)
-- ! Returns a list of tasks for which the UDA attributes need to be changed to reflect the order of the given list.
sortingChanges :: SortMode -> Seq Task -> Seq Task
sortingChanges mode list =
let addState = addSortState (getSortOrder mode)
assureSort delta =
applyUntil
(addState . unSetWorstUnsorted (unSetSortOrder mode) delta)
(tasksSorted delta)
sortedList = assureSort 0 $ addState list
finalList
| tasksSorted minDist sortedList = sortedList
| otherwise = assureSort minTouchedDist sortedList
getWrite (task, sortState)
| has #_WillWrite sortState || not (taskInList mode task) =
Just . setSortOrder mode (newValue sortState) . insertInList mode $ task
| otherwise =
Nothing
in mapMaybe getWrite finalList
applyUntil :: (a -> a) -> (a -> Bool) -> a -> a
applyUntil f condition x
| condition x = x
| otherwise = applyUntil f condition (f x)
minOrder, maxOrder, minDist, minTouchedDist :: Double
minOrder = -1
maxOrder = - minOrder
minDist = 10 ** (-6)
minTouchedDist = 10 ** (-3)
tasksSorted :: Show a => Double -> Seq (a, SortState) -> Bool
tasksSorted = isSortedOn (newValue . (^. _2))
isSortedOn :: Show a => (a -> Double) -> Double -> Seq a -> Bool
isSortedOn f delta = \case
IsEmpty -> True
IsNonEmpty (_ :<|| IsEmpty) -> True
IsNonEmpty (x :<|| IsNonEmpty (y :<|| ys)) -> f x + delta < f y && isSortedOn f delta (y <| ys)
unSetWorstUnsorted :: (a -> a) -> Double -> Seq (a, SortState) -> Seq a
unSetWorstUnsorted _ _ IsEmpty = mempty
unSetWorstUnsorted unSet delta (IsNonEmpty (x :<|| xs))
| ((fst <$>) -> fine, (fst <$>) -> IsNonEmpty (a :<|| alsoFine)) <-
Seq.breakl
((worst ==) . snd)
(toSeq badnesses) =
fine <> (unSet a <| alsoFine)
| otherwise =
error "Assumed wrong invariant in unSetWorstUnsorted" -- The list of badnesses has to contain its maximum
where
badnesses = go mempty (x :<|| xs) <&> \(a, _, badness) -> (a, badness)
worst = maximum1 $ snd <$> badnesses
go :: Seq Double -> NESeq (a, SortState) -> NESeq (a, Double, Int)
go before ((a, s@(newValue -> value)) :<|| ys)
| has #_WillWrite s = (a, value, 0) :<|| rest
| otherwise = (a, value, foundBefore + foundAfter) :<|| rest
where
foundBefore = length $ filter (value - delta <=) before
(foundAfter, rest) = maybe (0, mempty) countThroughRest (nonEmptySeq ys)
countThroughRest list =
(length $ filter (\(_, int, _) -> value + delta >= int) zs, zs)
where
zs = toSeq (go (value <| before) list)
newValue :: SortState -> Double
newValue (HasSortPos x) = x
newValue (WillWrite iprev (fromIntegral -> dprev) inext (fromIntegral -> dnext))
| dprev + dnext == 0 = iprev
| otherwise = iprev + ((inext - iprev) * dprev / (dprev + dnext))
sortStateNext :: SortState -> (Double, Int)
sortStateNext (HasSortPos a) = (a, 0)
sortStateNext (WillWrite _ _ int d) = (int, d)
addSortState :: forall a. (a -> Maybe Double) -> Seq a -> Seq (a, SortState)
addSortState f = go (minOrder, 0)
where
go :: (Double, Int) -> Seq a -> Seq (a, SortState)
go (iprev, dprev) list
| IsEmpty <- list = mempty
| IsNonEmpty (x :<|| xs) <- list
, Just int <- f x =
(x, HasSortPos int) <| go (int, 0) xs
| IsNonEmpty (x :<|| xs) <- list
, next@(IsNonEmpty ((_, sortStateNext -> (inext, dnext)) :<|| _)) <-
go
(iprev, dprev + 1)
xs =
(x, WillWrite iprev (dprev + 1) inext (dnext + 1)) <| next
| IsNonEmpty (x :<|| _) <- list =
one (x, WillWrite iprev (dprev + 1) maxOrder 1)
insertBefore :: Seq Task -> Seq Task -> Maybe UUID -> Seq Task
insertBefore list toInsert = \case
Just uuid ->
let (front, back) = Seq.breakl ((== uuid) . (^. #uuid)) cleanList
in front <> toInsert <> back
Nothing -> cleanList <> toInsert
where
cleanList = filter ((`notElem` (toInsert ^. mapping #uuid)) . (^. #uuid)) list
type InsertEvent t = R.Event t (NESeq Task, Maybe UUID)
saveSorting ::
R.Reflex t =>
R.Behavior t SortMode ->
R.Behavior t (Seq Task) ->
InsertEvent t ->
R.Event t (NESeq Task)
saveSorting modeB listB =
R.fmapMaybe nonEmptySeq
. R.attachWith attach ((,) <$> modeB <*> listB)
where
attach (mode, list) (toSeq -> tasks, before) =
sortingChanges mode $ insertBefore list tasks before

View file

@ -0,0 +1,104 @@
module Kassandra.State (
makeStateProvider,
StateProvider,
ClientSocket,
DataState (..),
) where
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Sequence as Seq
import Kassandra.Api (SocketMessage (..), SocketRequest (..))
import Kassandra.Calendar
import Kassandra.Config (UIConfig)
import Kassandra.Types (
DataChange,
TaskInfos (..),
TaskState,
WidgetIO,
)
import qualified Reflex as R
import qualified Reflex.Dom as D
import Taskwarrior.IO (createTask)
getParents :: HashMap UUID Task -> UUID -> Seq UUID
getParents tasks = go mempty (\uuid -> (^. #partof) =<< tasks ^. at uuid)
where
go :: (Eq a, Show a) => Seq a -> (a -> Maybe a) -> a -> Seq a
go accu f x
| x `elem` accu = mempty
| Just next <- f x = next <| go (x <| accu) f next
| otherwise = mempty
data DataState = DataState
{ taskState :: TaskState
, uiConfig :: UIConfig
, calendarData :: Seq CalendarEvent
}
makeLabels ''DataState
type StateProvider t m = R.Event t (NESeq DataChange) -> m (R.Dynamic t DataState)
type ClientSocket t m = R.Event t (NESeq SocketRequest) -> m (R.Dynamic t (R.Event t SocketMessage))
makeStateProvider :: forall t m. WidgetIO t m => ClientSocket t m -> StateProvider t m
makeStateProvider clientSocket dataChangeEvents = do
let fanEvent :: (b -> Maybe a) -> R.Event t (NESeq b) -> R.Event t (NESeq a)
fanEvent decons = R.fmapMaybe (nonEmptySeq . mapMaybe decons . toSeq)
createTaskEvent = fanEvent (^? #_CreateTask) dataChangeEvents
changeTaskEvent = fanEvent (^? #_ChangeTask) dataChangeEvents
setListEvent = fanEvent (^? #_SetEventList) dataChangeEvents
changesFromCreateEvents <- createToChangeEvent createTaskEvent
let localChanges = changeTaskEvent <> changesFromCreateEvents
rec remoteChanges <- R.switchDyn <$> clientSocket (fold eventsToSend)
let connectedEvent = (^? #_ConnectionEstablished) <$?> remoteChanges
eventsToSend =
[ one . ChangeTasks <$> localChanges
, AllTasks :<|| fromList [CalenderRequest, UIConfigRequest] <$ connectedEvent
, uncurry SetCalendarList <<$>> setListEvent
]
let errorEvent = (^? #_SocketError) <$?> remoteChanges
calendarData <- R.holdDyn mempty $ Seq.sortBy sortEvents <$> ((^? #_CalendarEvents) <$?> remoteChanges)
uiConfig <- R.holdDyn D.def $ (^? #_UIConfigResponse) <$?> remoteChanges
D.dynText =<< R.foldDyn (<>) "" errorEvent
tasksStateDyn <- buildTaskInfosMap <<$>> holdTasks (localChanges <> ((^? #_TaskUpdates) <$?> remoteChanges))
pure (DataState <$> tasksStateDyn <*> uiConfig <*> calendarData)
createToChangeEvent :: WidgetIO t m => D.Event t (NESeq (Text, Task -> Task)) -> m (D.Event t (NESeq Task))
createToChangeEvent = R.performEvent . fmap (liftIO . mapM (\(desc, properties) -> properties <$> createTask desc))
holdTasks :: WidgetIO t m => R.Event t (NESeq Task) -> m (R.Dynamic t (HashMap UUID Task))
holdTasks = R.foldDyn foldTasks mempty
foldTasks :: Foldable t => t Task -> HashMap UUID Task -> HashMap UUID Task
foldTasks = flip (foldr (\task -> HashMap.insert (task ^. #uuid) task))
buildChildrenMap :: HashMap a Task -> HashMap UUID (Seq a)
buildChildrenMap =
HashMap.fromListWith (<>)
. mapMaybe (\(uuid, task) -> (,pure uuid) <$> task ^. #partof)
. HashMap.toList
buildDependenciesMap :: HashMap a Task -> HashMap UUID (Seq a)
buildDependenciesMap = HashMap.fromListWith (<>) . (HashMap.toList >=> \(uuid, task) -> (,pure uuid) <$> toList (task ^. #depends))
buildTaskInfosMap :: HashMap UUID Task -> TaskState
buildTaskInfosMap tasks =
HashMap.mapWithKey foldTaskMap tasks
where
foldTaskMap uuid task =
TaskInfos
{ task = task
, children = HashMap.lookupDefault mempty uuid childrenMap
, parents = getParentTasks uuid
, revDepends = HashMap.lookupDefault mempty uuid dependenciesMap
, blocked = isBlockedTask task
}
isBlockedTask = isBlocked tasks
getParentTasks = getParents tasks
dependenciesMap = buildDependenciesMap tasks
childrenMap = buildChildrenMap tasks
isBlocked :: HashMap UUID Task -> Task -> Bool
isBlocked tasks task = any isActive . mapMaybe (`HashMap.lookup` tasks) . toList $ task ^. #depends
where
isActive t = has (#status % #_Pending) t

View file

@ -0,0 +1,409 @@
module Kassandra.TaskWidget (
taskTreeWidget,
taskList,
uuidWidget,
) where
import qualified Data.HashSet as HashSet
import qualified Data.Sequence as Seq
import qualified Data.Sequence.NonEmpty as NESeq
import qualified Data.Set as Set
import qualified Data.Text as Text
import Kassandra.BaseWidgets (
button,
icon, br
)
import Kassandra.Config (DefinitionElement)
import Kassandra.Debug (
Severity (..),
log,
)
import Kassandra.DragAndDrop (
childDropArea,
taskDropArea,
tellSelected,
)
import Kassandra.ReflexUtil (listWithGaps)
import Kassandra.Sorting (
SortMode (SortModePartof),
SortPosition (SortPosition),
sortTasks,
)
import Kassandra.TextEditWidget (
createTextWidget,
lineWidget,
)
import Kassandra.TimeWidgets (dateSelectionWidget)
import Kassandra.Types (
AppState,
Have,
StandardWidget,
TaskInfos,
TaskTreeState,
TaskTreeStateChange,
TaskTreeWidget,
ToggleEvent (ToggleEvent),
getAppState,
getExpandedTasks,
getIsExpanded,
getSelectState,
getTime,
)
import Kassandra.Util (lookupTaskM, lookupTasksDynM, lookupTasksM, stillTodo, tellNewTask, tellTask, tellToggle)
import qualified Reflex as R
import Reflex.Dom ((=:))
import qualified Reflex.Dom as D
import qualified Taskwarrior.Status as Status
import Taskwarrior.UDA (UDA)
type TaskWidget t m r e = (TaskTreeWidget t m r e, HaveTask m r)
type HaveTask m r = Have m r TaskInfos
instance LabelOptic "taskInfos" A_Lens (a, TaskInfos) (a, TaskInfos) TaskInfos TaskInfos where
labelOptic = _2
getTaskInfos :: HaveTask m r => m TaskInfos
getTaskInfos = ask ^. mapping typed
getChildren :: TaskWidget t m r e => m (R.Dynamic t (Seq TaskInfos))
getChildren = getTaskInfos ^. mapping #children >>= lookupTasksM
taskTreeWidget ::
forall t m r e. StandardWidget t m r e => R.Dynamic t TaskInfos -> m ()
taskTreeWidget taskInfosD = do
log Debug "Creating Tasktree Widget"
(appState :: AppState t) <- getAppState
rec treeState <-
R.foldDyn
( flip $
foldr
( \case
ToggleEvent uuid False -> HashSet.delete uuid
ToggleEvent uuid True -> HashSet.insert uuid
) ::
NESeq ToggleEvent -> HashSet UUID -> HashSet UUID
)
mempty
treeStateChanges
(_, events :: R.Event t (NESeq TaskTreeStateChange)) <-
R.runEventWriterT $
runReaderT (taskWidget taskInfosD) (appState, treeState)
let (appStateChanges, treeStateChanges) =
R.fanThese $ partitionEithersNESeq <$> events
R.tellEvent (fmap (_Typed #) <$> appStateChanges)
taskWidget ::
forall t m r e. (TaskTreeWidget t m r e) => R.Dynamic t TaskInfos -> m ()
taskWidget taskInfos' = D.divClass "task" $ do
taskInfosD <- R.holdUniqDyn taskInfos'
appState <- getAppState
treeState <- ask ^. mapping typed
D.dyn_ $ taskInfosD <&> \taskInfos -> runReaderT widgets (appState, taskInfos, treeState)
childrenWidget taskInfosD
where
widgets :: ReaderT (AppState t, TaskInfos, TaskTreeState t) m ()
widgets = D.divClass "uppertask" $ do
D.divClass "statusWrapper" statusWidget
D.divClass "righttask" $ do
collapseButton
descriptionWidget
tagsWidget
waitWidget
dueWidget
pathWidget
parentButton
dependenciesWidget
addChildWidget
deleteButton
completedWidget
selectWidget
dropChildWidget
pathWidget :: TaskWidget t m r e => m ()
pathWidget = do
parents <- getTaskInfos ^. mapping #parents >>= lookupTasksM ^. mapping (mapping (mapping (mapping #description)))
D.dyn_ $ flip whenJust showPath . nonEmptySeq <$> parents
where
showPath :: TaskWidget t m r e => NESeq Text -> m ()
showPath parents = D.elClass "span" "parentPath" $ do
br
makePath parents
makePath :: (TaskWidget t m r e) => NESeq Text -> m ()
makePath = D.elClass "span" "path" . D.text . Text.intercalate "" . toList . NESeq.reverse
dependenciesWidget :: (TaskWidget t m r e) => m ()
dependenciesWidget = do
taskInfos <- getTaskInfos
revDepends <- filter stillTodo <<$>> lookupTasksM (taskInfos ^. #revDepends)
depends <- filter stillTodo <<$>> (lookupTasksM . toList) (taskInfos ^. #depends)
D.dyn_ $
whenNotNull <$> depends
<*> pure
( \ds -> do
br
D.text "dependencies: "
forM_ ds $ \d -> do
makeOwnPath d
deleteEvent <- button "edit" $ icon "" "delete"
tellTask $ removeDep (taskInfos ^. #task) d <$ deleteEvent
br
)
D.dyn_ $
whenJust . nonEmptySeq <$> revDepends
<*> pure
( \rds -> do
br
D.text "reverse dependencies: "
forM_ rds $ \rd -> do
makeOwnPath rd
deleteEvent <- button "edit" $ icon "" "delete"
tellTask $ removeDep (rd ^. #task) taskInfos <$ deleteEvent
br
)
where
removeDep ::
( Is k1 A_Getter
, Is k2 A_Setter
, LabelOptic "depends" k2 s1 t (Set UUID) (Set UUID)
, LabelOptic "uuid" k1 s2 s2 UUID UUID
) =>
s1 ->
s2 ->
t
removeDep task dependency =
#depends %~ Set.filter (dependency ^. #uuid /=) $ task
makeOwnPath :: (TaskWidget t m r e) => TaskInfos -> m ()
makeOwnPath task =
D.dyn_
. fmap (makePath . (\ps -> task ^. #description :<|| ps))
=<< lookupTasksM (task ^. #parents) ^. mapping (mapping (mapping #description))
dropChildWidget :: (TaskWidget t m r e) => m ()
dropChildWidget = do
taskInfos <- getTaskInfos
childrenD <- getChildren
showIcon <- fmap not <$> getIsExpanded (taskInfos ^. #uuid)
D.dyn_ $
when <$> showIcon
<*> pure
( childDropArea
( SortPosition
(SortModePartof <$> taskInfos ^. #uuid % to R.constant)
(childrenD ^. mapping (mapping #task) % #current)
(R.constant Nothing)
)
( R.constDyn
(taskInfos ^. #uuid <| taskInfos ^. #parents <> taskInfos ^. #children)
)
$ icon "dropHere" "move_to_inbox"
)
taskDropArea
(taskInfos ^. #uuid % to (R.constDyn . one))
(icon "dropHere plusOne" "block")
$ fmap
( \dependencies ->
one $
#depends
%~ Set.union (Set.fromList $ toList $ (^. #uuid) <$> dependencies)
$ taskInfos
^. #task
)
taskDropArea
(taskInfos ^. #uuid % to (R.constDyn . one))
(icon "dropHere plusTwo" "schedule")
$ fmap (fmap ((#depends %~ Set.insert (taskInfos ^. #uuid)) . (^. #task)))
tagsWidget :: forall t m r e. TaskWidget t m r e => m ()
tagsWidget = do
task <- getTaskInfos ^. mapping #task
forM_ (task ^. #tags) $ \tag -> D.elClass "span" "tag" $ do
D.text tag
deleteEvent <- button "edit" $ icon "" "delete"
tellTask $ (#tags %~ Set.filter (tag /=) $ task) <$ deleteEvent
tagEvent <- createTextWidget . button "edit" $ icon "" "add_box"
tellTask $ (\tag -> #tags %~ Set.insert tag $ task) <$> tagEvent
getNewUDA :: forall t m r e. TaskWidget t m r e => m UDA
getNewUDA = one . ("partof",) . toJSON <$> getTaskInfos ^. mapping #uuid
addChildWidget :: TaskWidget t m r e => m ()
addChildWidget = do
descriptionEvent <- createTextWidget . button "edit" $ icon "" "add_task"
newUDA <- getNewUDA
tellNewTask $ (,#uda .~ newUDA) <$> descriptionEvent
childrenWidget :: forall t m r e. TaskTreeWidget t m r e => R.Dynamic t TaskInfos -> m ()
childrenWidget taskInfosD = do
expandedTasks <- getExpandedTasks
showChildren <-
R.holdUniqDyn $
R.zipDynWith HashSet.member (taskInfosD ^. mapping #uuid) expandedTasks
D.dyn_ $ showOptional <$> showChildren
where
showOptional :: Bool -> m ()
showOptional x = when x $ do
children <-
R.holdUniqDyn . fmap (filter stillTodo) =<< lookupTasksDynM
=<< R.holdUniqDyn
(taskInfosD ^. mapping #children)
let sortModeD = SortModePartof <$> taskInfosD ^. mapping #uuid
blacklist <-
R.holdUniqDyn $
liftA2 (<|) (taskInfosD ^. mapping #uuid) (taskInfosD ^. mapping #parents)
sortedList <- R.holdUniqDyn $ sortTasks <$> sortModeD <*> children
D.divClass "children" $
taskList (sortModeD ^. #current) sortedList blacklist taskWidget
taskList ::
StandardWidget t m r e =>
R.Behavior t SortMode ->
R.Dynamic t (Seq TaskInfos) ->
R.Dynamic t (Seq UUID) ->
(R.Dynamic t TaskInfos -> m ()) ->
m ()
taskList mode tasksD blacklistD elementWidget =
listWithGaps (uuidWidget elementWidget . pure) gapWidget uuidsD
where
gapWidget pairD =
childDropArea
(partialSortPosition (snd <$> R.current pairD))
((maybesToSeq <$> pairD) <> blacklistD)
$ icon "dropHere above" "forward"
maybesToSeq (a, b) = fromList (toList a <> toList b)
partialSortPosition = SortPosition mode (tasksD ^. mapping (mapping #task) % #current)
uuidsD = tasksD ^. mapping (mapping #uuid)
uuidWidget :: StandardWidget t m r e => (R.Dynamic t TaskInfos -> m ()) -> R.Dynamic t UUID -> m ()
uuidWidget widget uuid = do
maybeCurrentTaskD <- R.maybeDyn =<< R.holdUniqDyn =<< lookupTaskM uuid
D.dyn_ $
maybe
(D.dynText $ (\u -> [i|Task #{u} not found.|]) <$> uuid)
widget
<$> maybeCurrentTaskD
waitWidget :: forall t m r e. TaskWidget t m r e => m ()
waitWidget = do
task <- getTaskInfos ^. mapping #task
event <- getTaskInfos >>= ((^. #wait) >>> dateSelectionWidget "wait")
tellTask $ flip (#wait .~) task <$> event
dueWidget :: TaskWidget t m r e => m ()
dueWidget = do
task <- getTaskInfos ^. mapping #task
event <- dateSelectionWidget "due" $ task ^. #due
tellTask $ flip (#due .~) task <$> event
selectWidget :: TaskWidget t m r e => m ()
selectWidget = do
uuid <- getTaskInfos ^. mapping (#task % #uuid)
(dragEl, _) <- D.elClass' "span" "button" $ icon "" "filter_list"
selectStateB <- toggleContainUUID uuid <<$>> R.current <$> getSelectState
tellSelected $ R.tag selectStateB (D.domEvent D.Click dragEl)
where
toggleContainUUID :: UUID -> Seq DefinitionElement -> Seq DefinitionElement
toggleContainUUID ((#_ListElement % #_TaskwarriorTask #) -> entry) selectedTasks =
Seq.findIndexL (== entry) selectedTasks & maybe (selectedTasks |> entry) (`Seq.deleteAt` selectedTasks)
descriptionWidget :: TaskWidget t m r e => m ()
descriptionWidget = do
task <- getTaskInfos ^. mapping #task
event <- lineWidget $ task ^. #description
tellTask $ flip (#description .~) task <$> event
tellStatusByTime ::
TaskWidget t m r e => ((UTCTime -> Status) -> R.Event t a -> m ())
tellStatusByTime handler ev = do
time <- getTime
tellStatus $ handler . zonedTimeToUTC <$> R.tag (R.current time) ev
tellStatus :: TaskWidget t m r e => R.Event t Status -> m ()
tellStatus ev = do
task <- getTaskInfos ^. mapping #task
tellTask $ flip (#status .~) task <$> ev
parentButton :: forall t m r e. TaskWidget t m r e => m ()
parentButton = do
task <- getTaskInfos ^. mapping #task
when (isn't (#partof % _Nothing) task) $ do
event <- button "edit" (icon "" "layers_clear")
tellTask $ (#partof .~ Nothing $ task) <$ event
deleteButton :: forall t m r e. TaskWidget t m r e => m ()
deleteButton = do
task <- getTaskInfos ^. mapping #task
deleteWidget $ task ^. #status
where
deleteWidget :: Status -> m ()
deleteWidget (Status.Deleted time) = do
event <- dateSelectionWidget "deleted" $ Just time
tellStatus $ maybe Status.Pending Status.Deleted <$> event
deleteWidget _ =
button "edit" (icon "" "delete") >>= tellStatusByTime Status.Deleted
completedWidget :: forall t m r e. TaskWidget t m r e => m ()
completedWidget = do
status <- getTaskInfos ^. mapping (#task % #status)
whenJust (status ^? #_Completed) $ \time -> do
event <- dateSelectionWidget "completed" $ Just time
tellStatus $ maybe Status.Pending Status.Completed <$> event
statusWidget :: forall t m r e. TaskWidget t m r e => m ()
statusWidget = do
status <- getTaskInfos <&> (\t -> (t ^. #status, t ^. #blocked))
widget . widgetState $ status
where
widget :: (Text, Text, Maybe (Text, Text, UTCTime -> Status.Status)) -> m ()
widget (iconName, showClass, handlerMay) = do
let (altIcon, handler) = case handlerMay of
Nothing -> (D.blank, const D.blank)
Just (altIconLabel, altClass, handlerPure) ->
( D.elClass "i" ("material-icons " <> altClass <> " showable") $
D.text altIconLabel
, tellStatusByTime handlerPure . D.domEvent D.Mouseup
)
(el, ()) <- D.elAttr' "div" ("class" =: "checkbox") $ do
D.elClass
"i"
( "material-icons " <> showClass
<> if isJust handlerMay
then " hideable"
else ""
)
$ D.text iconName
altIcon
handler el
widgetState ::
(Status, Bool) ->
(Text, Text, Maybe (Text, Text, UTCTime -> Status.Status))
widgetState = \case
(Status.Pending, False) ->
("done", "hide", Just ("done", "grey", Status.Completed))
(Status.Pending, True) ->
("block", "show", Just ("done", "grey", Status.Completed))
(Status.Completed{}, _) ->
("done", "show", Just ("done", "hide", const Status.Pending))
(Status.Deleted{}, _) ->
("delete", "show", Just ("done", "hide", const Status.Pending))
(Status.Recurring{}, _) -> ("repeat", "show", Nothing)
collapseButton :: forall t m r e. TaskWidget t m r e => m ()
collapseButton = do
taskInfos <- getTaskInfos
hasChildren <-
R.holdUniqDyn . fmap (any (has (#task % #status % #_Pending)))
=<< lookupTasksM
(taskInfos ^. #children)
D.dyn_ $
when <$> hasChildren ?? do
open <- getIsExpanded $ taskInfos ^. #uuid
let label = \case
True -> "unfold_less"
False -> "unfold_more"
buttonEvent <-
button "slimButton" $
D.dyn_ (icon "collapse" . label <$> open)
tellToggle $ taskInfos ^. #uuid <$ buttonEvent

View file

@ -0,0 +1,57 @@
module Kassandra.TextEditWidget (
lineWidget,
createTextWidget,
enterTextWidget,
editText,
) where
import Kassandra.BaseWidgets (
button,
icon,
stateWidget,
)
import Kassandra.Types (Widget)
import qualified Reflex as R
import qualified Reflex.Dom as D
lineWidget :: Widget t m => Text -> m (R.Event t Text)
lineWidget text = enterTextWidget text (showText text)
createTextWidget :: Widget t m => m (R.Event t ()) -> m (R.Event t Text)
createTextWidget = enterTextWidget ""
enterTextWidget :: Widget t m => Text -> m (R.Event t ()) -> m (R.Event t Text)
enterTextWidget text altLabel = stateWidget False (selectWidget text altLabel)
selectWidget ::
Widget t m =>
Text ->
m (R.Event t ()) ->
Bool ->
m (R.Event t Text, R.Event t Bool)
selectWidget text _ True = do
editEvent <- editText text
pure (R.fmapMaybe id editEvent, False <$ editEvent)
selectWidget _ altLabel False = do
editEvent <- altLabel
pure (R.never, True <$ editEvent)
-- ! Takes a dynamic text and fires an event, when the user wants to edit it.
showText :: Widget t m => Text -> m (R.Event t ())
showText text = do
D.text text
button "edit slimButton" $ icon "" "edit"
-- ! Prompts the user for a text edit and fires an event, when the user confirms the result. Nothing is cancelation.
editText :: Widget t m => Text -> m (R.Event t (Maybe Text))
editText text = D.elClass "span" "activeEdit" $ do
textinput <-
D.inputElement $ D.def & lensVL D.inputElementConfig_initialValue .~ text
saveButton <- button "" $ icon "" "save"
let saveEvent =
Just
<$> R.tag
(textinput ^. to D._inputElement_value % #current)
(D.keypress D.Enter textinput <> saveButton)
cancelEvent <- button "" $ icon "" "cancel"
pure $ R.leftmost [saveEvent, Nothing <$ cancelEvent]

View file

@ -0,0 +1,91 @@
module Kassandra.TimeWidgets (
dateSelectionWidget,
) where
import Kassandra.BaseWidgets (
button,
icon,
stateWidget,
)
import Kassandra.TextEditWidget (editText)
import Kassandra.Types (
StandardWidget,
Widget,
getTime,
)
import qualified Reflex as R
import qualified Reflex.Dom as D
myFormatTime :: ZonedTime -> Text
myFormatTime = toText . formatTime defaultTimeLocale "%Y-%m-%d %H:%M"
myParseTime :: Text -> Either String LocalTime
myParseTime ((^. unpacked) -> t) =
maybeToRight ("'" <> t <> "' cannot be parsed as '%Y-%m-%d %H:%M'.")
. parseTimeM True defaultTimeLocale "%Y-%m-%d %H:%M"
$ t
inputDateWidget ::
forall t m. Widget t m => ZonedTime -> m (R.Event t (Maybe ZonedTime))
inputDateWidget time = do
textMayEvent <-
editText @t @m . myFormatTime $ time :: m (R.Event t (Maybe Text))
let cancelEvent = Nothing <$ R.ffilter isNothing textMayEvent
textEvent = R.fmapMaybe id textMayEvent
(failEvent, timeEvent) = R.fanEither $ myParseTime <$> textEvent
warning <- R.holdDyn "" $ toText <$> failEvent
D.elClass "span" "warning" $ D.dynText warning
pure $
R.leftmost
[ Just . (\t -> time{zonedTimeToLocalTime = t}) <$> timeEvent
, cancelEvent
]
dateSelectionWidget ::
forall t m r e.
StandardWidget t m r e =>
Text ->
Maybe UTCTime ->
m (R.Event t (Maybe UTCTime))
dateSelectionWidget label utcTime = do
timeZone <- zonedTimeZone <$> (R.sample . R.current =<< getTime)
let timeDyn = utcToZonedTime timeZone <$> utcTime
timeEvent <-
fmap (Just . zonedTimeToUTC)
<$> stateWidget False (selectTimeWidget label timeDyn)
deleteEvent <- deleteTime $ isJust utcTime
pure $ R.leftmost [deleteEvent, timeEvent]
where
deleteTime :: Bool -> m (R.Event t (Maybe a))
deleteTime False = pure R.never
deleteTime True = do
event <- button "edit" $ icon "" "delete"
pure $ Nothing <$ event
selectTimeWidget ::
(StandardWidget t m r e) =>
Text ->
Maybe ZonedTime ->
Bool ->
m (R.Event t ZonedTime, R.Event t Bool)
selectTimeWidget label time True = do
D.elClass "span" "" $ D.text (label <> ": ")
currentTimeDyn <- getTime
currentTime <- R.sample . R.current $ currentTimeDyn
editEvent <- inputDateWidget $ fromMaybe currentTime time
pure (R.fmapMaybe id editEvent, False <$ editEvent)
selectTimeWidget label time False = do
editEvent <- showTime label time
pure (R.never, True <$ editEvent)
showTime ::
forall t m. Widget t m => Text -> Maybe ZonedTime -> m (R.Event t ())
showTime label = maybe create showWithButton
where
showWithButton time = do
D.el "span" $ D.text label
D.text . myFormatTime $ time
button "edit" $ icon "" "edit"
create = button "edit" $ do
icon "" "add"
D.elClass "span" "edit" $ D.text label

View file

@ -0,0 +1,154 @@
module Kassandra.Types (
Widget,
WidgetIO,
WidgetJSM,
TaskInfos (..),
TaskState,
AppStateChange,
TaskTreeState,
TaskTreeStateChange,
DataChange (..),
FilterState (FilterState),
StandardWidget,
TaskTreeWidget,
ExpandedTasks,
ToggleEvent (ToggleEvent),
WriteApp,
Write,
Have,
HaveApp,
AppState (AppState),
SelectState,
getAppState,
getTasks,
getSelectState,
getTime,
getIsExpanded,
getExpandedTasks,
) where
import qualified Data.Aeson as Aeson
import Data.HashSet (member)
import Kassandra.Calendar
import Kassandra.Config (DefinitionElement, UIConfig)
import Language.Javascript.JSaddle (MonadJSM)
import qualified Reflex as R
import qualified Reflex.Dom as D
import qualified Taskwarrior.Status
import qualified Taskwarrior.Task
import Text.Show
type Widget t m =
( D.DomBuilder t m
, D.DomBuilderSpace m ~ D.GhcjsDomSpace
, MonadFix m
, R.MonadHold t m
, R.PostBuild t m
, MonadIO m
, R.TriggerEvent t m
, R.PerformEvent t m
, MonadIO (R.Performable m)
, HasCallStack
)
type WidgetJSM t m =
(MonadJSM (R.Performable m), MonadJSM m, WidgetIO t m)
type WidgetIO t m = Widget t m
data TaskInfos = TaskInfos {task :: Task, children :: Seq UUID, parents :: Seq UUID, revDepends :: Seq UUID, blocked :: Bool} deriving stock (Eq, Show, Generic)
makeLabels ''TaskInfos
type TaskState = HashMap UUID TaskInfos
data DataChange = ChangeTask Task | CreateTask Text (Task -> Task) | SetEventList Text CalendarList deriving stock (Generic)
instance Show DataChange where
show (ChangeTask task) = [i|ChangeTask (#{task})|]
show (CreateTask name _) = [i|CreateTask with name #{name}|]
show (SetEventList uid list) = [i|SetEventList #{uid} #{list}|]
makePrismLabels ''DataChange
data ToggleEvent = ToggleEvent UUID Bool deriving stock (Eq, Show, Generic)
makePrismLabels ''ToggleEvent
type SelectState = Seq DefinitionElement
type AppStateChange = Either SelectState DataChange
type TaskTreeStateChange = Either AppStateChange ToggleEvent
instance {-# OVERLAPPING #-} AsType AppStateChange AppStateChange where
_Typed = castOptic equality
instance {-# OVERLAPPING #-} AsType TaskTreeStateChange TaskTreeStateChange where
_Typed = castOptic equality
data FilterState = FilterState {deletedFade :: NominalDiffTime, completedFade :: NominalDiffTime} deriving stock (Eq, Show, Generic)
makeLabels ''FilterState
type ExpandedTasks = HashSet UUID
type TaskTreeState t = R.Dynamic t ExpandedTasks
data AppState t = AppState
{ taskState :: R.Dynamic t TaskState
, currentTime :: R.Dynamic t ZonedTime
, selectState :: R.Dynamic t SelectState
, calendarEvents :: R.Dynamic t (Seq CalendarEvent)
, uiConfig :: R.Dynamic t UIConfig
}
deriving stock (Generic)
makeLabels ''AppState
type Have m r s = (MonadReader r m, HasType s r)
type HaveApp t m r = (R.Reflex t, Have m r (AppState t))
type HaveTaskTree t m r = (Have m r (TaskTreeState t))
type Write t m e s = (R.Reflex t, R.EventWriter t (NESeq e) m, AsType s e)
type WriteApp t m e = (Write t m e AppStateChange)
type WriteTaskTree t m e = (Write t m e TaskTreeStateChange)
type StandardWidget t m r e = (Widget t m, HaveApp t m r, WriteApp t m e)
type TaskTreeWidget t m r e = (StandardWidget t m r e, HaveTaskTree t m r, WriteTaskTree t m e)
getIsExpanded ::
(Widget t m, HaveTaskTree t m r) => UUID -> m (R.Dynamic t Bool)
getIsExpanded uuid = R.holdUniqDyn . fmap (member uuid) =<< getExpandedTasks
getExpandedTasks :: HaveTaskTree t m r => m (TaskTreeState t)
getExpandedTasks = asks (^. typed)
getAppState :: (MonadReader r m, HasType (AppState t) r) => m (AppState t)
getAppState = asks (^. typed)
getTasks :: (MonadReader r m, HasType (AppState t) r) => m (R.Dynamic t TaskState)
getTasks = getAppState ^. mapping #taskState
getSelectState :: (MonadReader r m, HasType (AppState t) r) => m (R.Dynamic t SelectState)
getSelectState = getAppState ^. mapping #selectState
getTime ::
(MonadReader r m, HasType (AppState t) r) => m (R.Dynamic t ZonedTime)
getTime = getAppState ^. mapping #currentTime
deriving stock instance Generic Task
makeLabels ''Task
deriving stock instance Generic Status
makeLabels ''Status
makePrismLabels ''Status
deriving stock instance Generic (Aeson.Result a)
makePrismLabels ''Aeson.Result
instance LabelOptic "partof" A_Lens Task Task (Maybe UUID) (Maybe UUID) where
labelOptic =
lens
((^. #uda % at "partof") >=> (^? #_Success) . fromJSON)
(\task uuid -> #uda % at "partof" .~ (toJSON <$> uuid) $ task)
instance LabelOptic "description" A_Lens TaskInfos TaskInfos Text Text where
labelOptic = #task % #description
instance LabelOptic "uuid" A_Lens TaskInfos TaskInfos UUID UUID where
labelOptic = #task % #uuid
instance LabelOptic "status" A_Lens TaskInfos TaskInfos Status Status where
labelOptic = #task % #status
instance LabelOptic "tags" A_Lens TaskInfos TaskInfos (Set Text) (Set Text) where
labelOptic = #task % #tags
instance LabelOptic "partof" A_Lens TaskInfos TaskInfos (Maybe UUID) (Maybe UUID) where
labelOptic = #task % #partof
instance LabelOptic "modified" A_Lens TaskInfos TaskInfos (Maybe UTCTime) (Maybe UTCTime) where
labelOptic = #task % #modified
instance LabelOptic "wait" A_Lens TaskInfos TaskInfos (Maybe UTCTime) (Maybe UTCTime) where
labelOptic = #task % #wait
instance LabelOptic "depends" A_Lens TaskInfos TaskInfos (Set UUID) (Set UUID) where
labelOptic = #task % #depends
instance (R.Reflex t, c ~ R.Behavior t a) => LabelOptic "current" A_Getter (R.Dynamic t a) (R.Dynamic t a) c c where
labelOptic = to R.current

View file

@ -0,0 +1,91 @@
module Kassandra.Util (
tellSingleton,
tellTask,
tellToggle,
tellNewTask,
lookupTask,
lookupTaskM,
lookupTasks,
lookupTasksM,
lookupTasksDynM,
defDyn,
defDynDyn,
stillTodo,
) where
import Data.HashSet (member)
import Data.Witherable
import Kassandra.Types (
AppStateChange,
DataChange,
HaveApp,
StandardWidget,
TaskInfos,
TaskState,
TaskTreeStateChange,
TaskTreeWidget,
ToggleEvent,
Widget,
WriteApp,
getExpandedTasks,
getTasks,
)
import qualified Reflex as R
import qualified Reflex.Dom as D
stillTodo :: TaskInfos -> Bool
stillTodo = has (#status % #_Pending)
tellToggle :: TaskTreeWidget t m r e => R.Event t UUID -> m ()
tellToggle ev = do
expandedTasks <- getExpandedTasks <&> view #current
tellSingleton $
(_Typed @TaskTreeStateChange % _Typed @ToggleEvent #)
<$> R.attachWith
(\tasks uuid -> #_ToggleEvent # (uuid, not $ member uuid tasks))
expandedTasks
ev
tellTask :: WriteApp t m e => R.Event t Task -> m ()
tellTask =
tellSingleton
. fmap (_Typed @AppStateChange % _Typed @DataChange % #_ChangeTask #)
tellNewTask :: WriteApp t m e => R.Event t (Text, Task -> Task) -> m ()
tellNewTask =
tellSingleton
. fmap (_Typed @AppStateChange % _Typed @DataChange % #_CreateTask #)
tellSingleton ::
(R.Reflex t, R.EventWriter t (NESeq event) m) => R.Event t event -> m ()
tellSingleton = R.tellEvent . fmap one
lookupTask :: TaskState -> UUID -> Maybe TaskInfos
lookupTask tasks uuid = tasks ^. at uuid
lookupTasks :: Filterable f => TaskState -> f UUID -> f TaskInfos
lookupTasks tasks = mapMaybe (lookupTask tasks)
lookupTaskM ::
StandardWidget t m r e =>
R.Dynamic t UUID ->
m (R.Dynamic t (Maybe TaskInfos))
lookupTaskM uuid = getTasks <&> \tasks -> R.zipDynWith lookupTask tasks uuid
lookupTasksM :: (Filterable f, HaveApp t m r) => f UUID -> m (R.Dynamic t (f TaskInfos))
lookupTasksM = R.constDyn >>> lookupTasksDynM
lookupTasksDynM ::
(Filterable f, HaveApp t m r) => R.Dynamic t (f UUID) -> m (R.Dynamic t (f TaskInfos))
lookupTasksDynM uuids =
getTasks <&> \tasks -> flip lookupTasks <$> uuids <*> tasks
defDyn :: Widget t m => a -> R.Dynamic t (m a) -> m (R.Dynamic t a)
defDyn defVal = R.holdDyn defVal <=< D.dyn
defDynDyn ::
Widget t m =>
R.Dynamic t a ->
R.Dynamic t (m (R.Dynamic t a)) ->
m (R.Dynamic t a)
defDynDyn defDynamic = fmap join . defDyn defDynamic

View file

@ -0,0 +1,132 @@
{-# OPTIONS_GHC -Wno-deprecations #-}
module Prelude (
module Relude,
module Optics,
module Optics.TH,
module Data.Text.Optics,
partitionEithersNE,
Task,
i,
(<$?>),
(<&?>),
Aeson.ToJSON,
Aeson.FromJSON,
getZonedTime,
addUTCTime,
utcToZonedTime,
zonedTimeZone,
zonedTimeToUTC,
firstJust,
IOException,
UTCTime,
catch,
concurrently_,
forConcurrently_,
race_,
makeLabels,
Status,
MonadFix,
ZonedTime,
NominalDiffTime,
toJSON,
fromJSON,
UUID,
HasField',
field',
HasAny,
the,
HasType,
typed,
AsType,
_Typed,
AsConstructor',
_Ctor',
formatTime,
zonedTimeToLocalTime,
LocalTime,
defaultTimeLocale,
parseTimeM,
NESeq,
nonEmptySeq,
pattern IsEmpty,
pattern IsNonEmpty,
pattern (:<||),
pattern (:||>),
(|>),(<|),
toSeq,
mapMaybe,
filter,
partitionEithersNESeq
) where
import Control.Concurrent.Async (
concurrently_,
forConcurrently_,
race_,
)
import Control.Exception (
IOException,
catch,
)
import Control.Monad.Fix (MonadFix)
import Data.Aeson (
fromJSON,
toJSON,
)
import qualified Data.Aeson as Aeson
import Data.Generics.Product.Any (HasAny (the))
import Data.Generics.Product.Fields (HasField' (field'))
import Data.Generics.Product.Typed (HasType (typed))
import Data.Generics.Sum.Constructors (AsConstructor' (_Ctor'))
import Data.Generics.Sum.Typed (AsType (_Typed))
import Data.List.Extra (firstJust)
import Data.Sequence.NonEmpty hiding (filter, (|>), (<|))
import Data.Sequence ((|>),(<|))
import Data.String.Interpolate (i)
import Data.Text.Optics hiding (text)
import Data.These (partitionEithersNE, These(..))
import Data.Witherable ( mapMaybe, (<$?>), (<&?>), filter )
import Data.Time (
UTCTime,
addUTCTime,
defaultTimeLocale,
formatTime,
getZonedTime,
parseTimeM,
utcToZonedTime,
zonedTimeToUTC,
zonedTimeZone,
)
import Data.Time.Clock (NominalDiffTime)
import Data.Time.LocalTime (
LocalTime,
ZonedTime,
zonedTimeToLocalTime,
)
import Data.UUID (UUID)
import Language.Haskell.TH.Syntax (
Dec,
Name,
Q,
)
import Optics hiding ((|>), (<|))
import Optics.TH
import Relude hiding (uncons, mapMaybe, filter)
import Relude.Extra.Foldable1
import Taskwarrior.Status (Status)
import Taskwarrior.Task (Task)
instance One (NESeq a) where
type OneItem (NESeq a) = a
one = singleton
instance Foldable1 NESeq where
foldMap1 f = foldMapWithIndex (const f)
-- (lensField .~ noPrefixNamer $ fieldLabelsRules) == noPrefixFieldLabels but only in optics-th 0.2
makeLabels :: Name -> Q [Dec]
makeLabels = makeFieldLabelsWith noPrefixFieldLabels
partitionEithersNESeq :: NESeq (Either a b) -> These (NESeq a) (NESeq b)
partitionEithersNESeq = fold1 . fmap (either (This . one) (That . one))

View file

@ -0,0 +1,14 @@
{
"nixpkgs": {
"branch": "nixos-unstable",
"description": "Nix Packages collection",
"homepage": "",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f1c167688a6f81f4a51ab542e5f476c8c595e457",
"sha256": "00ac3axj7jdfcajj3macdydf9w9bvqqvgrqkh1xxr3rfi9q2fz1v",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/f1c167688a6f81f4a51ab542e5f476c8c595e457.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}

View file

@ -0,0 +1,194 @@
# This file has been generated by Niv.
let
#
# The fetchers. fetch_<type> fetches specs of type <type>.
#
fetch_file = pkgs: name: spec:
let
name' = sanitizeName name + "-src";
in
if spec.builtin or true then
builtins_fetchurl { inherit (spec) url sha256; name = name'; }
else
pkgs.fetchurl { inherit (spec) url sha256; name = name'; };
fetch_tarball = pkgs: name: spec:
let
name' = sanitizeName name + "-src";
in
if spec.builtin or true then
builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
else
pkgs.fetchzip { name = name'; inherit (spec) url sha256; };
fetch_git = name: spec:
let
ref =
if spec ? ref then spec.ref else
if spec ? branch then "refs/heads/${spec.branch}" else
if spec ? tag then "refs/tags/${spec.tag}" else
abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!";
submodules = if spec ? submodules then spec.submodules else false;
submoduleArg =
let
nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0;
emptyArgWithWarning =
if submodules == true
then
builtins.trace
(
"The niv input \"${name}\" uses submodules "
+ "but your nix's (${builtins.nixVersion}) builtins.fetchGit "
+ "does not support them"
)
{}
else {};
in
if nixSupportsSubmodules
then { inherit submodules; }
else emptyArgWithWarning;
in
builtins.fetchGit
({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg);
fetch_local = spec: spec.path;
fetch_builtin-tarball = name: throw
''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=tarball -a builtin=true'';
fetch_builtin-url = name: throw
''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=file -a builtin=true'';
#
# Various helpers
#
# https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695
sanitizeName = name:
(
concatMapStrings (s: if builtins.isList s then "-" else s)
(
builtins.split "[^[:alnum:]+._?=-]+"
((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name)
)
);
# The set of packages used when specs are fetched using non-builtins.
mkPkgs = sources: system:
let
sourcesNixpkgs =
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; };
hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
hasThisAsNixpkgsPath = <nixpkgs> == ./.;
in
if builtins.hasAttr "nixpkgs" sources
then sourcesNixpkgs
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
import <nixpkgs> {}
else
abort
''
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
add a package called "nixpkgs" to your sources.json.
'';
# The actual fetching function.
fetch = pkgs: name: spec:
if ! builtins.hasAttr "type" spec then
abort "ERROR: niv spec ${name} does not have a 'type' attribute"
else if spec.type == "file" then fetch_file pkgs name spec
else if spec.type == "tarball" then fetch_tarball pkgs name spec
else if spec.type == "git" then fetch_git name spec
else if spec.type == "local" then fetch_local spec
else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
else if spec.type == "builtin-url" then fetch_builtin-url name
else
abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
# If the environment variable NIV_OVERRIDE_${name} is set, then use
# the path directly as opposed to the fetched source.
replace = name: drv:
let
saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name;
ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
in
if ersatz == "" then drv else
# this turns the string into an actual Nix path (for both absolute and
# relative paths)
if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}";
# Ports of functions for older nix versions
# a Nix version of mapAttrs if the built-in doesn't exist
mapAttrs = builtins.mapAttrs or (
f: set: with builtins;
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
);
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1);
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
concatMapStrings = f: list: concatStrings (map f list);
concatStrings = builtins.concatStringsSep "";
# https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331
optionalAttrs = cond: as: if cond then as else {};
# fetchTarball version that is compatible between all the versions of Nix
builtins_fetchTarball = { url, name ? null, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchTarball;
in
if lessThan nixVersion "1.12" then
fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
else
fetchTarball attrs;
# fetchurl version that is compatible between all the versions of Nix
builtins_fetchurl = { url, name ? null, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchurl;
in
if lessThan nixVersion "1.12" then
fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
else
fetchurl attrs;
# Create the final "sources" from the config
mkSources = config:
mapAttrs (
name: spec:
if builtins.hasAttr "outPath" spec
then abort
"The values in sources.json should not have an 'outPath' attribute"
else
spec // { outPath = replace name (fetch config.pkgs name spec); }
) config.sources;
# The "config" used by the fetchers
mkConfig =
{ sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
, sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile)
, system ? builtins.currentSystem
, pkgs ? mkPkgs sources system
}: rec {
# The sources, i.e. the attribute set of spec name to spec
inherit sources;
# The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
inherit pkgs;
};
in
mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }

View file

@ -0,0 +1 @@
(import ./. { }).shells.ghc

View file

@ -0,0 +1,19 @@
{ pkgs ? import (import nix/sources.nix).nixpkgs { } }:
let
haskellPackages = pkgs.haskellPackages.extend (
self: super: {
kassandra = self.callCabal2nix "kassandra" ./kassandra { };
standalone = self.callCabal2nix "standalone" ./standalone { };
}
);
reflex-platform = import ./. { };
in
{
lib = haskellPackages.kassandra;
app = haskellPackages.standalone;
server = reflex-platform.exe;
android = pkgs.runCommand "kassandra-android-apk" { } ''
mkdir -p $out
cp ${reflex-platform.android.frontend}/android-app-release-unsigned.apk $out/de.maralorn.kassandra_${import ./code.nix}.apk
'';
}

5
apps/kassandra/shell.nix Normal file
View file

@ -0,0 +1,5 @@
{ pkgs ? import (import nix/sources.nix).nixpkgs { } }: let
outputs = import ./release.nix { inherit pkgs; };
in pkgs.haskellPackages.shellFor {
packages = (_: [outputs.lib outputs.app]);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
# Revision history for frontend
## 0.1.0.0 -- YYYY-mm-dd
* First version. Released on an unsuspecting world.

View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
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 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 work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
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 AGPL, see
<https://www.gnu.org/licenses/>.

View file

@ -0,0 +1,6 @@
module Main (main) where
import Kassandra.Standalone (standalone)
main :: IO ()
main = standalone

View file

@ -0,0 +1,329 @@
module Kassandra.Backend.Calendar (
getEvents,
newCache,
Cache (..),
setList,
loadCache,
saveCache,
) where
import qualified Control.Concurrent.STM as STM
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Lazy as LBS
import Data.Default (Default (def))
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Text as Text
import Data.Time (ZonedTime (ZonedTime), addDays, getCurrentTime, nominalDay, utc)
import Data.Time.Zones (
TZ,
loadLocalTZ,
loadTZFromDB,
localTimeToUTCTZ,
timeZoneForUTCTime,
)
import qualified StmContainers.Map as STM
import Streamly (IsStream, SerialT, async, asyncly, maxThreads)
import qualified Streamly.Prelude as S
import System.Directory (
XdgDirectory (XdgCache),
createDirectoryIfMissing,
getModificationTime,
getXdgDirectory,
setModificationTime,
)
import System.FilePath (dropFileName, (</>))
import System.FilePattern.Directory (getDirectoryFiles)
import Text.ICalendar (
DTEnd (DTEndDateTime, dtEndDateValue),
DTStart (DTStartDateTime, dtStartDateValue),
Date (dateValue),
DateTime (FloatingDateTime, UTCDateTime, ZonedDateTime),
Description (descriptionValue),
Location (locationValue),
OtherProperty (OtherProperty, otherName, otherValue),
Summary (summaryValue),
UID (uidValue),
VCalendar (vcEvents),
VEvent (
veDTEndDuration,
veDTStart,
veDescription,
veLocation,
veOther,
veSummary,
veUID
),
parseICalendarFile,
printICalendar,
)
import Control.Exception (onException)
import Data.Aeson (decodeStrict', encode)
import qualified DeferredFolds.UnfoldlM as UnfoldlM
import Kassandra.Calendar (
CalendarEvent (..),
CalendarList (CalendarList),
EventTime (AllDayEvent, SimpleEvent),
TZTime (TZTime),
tzTimeToUTC,
zonedDay,
)
import Kassandra.Debug (Severity (..), log)
import qualified Streamly.Data.Fold as FL
import Streamly.External.ByteString (fromArray, toArray)
import qualified Streamly.FileSystem.Handle as FS
import qualified Streamly.Internal.FileSystem.File as FSFile
import Streamly.Memory.Array as Mem (fromList)
import Streamly.Internal.Data.Array.Stream.Foreign (splitOn)
dirName :: FilePath
dirName = "/home/maralorn/.calendars/"
data FileInfo = FileInfo
{ lastRead :: UTCTime
, events :: Seq CalendarEvent
}
deriving stock (Show, Generic)
deriving anyclass (ToJSON, FromJSON)
loadCache :: Cache -> IO ()
loadCache cache = do
dir <- getCacheDir
let a =
S.drain $
readJSONStream (cache ^. #icsCache) (dir </> "fileinfo.cache")
`async` readJSONStream (cache ^. #uidCache) (dir </> "uid.cache")
catch a \(e :: IOException) -> log Warning [i|Error loading calendar Cache:#{e}|]
log Debug "Cache Loaded"
readJSONStream :: (IsStream t, Eq k, Hashable k, MonadIO (t IO), FromJSON k, FromJSON v) => STM.Map k v -> FilePath -> t IO ()
readJSONStream stmMap fileName =
FSFile.withFile fileName ReadMode $
S.fold
(foldSTMMap stmMap)
. asyncly
. S.mapMaybe (decodeStrict' . fromArray)
. splitOn 10
. S.unfold FS.readChunks
saveCache :: Cache -> IO ()
saveCache cache = do
dir <- getCacheDir
createDirectoryIfMissing True dir
let a =
S.drain $
writeJSONStream (cache ^. #icsCache) (dir </> "fileinfo.cache")
`async` writeJSONStream (cache ^. #uidCache) (dir </> "uid.cache")
catch a \(e :: IOException) -> log Warning [i|Error writing calendar Cache:#{e}|]
log Debug "Saved Cache"
writeJSONStream :: (IsStream t, MonadIO (t IO), ToJSON k, ToJSON v) => STM.Map k v -> FilePath -> t IO ()
writeJSONStream stmMap fileName =
FSFile.withFile fileName WriteMode \handle ->
liftIO $
S.fold (FS.writeChunks handle)
. asyncly
. S.intersperse (Mem.fromList [10])
. fmap (toArray . toStrict . encode)
$ streamSTMMap stmMap
streamSTMMap :: forall t k v. (MonadIO (t IO), IsStream t) => STM.Map k v -> t IO (k, v)
streamSTMMap = join . atomically . UnfoldlM.foldlM' (\x y -> pure $ S.cons y x) S.nil . STM.unfoldlM
getCacheDir :: IO FilePath
getCacheDir = getXdgDirectory XdgCache "kassandra"
foldSTMMap :: forall m k v. (Eq k, Hashable k, MonadIO m) => STM.Map k v -> FL.Fold m (k, v) ()
foldSTMMap cache = FL.foldMapM (\(k, v) -> atomically $ STM.insert v k cache)
type ICSCache = STM.Map FilePath FileInfo
type UIDCache = STM.Map Text FilePath
type TZCache = STM.Map (Maybe Text) (Maybe (Text, TZ))
data Cache = Cache
{ icsCache :: ICSCache
, tzCache :: TZCache
, uidCache :: UIDCache
} deriving (Generic)
newCache :: IO Cache
newCache = atomically $ Cache <$> STM.new <*> STM.new <*> STM.new
makeLabels ''Cache
foldToSeq :: Monad m => SerialT m a -> m (Seq a)
foldToSeq = S.foldl' (|>) mempty
setList :: Cache -> Text -> CalendarList -> IO ()
setList cache uid list = do
cachedFilename <- atomically (STM.lookup uid (cache ^. #uidCache))
cachedFilename & maybe
(log Error [i|Missing uid #{uid} in UIDCache.|])
\filename -> do
calendars <- foldToSeq (readCalendars filename)
insertList calendars & maybe
(log Error [i|Did not find Event with uid #{uid} in #{filename}.|])
\newCalendars -> do
withFile filename WriteMode \fileHandle -> do
forM_ newCalendars (LBS.hPut fileHandle . printICalendar def)
now <- getCurrentTime
setModificationTime (dropFileName filename) now
where
insertList :: Traversable m => m VCalendar -> Maybe (m VCalendar)
insertList cals = if modified then Just ret else Nothing
where
(ret, modified) = runState (insertListS cals) False
insertListS :: Traversable m => m VCalendar -> State Bool (m VCalendar)
insertListS = mapM \calendar -> do
newEvents <- forM (vcEvents calendar) \event ->
if uidValue (veUID event) == toLazy uid
then
event
{ veOther =
Set.insert (OtherProperty tasksFieldName (maskICSText $ JSON.encode list) def)
. Set.filter (not . isTasksOther)
. veOther
$ event
}
<$ put True
else pure event
pure calendar{vcEvents = newEvents}
maskICSText :: LByteString -> LByteString
maskICSText = LBS.concatMap \case
59 -> "\\;"
10 -> "\\n"
44 -> "\\,"
92 -> "\\\\"
c -> one c
unmaskICSText :: LByteString -> LByteString
unmaskICSText = maybe "" (uncurry f) . LBS.uncons
where
f 92 rest = maybe "" (uncurry w) (LBS.uncons rest)
f x rest = one x <> unmaskICSText rest
w 92 rest = "\\" <> unmaskICSText rest
w 110 rest = "\n" <> unmaskICSText rest
w 44 rest = "," <> unmaskICSText rest
w 59 rest = ";" <> unmaskICSText rest
w _ rest = unmaskICSText rest
tasksFieldName :: IsString t => t
tasksFieldName = "X-KASSANDRA-TASKS"
isTasksOther :: OtherProperty -> Bool
isTasksOther = (== tasksFieldName) . otherName
getEvents :: (MonadIO (stream IO), IsStream stream) => Cache -> stream IO CalendarEvent
getEvents cache = do
now <- liftIO getZonedTime
let calendarFiles = (dirName </>) <$> (S.fromList =<< liftIO (getDirectoryFiles dirName ["**/*.ics"]))
extractEvents = getWithCache cache (zonedTimeToUTC now)
filterRelevant = S.filter (onlyNextWeek now)
maxThreads 500 . filterRelevant $ extractEvents =<< calendarFiles
getWithCache :: (MonadIO (stream IO), IsStream stream) => Cache -> UTCTime -> FilePath -> stream IO CalendarEvent
getWithCache cache now fileName =
S.fromFoldable =<< liftIO do
infoMay <- atomically $ STM.lookup fileName (cache ^. #icsCache)
let update = do
newEvents <- foldToSeq (readEvents cache fileName)
atomically $ STM.insert (FileInfo now newEvents) fileName (cache ^. #icsCache)
pure newEvents
infoMay & maybe update \FileInfo{lastRead, events} -> do
modTime <- getModificationTime fileName
if lastRead >= modTime then pure events else update
readCalendars :: (MonadIO (stream IO), IsStream stream) => FilePath -> stream IO VCalendar
readCalendars path =
S.fromList
=<< liftIO
( parseICalendarFile def path >>= \case
Right (calendarsInFile, fmap toText -> _warnings) -> do
mapM_ (log Debug) _warnings
pure calendarsInFile
Left _parseError -> do
log Debug (toText _parseError)
pure mempty
)
readEvents :: (MonadIO (stream IO), IsStream stream) => Cache -> FilePath -> stream IO CalendarEvent
readEvents cache path = do
calendar <- readCalendars path
let uids = toStrict . fst <$> (Map.keys . vcEvents) calendar
atomically $ forM_ uids \uid -> STM.insert path uid (cache ^. #uidCache)
translateCalendar cache (tryExtractBaseDir (toText path)) calendar
tryExtractBaseDir :: Text -> Text
tryExtractBaseDir name = fromMaybe name . (viaNonEmpty last <=< viaNonEmpty init) . Text.splitOn "/" $ name
onlyNextWeek :: ZonedTime -> CalendarEvent -> Bool
onlyNextWeek (zonedTimeToUTC -> now) CalendarEvent{time}
| SimpleEvent (tzTimeToUTC -> start) (tzTimeToUTC -> end) <- time = end >= now && start <= addUTCTime (14 * nominalDay) now
onlyNextWeek now CalendarEvent{time}
| AllDayEvent startDay endDay <- time = endDay >= zonedDay now && startDay <= addDays 14 (zonedDay now)
onlyNextWeek _ _ = False
translateCalendar :: (Monad (stream IO), IsStream stream) => Cache -> Text -> VCalendar -> stream IO CalendarEvent
translateCalendar cache calendarName = translateEvent cache calendarName <=< S.fromList . Map.elems . vcEvents
translateEvent :: (Functor (stream IO), IsStream stream) => Cache -> Text -> VEvent -> stream IO CalendarEvent
translateEvent cache calendarName vEvent =
withTime <$> getTimes
where
withTime time = CalendarEvent{uid, description, todoList, time, calendarName, location, comment}
where
uid = toStrict (uidValue (veUID vEvent))
description = (maybe "" (toStrict . summaryValue) . veSummary) vEvent
todoList = find isTasksOther (veOther vEvent) >>= JSON.decode . unmaskICSText . otherValue & fromMaybe (CalendarList mempty mempty)
location = toStrict . locationValue <$> veLocation vEvent
comment = toStrict . descriptionValue <$> veDescription vEvent
-- TODO: This currently misses recurring events and
-- events with a duration configured
-- Also we are ignoring the timezone delivered with this calendar and taking our own
getTimes
| Just (DTStartDateTime start _) <- veDTStart vEvent
, Just (Left (DTEndDateTime end _)) <- veDTEndDuration vEvent =
S.yieldM . liftIO $ SimpleEvent <$> datetimeToTZTime cache start <*> datetimeToTZTime cache end
| Just (dateValue . dtStartDateValue -> start) <- veDTStart vEvent
, Just (Left (dateValue . dtEndDateValue -> end)) <- veDTEndDuration vEvent =
S.yield $ AllDayEvent start (addDays (-1) end)
| otherwise = S.nil
datetimeToTZTime :: Cache -> DateTime -> IO TZTime
datetimeToTZTime cache = \case
FloatingDateTime t -> withTZ t Nothing
UTCDateTime t -> pure $ TZTime (utcToZonedTime utc t) "UTC"
ZonedDateTime t (Just . toStrict -> tzname) -> withTZ t tzname
where
withTZ :: LocalTime -> Maybe Text -> IO TZTime
withTZ t tzname =
mkTZTime <$> getTZ (cache ^. #tzCache) tzname <*> pure t
getTZ :: TZCache -> Maybe Text -> IO (Text, TZ)
getTZ cache tzname = do
tzMay <- atomically $ do
cached <- STM.lookup tzname cache
cached & \case
Just (Just tz) -> pure (Just tz) -- Cache hit
Just Nothing -> STM.retry -- Another thread is already getting this value, wait for it.
Nothing -> STM.insert Nothing tzname cache >> pure Nothing -- We need to get this value
( tzMay & flip maybe pure do
newTz <- maybe (("Your Time",) <$> loadLocalTZ) loadTZ tzname
atomically $ STM.insert (Just newTz) tzname cache
pure newTz
)
`onException` atomically (STM.delete tzname cache)
where
loadTZ name =
((name,) <$> loadTZFromDB (toString name)) `catch` \(e :: IOException) -> do
log Debug [i|Timezone not found "#{name}" trying next.|]
log Debug [i|Timezone lookup error was: #{e}|]
getTZ cache (nextName name)
mkTZTime :: (Text, TZ) -> LocalTime -> TZTime
mkTZTime (tzname, tz) t = TZTime (ZonedTime t (timeZoneForUTCTime tz (localTimeToUTCTZ tz t))) tzname
nextName :: Text -> Maybe Text
nextName = fmap (Text.intercalate "/") . viaNonEmpty tail . filter (not . Text.null) . Text.splitOn "/"

View file

@ -0,0 +1,120 @@
module Kassandra.Config.Dhall (
DhallLoadConfig (..),
loadDhallConfig,
dhallType,
) where
import Data.Password.Argon2 (
Argon2,
PasswordHash (PasswordHash),
)
import qualified Data.UUID as UUID
import Dhall (
Decoder,
FromDhall,
auto,
autoWith,
expected,
extractError,
fromMonadic,
input,
toMonadic,
)
import qualified Dhall
import Dhall.Core (pretty)
import System.Environment ()
import Data.Either.Validation (validationToEither)
import Kassandra.Config (
AccountConfig,
DefinitionElement,
HabiticaList,
HabiticaTask,
ListItem,
LocalBackend,
NamedBackend,
NamedListQuery,
PasswordConfig,
PortConfig,
QueryFilter,
RemoteBackend,
TaskProperty,
TaskwarriorOption,
TreeOption,
UIConfig,
UIFeatures,
UserConfig,
Widget,
)
import System.Path.IO (
FsPath (FsPath),
doesFileExist,
fromFilePath,
)
instance FromDhall PasswordConfig
instance FromDhall (RemoteBackend PasswordConfig)
instance FromDhall TreeOption
instance FromDhall HabiticaTask
instance FromDhall ListItem
instance FromDhall DefinitionElement
instance FromDhall TaskProperty
instance FromDhall QueryFilter
instance FromDhall HabiticaList
instance FromDhall Widget
instance FromDhall UIFeatures
instance FromDhall UIConfig
instance FromDhall NamedListQuery
instance FromDhall PortConfig
instance FromDhall LocalBackend
instance FromDhall TaskwarriorOption
instance FromDhall UserConfig
instance FromDhall AccountConfig
instance FromDhall b => FromDhall (NamedBackend b)
postComposeMayDecoder :: Text -> (a -> Maybe b) -> Decoder a -> Decoder b
postComposeMayDecoder err f dec =
dec
{ Dhall.extract =
fromMonadic
. (maybe (toMonadic $ extractError err) Right . f <=< toMonadic . Dhall.extract dec)
}
instance FromDhall UUID where
autoWith =
postComposeMayDecoder "Text was no valid UUID" UUID.fromText . autoWith
instance FromDhall a => FromDhall (NonEmpty a) where
autoWith = postComposeMayDecoder "List was empty" nonEmpty . autoWith
instance FromDhall (PasswordHash Argon2) where
autoWith = fmap PasswordHash . autoWith
data DhallLoadConfig = DhallLoadConfig
{ envName :: Text
, defaultFile :: Text
, defaultConfig :: Text
}
deriving stock (Show, Eq, Ord)
dhallType :: forall a. FromDhall a => Text
dhallType = fromRight "" . validationToEither $ pretty <$> expected (auto @a)
loadDhallConfig :: FromDhall a => DhallLoadConfig -> Maybe Text -> IO a
loadDhallConfig loadConfig givenConfigFile = do
let defFile = defaultFile loadConfig
defConf = defaultConfig loadConfig
filename <-
firstJustM
[ pure givenConfigFile
, fmap toText <$> lookupEnv (toString $ envName loadConfig)
, doesPathExist defFile
<&> \doesExist -> if doesExist then Just defFile else Nothing
]
input auto $ maybe defConf (\name -> [i|(#{defConf}) // #{name}|]) filename
doesPathExist :: ToString a => a -> IO Bool
doesPathExist (fromFilePath . toString -> (FsPath path)) = doesFileExist path
firstJustM :: Monad m => [m (Maybe a)] -> m (Maybe a)
firstJustM [] = pure Nothing
firstJustM (a : as) = a >>= \x -> if isJust x then pure x else firstJustM as

View file

@ -0,0 +1,82 @@
module Kassandra.Standalone (
standalone,
) where
import Control.Concurrent.STM (TQueue, newTQueueIO)
import Control.Exception (SomeAsyncException, throwIO)
import Data.Typeable (typeOf)
import Kassandra.Config (NamedBackend (NamedBackend, backend, name))
import Kassandra.Css (cssAsBS)
import Kassandra.Debug (Severity (..), log, setLogLevel)
import Kassandra.LocalBackend (LocalBackendRequest)
import Kassandra.LocalBackendWidget (localBackendWidget)
import Kassandra.MainWidget (mainWidget)
import Kassandra.RemoteBackendWidget (CloseEvent (..), remoteBackendWidget)
import Kassandra.SelectorWidget (backendSelector)
import Kassandra.Standalone.Config (
StandaloneAccount (LocalAccount, RemoteAccount),
backends,
dhallTypes,
readConfig,
)
import Kassandra.Standalone.State (localBackendProvider)
import Kassandra.State (StateProvider)
import Kassandra.Types (WidgetJSM)
import Kassandra.Util (defDynDyn)
import qualified Reflex as R
import qualified Reflex.Dom as D
import Relude.Extra.Newtype (wrap)
import Say (say)
import System.Exit (ExitCode (ExitFailure))
import System.Posix.Process (exitImmediately)
-- | This function works around the fact that a reflex mainWidget does not exit when it catches an Async Exception.
exitImmediatelyOnLocalException :: IO a -> IO a
exitImmediatelyOnLocalException m = catch m catcher
where
catcher outer@(SomeException inner) = do
when (isNothing (fromException outer :: Maybe SomeAsyncException)) do
log Error [i|BackendProviderCrashed with error: #{inner} with type #{typeOf inner}|]
exitImmediately (ExitFailure 1)
throwIO inner
standalone :: IO ()
standalone = do
args <- getArgs
when (args == ["print-types"]) do
say dhallTypes
exitSuccess
hSetBuffering Prelude.stdout NoBuffering
setLogLevel $ Just Info
log Info "Started kassandra"
log Debug "Loading Config"
config <- readConfig Nothing
print config
log Debug "Loaded Config"
requestQueue <- newTQueueIO
race_ (exitImmediatelyOnLocalException (localBackendProvider requestQueue)) do
log Info "Hanging in front of main widget"
D.mainWidgetWithCss cssAsBS $ do
log Info "Entered main widget"
-- TODO: Use Config from stateProvider here
D.dyn_ . (maybe pass mainWidget <$>)
=<< standaloneWidget requestQueue
=<< backendSelector (backends config)
standaloneWidget ::
WidgetJSM t m =>
TQueue LocalBackendRequest ->
R.Dynamic t (NamedBackend StandaloneAccount) ->
m (R.Dynamic t (Maybe (StateProvider t m)))
standaloneWidget requestQueue accountDyn =
defDynDyn (R.constDyn Nothing) $
accountDyn
<&> \NamedBackend{name, backend} -> case backend of
RemoteAccount remoteAccount ->
remoteBackendWidget
(wrap $ void $ R.updated accountDyn)
remoteAccount
LocalAccount localAccount ->
localBackendWidget
requestQueue
NamedBackend{name, backend = localAccount}

View file

@ -0,0 +1,117 @@
{-# LANGUAGE DerivingStrategies #-}
module Kassandra.Standalone.Config (
readConfig,
StandaloneConfig,
StandaloneAccount (LocalAccount, RemoteAccount),
BackendConfig (..),
backends,
dhallTypes
) where
import Dhall (FromDhall)
import Kassandra.Config (
Dict,
AccountConfig,
DefinitionElement,
ListQuery,
LocalBackend,
NamedBackend,
NamedListQuery,
PasswordConfig,
PortConfig,
RemoteBackend,
TaskwarriorOption,
TreeOption,
UserConfig,
Widget, ListItem
)
import Kassandra.Config.Dhall (
DhallLoadConfig (..),
dhallType,
loadDhallConfig,
)
newtype StandaloneConfig = Config
{ backends :: NonEmpty (NamedBackend StandaloneAccount)
}
deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (FromDhall)
data StandaloneAccount = RemoteAccount {backend :: Maybe (RemoteBackend PasswordConfig)} | LocalAccount {userConfig :: UserConfig}
deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (FromDhall)
data BackendConfig = BackendConfig
{ users :: Dict AccountConfig
}
deriving (Show, Eq, Generic, FromDhall)
dhallTypes :: Text
dhallTypes =
[i|{
#{assignments}
}|]
where
types :: [(String, Text)]
types =
[ ("StandaloneAccount", dhallType @StandaloneAccount)
, ("BackendConfig", dhallType @BackendConfig)
, ("Widget", dhallType @Widget)
, ("NamedListQuery", dhallType @NamedListQuery)
, ("LocalBackend", dhallType @LocalBackend)
, ("TreeOption", dhallType @TreeOption)
, ("PortConfig", dhallType @PortConfig)
, ("TaskwarriorOption", dhallType @TaskwarriorOption)
, ("StandaloneConfig", dhallType @StandaloneConfig)
, ("PasswordConfig", dhallType @PasswordConfig)
, ("AccountConfig", dhallType @AccountConfig)
, ("DefinitionElement", dhallType @DefinitionElement)
, ("ListQuery", dhallType @ListQuery)
, ("RemoteBackend", dhallType @(RemoteBackend PasswordConfig))
, ("ListItem", dhallType @ListItem)
]
assignments =
intercalate ",\n" $ (\(name, value) -> [i|#{name} = #{value}|]) <$> types
readConfig :: Maybe Text -> IO StandaloneConfig
readConfig =
loadDhallConfig
DhallLoadConfig
{ envName = "KASSANDRA_CONFIG"
, defaultFile = "~/.config/kassandra/config.dhall"
, defaultConfig =
[i|
let
types = #{dhallTypes} in
{
backends = [
{
name = "Default local backend",
backend = types.StandaloneAccount.LocalAccount {
userConfig = {
localBackend = types.LocalBackend.TaskwarriorBackend {
createHooksOnStart = True,
hookListenPort = types.PortConfig.PortRange { min = 40000, max = 50000 },
hookSuffix = "kassandra",
removeHooksOnExit = True,
taskBin = None Text,
taskConfig = [] : List types.TaskwarriorOption,
taskDataPath = None Text,
taskRcPath = None Text
},
uiConfig = {
configuredLists = [] : List types.NamedListQuery ,
uiFeatures = {
sortInTag = False,
treeOption = types.TreeOption.NoTree
},
viewList = [ types.Widget.SearchWidget ]
}
}
}
}
]
}
|]
}

View file

@ -0,0 +1,156 @@
{-# LANGUAGE BlockArguments #-}
module Kassandra.Standalone.State (
localBackendProvider,
) where
import Control.Concurrent.STM (TQueue, readTQueue)
import Control.Concurrent.STM.TVar (stateTVar)
import Control.Monad.STM (retry)
import qualified Data.Aeson as Aeson
import qualified Data.Map as Map
import qualified Data.Sequence as Seq
import qualified Data.Sequence.NonEmpty as NESeq
import qualified Network.Simple.TCP as Net
import Say (say, sayErr)
import Streamly (
SerialT,
asyncly,
maxThreads,
parallel,
parallely,
serial,
)
import qualified Streamly.Prelude as S
import Taskwarrior.IO (getTasks, saveTasks)
import Kassandra.Api (
SocketMessage (..),
SocketRequest (..),
)
import Kassandra.Backend.Calendar (
Cache,
getEvents,
loadCache,
newCache,
saveCache,
setList,
)
import Kassandra.Config (LocalBackend, UserConfig (..))
import Kassandra.Debug (Severity (Debug), log)
import Kassandra.LocalBackend (
LocalBackendRequest (LocalBackendRequest),
alive,
requestQueue,
responseCallback,
userConfig,
)
foldToSeq :: Monad m => SerialT m a -> m (Seq a)
foldToSeq = S.foldl' (|>) mempty
waitTillFalse :: MonadIO m => TVar Bool -> m ()
waitTillFalse boolTvar = (atomically . whenM (readTVar boolTvar)) retry
concurrentWhileTrue :: TVar Bool -> IO a -> IO ()
concurrentWhileTrue boolTvar action = race_ action (waitTillFalse boolTvar)
lookupTMap :: (Ord k, MonadIO f) => k -> TVar (Map k a) -> f (Maybe a)
lookupTMap key tvarMap = Map.lookup key <$> readTVarIO tvarMap
insertOrAddTMap :: Ord k => k -> e -> TVar (Map k (Seq e)) -> STM Bool
insertOrAddTMap key entry tvarMap =
stateTVar tvarMap \theMap ->
second (\x -> Map.insert key x theMap) $
Map.lookup key theMap & \case
Just listeners -> (False, listeners |> entry)
Nothing -> (True, one entry)
type ClientMap = Map LocalBackend (Seq (TVar Bool, SocketMessage -> IO ()))
localBackendProvider :: TQueue LocalBackendRequest -> IO ()
localBackendProvider requestQueue = newTVarIO mempty >>= handleRequests requestQueue
handleRequests :: TQueue LocalBackendRequest -> TVar ClientMap -> IO ()
handleRequests requestQueue mapVar = do
cache <- newCache
let go = atomically (readTQueue requestQueue) >>= \req -> concurrently_ (handleRequest req cache mapVar) go
S.drain $
( liftIO (loadCache cache)
`serial` (asyncly . maxThreads 100 . void . getEvents) cache
`serial` liftIO (saveCache cache)
)
`parallel` liftIO go
monitorCallback :: LocalBackend -> TVar ClientMap -> NonEmpty Task -> IO ()
monitorCallback key mapVar tasks =
whenJustM (lookupTMap key mapVar) $
mapM_ (($ TaskUpdates (NESeq.fromList tasks)) . snd)
handleRequest :: LocalBackendRequest -> Cache -> TVar ClientMap -> IO ()
handleRequest req cache mapVar =
S.drain
. parallely
. S.fromFoldableM
$ [ do handleRequestsWhileAlive req cache; removeClientFromMap req mapVar
, launchOrAttachMonitor req mapVar
, responseCallback req ConnectionEstablished
, say "Client registered on backend"
]
removeClientFromMap :: MonadIO m => LocalBackendRequest -> TVar ClientMap -> m ()
removeClientFromMap req mapVar = atomically (modifyTVar' mapVar updateMap)
where
key = localBackend . userConfig $ req
filterClients = Seq.filter ((/= alive req) . fst)
setEntry newEntry clientMap
| null newEntry = Map.delete key clientMap
| otherwise = Map.insert key newEntry clientMap
updateMap clientMap =
Map.lookup key clientMap & \case
Just clients -> setEntry (filterClients clients) clientMap
Nothing -> clientMap
launchOrAttachMonitor :: LocalBackendRequest -> TVar ClientMap -> IO ()
launchOrAttachMonitor LocalBackendRequest{userConfig, alive, responseCallback} mapVar =
whenM (atomically $ insertOrAddTMap localBackend entry mapVar) $
do
whileClientsNotEmpty (taskMonitor localBackend (monitorCallback localBackend mapVar))
say "Stopped listening for changes"
where
UserConfig{localBackend} = userConfig
entry = (alive, responseCallback)
waitForClientsEmpty = atomically . whenJustM (Map.lookup localBackend <$> readTVar mapVar) . const $ retry
whileClientsNotEmpty action = race_ action waitForClientsEmpty
-- TODO: Use backend config
handleRequestsWhileAlive :: LocalBackendRequest -> Cache -> IO ()
handleRequestsWhileAlive LocalBackendRequest{userConfig, alive, responseCallback, requestQueue} cache =
concurrentWhileTrue alive go
where
handler = \case
UIConfigRequest -> (responseCallback . UIConfigResponse . uiConfig) userConfig
AllTasks -> whenNotNullM (getTasks []) (responseCallback . TaskUpdates . NESeq.fromList)
ChangeTasks tasks -> (saveTasks . toList) tasks
SetCalendarList uid list -> do
setList cache uid list
sendCalendarEvents
CalenderRequest -> sendCalendarEvents
go = do
nextRequest <- atomically (readTQueue requestQueue)
concurrently_ (handler nextRequest) go
sendCalendarEvents = do
events <- foldToSeq (getEvents cache)
log Debug [i|Sending #{Seq.length events} events|]
responseCallback . CalendarEvents $ events
taskMonitor :: LocalBackend -> (NonEmpty Task -> IO ()) -> IO ()
taskMonitor _ newTasksCallBack = do
say "Listening for changed or new tasks on 127.0.0.1:6545."
Net.serve (Net.Host "127.0.0.1") "6545" $ \(socket, _) -> Net.recv socket 4096 >>= unwrapChanges
where
unwrapChanges = maybe (sayErr "Unsuccessful connection attempt.") handleChanges
handleChanges changes =
either
(\err -> sayErr [i|Couldnt decode #{changes} as Task: #{err}|])
(newTasksCallBack . one)
. Aeson.eitherDecodeStrict @Task
$ changes

View file

@ -0,0 +1,112 @@
cabal-version: 2.4
name: standalone
version: 0.1.0.0
-- BEGIN dhall generated common configuration
-- generate with: dhall text --file common-config.dhall
license-file: LICENSE
author: Malte Brandy
maintainer: malte.brandy@maralorn.de
build-type: Simple
extra-source-files: CHANGELOG.md
common common-config
default-extensions:
AllowAmbiguousTypes
BlockArguments
ConstraintKinds
DataKinds
DeriveAnyClass
DeriveGeneric
DerivingStrategies
DuplicateRecordFields
EmptyCase
FlexibleContexts
FlexibleInstances
GADTs
LambdaCase
MultiParamTypeClasses
NamedFieldPuns
OverloadedLabels
OverloadedStrings
PartialTypeSignatures
PatternGuards
PatternSynonyms
QuasiQuotes
RankNTypes
RecursiveDo
ScopedTypeVariables
StandaloneDeriving
StrictData
TemplateHaskell
TupleSections
TypeApplications
TypeFamilies
UndecidableInstances
ViewPatterns
ghc-options:
-Wall -Wcompat -Wno-orphans -Wincomplete-uni-patterns
-Wincomplete-record-updates -Wmissing-export-lists -Widentities
-Wredundant-constraints -Wmissing-home-modules
mixins: base hiding (Prelude)
default-language: Haskell2010
-- END dhall generated common configuration
library
import: common-config
hs-source-dirs: src
exposed-modules:
Kassandra.Backend.Calendar
Kassandra.Config.Dhall
Kassandra.Standalone
Kassandra.Standalone.Config
Kassandra.Standalone.State
build-depends:
, aeson
, base
, bytestring
, containers
, data-default
, deferred-folds
, dhall
, directory
, either
, filepath
, filepattern
, iCalendar >=0.4
, kassandra
, network-simple
, nonempty-containers
, password >=2.0.1.0
, paths
, reflex
, reflex-dom
, relude
, say
, stm
, stm-containers
, streamly
, streamly-bytestring
, taskwarrior
, text
, time
, tz
, unix
, uuid
hs-source-dirs: src
executable kassandra2
import: common-config
main-is: main.hs
hs-source-dirs: src-bin
build-depends:
, base
, kassandra
, standalone
ghc-options: -threaded

View file

Binary file not shown.

View file

@ -0,0 +1 @@
{ roots = [ "^Standalone.standalone$", "^Main.main$", "^Backend.backend$", "^Frontend.frontend$", "^Prelude.makeLabels$", "^Kassandra.Debug.log", "Kassandra.ReflexUtil" ], type-class-roots = True }