Initial commit
This commit is contained in:
commit
d13cdac9e0
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
dist/
|
||||||
|
.cabal-sandbox/
|
||||||
|
cabal.sandbox.config
|
339
LICENSE
Normal file
339
LICENSE
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
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
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the 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 a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
188
data/Gtk/builder.xml
Normal file
188
data/Gtk/builder.xml
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.19.0 -->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.16"/>
|
||||||
|
<object class="GtkApplicationWindow" id="rootWin">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="box1">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuBar" id="menubar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem" id="menubarFile">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">_File</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<child type="submenu">
|
||||||
|
<object class="GtkMenu" id="menu1">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem" id="menubarFileOpen">
|
||||||
|
<property name="label">gtk-open</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSeparatorMenuItem" id="separatormenuitem1">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem" id="menubarFileQuit">
|
||||||
|
<property name="label">gtk-quit</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem" id="menubarEdit">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">_Edit</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<child type="submenu">
|
||||||
|
<object class="GtkMenu" id="menu2">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem" id="menubarFileCut">
|
||||||
|
<property name="label">gtk-cut</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem" id="menubarFileCopy">
|
||||||
|
<property name="label">gtk-copy</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem" id="menubarFilePaste">
|
||||||
|
<property name="label">gtk-paste</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem" id="menubarFileDelete">
|
||||||
|
<property name="label">gtk-delete</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem" id="menubarView">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">_View</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem" id="menubarHelp">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">_Help</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<child type="submenu">
|
||||||
|
<object class="GtkMenu" id="menu3">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem" id="menubarHelpAbout">
|
||||||
|
<property name="label">gtk-about</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="box2">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEntry" id="urlBar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="input_purpose">url</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow" id="mainScroll">
|
||||||
|
<property name="width_request">300</property>
|
||||||
|
<property name="height_request">500</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
BIN
data/Gtk/icons/error.png
Normal file
BIN
data/Gtk/icons/error.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
data/Gtk/icons/gtk-directory.png
Normal file
BIN
data/Gtk/icons/gtk-directory.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
BIN
data/Gtk/icons/gtk-file.png
Normal file
BIN
data/Gtk/icons/gtk-file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
64
hsfm.cabal
Normal file
64
hsfm.cabal
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: hsfm
|
||||||
|
version: 0.0.0.1
|
||||||
|
synopsis: Haskell FileManager
|
||||||
|
description: Lazy FileManager written in haskell
|
||||||
|
license: GPL-2
|
||||||
|
license-file: LICENSE
|
||||||
|
author: Julian Ospald
|
||||||
|
maintainer: hasufell@hasufell.de
|
||||||
|
copyright: Copyright: (c) 2015 Julian Ospald
|
||||||
|
category: Desktop
|
||||||
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
|
|
||||||
|
library
|
||||||
|
exposed-modules: Data.DirTree
|
||||||
|
Data.DirTree.Zipper
|
||||||
|
IO.Utils
|
||||||
|
IO.File
|
||||||
|
IO.Error
|
||||||
|
|
||||||
|
build-depends: base >= 4.7,
|
||||||
|
data-default,
|
||||||
|
bifunctors >= 5,
|
||||||
|
directory >= 1.1.0.0 && < 1.2.3.0,
|
||||||
|
easy-file >= 0.2.0,
|
||||||
|
filepath >= 1.3.0.0,
|
||||||
|
mtl >= 2.2,
|
||||||
|
old-locale >= 1,
|
||||||
|
process,
|
||||||
|
stm,
|
||||||
|
text,
|
||||||
|
time >= 1.4.2
|
||||||
|
hs-source-dirs: src
|
||||||
|
default-language: Haskell2010
|
||||||
|
Default-Extensions: RecordWildCards
|
||||||
|
ghc-options:
|
||||||
|
-O2
|
||||||
|
|
||||||
|
executable hsfm-gtk
|
||||||
|
main-is: GUI/Gtk.hs
|
||||||
|
other-modules: GUI.Gtk.Callbacks
|
||||||
|
GUI.Gtk.Icons
|
||||||
|
GUI.Gtk.Gui
|
||||||
|
build-depends: hsfm,
|
||||||
|
base >= 4.7,
|
||||||
|
data-default,
|
||||||
|
gtk3 >= 0.14.1,
|
||||||
|
glib >= 0.13,
|
||||||
|
bifunctors >= 5,
|
||||||
|
directory >= 1.1.0.0 && < 1.2.3.0,
|
||||||
|
easy-file >= 0.2.0,
|
||||||
|
filepath >= 1.3.0.0,
|
||||||
|
mtl >= 2.2,
|
||||||
|
old-locale >= 1,
|
||||||
|
process,
|
||||||
|
stm,
|
||||||
|
text,
|
||||||
|
time >= 1.4.2,
|
||||||
|
transformers >= 0.4
|
||||||
|
hs-source-dirs: src
|
||||||
|
default-language: Haskell2010
|
||||||
|
Default-Extensions: RecordWildCards
|
||||||
|
ghc-options:
|
||||||
|
-O2
|
714
src/Data/DirTree.hs
Normal file
714
src/Data/DirTree.hs
Normal file
@ -0,0 +1,714 @@
|
|||||||
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
||||||
|
|
||||||
|
module Data.DirTree where
|
||||||
|
|
||||||
|
|
||||||
|
import Control.Applicative
|
||||||
|
(
|
||||||
|
(<*>)
|
||||||
|
, (<$>)
|
||||||
|
, (<|>)
|
||||||
|
, pure
|
||||||
|
)
|
||||||
|
import Control.Arrow
|
||||||
|
(
|
||||||
|
first
|
||||||
|
)
|
||||||
|
import Control.Exception
|
||||||
|
(
|
||||||
|
handle
|
||||||
|
)
|
||||||
|
import Control.Exception.Base
|
||||||
|
(
|
||||||
|
IOException
|
||||||
|
)
|
||||||
|
import Control.Monad.State.Lazy
|
||||||
|
(
|
||||||
|
|
||||||
|
)
|
||||||
|
import Data.Default
|
||||||
|
import Data.Ord
|
||||||
|
(
|
||||||
|
comparing
|
||||||
|
)
|
||||||
|
import Data.List
|
||||||
|
(
|
||||||
|
delete
|
||||||
|
, sort
|
||||||
|
, sortBy
|
||||||
|
, (\\)
|
||||||
|
)
|
||||||
|
import Data.Time
|
||||||
|
(
|
||||||
|
UTCTime
|
||||||
|
, formatTime
|
||||||
|
)
|
||||||
|
import Data.Word
|
||||||
|
(
|
||||||
|
Word64
|
||||||
|
)
|
||||||
|
import System.Directory
|
||||||
|
(
|
||||||
|
Permissions(..)
|
||||||
|
, createDirectoryIfMissing
|
||||||
|
, doesFileExist
|
||||||
|
, getDirectoryContents
|
||||||
|
, getModificationTime
|
||||||
|
, getPermissions
|
||||||
|
, writable
|
||||||
|
, searchable
|
||||||
|
)
|
||||||
|
import System.EasyFile
|
||||||
|
(
|
||||||
|
getCreationTime
|
||||||
|
, getChangeTime
|
||||||
|
, getAccessTime
|
||||||
|
, getFileSize
|
||||||
|
, hasSubDirectories
|
||||||
|
, isSymlink
|
||||||
|
)
|
||||||
|
import System.FilePath
|
||||||
|
(
|
||||||
|
combine
|
||||||
|
, equalFilePath
|
||||||
|
, joinPath
|
||||||
|
, splitDirectories
|
||||||
|
, (</>)
|
||||||
|
)
|
||||||
|
import System.IO
|
||||||
|
(
|
||||||
|
IOMode
|
||||||
|
, Handle
|
||||||
|
, openFile
|
||||||
|
)
|
||||||
|
import System.IO.Error
|
||||||
|
(
|
||||||
|
ioeGetErrorType
|
||||||
|
, isDoesNotExistErrorType
|
||||||
|
)
|
||||||
|
import System.IO.Unsafe
|
||||||
|
(
|
||||||
|
unsafeInterleaveIO
|
||||||
|
)
|
||||||
|
import System.Locale
|
||||||
|
(
|
||||||
|
defaultTimeLocale
|
||||||
|
, rfc822DateFormat
|
||||||
|
)
|
||||||
|
import qualified Data.Bitraversable as BT
|
||||||
|
import qualified Data.Bifunctor as BF
|
||||||
|
import qualified Data.Bifoldable as BFL
|
||||||
|
import qualified Data.Traversable as T
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------
|
||||||
|
--[ BASE TYPES ]--
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
-- |Weak type to distinguish between FilePath and FileName.
|
||||||
|
type FileName = String
|
||||||
|
|
||||||
|
|
||||||
|
-- |A simple wrapper to hold a base directory name, which can be either an
|
||||||
|
-- absolute or relative path. This lets us give the DirTree a context, while
|
||||||
|
-- still letting us store only directory and file /names/ (not full paths) in
|
||||||
|
-- the DirTree. (uses an infix constructor; don't be scared)
|
||||||
|
data AnchoredDirTree a b =
|
||||||
|
(:/) { anchor :: FilePath, dirTree :: DirTree a b }
|
||||||
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
|
|
||||||
|
-- |The String in the name field is always a file name, never a full path.
|
||||||
|
-- The free type variable is used in the File/Dir constructor and can hold
|
||||||
|
-- Handles, Strings representing a file's contents or anything else you can
|
||||||
|
-- think of. We catch any IO errors in the Failed constructor. an Exception
|
||||||
|
-- can be converted to a String with 'show'.
|
||||||
|
data DirTree a b =
|
||||||
|
Failed {
|
||||||
|
name :: FileName
|
||||||
|
, err :: IOException
|
||||||
|
}
|
||||||
|
| Dir {
|
||||||
|
name :: FileName
|
||||||
|
, contents :: [DirTree a b]
|
||||||
|
, dir :: a
|
||||||
|
}
|
||||||
|
| File {
|
||||||
|
name :: FileName
|
||||||
|
, file :: b
|
||||||
|
} deriving Show
|
||||||
|
|
||||||
|
|
||||||
|
-- |All possible directory information we could ever need from a directory.
|
||||||
|
data DirTreeInfo =
|
||||||
|
DirInfo {
|
||||||
|
permissions :: Permissions
|
||||||
|
, creationTime :: Maybe UTCTime
|
||||||
|
, changeTime :: Maybe UTCTime
|
||||||
|
, modTime :: UTCTime
|
||||||
|
, accessTime :: UTCTime
|
||||||
|
, sym :: Bool
|
||||||
|
, hasSubDirs :: Maybe Bool
|
||||||
|
}
|
||||||
|
| FileInfo {
|
||||||
|
permissions :: Permissions
|
||||||
|
, creationTime :: Maybe UTCTime
|
||||||
|
, changeTime :: Maybe UTCTime
|
||||||
|
, modTime :: UTCTime
|
||||||
|
, accessTime :: UTCTime
|
||||||
|
, sym :: Bool
|
||||||
|
, fileSize :: Word64
|
||||||
|
}
|
||||||
|
deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------
|
||||||
|
--[ INSTANCES ]--
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
instance BF.Bifunctor DirTree where
|
||||||
|
bimap = BT.bimapDefault
|
||||||
|
|
||||||
|
|
||||||
|
instance BFL.Bifoldable DirTree where
|
||||||
|
bifoldMap = BT.bifoldMapDefault
|
||||||
|
|
||||||
|
|
||||||
|
instance BT.Bitraversable DirTree where
|
||||||
|
bitraverse f1 f2 (Dir n cs b) =
|
||||||
|
Dir n
|
||||||
|
<$> T.traverse (BT.bitraverse f1 f2) cs
|
||||||
|
<*> f1 b
|
||||||
|
bitraverse _ f2 (File n a) =
|
||||||
|
File n <$> f2 a
|
||||||
|
bitraverse _ _ (Failed n e) =
|
||||||
|
pure (Failed n e)
|
||||||
|
|
||||||
|
|
||||||
|
-- | Two DirTrees are equal if they have the same constructor, the same name
|
||||||
|
-- (and in the case of `Dir`s) their sorted `contents` are equal:
|
||||||
|
instance (Eq a, Eq b) => Eq (DirTree a b) where
|
||||||
|
(File n a) == (File n' a') = n == n' && a == a'
|
||||||
|
(Dir n cs _) == (Dir n' cs' _) =
|
||||||
|
n == n' && sortBy comparingConstr cs == sortBy comparingConstr cs'
|
||||||
|
-- after comparing above we can hand off to shape equality function:
|
||||||
|
d == d' = equalShape d d'
|
||||||
|
|
||||||
|
|
||||||
|
-- | First compare constructors: Failed < Dir < File...
|
||||||
|
-- Then compare `name`...
|
||||||
|
-- Then compare free variable parameter of `File` constructors
|
||||||
|
instance (Ord a, Ord b, Eq a, Eq b) => Ord (DirTree a b) where
|
||||||
|
compare (File n a) (File n' a') =
|
||||||
|
case compare n n' of
|
||||||
|
EQ -> compare a a'
|
||||||
|
el -> el
|
||||||
|
compare (Dir n cs b) (Dir n' cs' b') =
|
||||||
|
case compare n n' of
|
||||||
|
EQ -> case compare b b' of
|
||||||
|
EQ -> comparing sort cs cs'
|
||||||
|
el -> el
|
||||||
|
el -> el
|
||||||
|
-- after comparing above we can hand off to shape ord function:
|
||||||
|
compare d d' = comparingShape d d'
|
||||||
|
|
||||||
|
|
||||||
|
-- for convenience:
|
||||||
|
instance BF.Bifunctor AnchoredDirTree where
|
||||||
|
bimap fd ff (b:/d) = b :/ BF.bimap fd ff d
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- given the same fixity as <$>, is that right?
|
||||||
|
infixl 4 </$>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------
|
||||||
|
--[ HIGH LEVEL FUNCTIONS ]--
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
-- | build an AnchoredDirTree, given the path to a directory, opening the files
|
||||||
|
-- using readFile.
|
||||||
|
-- Uses `readDirectoryWith` internally and has the effect of traversing the
|
||||||
|
-- entire directory structure. See `readDirectoryWithL` for lazy production
|
||||||
|
-- of a DirTree structure.
|
||||||
|
readDirectory :: FilePath -> IO (AnchoredDirTree String String)
|
||||||
|
readDirectory = readDirectoryWith readFile return
|
||||||
|
|
||||||
|
|
||||||
|
-- | same as readDirectory but allows us to, for example, use
|
||||||
|
-- ByteString.readFile to return a tree of ByteStrings.
|
||||||
|
readDirectoryWith :: (FilePath -> IO a)
|
||||||
|
-> (FilePath -> IO b)
|
||||||
|
-> FilePath
|
||||||
|
-> IO (AnchoredDirTree a b)
|
||||||
|
readDirectoryWith fd ff p = buildWith' buildAtOnce' fd ff p
|
||||||
|
|
||||||
|
|
||||||
|
-- | A "lazy" version of `readDirectoryWith` that does IO operations as needed
|
||||||
|
-- i.e. as the tree is traversed in pure code.
|
||||||
|
-- /NOTE:/ This function uses unsafeInterleaveIO under the hood. I believe
|
||||||
|
-- our use here is safe, but this function is experimental in this release:
|
||||||
|
readDirectoryWithL :: (FilePath -> IO a)
|
||||||
|
-> (FilePath -> IO b)
|
||||||
|
-> FilePath
|
||||||
|
-> IO (AnchoredDirTree a b)
|
||||||
|
readDirectoryWithL fd ff p = buildWith' buildLazilyUnsafe' fd ff p
|
||||||
|
|
||||||
|
|
||||||
|
-- | write a DirTree of strings to disk. Clobbers files of the same name.
|
||||||
|
-- Doesn't affect files in the directories (if any already exist) with
|
||||||
|
-- different names. Returns a new AnchoredDirTree where failures were
|
||||||
|
-- lifted into a `Failed` constructor:
|
||||||
|
writeDirectory :: AnchoredDirTree String String -> IO (AnchoredDirTree () ())
|
||||||
|
writeDirectory = writeDirectoryWith writeFile
|
||||||
|
|
||||||
|
|
||||||
|
-- | writes the directory structure to disk and uses the provided function to
|
||||||
|
-- write the contents of `Files` to disk. The return value of the function will
|
||||||
|
-- become the new `contents` of the returned, where IO errors at each node are
|
||||||
|
-- replaced with `Failed` constructors. The returned tree can be compared to
|
||||||
|
-- the passed tree to see what operations, if any, failed:
|
||||||
|
writeDirectoryWith :: (FilePath -> af -> IO bf)
|
||||||
|
-> AnchoredDirTree ad af
|
||||||
|
-> IO (AnchoredDirTree () bf)
|
||||||
|
writeDirectoryWith f (b:/t) = (b:/) <$> write' b t
|
||||||
|
where write' b' (File n a) = handleDT n $
|
||||||
|
File n <$> f (b'</>n) a
|
||||||
|
write' b' (Dir n cs _) = handleDT n $
|
||||||
|
do let bas = b'</>n
|
||||||
|
createDirectoryIfMissing True bas
|
||||||
|
Dir n <$> mapM (write' bas) cs <*> return ()
|
||||||
|
write' _ (Failed n e) = return $ Failed n e
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------
|
||||||
|
--[ LOWER LEVEL FUNCTIONS ]--
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
-- | a simple application of readDirectoryWith openFile:
|
||||||
|
openDirectory :: FilePath -> IOMode -> IO (AnchoredDirTree () Handle)
|
||||||
|
openDirectory p m = readDirectoryWith (\_ -> return ()) (flip openFile m) p
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- | builds a DirTree from the contents of the directory passed to it, saving
|
||||||
|
-- the base directory in the Anchored* wrapper. Errors are caught in the tree in
|
||||||
|
-- the Failed constructor. The 'file' fields initially are populated with full
|
||||||
|
-- paths to the files they are abstracting.
|
||||||
|
build :: FilePath -> IO (AnchoredDirTree FilePath FilePath)
|
||||||
|
build = buildWith' buildAtOnce' return return -- we say 'return' here to get
|
||||||
|
-- back a tree of FilePaths
|
||||||
|
|
||||||
|
|
||||||
|
-- | identical to `build` but does directory reading IO lazily as needed:
|
||||||
|
buildL :: FilePath -> IO (AnchoredDirTree FilePath FilePath)
|
||||||
|
buildL = buildWith' buildLazilyUnsafe' return return
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- -- -- helpers: -- -- --
|
||||||
|
|
||||||
|
|
||||||
|
type UserIO a = FilePath -> IO a
|
||||||
|
type Builder a b = UserIO a -> UserIO b -> FilePath -> IO (DirTree a b)
|
||||||
|
|
||||||
|
-- remove non-existent file errors, which are artifacts of the "non-atomic"
|
||||||
|
-- nature of traversing a system firectory tree:
|
||||||
|
buildWith' :: Builder a b
|
||||||
|
-> UserIO a
|
||||||
|
-> UserIO b
|
||||||
|
-> FilePath
|
||||||
|
-> IO (AnchoredDirTree a b)
|
||||||
|
buildWith' bf' fd ff p =
|
||||||
|
do tree <- bf' fd ff p
|
||||||
|
return (baseDir p :/ removeNonexistent tree)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- IO function passed to our builder and finally executed here:
|
||||||
|
buildAtOnce' :: Builder a b
|
||||||
|
buildAtOnce' fd ff p = handleDT n $
|
||||||
|
do isFile <- doesFileExist p
|
||||||
|
if isFile
|
||||||
|
then File n <$> ff p
|
||||||
|
else do cs <- getDirsFiles p
|
||||||
|
Dir n
|
||||||
|
<$> T.mapM (buildAtOnce' fd ff . combine p) cs
|
||||||
|
<*> fd p
|
||||||
|
where n = topDir p
|
||||||
|
|
||||||
|
|
||||||
|
-- using unsafeInterleaveIO to get "lazy" traversal:
|
||||||
|
buildLazilyUnsafe' :: Builder a b
|
||||||
|
buildLazilyUnsafe' fd ff p = handleDT n $
|
||||||
|
do isFile <- doesFileExist p
|
||||||
|
if isFile
|
||||||
|
then File n <$> ff p
|
||||||
|
-- HERE IS THE UNSAFE CODE:
|
||||||
|
else do
|
||||||
|
-- this is not behind unsafeInterleaveIO on purpose
|
||||||
|
-- otherwise we might get runtime exceptions
|
||||||
|
files <- getDirsFiles p
|
||||||
|
contents <- unsafeInterleaveIO $
|
||||||
|
mapM (rec . combine p) files
|
||||||
|
d <- fd p
|
||||||
|
return $ Dir n contents d
|
||||||
|
where rec = buildLazilyUnsafe' fd ff
|
||||||
|
n = topDir p
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
--[ UTILITIES ]--
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---- HANDLING FAILURES ----
|
||||||
|
|
||||||
|
|
||||||
|
-- | True if any Failed constructors in the tree
|
||||||
|
anyFailed :: DirTree a b -> Bool
|
||||||
|
anyFailed = not . successful
|
||||||
|
|
||||||
|
-- | True if there are no Failed constructors in the tree
|
||||||
|
successful :: DirTree a b -> Bool
|
||||||
|
successful = null . failures
|
||||||
|
|
||||||
|
|
||||||
|
-- | returns true if argument is a `Failed` constructor:
|
||||||
|
failed :: DirTree a b -> Bool
|
||||||
|
failed (Failed _ _) = True
|
||||||
|
failed _ = False
|
||||||
|
|
||||||
|
|
||||||
|
-- | returns a list of 'Failed' constructors only:
|
||||||
|
failures :: DirTree a b -> [DirTree a b]
|
||||||
|
failures = filter failed . flattenDir
|
||||||
|
|
||||||
|
|
||||||
|
-- | maps a function to convert Failed DirTrees to Files or Dirs
|
||||||
|
failedMap :: (FileName -> IOException -> DirTree a b) -> DirTree a b -> DirTree a b
|
||||||
|
failedMap f = transformDir unFail
|
||||||
|
where unFail (Failed n e) = f n e
|
||||||
|
unFail c = c
|
||||||
|
|
||||||
|
|
||||||
|
---- ORDERING AND EQUALITY ----
|
||||||
|
|
||||||
|
|
||||||
|
-- | Recursively sort a directory tree according to the Ord instance
|
||||||
|
sortDir :: (Ord a, Ord b) => DirTree a b -> DirTree a b
|
||||||
|
sortDir = sortDirBy compare
|
||||||
|
|
||||||
|
-- | Recursively sort a tree as in `sortDir` but ignore the file contents of a
|
||||||
|
-- File constructor
|
||||||
|
sortDirShape :: DirTree a b -> DirTree a b
|
||||||
|
sortDirShape = sortDirBy comparingShape where
|
||||||
|
|
||||||
|
-- HELPER:
|
||||||
|
sortDirBy :: (DirTree a b -> DirTree a b -> Ordering) -> DirTree a b -> DirTree a b
|
||||||
|
sortDirBy cf = transformDir sortD
|
||||||
|
where sortD (Dir n cs a) = Dir n (sortBy cf cs) a
|
||||||
|
sortD c = c
|
||||||
|
|
||||||
|
|
||||||
|
-- | Tests equality of two trees, ignoring their free variable portion. Can be
|
||||||
|
-- used to check if any files have been added or deleted, for instance.
|
||||||
|
equalShape :: DirTree a b -> DirTree c d -> Bool
|
||||||
|
equalShape d d' = comparingShape d d' == EQ
|
||||||
|
|
||||||
|
-- TODO: we should use equalFilePath here, but how to sort properly? with System.Directory.canonicalizePath, before compare?
|
||||||
|
|
||||||
|
-- | a compare function that ignores the free "file" type variable:
|
||||||
|
comparingShape :: DirTree a b -> DirTree c d -> Ordering
|
||||||
|
comparingShape (Dir n cs _) (Dir n' cs' _) =
|
||||||
|
case compare n n' of
|
||||||
|
EQ -> comp (sortCs cs) (sortCs cs')
|
||||||
|
el -> el
|
||||||
|
where sortCs = sortBy comparingConstr
|
||||||
|
-- stolen from [] Ord instance:
|
||||||
|
comp [] [] = EQ
|
||||||
|
comp [] (_:_) = LT
|
||||||
|
comp (_:_) [] = GT
|
||||||
|
comp (x:xs) (y:ys) = case comparingShape x y of
|
||||||
|
EQ -> comp xs ys
|
||||||
|
other -> other
|
||||||
|
-- else simply compare the flat constructors, non-recursively:
|
||||||
|
comparingShape t t' = comparingConstr t t'
|
||||||
|
|
||||||
|
|
||||||
|
-- HELPER: a non-recursive comparison
|
||||||
|
comparingConstr :: DirTree a b -> DirTree a1 b1 -> Ordering
|
||||||
|
comparingConstr (Failed _ _) (Dir _ _ _) = LT
|
||||||
|
comparingConstr (Failed _ _) (File _ _) = LT
|
||||||
|
comparingConstr (File _ _) (Failed _ _) = GT
|
||||||
|
comparingConstr (File _ _) (Dir _ _ _) = GT
|
||||||
|
comparingConstr (Dir _ _ _) (Failed _ _) = GT
|
||||||
|
comparingConstr (Dir _ _ _) (File _ _) = LT
|
||||||
|
-- else compare on the names of constructors that are the same, without
|
||||||
|
-- looking at the contents of Dir constructors:
|
||||||
|
comparingConstr t t' = compare (name t) (name t')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---- OTHER ----
|
||||||
|
|
||||||
|
-- | If the argument is a 'Dir' containing a sub-DirTree matching 'FileName'
|
||||||
|
-- then return that subtree, appending the 'name' of the old root 'Dir' to the
|
||||||
|
-- 'anchor' of the AnchoredDirTree wrapper. Otherwise return @Nothing@.
|
||||||
|
dropTo :: FileName -> AnchoredDirTree a b -> Maybe (AnchoredDirTree a b)
|
||||||
|
dropTo n' (p :/ Dir n ds' _) = search ds'
|
||||||
|
where search [] = Nothing
|
||||||
|
search (d:ds) | equalFilePath n' (name d) = Just ((p</>n) :/ d)
|
||||||
|
| otherwise = search ds
|
||||||
|
dropTo _ _ = Nothing
|
||||||
|
|
||||||
|
|
||||||
|
find :: FilePath
|
||||||
|
-> AnchoredDirTree a b
|
||||||
|
-> Either String (AnchoredDirTree a b)
|
||||||
|
find f d = findAbs f d <|> findRel f d
|
||||||
|
|
||||||
|
|
||||||
|
-- |Finds a file or directory inside an @AnchoredDirTree@. This only
|
||||||
|
-- looks at the subdirectories of the underlying @DirTree@. If you
|
||||||
|
-- want to compare the name of the topmost @DirTree@ as well, use @find'@.
|
||||||
|
findRel :: FilePath
|
||||||
|
-> AnchoredDirTree a b
|
||||||
|
-> Either String (AnchoredDirTree a b)
|
||||||
|
findRel f d =
|
||||||
|
go (splitDirectories f) d
|
||||||
|
where
|
||||||
|
go (f:fs) (p :/ Dir n ds _) = search ds f >>= go fs
|
||||||
|
where
|
||||||
|
search [] _ = Left "Directory or file not found!"
|
||||||
|
search (d:ds) n | equalFilePath n (name d) = Right ((p</>n) :/ d)
|
||||||
|
| otherwise = search ds n
|
||||||
|
go [] d = Right d
|
||||||
|
go _ (p :/ Failed _ err) = Left $ show err
|
||||||
|
go _ _ = Left "Directory or file not found!"
|
||||||
|
|
||||||
|
|
||||||
|
-- |Finds a file or directory inside an @AnchoredDirTree@. This also
|
||||||
|
-- looks at the topmost @DirTree@ and compares the first path component
|
||||||
|
-- with it. If you only want to look at subdirectories, use @find@.
|
||||||
|
findAbs :: FilePath
|
||||||
|
-> AnchoredDirTree a b
|
||||||
|
-> Either String (AnchoredDirTree a b)
|
||||||
|
findAbs f d =
|
||||||
|
go (splitDirectories f) d
|
||||||
|
where
|
||||||
|
go (f':fs) (_ :/ Dir n _ _)
|
||||||
|
| equalFilePath f' n = find (joinPath fs) d
|
||||||
|
| otherwise = Left "Directory or file not found!"
|
||||||
|
go _ (p :/ Failed _ err) = Left $ show err
|
||||||
|
go _ _ = Left "Directory or file not found!"
|
||||||
|
|
||||||
|
|
||||||
|
-- | applies the predicate to each constructor in the tree, removing it (and
|
||||||
|
-- its children, of course) when the predicate returns False. The topmost
|
||||||
|
-- constructor will always be preserved:
|
||||||
|
filterDir :: (DirTree a b -> Bool) -> DirTree a b -> DirTree a b
|
||||||
|
filterDir p = transformDir filterD
|
||||||
|
where filterD (Dir n cs a) = Dir n (filter p cs) a
|
||||||
|
filterD c = c
|
||||||
|
|
||||||
|
|
||||||
|
-- | Flattens a `DirTree` into a (never empty) list of tree constructors. `Dir`
|
||||||
|
-- constructors will have [] as their `contents`:
|
||||||
|
flattenDir :: DirTree a b -> [ DirTree a b ]
|
||||||
|
flattenDir (Dir n cs a) = Dir n [] a : concatMap flattenDir cs
|
||||||
|
flattenDir f = [f]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- | Allows for a function on a bare DirTree to be applied to an AnchoredDirTree
|
||||||
|
-- within a Functor. Very similar to and useful in combination with `<$>`:
|
||||||
|
(</$>) :: (Functor f) => (DirTree a a1 -> DirTree b b1) -> f (AnchoredDirTree a a1) ->
|
||||||
|
f (AnchoredDirTree b b1)
|
||||||
|
(</$>) f = fmap (\(b :/ t) -> b :/ f t)
|
||||||
|
|
||||||
|
|
||||||
|
---------------
|
||||||
|
--[ HELPERS ]--
|
||||||
|
---------------
|
||||||
|
|
||||||
|
|
||||||
|
---- CONSTRUCTOR IDENTIFIERS ----
|
||||||
|
isFileC :: DirTree a b -> Bool
|
||||||
|
isFileC (File _ _) = True
|
||||||
|
isFileC _ = False
|
||||||
|
|
||||||
|
isDirC :: DirTree a b -> Bool
|
||||||
|
isDirC (Dir _ _ _) = True
|
||||||
|
isDirC _ = False
|
||||||
|
|
||||||
|
|
||||||
|
---- PATH CONVERSIONS ----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- | tuple up the complete file path with the 'file' contents, by building up the
|
||||||
|
-- path, trie-style, from the root. The filepath will be relative to \"anchored\"
|
||||||
|
-- directory.
|
||||||
|
--
|
||||||
|
-- This allows us to, for example, @mapM_ uncurry writeFile@ over a DirTree of
|
||||||
|
-- strings, although 'writeDirectory' does a better job of this.
|
||||||
|
zipPaths :: AnchoredDirTree a b -> DirTree (FilePath, a) (FilePath, b)
|
||||||
|
zipPaths (b :/ t) = zipP b t
|
||||||
|
where zipP p (File n a) = File n (p</>n , a)
|
||||||
|
zipP p (Dir n cs a) = Dir n (map (zipP $ p</>n) cs) (p</>n , a)
|
||||||
|
zipP _ (Failed n e) = Failed n e
|
||||||
|
|
||||||
|
|
||||||
|
-- extracting pathnames and base names:
|
||||||
|
topDir, baseDir :: FilePath -> FilePath
|
||||||
|
topDir = last . splitDirectories
|
||||||
|
baseDir = joinPath . init . splitDirectories
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---- IO HELPERS: ----
|
||||||
|
|
||||||
|
|
||||||
|
-- | writes the directory structure (not files) of a DirTree to the anchored
|
||||||
|
-- directory. Returns a structure identical to the supplied tree with errors
|
||||||
|
-- replaced by `Failed` constructors:
|
||||||
|
writeJustDirs :: AnchoredDirTree a b -> IO (AnchoredDirTree () b)
|
||||||
|
writeJustDirs = writeDirectoryWith (const return)
|
||||||
|
|
||||||
|
|
||||||
|
----- the let expression is an annoying hack, because dropFileName "." == ""
|
||||||
|
----- and getDirectoryContents fails epically on ""
|
||||||
|
-- prepares the directory contents list. we sort so that we can be sure of
|
||||||
|
-- a consistent fold/traversal order on the same directory:
|
||||||
|
getDirsFiles :: String -> IO [FilePath]
|
||||||
|
getDirsFiles cs = do let cs' = if null cs then "." else cs
|
||||||
|
dfs <- getDirectoryContents cs'
|
||||||
|
return $ dfs \\ [".",".."]
|
||||||
|
|
||||||
|
|
||||||
|
readPath :: FilePath
|
||||||
|
-> IO (AnchoredDirTree DirTreeInfo DirTreeInfo)
|
||||||
|
readPath = readDirectoryWithL mkDirInfo mkFileInfo
|
||||||
|
|
||||||
|
|
||||||
|
mkFileInfo :: FilePath -> IO DirTreeInfo
|
||||||
|
mkFileInfo fp =
|
||||||
|
FileInfo
|
||||||
|
<$> getPermissions fp
|
||||||
|
<*> getCreationTime fp
|
||||||
|
<*> getChangeTime fp
|
||||||
|
<*> getModificationTime fp
|
||||||
|
<*> getAccessTime fp
|
||||||
|
<*> isSymlink fp
|
||||||
|
<*> getFileSize fp
|
||||||
|
|
||||||
|
|
||||||
|
mkDirInfo :: FilePath -> IO DirTreeInfo
|
||||||
|
mkDirInfo fp =
|
||||||
|
DirInfo
|
||||||
|
<$> getPermissions fp
|
||||||
|
<*> getCreationTime fp
|
||||||
|
<*> getChangeTime fp
|
||||||
|
<*> getModificationTime fp
|
||||||
|
<*> getAccessTime fp
|
||||||
|
<*> isSymlink fp
|
||||||
|
<*> hasSubDirectories fp
|
||||||
|
|
||||||
|
|
||||||
|
getFreeVar :: DirTree a a -> Maybe a
|
||||||
|
getFreeVar (File _ f) = Just f
|
||||||
|
getFreeVar (Dir _ _ d) = Just d
|
||||||
|
getFreeVar _ = Nothing
|
||||||
|
|
||||||
|
|
||||||
|
---- FAILURE HELPERS: ----
|
||||||
|
|
||||||
|
|
||||||
|
-- handles an IO exception by returning a Failed constructor filled with that
|
||||||
|
-- exception:
|
||||||
|
handleDT :: FileName -> IO (DirTree a b) -> IO (DirTree a b)
|
||||||
|
handleDT n = handle (return . Failed n)
|
||||||
|
|
||||||
|
|
||||||
|
-- DoesNotExist errors not present at the topmost level could happen if a
|
||||||
|
-- named file or directory is deleted after being listed by
|
||||||
|
-- getDirectoryContents but before we can get it into memory.
|
||||||
|
-- So we filter those errors out because the user should not see errors
|
||||||
|
-- raised by the internal implementation of this module:
|
||||||
|
-- This leaves the error if it exists in the top (user-supplied) level:
|
||||||
|
removeNonexistent :: DirTree a b -> DirTree a b
|
||||||
|
removeNonexistent = filterDir isOkConstructor
|
||||||
|
where isOkConstructor c = not (failed c) || isOkError c
|
||||||
|
isOkError = not . isDoesNotExistErrorType . ioeGetErrorType . err
|
||||||
|
|
||||||
|
|
||||||
|
-- | At 'Dir' constructor, apply transformation function to all of directory's
|
||||||
|
-- contents, then remove the Nothing's and recurse. This always preserves the
|
||||||
|
-- topomst constructor.
|
||||||
|
transformDir :: (DirTree a b -> DirTree a b) -> DirTree a b -> DirTree a b
|
||||||
|
transformDir f t = case f t of
|
||||||
|
(Dir n cs a) -> Dir n (map (transformDir f) cs) a
|
||||||
|
t' -> t'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---- OTHER: ----
|
||||||
|
|
||||||
|
|
||||||
|
anchoredToPath :: AnchoredDirTree a b -> FilePath
|
||||||
|
anchoredToPath a = anchor a </> (name . dirTree $ a)
|
||||||
|
|
||||||
|
|
||||||
|
ls :: DirTree DirTreeInfo DirTreeInfo
|
||||||
|
-> [(FileName, String)]
|
||||||
|
ls dt = fmap (\x -> (name x, packModTime x)) (contents dt)
|
||||||
|
|
||||||
|
|
||||||
|
fromFreeVar :: (Default d) => (a -> d) -> DirTree a a -> d
|
||||||
|
fromFreeVar f dt = maybeD f $ getFreeVar dt
|
||||||
|
|
||||||
|
|
||||||
|
maybeD :: (Default b) => (a -> b) -> Maybe a -> b
|
||||||
|
maybeD = maybe def
|
||||||
|
|
||||||
|
|
||||||
|
-- |Pack the modification time
|
||||||
|
packModTime :: DirTree DirTreeInfo DirTreeInfo
|
||||||
|
-> String
|
||||||
|
packModTime = fromFreeVar
|
||||||
|
$ formatTime defaultTimeLocale rfc822DateFormat
|
||||||
|
. modTime
|
||||||
|
|
||||||
|
|
||||||
|
packPermissions :: DirTree DirTreeInfo DirTreeInfo
|
||||||
|
-> String
|
||||||
|
packPermissions dt = fromFreeVar (pStr . permissions) dt
|
||||||
|
where
|
||||||
|
pStr perm = str perm readable "r"
|
||||||
|
++ str perm writable "w"
|
||||||
|
++ str perm (if isDirC dt then searchable else executable)
|
||||||
|
"x"
|
||||||
|
str perm f ch
|
||||||
|
| f perm = ch
|
||||||
|
| otherwise = "-"
|
178
src/Data/DirTree/Zipper.hs
Normal file
178
src/Data/DirTree/Zipper.hs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
||||||
|
|
||||||
|
module Data.DirTree.Zipper where
|
||||||
|
|
||||||
|
|
||||||
|
import Control.Applicative
|
||||||
|
(
|
||||||
|
(<$>)
|
||||||
|
)
|
||||||
|
import Control.Arrow
|
||||||
|
(
|
||||||
|
first
|
||||||
|
)
|
||||||
|
import Data.DirTree
|
||||||
|
import System.Directory
|
||||||
|
(
|
||||||
|
canonicalizePath
|
||||||
|
)
|
||||||
|
import System.FilePath
|
||||||
|
(
|
||||||
|
equalFilePath
|
||||||
|
, splitPath
|
||||||
|
, takeDirectory
|
||||||
|
, (</>)
|
||||||
|
)
|
||||||
|
import System.IO.Unsafe
|
||||||
|
(
|
||||||
|
unsafeInterleaveIO
|
||||||
|
)
|
||||||
|
import qualified Data.List as DL
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------
|
||||||
|
--[ ZIPPING ]--
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
-- |The zipper type, left is the (current) directory, right
|
||||||
|
-- are the breadcrumbs.
|
||||||
|
type DTZipper a b = (DirTree a b, [DirTree a b])
|
||||||
|
|
||||||
|
|
||||||
|
-- |The base zipper of a tree with empty crumbs element.
|
||||||
|
baseZipper :: DirTree a b -> DTZipper a b
|
||||||
|
baseZipper dt = (dt, [])
|
||||||
|
|
||||||
|
|
||||||
|
-- |Goes down the given subdir or file in a given directory. Returns `Nothing`
|
||||||
|
-- if the subdir or file does not exist.
|
||||||
|
--
|
||||||
|
-- Note that this function can be slow, so it's not supposed to be called
|
||||||
|
-- over a list of zippers. Use `goAllDown` instead.
|
||||||
|
goDown :: FileName -> DTZipper a b -> Maybe (DTZipper a b)
|
||||||
|
goDown ['.'] dtz = Just dtz
|
||||||
|
goDown ['.', '.'] dtz = Just $ goUp dtz
|
||||||
|
goDown fn (dtp@(Dir n cs d), xs) =
|
||||||
|
case mcdt of
|
||||||
|
Just cdt -> Just (cdt, Dir n (crumb' fn cs) d : xs)
|
||||||
|
Nothing -> Nothing
|
||||||
|
where
|
||||||
|
mcdt = DL.find (\x -> equalFilePath (name x) fn) cs
|
||||||
|
goDown _ _ = Nothing
|
||||||
|
|
||||||
|
|
||||||
|
-- |Goes down all subdirs of a given directory.
|
||||||
|
goAllDown :: DTZipper a b -> [DTZipper a b]
|
||||||
|
goAllDown (Dir n cs d, xs) = fmap (\x -> (x, Dir n (crumb x cs) d : xs)) cs
|
||||||
|
goAllDown _ = []
|
||||||
|
|
||||||
|
|
||||||
|
-- |Goes down the given subpath in a given directory. Returns `Nothing`
|
||||||
|
-- if the subpath does not exist.
|
||||||
|
goDown' :: FilePath -> DTZipper a b -> Maybe (DTZipper a b)
|
||||||
|
goDown' fp dz = go (splitPath fp) dz
|
||||||
|
where
|
||||||
|
go [] dz = Just dz
|
||||||
|
go (fn:fns) dz = goDown fn dz >>= go fns
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO: error handling if the parent of a file is a file too (wat?)
|
||||||
|
-- |Goes up one directory. This cannot fail. If you call it on the
|
||||||
|
-- root node of the zipper, you get it back untouched.
|
||||||
|
goUp :: DTZipper a b -> DTZipper a b
|
||||||
|
goUp dz@(_, []) = dz
|
||||||
|
goUp (dt, Dir n cs d : xs) = (Dir n (dt:cs) d, xs)
|
||||||
|
|
||||||
|
|
||||||
|
-- |Goes up to the root directory/node of the zipper.
|
||||||
|
goRoot :: DTZipper a b -> DTZipper a b
|
||||||
|
goRoot dz@(_, []) = dz
|
||||||
|
goRoot dz = goRoot (goUp dz)
|
||||||
|
|
||||||
|
|
||||||
|
-- |Gets the full path of the current directory in the zipper context.
|
||||||
|
-- This might not be a real absolute filesystem path, because it depends
|
||||||
|
-- on the zipper context.
|
||||||
|
getFullPath :: DTZipper a b -> FilePath
|
||||||
|
getFullPath dz@(dt, _:_) = getFullPath (goUp dz) </> name dt
|
||||||
|
getFullPath (dt, []) = name dt
|
||||||
|
|
||||||
|
|
||||||
|
-- |The zipper that describes the ".." file inside a directory. The name
|
||||||
|
-- is set to ".." too.
|
||||||
|
upDirZipper :: DTZipper a b -> DTZipper a b
|
||||||
|
upDirZipper dz = zipMap (\x -> x { name = "..", contents = [] }) $ goUp dz
|
||||||
|
|
||||||
|
|
||||||
|
-- |The zipper that describes the "." file inside a directory. The name
|
||||||
|
-- is set to "." too.
|
||||||
|
curDirZipper :: DTZipper a b -> DTZipper a b
|
||||||
|
curDirZipper dz = zipMap (\x -> x { name = ".", contents = [] }) dz
|
||||||
|
|
||||||
|
|
||||||
|
-- |Retrieve the (current) directory component from the zipper.
|
||||||
|
unZip :: DTZipper a b -> DirTree a b
|
||||||
|
unZip = fst
|
||||||
|
|
||||||
|
|
||||||
|
-- |Retrieve the (current) directory component from the zipper and
|
||||||
|
-- transform it to an `AnchoredDirTree`.
|
||||||
|
unZip' :: DTZipper a b -> AnchoredDirTree a b
|
||||||
|
unZip' dz@(dt, _) = (takeDirectory . getFullPath $ dz) :/ dt
|
||||||
|
|
||||||
|
|
||||||
|
-- |Map a function over the (current) directory component of the zipper.
|
||||||
|
zipMap :: (DirTree a b -> DirTree a b) -> DTZipper a b -> DTZipper a b
|
||||||
|
zipMap = first
|
||||||
|
|
||||||
|
|
||||||
|
-- |Creates a zipper at the given location with lazy breadcrumbs. That
|
||||||
|
-- means it doesn't traverse to the destination directory through the whole
|
||||||
|
-- tree.
|
||||||
|
--
|
||||||
|
-- This can throw an exception on `canonicalizePath`.
|
||||||
|
--
|
||||||
|
-- It uses `unsafeInterleaveIO` and `readDirectoryWithL` to achieve
|
||||||
|
-- lazy traversal.
|
||||||
|
zipLazy :: (FilePath -> IO a) -- ^ builder function for the free dir var
|
||||||
|
-> (FilePath -> IO b) -- ^ builder function for the free file var
|
||||||
|
-> FilePath -- ^ file path to drop to
|
||||||
|
-> IO (DTZipper a b)
|
||||||
|
zipLazy fd ff fp = do
|
||||||
|
dt <- dirTree <$> readDirectoryWithL fd ff fp
|
||||||
|
go dt fp
|
||||||
|
where
|
||||||
|
go dt fp' = do
|
||||||
|
-- TODO: I hope parentDir doesn't blow up
|
||||||
|
parentDir <- canonicalizePath (fp' ++ "/..")
|
||||||
|
if fp' == parentDir
|
||||||
|
then return $ baseZipper dt
|
||||||
|
else do
|
||||||
|
-- HERE IS THE UNSAFE CODE:
|
||||||
|
crumbs <- unsafeInterleaveIO $ crumbrec parentDir
|
||||||
|
return (dt, crumbs)
|
||||||
|
where
|
||||||
|
crumbrec pD = do
|
||||||
|
pdt@(Dir n cs d) <- dirTree <$> readDirectoryWithL fd ff pD
|
||||||
|
(_, pc) <- go pdt pD
|
||||||
|
return $ Dir n (crumb dt cs) d : pc
|
||||||
|
|
||||||
|
|
||||||
|
readPath' :: FilePath -> IO (DTZipper DirTreeInfo DirTreeInfo)
|
||||||
|
readPath' = zipLazy mkDirInfo mkFileInfo
|
||||||
|
|
||||||
|
|
||||||
|
---------------
|
||||||
|
--[ HELPERS ]--
|
||||||
|
---------------
|
||||||
|
|
||||||
|
|
||||||
|
crumb :: DirTree a b -> [DirTree a b] -> [DirTree a b]
|
||||||
|
crumb dt cs = crumb' (name dt) cs
|
||||||
|
|
||||||
|
|
||||||
|
crumb' :: FileName -> [DirTree a b] -> [DirTree a b]
|
||||||
|
crumb' fn cs =
|
||||||
|
foldr (\x y -> if equalFilePath fn (name x) then y else x : y)
|
||||||
|
[] cs
|
14
src/GUI/Gtk.hs
Normal file
14
src/GUI/Gtk.hs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
||||||
|
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Graphics.UI.Gtk
|
||||||
|
import GUI.Gtk.Gui
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
_ <- initGUI
|
||||||
|
|
||||||
|
startMainWindow
|
||||||
|
|
||||||
|
mainGUI
|
3
src/GUI/Gtk/Callbacks.hs
Normal file
3
src/GUI/Gtk/Callbacks.hs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
||||||
|
|
||||||
|
module GUI.Gtk.Callbacks (startGUI) where
|
424
src/GUI/Gtk/Gui.hs
Normal file
424
src/GUI/Gtk/Gui.hs
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
||||||
|
|
||||||
|
module GUI.Gtk.Gui (startMainWindow) where
|
||||||
|
|
||||||
|
|
||||||
|
import Control.Applicative
|
||||||
|
(
|
||||||
|
(<$>)
|
||||||
|
, (<*>)
|
||||||
|
)
|
||||||
|
import Control.Concurrent
|
||||||
|
(
|
||||||
|
forkIO
|
||||||
|
)
|
||||||
|
import Control.Concurrent.STM
|
||||||
|
(
|
||||||
|
TVar
|
||||||
|
, newTVarIO
|
||||||
|
, readTVarIO
|
||||||
|
)
|
||||||
|
import Control.Exception
|
||||||
|
(
|
||||||
|
try
|
||||||
|
, Exception
|
||||||
|
, SomeException
|
||||||
|
)
|
||||||
|
import Control.Monad
|
||||||
|
(
|
||||||
|
when
|
||||||
|
, void
|
||||||
|
)
|
||||||
|
import Control.Monad.IO.Class
|
||||||
|
(
|
||||||
|
liftIO
|
||||||
|
)
|
||||||
|
import Data.DirTree
|
||||||
|
import Data.DirTree.Zipper
|
||||||
|
import Data.Foldable
|
||||||
|
(
|
||||||
|
for_
|
||||||
|
)
|
||||||
|
import Data.List
|
||||||
|
(
|
||||||
|
sort
|
||||||
|
, isPrefixOf
|
||||||
|
)
|
||||||
|
import Data.Maybe
|
||||||
|
(
|
||||||
|
fromJust
|
||||||
|
, catMaybes
|
||||||
|
)
|
||||||
|
import Graphics.UI.Gtk
|
||||||
|
import Graphics.UI.Gtk.Abstract.Box
|
||||||
|
import Graphics.UI.Gtk.Builder
|
||||||
|
import Graphics.UI.Gtk.ModelView
|
||||||
|
import GUI.Gtk.Icons
|
||||||
|
import IO.Error
|
||||||
|
import IO.File
|
||||||
|
import IO.Utils
|
||||||
|
import System.Directory
|
||||||
|
(
|
||||||
|
executable
|
||||||
|
, doesFileExist
|
||||||
|
, doesDirectoryExist
|
||||||
|
)
|
||||||
|
import System.Environment
|
||||||
|
(
|
||||||
|
getArgs
|
||||||
|
)
|
||||||
|
import System.FilePath.Posix
|
||||||
|
(
|
||||||
|
isAbsolute
|
||||||
|
)
|
||||||
|
import System.Glib.UTFString
|
||||||
|
(
|
||||||
|
glibToString
|
||||||
|
)
|
||||||
|
import System.IO.Unsafe
|
||||||
|
(
|
||||||
|
unsafePerformIO
|
||||||
|
)
|
||||||
|
import System.Process
|
||||||
|
(
|
||||||
|
spawnProcess
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO: simplify where we modify the TVars
|
||||||
|
-- TODO: double check garbage collection/gtk ref counting
|
||||||
|
-- TODO: file watching, when and what to reread
|
||||||
|
|
||||||
|
|
||||||
|
-- |Monolithic object passed to various GUI functions in order
|
||||||
|
-- to keep the API stable and not alter the parameters too much.
|
||||||
|
-- This only holds GUI widgets that are needed to be read during
|
||||||
|
-- runtime.
|
||||||
|
data MyGUI = MkMyGUI {
|
||||||
|
-- |main Window
|
||||||
|
rootWin :: Window
|
||||||
|
, menubarFileQuit :: ImageMenuItem
|
||||||
|
, menubarFileOpen :: ImageMenuItem
|
||||||
|
, menubarFileCut :: ImageMenuItem
|
||||||
|
, menubarFileCopy :: ImageMenuItem
|
||||||
|
, menubarFilePaste :: ImageMenuItem
|
||||||
|
, menubarFileDelete :: ImageMenuItem
|
||||||
|
, menubarHelpAbout :: ImageMenuItem
|
||||||
|
, urlBar :: Entry
|
||||||
|
-- |tree view
|
||||||
|
, treeView :: TreeView
|
||||||
|
-- |first column
|
||||||
|
, cF :: TreeViewColumn
|
||||||
|
-- |second column
|
||||||
|
, cMD :: TreeViewColumn
|
||||||
|
-- |renderer used for the treeView
|
||||||
|
, renderTxt :: CellRendererText
|
||||||
|
, renderPix :: CellRendererPixbuf
|
||||||
|
, settings :: TVar FMSettings
|
||||||
|
, folderPix :: Pixbuf
|
||||||
|
, filePix :: Pixbuf
|
||||||
|
, errorPix :: Pixbuf
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- |FM-wide settings.
|
||||||
|
data FMSettings = MkFMSettings {
|
||||||
|
showHidden :: Bool
|
||||||
|
, isLazy :: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- |This describes the contents of the treeView and is separated from MyGUI,
|
||||||
|
-- because we might want to have multiple views.
|
||||||
|
data MyView = MkMyView {
|
||||||
|
-- |raw model with unsorted data
|
||||||
|
rawModel :: TVar (ListStore (DTZipper DirTreeInfo DirTreeInfo))
|
||||||
|
-- |sorted proxy model
|
||||||
|
, sortedModel :: TVar (TypedTreeModelSort
|
||||||
|
(DTZipper DirTreeInfo DirTreeInfo))
|
||||||
|
-- |filtered proxy model
|
||||||
|
, filteredModel :: TVar (TypedTreeModelFilter
|
||||||
|
(DTZipper DirTreeInfo DirTreeInfo))
|
||||||
|
, fsState :: TVar (DTZipper DirTreeInfo DirTreeInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- |Set hotkeys.
|
||||||
|
setBindings :: MyGUI -> MyView -> IO ()
|
||||||
|
setBindings mygui myview = do
|
||||||
|
_ <- rootWin mygui `on` keyPressEvent $ tryEvent $ do
|
||||||
|
[Control] <- eventModifier
|
||||||
|
"q" <- fmap glibToString eventKeyName
|
||||||
|
liftIO mainQuit
|
||||||
|
_ <- rootWin mygui `on` keyPressEvent $ tryEvent $ do
|
||||||
|
[Control] <- eventModifier
|
||||||
|
"h" <- fmap glibToString eventKeyName
|
||||||
|
liftIO $ modifyTVarIO (settings mygui)
|
||||||
|
(\x -> x { showHidden = not . showHidden $ x})
|
||||||
|
>> updateTreeView mygui myview
|
||||||
|
_ <- rootWin mygui `on` keyPressEvent $ tryEvent $ do
|
||||||
|
[Alt] <- eventModifier
|
||||||
|
"Up" <- fmap glibToString eventKeyName
|
||||||
|
liftIO $ upDir mygui myview
|
||||||
|
_ <- treeView mygui `on` rowActivated $ openRow mygui myview
|
||||||
|
_ <- menubarFileQuit mygui `on` menuItemActivated $ mainQuit
|
||||||
|
_ <- urlBar mygui `on` entryActivated $ urlGoTo mygui myview
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
-- |Go the the url given at the `urlBar` and visualize it in the given
|
||||||
|
-- treeView.
|
||||||
|
--
|
||||||
|
-- This might update the TVar `rawModel`.
|
||||||
|
urlGoTo :: MyGUI -> MyView -> IO ()
|
||||||
|
urlGoTo mygui myview = do
|
||||||
|
fp <- entryGetText (urlBar mygui)
|
||||||
|
let abs = isAbsolute fp
|
||||||
|
exists <- (||) <$> doesDirectoryExist fp <*> doesFileExist fp
|
||||||
|
-- TODO: more explicit error handling?
|
||||||
|
when (abs && exists) $ do
|
||||||
|
newFsState <- readPath' fp
|
||||||
|
newRawModel <- fileListStore newFsState myview
|
||||||
|
writeTVarIO (rawModel myview) newRawModel
|
||||||
|
updateTreeView mygui myview
|
||||||
|
|
||||||
|
|
||||||
|
-- |Enter a subdirectory and visualize it in the treeView or
|
||||||
|
-- open a file.
|
||||||
|
--
|
||||||
|
-- This might update the TVar `rawModel`.
|
||||||
|
openRow :: MyGUI -> MyView -> TreePath -> TreeViewColumn -> IO ()
|
||||||
|
openRow mygui myview tp tvc = do
|
||||||
|
rawModel' <- readTVarIO $ rawModel myview
|
||||||
|
sortedModel' <- readTVarIO $ sortedModel myview
|
||||||
|
filteredModel' <- readTVarIO $ filteredModel myview
|
||||||
|
miter <- treeModelGetIter sortedModel' tp
|
||||||
|
for_ miter $ \iter -> do
|
||||||
|
cIter' <- treeModelSortConvertIterToChildIter sortedModel' iter
|
||||||
|
cIter <- treeModelFilterConvertIterToChildIter filteredModel' cIter'
|
||||||
|
row <- treeModelGetRow rawModel' cIter
|
||||||
|
case row of
|
||||||
|
(Dir _ _ _, _) -> do
|
||||||
|
newRawModel <- fileListStore row myview
|
||||||
|
rm <- readTVarIO (rawModel myview)
|
||||||
|
writeTVarIO (rawModel myview) newRawModel
|
||||||
|
updateTreeView mygui myview
|
||||||
|
dz@(File _ _, _) ->
|
||||||
|
withErrorDialog $ openFile (getFullPath dz)
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
|
||||||
|
-- |Go up one directory and visualize it in the treeView.
|
||||||
|
--
|
||||||
|
-- This will update the TVar `rawModel`.
|
||||||
|
upDir :: MyGUI -> MyView -> IO ()
|
||||||
|
upDir mygui myview = do
|
||||||
|
rawModel' <- readTVarIO $ rawModel myview
|
||||||
|
sortedModel' <- readTVarIO $ sortedModel myview
|
||||||
|
fS <- readTVarIO $ fsState myview
|
||||||
|
newRawModel <- fileListStore (goUp fS) myview
|
||||||
|
writeTVarIO (rawModel myview) newRawModel
|
||||||
|
updateTreeView mygui myview
|
||||||
|
|
||||||
|
|
||||||
|
-- |Create the `ListStore` of files/directories from the current directory.
|
||||||
|
-- This is the function which maps the Data.DirTree data structures
|
||||||
|
-- into the GTK+ data structures.
|
||||||
|
--
|
||||||
|
-- This also updates the TVar `fsState` inside the given view.
|
||||||
|
fileListStore :: DTZipper DirTreeInfo DirTreeInfo -- ^ current dir
|
||||||
|
-> MyView
|
||||||
|
-> IO (ListStore (DTZipper DirTreeInfo DirTreeInfo))
|
||||||
|
fileListStore dtz myview = do
|
||||||
|
writeTVarIO (fsState myview) dtz
|
||||||
|
listStoreNew (goAllDown dtz)
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO: make this function more slim so only the most necessary parts are
|
||||||
|
-- called
|
||||||
|
-- |Updates the visible TreeView with the current underlying mutable models,
|
||||||
|
-- which are retrieved from `MyGUI`.
|
||||||
|
--
|
||||||
|
-- This also updates the TVars `filteredModel` and `sortedModel` in the process.
|
||||||
|
updateTreeView :: MyGUI
|
||||||
|
-> MyView
|
||||||
|
-> IO ()
|
||||||
|
updateTreeView mygui myview = do
|
||||||
|
let treeView' = treeView mygui
|
||||||
|
cF' = cF mygui
|
||||||
|
cMD' = cMD mygui
|
||||||
|
render' = renderTxt mygui
|
||||||
|
|
||||||
|
-- update urlBar, this will break laziness slightly, probably
|
||||||
|
fsState <- readTVarIO $ fsState myview
|
||||||
|
let urlpath = getFullPath fsState
|
||||||
|
entrySetText (urlBar mygui) urlpath
|
||||||
|
|
||||||
|
rawModel' <- readTVarIO $ rawModel myview
|
||||||
|
|
||||||
|
-- filtering
|
||||||
|
filteredModel' <- treeModelFilterNew rawModel' []
|
||||||
|
writeTVarIO (filteredModel myview) filteredModel'
|
||||||
|
treeModelFilterSetVisibleFunc filteredModel' $ \iter -> do
|
||||||
|
hidden <- showHidden <$> readTVarIO (settings mygui)
|
||||||
|
row <- treeModelGetRow rawModel' iter
|
||||||
|
if hidden
|
||||||
|
then return True
|
||||||
|
else return $ not ("." `isPrefixOf` (name . unZip $ row))
|
||||||
|
|
||||||
|
-- sorting
|
||||||
|
sortedModel' <- treeModelSortNewWithModel filteredModel'
|
||||||
|
writeTVarIO (sortedModel myview) sortedModel'
|
||||||
|
treeSortableSetSortFunc sortedModel' 1 $ \iter1 iter2 -> do
|
||||||
|
cIter1 <- treeModelFilterConvertIterToChildIter filteredModel' iter1
|
||||||
|
cIter2 <- treeModelFilterConvertIterToChildIter filteredModel' iter2
|
||||||
|
row1 <- treeModelGetRow rawModel' cIter1
|
||||||
|
row2 <- treeModelGetRow rawModel' cIter2
|
||||||
|
return $ compare (unZip row1) (unZip row2)
|
||||||
|
treeSortableSetSortColumnId sortedModel' 1 SortAscending
|
||||||
|
|
||||||
|
-- set values
|
||||||
|
treeModelSetColumn rawModel' (makeColumnIdPixbuf 0)
|
||||||
|
(dirtreePix . unZip)
|
||||||
|
treeModelSetColumn rawModel' (makeColumnIdString 1)
|
||||||
|
(name . unZip)
|
||||||
|
treeModelSetColumn rawModel' (makeColumnIdString 2)
|
||||||
|
(packModTime . unZip)
|
||||||
|
treeModelSetColumn rawModel' (makeColumnIdString 3)
|
||||||
|
(packPermissions . unZip)
|
||||||
|
|
||||||
|
-- update treeview model
|
||||||
|
treeViewSetModel treeView' sortedModel'
|
||||||
|
|
||||||
|
return ()
|
||||||
|
where
|
||||||
|
dirtreePix (Dir {}) = folderPix mygui
|
||||||
|
dirtreePix (File {}) = filePix mygui
|
||||||
|
dirtreePix (Failed {}) = errorPix mygui
|
||||||
|
|
||||||
|
|
||||||
|
-- |Pops up an error Dialog with the given String.
|
||||||
|
showErrorDialog :: String -> IO ()
|
||||||
|
showErrorDialog str = do
|
||||||
|
errorDialog <- messageDialogNew Nothing
|
||||||
|
[DialogDestroyWithParent]
|
||||||
|
MessageError
|
||||||
|
ButtonsClose
|
||||||
|
str
|
||||||
|
_ <- dialogRun errorDialog
|
||||||
|
widgetDestroy errorDialog
|
||||||
|
|
||||||
|
|
||||||
|
-- |Execute the given IO action. If the action throws exceptions,
|
||||||
|
-- visualize them via `showErrorDialog`.
|
||||||
|
withErrorDialog :: IO a -> IO ()
|
||||||
|
withErrorDialog io = do
|
||||||
|
r <- try io
|
||||||
|
either (\e -> showErrorDialog $ show (e :: SomeException))
|
||||||
|
(\_ -> return ())
|
||||||
|
r
|
||||||
|
|
||||||
|
|
||||||
|
-- |Set up the GUI.
|
||||||
|
startMainWindow :: IO ()
|
||||||
|
startMainWindow = do
|
||||||
|
|
||||||
|
settings <- newTVarIO (MkFMSettings False True)
|
||||||
|
|
||||||
|
-- get the icons
|
||||||
|
iT <- iconThemeGetDefault
|
||||||
|
folderPix <- getIcon IFolder 24
|
||||||
|
filePix <- getIcon IFile 24
|
||||||
|
errorPix <- getIcon IError 24
|
||||||
|
|
||||||
|
fsState <- readPath "/" >>= (newTVarIO . baseZipper . dirTree)
|
||||||
|
|
||||||
|
builder <- builderNew
|
||||||
|
builderAddFromFile builder "data/Gtk/builder.xml"
|
||||||
|
|
||||||
|
-- get the pre-defined gui widgets
|
||||||
|
rootWin <- builderGetObject builder castToWindow
|
||||||
|
"rootWin"
|
||||||
|
scroll <- builderGetObject builder castToScrolledWindow
|
||||||
|
"mainScroll"
|
||||||
|
menubarFileQuit <- builderGetObject builder castToImageMenuItem
|
||||||
|
"menubarFileQuit"
|
||||||
|
menubarFileOpen <- builderGetObject builder castToImageMenuItem
|
||||||
|
"menubarFileOpen"
|
||||||
|
menubarFileCut <- builderGetObject builder castToImageMenuItem
|
||||||
|
"menubarFileCut"
|
||||||
|
menubarFileCopy <- builderGetObject builder castToImageMenuItem
|
||||||
|
"menubarFileCopy"
|
||||||
|
menubarFilePaste <- builderGetObject builder castToImageMenuItem
|
||||||
|
"menubarFilePaste"
|
||||||
|
menubarFileDelete <- builderGetObject builder castToImageMenuItem
|
||||||
|
"menubarFileDelete"
|
||||||
|
menubarHelpAbout <- builderGetObject builder castToImageMenuItem
|
||||||
|
"menubarHelpAbout"
|
||||||
|
urlBar <- builderGetObject builder castToEntry "urlBar"
|
||||||
|
|
||||||
|
-- create initial list store model with unsorted data
|
||||||
|
rawModel <- newTVarIO =<< listStoreNew . goAllDown =<< readTVarIO fsState
|
||||||
|
|
||||||
|
filteredModel <- newTVarIO =<< (\x -> treeModelFilterNew x [])
|
||||||
|
=<< readTVarIO rawModel
|
||||||
|
|
||||||
|
-- create an initial sorting proxy model
|
||||||
|
sortedModel <- newTVarIO =<< treeModelSortNewWithModel
|
||||||
|
=<< readTVarIO filteredModel
|
||||||
|
|
||||||
|
-- create the final view
|
||||||
|
treeView <- treeViewNew
|
||||||
|
|
||||||
|
-- create final tree model columns
|
||||||
|
renderTxt <- cellRendererTextNew
|
||||||
|
renderPix <- cellRendererPixbufNew
|
||||||
|
let ct = cellText :: (CellRendererTextClass cr) => Attr cr String
|
||||||
|
cp = cellPixbuf :: (CellRendererPixbufClass self) => Attr self Pixbuf
|
||||||
|
|
||||||
|
-- filename column
|
||||||
|
cF <- treeViewColumnNew
|
||||||
|
treeViewColumnSetTitle cF "Filename"
|
||||||
|
treeViewColumnSetResizable cF True
|
||||||
|
treeViewColumnSetClickable cF True
|
||||||
|
treeViewColumnSetSortColumnId cF 1
|
||||||
|
cellLayoutPackStart cF renderPix False
|
||||||
|
cellLayoutPackStart cF renderTxt True
|
||||||
|
_ <- treeViewAppendColumn treeView cF
|
||||||
|
cellLayoutAddColumnAttribute cF renderPix cp $ makeColumnIdPixbuf 0
|
||||||
|
cellLayoutAddColumnAttribute cF renderTxt ct $ makeColumnIdString 1
|
||||||
|
|
||||||
|
-- date column
|
||||||
|
cMD <- treeViewColumnNew
|
||||||
|
treeViewColumnSetTitle cMD "Date"
|
||||||
|
treeViewColumnSetResizable cMD True
|
||||||
|
treeViewColumnSetClickable cMD True
|
||||||
|
treeViewColumnSetSortColumnId cMD 2
|
||||||
|
cellLayoutPackStart cMD renderTxt True
|
||||||
|
_ <- treeViewAppendColumn treeView cMD
|
||||||
|
cellLayoutAddColumnAttribute cMD renderTxt ct $ makeColumnIdString 2
|
||||||
|
|
||||||
|
-- permissions column
|
||||||
|
cP <- treeViewColumnNew
|
||||||
|
treeViewColumnSetTitle cP "Permission"
|
||||||
|
treeViewColumnSetResizable cP True
|
||||||
|
treeViewColumnSetClickable cP True
|
||||||
|
treeViewColumnSetSortColumnId cP 3
|
||||||
|
cellLayoutPackStart cP renderTxt True
|
||||||
|
_ <- treeViewAppendColumn treeView cP
|
||||||
|
cellLayoutAddColumnAttribute cP renderTxt ct $ makeColumnIdString 3
|
||||||
|
|
||||||
|
-- construct the gui object
|
||||||
|
let mygui = MkMyGUI {..}
|
||||||
|
let myview = MkMyView {..}
|
||||||
|
|
||||||
|
-- create the tree model with its contents
|
||||||
|
updateTreeView mygui myview
|
||||||
|
|
||||||
|
-- set the bindings
|
||||||
|
setBindings mygui myview
|
||||||
|
|
||||||
|
-- add the treeview to the scroll container
|
||||||
|
containerAdd scroll treeView
|
||||||
|
|
||||||
|
widgetShowAll rootWin
|
31
src/GUI/Gtk/Icons.hs
Normal file
31
src/GUI/Gtk/Icons.hs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
||||||
|
|
||||||
|
module GUI.Gtk.Icons where
|
||||||
|
|
||||||
|
|
||||||
|
import Graphics.UI.Gtk
|
||||||
|
import Graphics.UI.Gtk.Gdk.Pixbuf
|
||||||
|
|
||||||
|
|
||||||
|
-- |Icon type we use in our GUI.
|
||||||
|
data GtkIcon = IFolder
|
||||||
|
| IFile
|
||||||
|
| IError
|
||||||
|
|
||||||
|
|
||||||
|
-- |Gets an icon from the default icon theme and falls back to project-icons
|
||||||
|
-- if not found. The requested icon size is not guaranteed.
|
||||||
|
getIcon :: GtkIcon -- ^ icon we want
|
||||||
|
-> Int -- ^ requested icon size
|
||||||
|
-> IO Pixbuf
|
||||||
|
getIcon icon isize = do
|
||||||
|
let iname = iconToStr icon
|
||||||
|
iT <- iconThemeGetDefault
|
||||||
|
mpix <- iconThemeLoadIcon iT iname isize IconLookupUseBuiltin
|
||||||
|
case mpix of
|
||||||
|
Just pix -> return pix
|
||||||
|
Nothing -> pixbufNewFromFile ("data/Gtk/icons/" ++ iname)
|
||||||
|
where
|
||||||
|
iconToStr IFolder = "gtk-directory"
|
||||||
|
iconToStr IFile = "gtk-file"
|
||||||
|
iconToStr IError = "error"
|
23
src/IO/Error.hs
Normal file
23
src/IO/Error.hs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
||||||
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
|
|
||||||
|
module IO.Error where
|
||||||
|
|
||||||
|
|
||||||
|
import Control.Exception
|
||||||
|
import Control.Monad
|
||||||
|
(
|
||||||
|
mzero
|
||||||
|
, MonadPlus
|
||||||
|
)
|
||||||
|
import Data.Typeable
|
||||||
|
|
||||||
|
|
||||||
|
data FmIOException = FileDoesNotExist String
|
||||||
|
| PathNotAbsolute String
|
||||||
|
| FileNotExecutable String
|
||||||
|
deriving (Show, Typeable)
|
||||||
|
|
||||||
|
|
||||||
|
instance Exception FmIOException
|
||||||
|
|
68
src/IO/File.hs
Normal file
68
src/IO/File.hs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
||||||
|
|
||||||
|
module IO.File (
|
||||||
|
openFile
|
||||||
|
, executeFile
|
||||||
|
) where
|
||||||
|
|
||||||
|
|
||||||
|
import Control.Exception
|
||||||
|
(
|
||||||
|
throw
|
||||||
|
)
|
||||||
|
import Control.Monad
|
||||||
|
(
|
||||||
|
unless
|
||||||
|
, void
|
||||||
|
)
|
||||||
|
import IO.Error
|
||||||
|
import System.Directory
|
||||||
|
(
|
||||||
|
doesFileExist
|
||||||
|
, getPermissions
|
||||||
|
, executable
|
||||||
|
)
|
||||||
|
import System.FilePath.Posix
|
||||||
|
(
|
||||||
|
isAbsolute
|
||||||
|
)
|
||||||
|
import System.Process
|
||||||
|
(
|
||||||
|
spawnProcess
|
||||||
|
, ProcessHandle
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
-- |Opens a file appropriately by invoking xdg-open.
|
||||||
|
--
|
||||||
|
-- This will throw an exception if the filepath is not absolute
|
||||||
|
-- or the file does not exist.
|
||||||
|
openFile :: FilePath -- ^ absolute path to file
|
||||||
|
-> IO ProcessHandle
|
||||||
|
openFile fp = do
|
||||||
|
fileSanityThrow fp
|
||||||
|
spawnProcess "xdg-open" [fp]
|
||||||
|
|
||||||
|
|
||||||
|
-- |Executes a program with the given arguments.
|
||||||
|
--
|
||||||
|
-- This will throw an exception if the filepath is not absolute
|
||||||
|
-- or the file does not exist. It will also throw an exception
|
||||||
|
-- if the file is not executable.
|
||||||
|
executeFile :: FilePath -- ^ absolute path to program
|
||||||
|
-> [String] -- ^ arguments
|
||||||
|
-> IO ProcessHandle
|
||||||
|
executeFile fp args = do
|
||||||
|
fileSanityThrow fp
|
||||||
|
p <- getPermissions fp
|
||||||
|
unless (executable p) (throw $ FileNotExecutable fp)
|
||||||
|
spawnProcess fp args
|
||||||
|
|
||||||
|
|
||||||
|
-- Throws an exception if the filepath is not absolute
|
||||||
|
-- or the file does not exist.
|
||||||
|
fileSanityThrow :: FilePath -> IO ()
|
||||||
|
fileSanityThrow fp = do
|
||||||
|
unless (isAbsolute fp) (throw $ PathNotAbsolute fp)
|
||||||
|
exists <- doesFileExist fp
|
||||||
|
unless exists (throw $ FileDoesNotExist fp)
|
23
src/IO/Utils.hs
Normal file
23
src/IO/Utils.hs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
||||||
|
|
||||||
|
module IO.Utils where
|
||||||
|
|
||||||
|
|
||||||
|
import Control.Concurrent.STM
|
||||||
|
(
|
||||||
|
atomically
|
||||||
|
)
|
||||||
|
import Control.Concurrent.STM.TVar
|
||||||
|
(
|
||||||
|
writeTVar
|
||||||
|
, modifyTVar
|
||||||
|
, TVar
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
writeTVarIO :: TVar a -> a -> IO ()
|
||||||
|
writeTVarIO tvar val = atomically $ writeTVar tvar val
|
||||||
|
|
||||||
|
|
||||||
|
modifyTVarIO :: TVar a -> (a -> a) -> IO ()
|
||||||
|
modifyTVarIO tvar f = atomically $ modifyTVar tvar f
|
Loading…
Reference in New Issue
Block a user