Merge ../kassandra
This commit is contained in:
commit
91949d9ca6
2
.obelisk/impl/default.nix
Normal file
2
.obelisk/impl/default.nix
Normal file
|
@ -0,0 +1,2 @@
|
|||
# DO NOT HAND-EDIT THIS FILE
|
||||
import (import ./thunk.nix)
|
8
.obelisk/impl/github.json
Normal file
8
.obelisk/impl/github.json
Normal 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
9
.obelisk/impl/thunk.nix
Normal 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
14
apps/kassandra/.gitignore
vendored
Normal 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
3086
apps/kassandra/.hlint.yaml
Normal file
File diff suppressed because it is too large
Load diff
37
apps/kassandra/README.md
Normal file
37
apps/kassandra/README.md
Normal 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 don‘t 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.
|
661
apps/kassandra/backend/LICENSE
Normal file
661
apps/kassandra/backend/LICENSE
Normal 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/>.
|
106
apps/kassandra/backend/backend.cabal
Normal file
106
apps/kassandra/backend/backend.cabal
Normal 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
|
1
apps/kassandra/backend/frontend.jsexe
Symbolic link
1
apps/kassandra/backend/frontend.jsexe
Symbolic link
|
@ -0,0 +1 @@
|
|||
../frontend-js/bin/frontend.jsexe
|
1
apps/kassandra/backend/frontendJs/frontend.jsexe
Symbolic link
1
apps/kassandra/backend/frontendJs/frontend.jsexe
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../frontend-js/bin/frontend.jsexe
|
2
apps/kassandra/backend/hie.yaml
Normal file
2
apps/kassandra/backend/hie.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
cradle:
|
||||
none:
|
8
apps/kassandra/backend/src-bin/main.hs
Normal file
8
apps/kassandra/backend/src-bin/main.hs
Normal file
|
@ -0,0 +1,8 @@
|
|||
module Main (main) where
|
||||
|
||||
import Backend
|
||||
import Frontend
|
||||
import Obelisk.Backend
|
||||
|
||||
main :: IO ()
|
||||
main = runBackend backend frontend
|
87
apps/kassandra/backend/src/Backend.hs
Normal file
87
apps/kassandra/backend/src/Backend.hs
Normal 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
|
31
apps/kassandra/backend/src/Backend/Config.hs
Normal file
31
apps/kassandra/backend/src/Backend/Config.hs
Normal 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 = "{ = }"
|
||||
}
|
1
apps/kassandra/backend/static
Symbolic link
1
apps/kassandra/backend/static
Symbolic link
|
@ -0,0 +1 @@
|
|||
../static
|
3
apps/kassandra/cabal.project
Normal file
3
apps/kassandra/cabal.project
Normal file
|
@ -0,0 +1,3 @@
|
|||
packages:
|
||||
kassandra/
|
||||
standalone/
|
2
apps/kassandra/cabal.project.local
Normal file
2
apps/kassandra/cabal.project.local
Normal file
|
@ -0,0 +1,2 @@
|
|||
package *
|
||||
ghc-options: -fwrite-ide-info
|
1
apps/kassandra/code.nix
Normal file
1
apps/kassandra/code.nix
Normal file
|
@ -0,0 +1 @@
|
|||
"16"
|
71
apps/kassandra/common-config.dhall
Normal file
71
apps/kassandra/common-config.dhall
Normal 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
|
||||
|
||||
''
|
1
apps/kassandra/config/common/example
Normal file
1
apps/kassandra/config/common/example
Normal file
|
@ -0,0 +1 @@
|
|||
This string comes from config/common/example
|
1
apps/kassandra/config/common/route
Normal file
1
apps/kassandra/config/common/route
Normal file
|
@ -0,0 +1 @@
|
|||
http://localhost:8000
|
9
apps/kassandra/config/readme.md
Normal file
9
apps/kassandra/config/readme.md
Normal 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
147
apps/kassandra/default.nix
Normal 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;
|
||||
};
|
||||
}
|
||||
)
|
661
apps/kassandra/frontend/LICENSE
Normal file
661
apps/kassandra/frontend/LICENSE
Normal 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/>.
|
104
apps/kassandra/frontend/frontend.cabal
Normal file
104
apps/kassandra/frontend/frontend.cabal
Normal 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)
|
2
apps/kassandra/frontend/hie.yaml
Normal file
2
apps/kassandra/frontend/hie.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
cradle:
|
||||
none:
|
12
apps/kassandra/frontend/src-bin/main.hs
Normal file
12
apps/kassandra/frontend/src-bin/main.hs
Normal 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
|
39
apps/kassandra/frontend/src/Frontend.hs
Normal file
39
apps/kassandra/frontend/src/Frontend.hs
Normal 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
|
55
apps/kassandra/frontend/src/Frontend/Route.hs
Normal file
55
apps/kassandra/frontend/src/Frontend/Route.hs
Normal 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
2
apps/kassandra/hie.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
cradle:
|
||||
cabal:
|
5
apps/kassandra/kassandra/CHANGELOG.md
Normal file
5
apps/kassandra/kassandra/CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Revision history for kassandra
|
||||
|
||||
## 0.1.0.0 -- YYYY-mm-dd
|
||||
|
||||
* First version. Released on an unsuspecting world.
|
661
apps/kassandra/kassandra/LICENSE
Normal file
661
apps/kassandra/kassandra/LICENSE
Normal 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/>.
|
3
apps/kassandra/kassandra/Setup.hs
Normal file
3
apps/kassandra/kassandra/Setup.hs
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Distribution.Simple
|
||||
|
||||
main = defaultMain
|
124
apps/kassandra/kassandra/kassandra.cabal
Normal file
124
apps/kassandra/kassandra/kassandra.cabal
Normal 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
|
98
apps/kassandra/kassandra/src/Kassandra/AgendaWidget.hs
Normal file
98
apps/kassandra/kassandra/src/Kassandra/AgendaWidget.hs
Normal 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
|
29
apps/kassandra/kassandra/src/Kassandra/Api.hs
Normal file
29
apps/kassandra/kassandra/src/Kassandra/Api.hs
Normal 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
|
32
apps/kassandra/kassandra/src/Kassandra/BaseWidgets.hs
Normal file
32
apps/kassandra/kassandra/src/Kassandra/BaseWidgets.hs
Normal 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)
|
76
apps/kassandra/kassandra/src/Kassandra/Calendar.hs
Normal file
76
apps/kassandra/kassandra/src/Kassandra/Calendar.hs
Normal 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
|
216
apps/kassandra/kassandra/src/Kassandra/Config.hs
Normal file
216
apps/kassandra/kassandra/src/Kassandra/Config.hs
Normal 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
|
166
apps/kassandra/kassandra/src/Kassandra/Css.hs
Normal file
166
apps/kassandra/kassandra/src/Kassandra/Css.hs
Normal 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
|
164
apps/kassandra/kassandra/src/Kassandra/Debug.hs
Normal file
164
apps/kassandra/kassandra/src/Kassandra/Debug.hs
Normal 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
|
83
apps/kassandra/kassandra/src/Kassandra/DragAndDrop.hs
Normal file
83
apps/kassandra/kassandra/src/Kassandra/DragAndDrop.hs
Normal 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)
|
128
apps/kassandra/kassandra/src/Kassandra/ListElementWidget.hs
Normal file
128
apps/kassandra/kassandra/src/Kassandra/ListElementWidget.hs
Normal 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
|
52
apps/kassandra/kassandra/src/Kassandra/ListWidget.hs
Normal file
52
apps/kassandra/kassandra/src/Kassandra/ListWidget.hs
Normal 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)
|
41
apps/kassandra/kassandra/src/Kassandra/LocalBackend.hs
Normal file
41
apps/kassandra/kassandra/src/Kassandra/LocalBackend.hs
Normal 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
|
22
apps/kassandra/kassandra/src/Kassandra/LocalBackendWidget.hs
Normal file
22
apps/kassandra/kassandra/src/Kassandra/LocalBackendWidget.hs
Normal 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
|
4
apps/kassandra/kassandra/src/Kassandra/LogWidget.hs
Normal file
4
apps/kassandra/kassandra/src/Kassandra/LogWidget.hs
Normal file
|
@ -0,0 +1,4 @@
|
|||
module Kassandra.LogWidget (logWidget) where
|
||||
|
||||
logWidget :: Monad m => m ()
|
||||
logWidget = pass
|
200
apps/kassandra/kassandra/src/Kassandra/MainWidget.hs
Normal file
200
apps/kassandra/kassandra/src/Kassandra/MainWidget.hs
Normal 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)
|
84
apps/kassandra/kassandra/src/Kassandra/ReflexUtil.hs
Normal file
84
apps/kassandra/kassandra/src/Kassandra/ReflexUtil.hs
Normal 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
|
167
apps/kassandra/kassandra/src/Kassandra/RemoteBackendWidget.hs
Normal file
167
apps/kassandra/kassandra/src/Kassandra/RemoteBackendWidget.hs
Normal 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
|
18
apps/kassandra/kassandra/src/Kassandra/SelectorWidget.hs
Normal file
18
apps/kassandra/kassandra/src/Kassandra/SelectorWidget.hs
Normal 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)
|
182
apps/kassandra/kassandra/src/Kassandra/Sorting.hs
Normal file
182
apps/kassandra/kassandra/src/Kassandra/Sorting.hs
Normal 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
|
104
apps/kassandra/kassandra/src/Kassandra/State.hs
Normal file
104
apps/kassandra/kassandra/src/Kassandra/State.hs
Normal 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
|
409
apps/kassandra/kassandra/src/Kassandra/TaskWidget.hs
Normal file
409
apps/kassandra/kassandra/src/Kassandra/TaskWidget.hs
Normal 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
|
57
apps/kassandra/kassandra/src/Kassandra/TextEditWidget.hs
Normal file
57
apps/kassandra/kassandra/src/Kassandra/TextEditWidget.hs
Normal 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]
|
91
apps/kassandra/kassandra/src/Kassandra/TimeWidgets.hs
Normal file
91
apps/kassandra/kassandra/src/Kassandra/TimeWidgets.hs
Normal 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
|
154
apps/kassandra/kassandra/src/Kassandra/Types.hs
Normal file
154
apps/kassandra/kassandra/src/Kassandra/Types.hs
Normal 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
|
91
apps/kassandra/kassandra/src/Kassandra/Util.hs
Normal file
91
apps/kassandra/kassandra/src/Kassandra/Util.hs
Normal 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
|
132
apps/kassandra/kassandra/src/Prelude.hs
Normal file
132
apps/kassandra/kassandra/src/Prelude.hs
Normal 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))
|
14
apps/kassandra/nix/sources.json
Normal file
14
apps/kassandra/nix/sources.json
Normal 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"
|
||||
}
|
||||
}
|
194
apps/kassandra/nix/sources.nix
Normal file
194
apps/kassandra/nix/sources.nix
Normal 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); }
|
1
apps/kassandra/obelisk-shell.nix
Normal file
1
apps/kassandra/obelisk-shell.nix
Normal file
|
@ -0,0 +1 @@
|
|||
(import ./. { }).shells.ghc
|
19
apps/kassandra/release.nix
Normal file
19
apps/kassandra/release.nix
Normal 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
5
apps/kassandra/shell.nix
Normal 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]);
|
||||
}
|
3086
apps/kassandra/standalone/.hlint.yaml
Normal file
3086
apps/kassandra/standalone/.hlint.yaml
Normal file
File diff suppressed because it is too large
Load diff
5
apps/kassandra/standalone/CHANGELOG.md
Normal file
5
apps/kassandra/standalone/CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Revision history for frontend
|
||||
|
||||
## 0.1.0.0 -- YYYY-mm-dd
|
||||
|
||||
* First version. Released on an unsuspecting world.
|
661
apps/kassandra/standalone/LICENSE
Normal file
661
apps/kassandra/standalone/LICENSE
Normal 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/>.
|
6
apps/kassandra/standalone/src-bin/main.hs
Normal file
6
apps/kassandra/standalone/src-bin/main.hs
Normal file
|
@ -0,0 +1,6 @@
|
|||
module Main (main) where
|
||||
|
||||
import Kassandra.Standalone (standalone)
|
||||
|
||||
main :: IO ()
|
||||
main = standalone
|
329
apps/kassandra/standalone/src/Kassandra/Backend/Calendar.hs
Normal file
329
apps/kassandra/standalone/src/Kassandra/Backend/Calendar.hs
Normal 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 "/"
|
120
apps/kassandra/standalone/src/Kassandra/Config/Dhall.hs
Normal file
120
apps/kassandra/standalone/src/Kassandra/Config/Dhall.hs
Normal 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
|
82
apps/kassandra/standalone/src/Kassandra/Standalone.hs
Normal file
82
apps/kassandra/standalone/src/Kassandra/Standalone.hs
Normal 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}
|
117
apps/kassandra/standalone/src/Kassandra/Standalone/Config.hs
Normal file
117
apps/kassandra/standalone/src/Kassandra/Standalone/Config.hs
Normal 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 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|]
|
||||
}
|
156
apps/kassandra/standalone/src/Kassandra/Standalone/State.hs
Normal file
156
apps/kassandra/standalone/src/Kassandra/Standalone/State.hs
Normal 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|Couldn‘t decode #{changes} as Task: #{err}|])
|
||||
(newTasksCallBack . one)
|
||||
. Aeson.eitherDecodeStrict @Task
|
||||
$ changes
|
112
apps/kassandra/standalone/standalone.cabal
Normal file
112
apps/kassandra/standalone/standalone.cabal
Normal 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
|
0
apps/kassandra/static/.empty
Normal file
0
apps/kassandra/static/.empty
Normal file
BIN
apps/kassandra/static/MaterialIcons-Regular-Outlined.otf
Normal file
BIN
apps/kassandra/static/MaterialIcons-Regular-Outlined.otf
Normal file
Binary file not shown.
BIN
apps/kassandra/static/MaterialIcons-Regular.ttf
Normal file
BIN
apps/kassandra/static/MaterialIcons-Regular.ttf
Normal file
Binary file not shown.
1
apps/kassandra/weeder.dhall
Normal file
1
apps/kassandra/weeder.dhall
Normal file
|
@ -0,0 +1 @@
|
|||
{ roots = [ "^Standalone.standalone$", "^Main.main$", "^Backend.backend$", "^Frontend.frontend$", "^Prelude.makeLabels$", "^Kassandra.Debug.log", "Kassandra.ReflexUtil" ], type-class-roots = True }
|
Loading…
Reference in a new issue