Imported Upstream version 3.0.40
SVN-Git Migration
6 years ago
0 | include etmTk/CHANGES | |
1 | include etmTk/etm.1 | |
2 | include etmTk/etm.xpm | |
3 | include etmTk/etm.desktop | |
4 | include etmTk/etm.appdata.xml | |
5 | include etmTk/help/UserManual.html |
0 | Metadata-Version: 1.1 | |
1 | Name: etmtk | |
2 | Version: 3.0.40 | |
3 | Summary: event and task manager | |
4 | Home-page: http://people.duke.edu/~dgraham/etmtk | |
5 | Author: Daniel A Graham | |
6 | Author-email: daniel.graham@duke.edu | |
7 | License: License :: OSI Approved :: GNU General Public License (GPL) | |
8 | Description: manage events and tasks using simple text files | |
9 | Platform: Any | |
10 | Classifier: Development Status :: 5 - Production/Stable | |
11 | Classifier: Environment :: Console | |
12 | Classifier: Environment :: MacOS X | |
13 | Classifier: Environment :: Win32 (MS Windows) | |
14 | Classifier: Environment :: X11 Applications | |
15 | Classifier: Intended Audience :: End Users/Desktop | |
16 | Classifier: License :: OSI Approved :: GNU General Public License (GPL) | |
17 | Classifier: Operating System :: MacOS :: MacOS X | |
18 | Classifier: Operating System :: Microsoft :: Windows :: Windows Vista | |
19 | Classifier: Operating System :: Microsoft :: Windows :: Windows 7 | |
20 | Classifier: Operating System :: OS Independent | |
21 | Classifier: Operating System :: POSIX | |
22 | Classifier: Operating System :: POSIX :: Linux | |
23 | Classifier: Programming Language :: Python | |
24 | Classifier: Programming Language :: Python :: 2.7 | |
25 | Classifier: Programming Language :: Python :: 3.3 | |
26 | Classifier: Programming Language :: Python :: 3.4 | |
27 | Classifier: Topic :: Office/Business | |
28 | Classifier: Topic :: Office/Business :: News/Diary | |
29 | Classifier: Topic :: Office/Business :: Scheduling |
0 | etm Tk | |
1 | ||
2 | manage events and tasks using simple text files | |
3 | etm is an acronym for event and task manager. | |
4 | ||
5 | In contrast to most calendar/todo applications, creating items (events, | |
6 | tasks, and so forth) in etm does not require filling out fields in a | |
7 | form. Instead, items are created as free-form text entries using a | |
8 | simple, intuitive format and stored in plain text files. | |
9 | ||
10 | This project is hosted on GitHub. | |
11 | ||
12 | Sample entries | |
13 | -------------- | |
14 | ||
15 | Items in etm begin with a type character such as an asterisk (event) and | |
16 | continue on one or more lines either until the end of the file is | |
17 | reached or another line is found that begins with a type character. The | |
18 | beginning type character for each item is followed by the item summary | |
19 | and then, perhaps, by one or more @key value pairs. The order in which | |
20 | such pairs are entered does not matter. | |
21 | ||
22 | - A sales meeting (an event) [s]tarting seven days from today at | |
23 | 9:00am and [e]xtending for one hour with a default [a]lert 5 minutes | |
24 | before the start: | |
25 | ||
26 | * sales meeting @s +7 9a @e 1h @a 5 | |
27 | ||
28 | - The sales meeting with another [a]lert 2 days before the meeting to | |
29 | (e)mail a reminder to a list of recipients: | |
30 | ||
31 | * sales meeting @s +7 9a @e 1h @a 5 | |
32 | @a 2d: e; who@when.com, what@where.org | |
33 | ||
34 | - Prepare a report (a task) for the sales meeting [b]eginning 3 days | |
35 | early: | |
36 | ||
37 | - prepare report @s +7 @b 3 | |
38 | ||
39 | - A period [e]xtending 35 minutes (an action) spent working on the | |
40 | report yesterday: | |
41 | ||
42 | ~ report preparation @s -1 @e 35 | |
43 | ||
44 | - Get a haircut (a task) on the 24th of the current month and then | |
45 | [r]epeatedly at (d)aily [i]ntervals of (14) days and, [o]n | |
46 | completion, (r)estart from the completion date: | |
47 | ||
48 | - get haircut @s 24 @r d &i 14 @o r | |
49 | ||
50 | - Payday (an occasion) on the last week day of each month. The &s -1 | |
51 | part of the entry extracts the last date which is both a weekday and | |
52 | falls within the last three days of the month): | |
53 | ||
54 | ^ payday @s 1/1 @r m &w MO, TU, WE, TH, FR &m -1, -2, -3 &s -1 | |
55 | ||
56 | - Take a prescribed medication daily (a reminder) [s]tarting today and | |
57 | [r]epeating (d)aily at [h]ours 10am, 2pm, 6pm and 10pm [u]ntil (12am | |
58 | on) the fourth day from today. Trigger the default [a]lert zero | |
59 | minutes before each reminder: | |
60 | ||
61 | * take Rx @s +0 @r d &h 10, 14, 18, 22 &u +4 @a 0 | |
62 | ||
63 | - Move the water sprinkler (a reminder) every thirty mi[n]utes on | |
64 | Sunday afternoons using the default alert zero minutes before each | |
65 | reminder: | |
66 | ||
67 | * Move sprinkler @s 1 @r w &w SU &h 14, 15, 16, 17 &n 0, 30 @a 0 | |
68 | ||
69 | To limit the sprinkler movement reminders to the [M]onths of April | |
70 | through September each year change the @r entry to this: | |
71 | ||
72 | @r w &w SU &h 14, 15, 16, 17 &n 0, 30 &M 4, 5, 6, 7, 8, 9 | |
73 | ||
74 | or this: | |
75 | ||
76 | @r n &i 30 &w SU &h 14, 15, 16, 17 &M 4, 5, 6, 7, 8, 9 | |
77 | ||
78 | - Presidential election day (an occasion) every four years on the | |
79 | first Tuesday after a Monday in November: | |
80 | ||
81 | ^ Presidential Election Day @s 2012-11-06 | |
82 | @r y &i 4 &M 11 &m 2, 3, 4, 5, 6, 7, 8 &w TU | |
83 | ||
84 | - Join the etm discussion group (a task) [s]tarting on the first day | |
85 | of the next month. Because of the @g (goto) link, pressing Ctrl-G | |
86 | when the details of this item are displayed in the gui would open | |
87 | the link using the system default application which, in this case, | |
88 | would be your default browser: | |
89 | ||
90 | - join the etm discussion group @s +1/1 | |
91 | @g http://groups.google.com/group/eventandtaskmanager/topics | |
92 | ||
93 | Installation | |
94 | ------------ | |
95 | ||
96 | Source installation under OS X, Linux or Windows | |
97 | ||
98 | Python 2.7.x or python >= 3.3.0 is required. | |
99 | ||
100 | The following python packages are required for etm but are not included | |
101 | in the python standard library: | |
102 | ||
103 | - dateutil (1.5 is OK but >= 2.1 is strongly recommended) | |
104 | - PyYaml (>= 3.10) | |
105 | - icalendar (>=3.5 for python 2, >= 3.6 for python 3) | |
106 | ||
107 | Tk and the python module tkinter are also required but are typically | |
108 | already installed on most modern operating systems. If needed, | |
109 | installation instructions are given at | |
110 | www.tkdocs.com/tutorial/install.html. | |
111 | ||
112 | Installing etm | |
113 | ||
114 | Download 'etmtk-x.x.x.tar.gz' from this site, unpack the tarball, cd to | |
115 | the resulting directory and do the normal | |
116 | ||
117 | sudo python setup.py install | |
118 | ||
119 | for a system installation. You can then run from any directory either | |
120 | ||
121 | $ etm ? | |
122 | ||
123 | for information about command line usage or | |
124 | ||
125 | $ etm | |
126 | ||
127 | to open the etm gui. | |
128 | ||
129 | Alternatively, you can avoid doing a system installation and simply run | |
130 | either | |
131 | ||
132 | $ python etm ? | |
133 | ||
134 | or | |
135 | ||
136 | $ python etm | |
137 | ||
138 | or | |
139 | ||
140 | $ ./etm | |
141 | ||
142 | from this directory. | |
143 | ||
144 | Installing Git or Mercurial | |
145 | ||
146 | Having one of these version control systems is optional but strongly | |
147 | recommended! | |
148 | ||
149 | With either progam installed, etm will automatically commit any change | |
150 | made to any data file. You can see the history of your changes either to | |
151 | a specific file or to any file from the GUI and, of course, you have the | |
152 | entire range of possibilities for showing changes, restoring previous | |
153 | versions and so forth from the command line. | |
154 | ||
155 | Git | |
156 | ||
157 | Download Git from | |
158 | ||
159 | http://git-scm.com/downloads | |
160 | ||
161 | Install git and then in a terminal enter your personal information | |
162 | ||
163 | $ git config --global user.name "John Doe" | |
164 | $ git config --global user.email johndoe@example.com | |
165 | ||
166 | the editor you would like to use | |
167 | ||
168 | $ git config --global core.editor vim | |
169 | ||
170 | and the diff program | |
171 | ||
172 | $ git config --global merge.tool vimdiff | |
173 | ||
174 | Usage information can be obtained in several ways from the terminal | |
175 | ||
176 | $ git help <verb> | |
177 | $ git <verb> --help | |
178 | $ man git-<verb> | |
179 | ||
180 | Finally, Pro Git by Scott Chacon is available to read or download at: | |
181 | ||
182 | http://git-scm.com/book/en | |
183 | ||
184 | If you have been using Mercurial and would like to give Git a try, you | |
185 | can import your etm Mercurial records into Git as follows: | |
186 | ||
187 | $ cd | |
188 | $ git clone git://repo.or.cz/fast-export.git | |
189 | $ git init new_temp_repo | |
190 | $ cd new_temp_repo | |
191 | $ ~/fast-export/hg-fast-export.sh -r /path/to/etm/datadir | |
192 | $ git checkout HEAD | |
193 | ||
194 | If an "unnamed head" error is reported, try adding --force to the end of | |
195 | the fast-export line. | |
196 | ||
197 | At this point, you should have a copy of your etm datadir in | |
198 | new_temp_repo along with a directory, .git, that you can copy to the | |
199 | root of your etm datadir where it will join its Mercurial counterpart, | |
200 | .hg. You can then delete new_temp_repo. | |
201 | ||
202 | You can now open etmtk.cfg for editing and change the setting for | |
203 | vcs_system to | |
204 | ||
205 | vcs_system: git | |
206 | ||
207 | Mercurial | |
208 | ||
209 | Download Mercurial from | |
210 | ||
211 | http://mercurial.selenic.com/ | |
212 | ||
213 | install it and then create the file ~/.hgrc, if it doesn't already | |
214 | exist, with at least the following two lines: | |
215 | ||
216 | [ui] | |
217 | username = Your Name <your email address> | |
218 | ||
219 | New etm users | |
220 | ||
221 | By default, etm will use the directory | |
222 | ||
223 | ~/.etm | |
224 | ||
225 | The first time you run etm it will create, if necessary, the following: | |
226 | ||
227 | ~/.etm/ | |
228 | ~/.etm/etmtk.cfg | |
229 | ~/.etm/completions.cfg | |
230 | ~/.etm/reports.cfg | |
231 | ~/.etm/data/ | |
232 | ||
233 | If the data directory needs to be created, then a file | |
234 | ~/.etm/data/sample.txt will be added with illustrative entries. | |
235 | Similarly, the *.cfg files will be populated with useful entries. | |
236 | ||
237 | Previous etm users | |
238 | ||
239 | The first time you run etm, it will copy your current configuration | |
240 | settings from ~/.etm/etm.cfg to ~/.etm/etmtk.cfg. You can make any | |
241 | changes you like to the latter file without affecting the former. | |
242 | ||
243 | You can switch back and forth between etm_qt and etm. Any changes made | |
244 | to your data files by either one will be compatible with the other one. | |
245 | ||
246 | Feedback | |
247 | -------- | |
248 | ||
249 | Please share your ideas in the discussion group at GoogleGroups. | |
250 | ||
251 | License | |
252 | ------- | |
253 | ||
254 | Copyright (c) 2009-2014 Daniel Graham. All rights reserved. | |
255 | ||
256 | This program is free software; you can redistribute it and/or modify it | |
257 | under the terms of the GNU General Public License as published by the | |
258 | Free Software Foundation; either version 3 of the License, or (at your | |
259 | option) any later version. |
0 | #!/usr/bin/env python3 | |
1 | import os | |
2 | import sys | |
3 | ||
4 | lib_path = os.path.relpath('etmTk/') | |
5 | sys.path.append(lib_path) | |
6 | ||
7 | from etmTk import view | |
8 | from etmTk.data import setup_logging | |
9 | ||
10 | import logging | |
11 | import logging.config | |
12 | logger = logging.getLogger() | |
13 | ||
14 | log_levels = [str(x) for x in range(1, 6)] | |
15 | ||
16 | if __name__ == "__main__": | |
17 | etmdir = '' | |
18 | loglevel = '3' | |
19 | help = False | |
20 | argstr = " ".join(sys.argv) | |
21 | etm = sys.argv[0] | |
22 | if len(sys.argv) > 1 and sys.argv[1] in log_levels: | |
23 | loglevel = sys.argv.pop(1) | |
24 | ||
25 | if len(sys.argv) > 1 and os.path.isdir(sys.argv[1]): | |
26 | temp = sys.argv.pop(1) | |
27 | logger.debug("got directory: {0}".format(temp)) | |
28 | oldpath = os.path.join(temp, 'etm.cfg') | |
29 | newpath = os.path.join(temp, 'etmtk.cfg') | |
30 | if os.path.isfile(newpath) or os.path.isfile(oldpath): | |
31 | etmdir = temp | |
32 | ||
33 | setup_logging(loglevel, etmdir=etmdir) | |
34 | ||
35 | if len(sys.argv) > 1: | |
36 | logger.debug("calling data.main with etmdir: {0}, argv: {1}".format(etmdir, sys.argv)) | |
37 | import etmTk.data as data | |
38 | data.main(etmdir, sys.argv) | |
39 | ||
40 | else: | |
41 | logger.debug("calling view.main with etmdir: {0}".format(etmdir)) | |
42 | view.main(dir=etmdir) | |
43 | sys.exit() |
0 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
1 | <html xmlns="http://www.w3.org/1999/xhtml"> | |
2 | <head> | |
3 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
4 | <meta http-equiv="Content-Style-Type" content="text/css" /> | |
5 | <meta name="generator" content="pandoc" /> | |
6 | <title>ETM Users Manual</title> | |
7 | <style type="text/css">code{white-space: pre;}</style> | |
8 | </head> | |
9 | <body> | |
10 | <style> | |
11 | body { | |
12 | margin: auto; | |
13 | padding-right: 1em; | |
14 | padding-left: 1em; | |
15 | max-width: 44em; | |
16 | border-left: 1px solid black; | |
17 | border-right: 1px solid black; | |
18 | color: black; | |
19 | font-family: Verdana, sans-serif; | |
20 | font-size: 100%; | |
21 | line-height: 140%; | |
22 | color: #333; | |
23 | } | |
24 | pre, tt{ | |
25 | font-family: monospace; | |
26 | padding: 2px 4px; | |
27 | } | |
28 | code{ | |
29 | white-space: pre-wrap; | |
30 | font-size: 110%; | |
31 | padding: 1px 1px; | |
32 | } | |
33 | h1 a, h2 a, h3 a, h4 a, h5 a, li a { | |
34 | text-decoration: none; | |
35 | color: #7a5ada; | |
36 | } | |
37 | header, h1, h2, h3, h4, h5 { font-family: verdana; | |
38 | font-weight: bold; | |
39 | /*border-bottom: 1px dotted black;*/ | |
40 | color: #7a5ada; } | |
41 | header { | |
42 | font-size: 130%; | |
43 | } | |
44 | ||
45 | h1 { | |
46 | font-size: 130%; | |
47 | } | |
48 | ||
49 | h2 { | |
50 | font-size: 110%; | |
51 | border-bottom: 1px dotted black; | |
52 | } | |
53 | ||
54 | h3 { | |
55 | font-size: 100%; | |
56 | border-bottom: 1px dotted black; | |
57 | } | |
58 | ||
59 | h4 { | |
60 | font-size: 90%; | |
61 | font-style: italic; | |
62 | border-bottom: 1px dotted black; | |
63 | } | |
64 | ||
65 | h5 { | |
66 | font-size: 85%; | |
67 | font-style: italic; | |
68 | border-bottom: 1px dotted black; | |
69 | } | |
70 | ||
71 | h1.title { | |
72 | font-size: 200%; | |
73 | font-weight: bold; | |
74 | padding-top: 0.2em; | |
75 | padding-bottom: 0.2em; | |
76 | text-align: left; | |
77 | border: none; | |
78 | } | |
79 | ||
80 | dt code { | |
81 | font-weight: bold; | |
82 | } | |
83 | dd p { | |
84 | margin-top: 0; | |
85 | } | |
86 | ||
87 | #footer { | |
88 | padding-top: 1em; | |
89 | font-size: 70%; | |
90 | color: gray; | |
91 | text-align: center; | |
92 | } | |
93 | </style> | |
94 | <div id="header"> | |
95 | <h1 class="title">ETM Users Manual</h1> | |
96 | </div> | |
97 | <div id="TOC"> | |
98 | <ul> | |
99 | <li><a href="#overview">Overview</a><ul> | |
100 | <li><a href="#sample-entries">Sample entries</a></li> | |
101 | <li><a href="#starting-etm">Starting etm</a></li> | |
102 | <li><a href="#views">Views</a></li> | |
103 | <li><a href="#creating-new-items">Creating New Items</a></li> | |
104 | <li><a href="#editing-existing-items">Editing Existing Items</a></li> | |
105 | <li><a href="#sharing-with-other-calendar-applications">Sharing with other calendar applications</a></li> | |
106 | <li><a href="#tools">Tools</a></li> | |
107 | <li><a href="#data-organization-and-calendars">Data Organization and Calendars</a></li> | |
108 | </ul></li> | |
109 | <li><a href="#item-types">Item types</a><ul> | |
110 | <li><a href="#action">~ Action</a></li> | |
111 | <li><a href="#event">* Event</a></li> | |
112 | <li><a href="#occasion">^ Occasion</a></li> | |
113 | <li><a href="#note">! Note</a></li> | |
114 | <li><a href="#task">- Task</a></li> | |
115 | <li><a href="#delegated-task">% Delegated task</a></li> | |
116 | <li><a href="#task-group">+ Task group</a></li> | |
117 | <li><a href="#in-basket">$ In basket</a></li> | |
118 | <li><a href="#someday-maybe">? Someday maybe</a></li> | |
119 | <li><a href="#hidden"># Hidden</a></li> | |
120 | <li><a href="#defaults">= Defaults</a></li> | |
121 | </ul></li> | |
122 | <li><a href="#keys">@Keys</a><ul> | |
123 | <li><a href="#a-alert"><span class="citation">@a</span> alert</a></li> | |
124 | <li><a href="#b-beginby"><span class="citation">@b</span> beginby</a></li> | |
125 | <li><a href="#c-context"><span class="citation">@c</span> context</a></li> | |
126 | <li><a href="#d-description"><span class="citation">@d</span> description</a></li> | |
127 | <li><a href="#e-extent"><span class="citation">@e</span> extent</a></li> | |
128 | <li><a href="#f-done-due"><span class="citation">@f</span> done[; due]</a></li> | |
129 | <li><a href="#g-goto"><span class="citation">@g</span> goto</a></li> | |
130 | <li><a href="#h-history"><span class="citation">@h</span> history</a></li> | |
131 | <li><a href="#j-job"><span class="citation">@j</span> job</a></li> | |
132 | <li><a href="#k-keyword"><span class="citation">@k</span> keyword</a></li> | |
133 | <li><a href="#l-location"><span class="citation">@l</span> location</a></li> | |
134 | <li><a href="#m-memo"><span class="citation">@m</span> memo</a></li> | |
135 | <li><a href="#o-overdue"><span class="citation">@o</span> overdue</a></li> | |
136 | <li><a href="#p-priority"><span class="citation">@p</span> priority</a></li> | |
137 | <li><a href="#r-repetition-rule"><span class="citation">@r</span> repetition rule</a></li> | |
138 | <li><a href="#s-starting-datetime"><span class="citation">@s</span> starting datetime</a></li> | |
139 | <li><a href="#t-tags"><span class="citation">@t</span> tags</a></li> | |
140 | <li><a href="#u-user"><span class="citation">@u</span> user</a></li> | |
141 | <li><a href="#v-action_rates-key"><span class="citation">@v</span> action_rates key</a></li> | |
142 | <li><a href="#w-action_markups-key"><span class="citation">@w</span> action_markups key</a></li> | |
143 | <li><a href="#x-expense"><span class="citation">@x</span> expense</a></li> | |
144 | <li><a href="#z-time-zone"><span class="citation">@z</span> time zone</a></li> | |
145 | <li><a href="#include">@+ include</a></li> | |
146 | <li><a href="#exclude">@- exclude</a></li> | |
147 | </ul></li> | |
148 | <li><a href="#dates">Dates</a><ul> | |
149 | <li><a href="#fuzzy-dates">Fuzzy dates</a></li> | |
150 | <li><a href="#time-periods">Time periods</a></li> | |
151 | <li><a href="#time-zones">Time zones</a></li> | |
152 | <li><a href="#anniversary-substitutions">Anniversary substitutions</a></li> | |
153 | <li><a href="#easter">Easter</a></li> | |
154 | </ul></li> | |
155 | <li><a href="#preferences">Preferences</a><ul> | |
156 | <li><a href="#template-expansions">Template expansions</a></li> | |
157 | <li><a href="#options">Options</a></li> | |
158 | </ul></li> | |
159 | <li><a href="#reports">Reports</a><ul> | |
160 | <li><a href="#report-type-characters">Report type characters</a></li> | |
161 | <li><a href="#groupby-setting">Groupby setting</a></li> | |
162 | <li><a href="#options-1">Options</a></li> | |
163 | </ul></li> | |
164 | <li><a href="#shortcuts">Shortcuts</a><ul> | |
165 | <li><a href="#menubar">Menubar</a></li> | |
166 | <li><a href="#main">Main</a></li> | |
167 | <li><a href="#edit">Edit</a></li> | |
168 | </ul></li> | |
169 | </ul> | |
170 | </div> | |
171 | <h1 id="overview"><a href="#overview">Overview</a></h1> | |
172 | <p>In contrast to most calendar/todo applications, creating items (events, tasks, and so forth) in etm does not require filling out fields in a form. Instead, items are created as free-form text entries using a simple, intuitive format and stored in plain text files.</p> | |
173 | <p>Dates in the examples below are entered using <em>fuzzy parsing</em> - e.g., <code>+7</code> for seven days from today, <code>fri</code> for the first Friday on or after today, <code>+1/1</code> for the first day of next month, <code>sun - 6d</code> for Monday of the current week. See <a href="DATES#dates">Dates</a> for details.</p> | |
174 | <h2 id="sample-entries"><a href="#sample-entries">Sample entries</a></h2> | |
175 | <ul> | |
176 | <li><p>A sales meeting (an event) [s]tarting seven days from today at 9:00am with an [e]xtent of one hour and a default [a]lert 5 minutes before the start:</p> | |
177 | <pre><code>* sales meeting @s +7 9a @e 1h @a 5</code></pre></li> | |
178 | <li><p>The sales meeting with another [a]lert 2 days before the meeting to (e)mail a reminder to a list of recipients:</p> | |
179 | <pre><code>* sales meeting @s +7 9a @e 1h @a 5 | |
180 | @a 2d: e; who@when.com, what@where.org</code></pre></li> | |
181 | <li><p>Prepare a report (a task) for the sales meeting [b]eginning 3 days early:</p> | |
182 | <pre><code>- prepare report @s +7 @b 3</code></pre></li> | |
183 | <li><p>A period [e]xtending 35 minutes (an action) spent working on the report yesterday:</p> | |
184 | <pre><code>~ report preparation @s -1 @e 35</code></pre></li> | |
185 | <li><p>Get a haircut (a task) on the 24th of the current month and then [r]epeatedly at (d)aily [i]ntervals of (14) days and, [o]n completion, (r)estart from the completion date:</p> | |
186 | <pre><code>- get haircut @s 24 @r d &i 14 @o r</code></pre></li> | |
187 | <li><p>Payday (an occasion) on the last week day of each month. The <code>&s -1</code> part of the entry extracts the last date which is both a weekday and falls within the last three days of the month):</p> | |
188 | <pre><code>^ payday @s 1/1 @r m &w MO, TU, WE, TH, FR | |
189 | &m -1, -2, -3 &s -1</code></pre></li> | |
190 | <li><p>Take a prescribed medication daily (a reminder) [s]tarting today and [r]epeating (d)aily at [h]ours 10am, 2pm, 6pm and 10pm [u]ntil (12am on) the fourth day from today. Trigger the default [a]lert zero minutes before each reminder:</p> | |
191 | <pre><code>* take Rx @s +0 @r d &h 10, 14, 18, 22 &u +4 @a 0</code></pre></li> | |
192 | <li><p>Move the water sprinkler (a reminder) every thirty mi[n]utes on Sunday afternoons using the default alert zero minutes before each reminder:</p> | |
193 | <pre><code>* Move sprinkler @s 1 @r w &w SU &h 14, 15, 16, 17 &n 0, 30 @a 0</code></pre> | |
194 | <p>To limit the sprinkler movement reminders to the [M]onths of April through September each year change the <span class="citation">@r</span> entry to this:</p> | |
195 | <pre><code>@r w &w SU &h 14, 15, 16, 17 &n 0, 30 &M 4, 5, 6, 7, 8, 9</code></pre> | |
196 | <p>or this:</p> | |
197 | <pre><code>@r n &i 30 &w SU &h 14, 15, 16, 17 &M 4, 5, 6, 7, 8, 9</code></pre></li> | |
198 | <li><p>Presidential election day (an occasion) every four years on the first Tuesday after a Monday in November:</p> | |
199 | <pre><code>^ Presidential Election Day @s 2012-11-06 | |
200 | @r y &i 4 &M 11 &m 2, 3, 4, 5, 6, 7, 8 &w TU</code></pre></li> | |
201 | <li><p>Join the etm discussion group (a task) [s]tarting 14 days from today. Because of the <span class="citation">@g</span> (goto) link, pressing <em>G</em> when this item is selected in the gui would open the link using the system default application which, in this case, would be your default browser:</p> | |
202 | <pre><code>- join the etm discussion group @s +14 | |
203 | @g groups.google.com/group/eventandtaskmanager/topics</code></pre></li> | |
204 | </ul> | |
205 | <h2 id="starting-etm"><a href="#starting-etm">Starting etm</a></h2> | |
206 | <p>To start the etm GUI open a terminal window and enter <code>etm</code> at the prompt:</p> | |
207 | <pre><code>$ etm</code></pre> | |
208 | <p>If you have not done a system installation of etm you will need first to cd to the directory where you unpacked etm.</p> | |
209 | <p>You can add a command to use the CLI instead of the GUI. For example, to get the complete command line usage information printed to the terminal window just add a question mark:</p> | |
210 | <pre><code>$ etm ? | |
211 | Usage: | |
212 | ||
213 | etm [logging level] [path] [?] [acmsv] | |
214 | ||
215 | With no arguments, etm will set logging level 3 (warn), use settings from | |
216 | the configuration file ~/.etm/etmtk.cfg, and open the GUI. | |
217 | ||
218 | If the first argument is an integer not less than 1 (debug) and not greater | |
219 | than 5 (critical), then set that logging level and remove the argument. | |
220 | ||
221 | If the first (remaining) argument is the path to a directory that contains | |
222 | a file named etmtk.cfg, then use that configuration file and remove the | |
223 | argument. | |
224 | ||
225 | If the first (remaining) argument is one of the commands listed below, then | |
226 | execute the remaining arguments without opening the GUI. | |
227 | ||
228 | a ARG display the agenda view using ARG, if given, as a filter. | |
229 | c ARGS display a custom view using the remaining arguments as the | |
230 | specification. (Enclose ARGS in single quotes to prevent shell | |
231 | expansion.) | |
232 | d ARG display the day view using ARG, if given, as a filter. | |
233 | k ARG display the keywords view using ARG, if given, as a filter. | |
234 | m INT display a report using the remaining argument, which must be a | |
235 | positive integer, to display a report using the corresponding | |
236 | entry from the file given by report_specifications in etmtk.cfg. | |
237 | Use ? m to display the numbered list of entries from this file. | |
238 | n ARG display the notes view using ARG, if given, as a filter. | |
239 | N ARGS Create a new item using the remaining arguments as the item | |
240 | specification. (Enclose ARGS in single quotes to prevent shell | |
241 | expansion.) | |
242 | p ARG display the path view using ARG, if given, as a filter. | |
243 | t ARG display the tags view using ARG, if given, as a filter. | |
244 | v display information about etm and the operating system. | |
245 | ? ARG display (this) command line help information if ARGS = '' or, | |
246 | if ARGS = X where X is one of the above commands, then display | |
247 | details about command X. 'X ?' is equivalent to '? X'.</code></pre> | |
248 | <p>For example, you can print your agenda to the terminal window by adding the letter "a":</p> | |
249 | <pre><code>$ etm a | |
250 | Sun Apr 06, 2014 | |
251 | > set up luncheon meeting with Joe Smith 4d | |
252 | Mon Apr 07, 2014 | |
253 | * test command line event 3pm ~ 4pm | |
254 | * Aerobics 5pm ~ 6pm | |
255 | - follow up with Mary Jones | |
256 | Wed Apr 09, 2014 | |
257 | * Aerobics 5pm ~ 6pm | |
258 | Thu Apr 10, 2014 | |
259 | * Frank Burns conference call 1pm Pacif.. 4pm ~ 5:30pm | |
260 | * Book club 7pm ~ 9pm | |
261 | - sales meeting | |
262 | - set up luncheon meeting with Joe Smith 15m | |
263 | Now | |
264 | Available | |
265 | - Hair cut -1d | |
266 | Next | |
267 | errands | |
268 | - milk and eggs | |
269 | phone | |
270 | - reservation for Saturday dinner | |
271 | Someday | |
272 | ? lose weight and exercise more</code></pre> | |
273 | <p>You can filter the output by adding a (case-insensitive) argument:</p> | |
274 | <pre><code>$ etm a smith | |
275 | Sun Apr 06, 2014 | |
276 | > set up luncheon meeting with Joe Smith 4d | |
277 | Thu Apr 10, 2014 | |
278 | - set up luncheon meeting with Joe Smith 15m</code></pre> | |
279 | <p>or <code>etm d mar .*2014</code> to show your items for March, 2014.</p> | |
280 | <p>You can add a question mark to a command to get details about the commmand, e.g.:</p> | |
281 | <pre><code>Usage: | |
282 | ||
283 | etm c <type> <groupby> [options] | |
284 | ||
285 | Generate a custom view where type is either 'a' (action) or 'c' (composite). | |
286 | Groupby can include *semicolon* separated date specifications and | |
287 | elements from: | |
288 | c context | |
289 | f file path | |
290 | k keyword | |
291 | t tag | |
292 | u user | |
293 | ||
294 | A *date specification* is either | |
295 | w: week number | |
296 | or a combination of one or more of the following: | |
297 | yy: 2-digit year | |
298 | yyyy: 4-digit year | |
299 | MM: month: 01 - 12 | |
300 | MMM: locale specific abbreviated month name: Jan - Dec | |
301 | MMMM: locale specific month name: January - December | |
302 | dd: month day: 01 - 31 | |
303 | ddd: locale specific abbreviated week day: Mon - Sun | |
304 | dddd: locale specific week day: Monday - Sunday | |
305 | ||
306 | Options include: | |
307 | -b begin date | |
308 | -c context regex | |
309 | -d depth (CLI a reports only) | |
310 | -e end date | |
311 | -f file regex | |
312 | -k keyword regex | |
313 | -l location regex | |
314 | -o omit (r reports only) | |
315 | -s summary regex | |
316 | -S search regex | |
317 | -t tags regex | |
318 | -u user regex | |
319 | -w column 1 width | |
320 | -W column 2 width | |
321 | ||
322 | Example: | |
323 | ||
324 | etm c 'c ddd, MMM dd yyyy -b 1 -e +1/1'</code></pre> | |
325 | <p>Note: The CLI offers the same views and reporting, with the exception of week and month view, as the GUI. It is also possible to create new items in the CLI with the <code>n</code> command. Other modifications such as copying, deleting, finishing and so forth, can only be done in the GUI or, perhaps, in your favorite text editor. An advantage to using the GUI is that it provides auto-completion and validation.</p> | |
326 | <p>Tip: If you have a terminal open, you can create a new item or put something to finish later in your inbox quickly and easily with the "N" command. For example,</p> | |
327 | <pre><code> etm N '123 456-7890'</code></pre> | |
328 | <p>would create an entry in your inbox with this phone number. (With no type character an "$" would be supplied automatically to make the item an inbox entry and no further validation would be done.)</p> | |
329 | <h2 id="views"><a href="#views">Views</a></h2> | |
330 | <p>All views display only items consistent with the current choices of active calendars.</p> | |
331 | <p>If a (case-insensitive) filter is entered then the display in all views other than week, month and custom view will be limited to items that match somewhere in either the branch or the leaf. Relevant branches will automatically be expanded to show matches.</p> | |
332 | <p>In day, week and month views, pressing the space bar will move the display to the current date. In all other views, pressing the space bar will move the display to the first item in the outline.</p> | |
333 | <p>In day, week and month views, pressing <em>J</em> will first prompt for a fuzzy-parsed date and then "jump" to the specified date.</p> | |
334 | <p>If you scroll or jump to a date in day, week or month view and then switch to another one of these views, the same day(s) will be displayed.</p> | |
335 | <p>In all views, pressing <em>Return</em> with an item selected or double clicking an item or a busy period in week view will open a context menu with options to copy, delete, edit and so forth.</p> | |
336 | <p>In all views, clicking in the details panel with an item selected will open the item for editing if it is not repeating and otherwise prompt for the instance(s) to be changed.</p> | |
337 | <p>In all views other than week and month view, pressing <em>O</em> will open a dialog to choose the outline depth.</p> | |
338 | <p>In all views other than week and month view, pressing <em>L</em> will toggle the display of a column displaying item <em>labels</em> where, for example, an item with <span class="citation">@a</span>, <span class="citation">@d</span> and <span class="citation">@u</span> fields would have the label "adu".</p> | |
339 | <p>In all views other than week and month view, pressing <em>S</em> will show a text verion of the current display suitable for copy and paste. The text version will respect the current outline depth in the view.</p> | |
340 | <p>In custom view it is possible to export the current report in either text or CSV (comma separated values) format to a file of your choosing.</p> | |
341 | <p>Note. In custom view you need to move the focus from the report specification entry field in order for the shortcuts <em>O</em>, <em>L</em> and <em>S</em> to work.</p> | |
342 | <p>In all views:</p> | |
343 | <ul> | |
344 | <li><p>if an item is selected:</p> | |
345 | <ul> | |
346 | <li><p>pressing <em>Shift-H</em> will show a history of changes for the file containing the selected item, first prompting for the number of changes.</p></li> | |
347 | <li><p>pressing <em>Shift-X</em> will export the selected item in iCal format.</p></li> | |
348 | </ul></li> | |
349 | <li><p>if an item is not selected:</p> | |
350 | <ul> | |
351 | <li><p>pressing <em>Shift-H</em> will show a history of changes for all files, first prompting for the number of changes.</p></li> | |
352 | <li><p>pressing <em>Shift-X</em> will export all items in active calendars in iCal format.</p></li> | |
353 | </ul></li> | |
354 | </ul> | |
355 | <h3 id="agenda-view"><a href="#agenda-view">Agenda View</a></h3> | |
356 | <p>What you need to know now beginning with your schedule for the next few days and followed by items in these groups:</p> | |
357 | <ul> | |
358 | <li><p><strong>In basket</strong>: In basket items and items with missing types or other errors.</p></li> | |
359 | <li><p><strong>Now</strong>: All <em>scheduled</em> (dated) tasks whose due dates have passed including delegated tasks and waiting tasks (tasks with unfinished prerequisites) grouped by available, delegated and waiting and, within each group, by the due date.</p></li> | |
360 | <li><p><strong>Next</strong>: All <em>unscheduled</em> (undated) tasks grouped by context (home, office, phone, computer, errands and so forth) and sorted by priority and extent. These tasks correspond to GTD's <em>next actions</em>. These are tasks which don't really have a deadline and can be completed whenever a convenient opportunity arises. Check this view, for example, before you leave to run errands for opportunities to clear other errands.</p></li> | |
361 | <li><p><strong>Someday</strong>: Someday (maybe) items for periodic review.</p></li> | |
362 | </ul> | |
363 | <p>Note: Finished tasks, actions and notes are not displayed in this view.</p> | |
364 | <h3 id="day-view"><a href="#day-view">Day View</a></h3> | |
365 | <p>All dated items appear in this view, grouped by date and sorted by starting time and item type. This includes:</p> | |
366 | <ul> | |
367 | <li><p>All non-repeating, dated items.</p></li> | |
368 | <li><p>All repetitions of repeating items with a finite number of repetitions. This includes 'list-only' repeating items and items with <code>&u</code> (until) or <code>&t</code> (total number of repetitions) entries.</p></li> | |
369 | <li><p>For repeating items with an infinite number of repetitions, those repetitions that occur within the first <code>weeks_after</code> weeks after the current week are displayed along with the first repetition after this interval. This assures that at least one repetition will be displayed for infrequently repeating items such as voting for president.</p></li> | |
370 | </ul> | |
371 | <p>Tip: Want to see your next appointment with Dr. Jones? Switch to day view and enter "jones" in the filter.</p> | |
372 | <h3 id="week-view"><a href="#week-view">Week View</a></h3> | |
373 | <p>Events and occasions displayed graphically by week with one column for each day. Left and right cursor keys change, respectively, to the previous and next week. Up and down cursor keys select, respectively, the previous and next items within the given week. Items can also be selected by moving the mouse over the item. The details for the selected item are displayed at the bottom of the screen. Pressing return with an item selected or double-clicking an item opens a context menu. Control-clicking an unscheduled time opens a dialog to create an event for that date and time.</p> | |
374 | <p>Days with events that fall outside the 7am - 11pm range will have a red line at the top (earlier than 7am) or at the bottom (later than 11pm).</p> | |
375 | <p>Tip. You can display a list of busy times or, after providing the needed period in minutes, a list of free times that would accomodate the requirement within the selected week. Both options are in the <em>View</em> menu.</p> | |
376 | <h3 id="month-view"><a href="#month-view">Month View</a></h3> | |
377 | <p>Events and occasions displayed graphically by month. Left and right cursor keys change, respectively, to the previous and next month. Up and down cursor keys select, respectively, the previous and next days within the given month. Days can also be selected by moving the mouse over the item. A list of occasions and events for the selected day is displayed at the bottom of the screen. Double clicking a date or pressing <em>Return</em> with a date selected opens a dialog to create an item for that date.</p> | |
378 | <p>The current date and days with occasions are highlighted.</p> | |
379 | <p>Days with scheduled events have an <em>active times</em> border that wraps clockwise around the date box with 7am - 11am to the right along the top, 11am - 3pm down the right side, 3pm - 7pm to the left along the bottom and 7pm - 11pm upward along the left side. Days with events scheduled that fall outside the 7am - 11pm range have a red box in the top, left-hand corner of the date box.</p> | |
380 | <p>Tip. You can display a list of busy times or, after providing the needed period in minutes, a list of free times that would accomodate the requirement within the selected month. Both options are in the <em>View</em> menu.</p> | |
381 | <h3 id="tag-view"><a href="#tag-view">Tag View</a></h3> | |
382 | <p>All items with tag entries grouped by tag and sorted by type and <em>relevant datetime</em>. Note that items with multiple tags will be listed under each tag.</p> | |
383 | <p>Tip: Use the filter to limit the display to items with a particular tag.</p> | |
384 | <h3 id="keyword-view"><a href="#keyword-view">Keyword View</a></h3> | |
385 | <p>All items grouped by keyword and sorted by type and <em>relevant datetime</em>.</p> | |
386 | <h3 id="path-view"><a href="#path-view">Path View</a></h3> | |
387 | <p>All items grouped by file path and sorted by type and <em>relevant datetime</em>. Use this view to review the status of your projects.</p> | |
388 | <p>The <em>relevant datetime</em> is the past due date for any past due task, the starting datetime for any non-repeating item and the datetime of the next instance for any repeating item.</p> | |
389 | <p>Note: Items that you have "commented out" by beginning the item with a <code>#</code> will only be visible in this view.</p> | |
390 | <h3 id="note-view"><a href="#note-view">Note View</a></h3> | |
391 | <p>All notes grouped and sorted by keyword and summary.</p> | |
392 | <h3 id="custom-view"><a href="#custom-view">Custom View</a></h3> | |
393 | <p>Design your own view. See <a href="#reports">Reports</a> for details.</p> | |
394 | <h2 id="creating-new-items"><a href="#creating-new-items">Creating New Items</a></h2> | |
395 | <p>Items of any type can be created by pressing <em>N</em> in the GUI and then providing the details for the item in the resulting dialog.</p> | |
396 | <p>An event can also be created by double-clicking in a free period in the Week View - the date and time corresponding to the mouse position will be entered as the starting datetime when the dialog opens.</p> | |
397 | <p>An action can also be created by pressing <em>T</em> to start a timer for the action. You will be prompted for a summary (title) and, optionally, an <code>@e</code> entry to specify a starting time for the timer. If an item is selected when you press <em>T</em> then you will have the additional option of creating the action as a copy of the selected item.</p> | |
398 | <p>The timer starts automatically when you close the dialog. Once the timer is running, pressing <em>T</em> toggles the timer between running and paused. Pressing <em>Shift-T</em> when a timer is active (either running or paused) stops the timer and begins a dialog to provide the details of the action - the elapsed time will already be entered.</p> | |
399 | <p>While a timer is active, the title, elapsed time and status - running or paused - is displayed in the status bar.</p> | |
400 | <p>When editing an item, clicking on <em>Finish</em> or pressing <em>Shift-Return</em> will validate your entry. If there are errors, they will be displayed and you can return to the editor to correct them. If there are no errors, this will be indicated in a dialog, e.g.,</p> | |
401 | <pre><code>Task scheduled for Tue Jun 03 | |
402 | ||
403 | Save changes and exit?</code></pre> | |
404 | <p>Tip. When creating or editing a repeating item, pressing <em>Finish</em> will also display a list of instances that will be generated.</p> | |
405 | <p>Click on <em>Ok</em> or press <em>Return</em> or <em>Shift-Return</em> to save the item and close the editor. Click on <em>Cancel</em> or press <em>Escape</em> to return to the editor.</p> | |
406 | <p>If this is a new item and there are no errors, clicking on <em>Ok</em> or pressing <em>Return</em> will open a dialog to select the file to store the item with the current monthly file already selected. Pressing <em>Shift-Return</em> will bypass the file selection dialog and save to the current monthly file.</p> | |
407 | <p>Idle timing is also supported. An illustrative work flow would be to activate the idle timer first thing each morning by pressing <em>I</em>. The current value of this timer will then be displayed in brackets in the lower, left-hand corner of the GUI.</p> | |
408 | <p>If you later start an action timer, a dialog will open showing the current idle time and offering the opportunity to assign some or all of this period to a keyword that you select from a list. You can continue assigning time in this fashion until idle time is zero or you can press <em>Cancel</em> or <em>Escape</em> at any point to cancel the dialog and open the action timer dialog. While the action timer is running, the idle timer will be paused and vice-versa.</p> | |
409 | <p>If an action timer is canceled (stopped and not recorded), then the time for the action timer will be added to the current idle time.</p> | |
410 | <p>You can assign idle time without starting an action timer by pressing <em>I</em> or, at the end of the day, by pressing <em>Shift-I</em> to stop the idle timer.</p> | |
411 | <p>Note that the dialog to assign idle time will only open when starting or restarting an action timer if the current idle time is at least the number of minutes specified by <code>idle_minimum</code> in your etmtk.cfg file.</p> | |
412 | <h2 id="editing-existing-items"><a href="#editing-existing-items">Editing Existing Items</a></h2> | |
413 | <p>Double-clicking an item or pressing <em>Return</em> when an item is selected will open a context menu of possible actions:</p> | |
414 | <ul> | |
415 | <li>Copy</li> | |
416 | <li>Delete</li> | |
417 | <li>Edit</li> | |
418 | <li>Edit file</li> | |
419 | <li>Finish (unfinished tasks only)</li> | |
420 | <li>Reschedule</li> | |
421 | <li>Schedule new</li> | |
422 | <li>Open link (items with <code>@g</code> entries only)</li> | |
423 | <li>Show user details (items with <code>@u</code> entries only)</li> | |
424 | </ul> | |
425 | <p>When either <em>Copy</em> or <em>Edit</em> is chosen for a repeating item, you can further choose:</p> | |
426 | <ol style="list-style-type: decimal"> | |
427 | <li>this instance</li> | |
428 | <li>this and all subsequent instances</li> | |
429 | <li>all instances</li> | |
430 | </ol> | |
431 | <p>When <em>Delete</em> is chosen for a repeating item, a further choice is available:</p> | |
432 | <ol start="4" style="list-style-type: decimal"> | |
433 | <li>all previous instances</li> | |
434 | </ol> | |
435 | <p>Tip: Use <em>Reschedule</em> to enter a date for an undated item or to change the scheduled date for the item or the selected instance of a repeating item. All you have to do is enter the new (fuzzy parsed) datetime.</p> | |
436 | <h2 id="sharing-with-other-calendar-applications"><a href="#sharing-with-other-calendar-applications">Sharing with other calendar applications</a></h2> | |
437 | <p>Both export and import are supported for files in iCalendar format in ways that depend upon settings in <code>etmtk.cfg</code>.</p> | |
438 | <p>If an absolute path is entered for <code>current_icsfolder</code>, for example, then <code>.ics</code> files corresponding to the entries in <code>calendars</code> will be created in this folder and updated as necessary. If there are no entries in calendars, then a single file, <code>all.ics</code>, will be created in this folder and updated as necessary.</p> | |
439 | <p>If an item is selected, then pressing Shift-X in the gui will export the selected item in iCalendar format to <code>icsitem_file</code>. If an item is not selected, pressing Shift-X will export the active calendars in iCalendar format to <code>icscal_file</code>.</p> | |
440 | <p>If <code>icssync_folder</code> is given, then files in this folder with the extension <code>.txt</code> and <code>.ics</code> will automatically kept concurrent using export to iCalendar and import from iCalendar. I.e., if the <code>.txt</code> file is more recent than than the <code>.ics</code> then the <code>.txt</code> file will be exported to the <code>.ics</code> file. On the other hand, if the <code>.ics</code> file is more recent then it will be imported to the <code>.txt</code> file. In either case, the contents of the file to be updated will be overwritten with the new content and the last acess/modified times for both will be set to the current time.</p> | |
441 | <p>If <code>ics_subscriptions</code> is given, it should be a list of [URL, FILE] tuples. The URL is a calendar subscription, e.g., for a Google Calendar subscription the URL, FILE tuple would be something like:</p> | |
442 | <pre><code> ['https://www.google.com/calendar/ical/.../basic.ics', 'personal/google.txt'] | |
443 | </code></pre> | |
444 | <p>With this entry, pressing Shift-M in the gui would import the calendar from the URL, convert it from ics to etm format and then write the result to <code>personal/google.txt</code> in the etm data directory. Note that this data file should be regarded as read-only since any changes made to it will be lost with the next subscription update.</p> | |
445 | <p>Finally, when creating a new item in the etm editor, you can paste an iCalendar entry such as the following VEVENT:</p> | |
446 | <pre><code>BEGIN:VCALENDAR | |
447 | VERSION:2.0 | |
448 | PRODID:-//ForeTees//NONSGML v1.0//EN | |
449 | CALSCALE:GREGORIAN | |
450 | METHOD:PUBLISH | |
451 | BEGIN:VEVENT | |
452 | UID:1403607754438-11547@127.0.0.1-33 | |
453 | DTSTAMP:20140624T070234 | |
454 | DTSTART:20140630T080000 | |
455 | SUMMARY:8:00 AM Tennis Reservation | |
456 | LOCATION:Governors Club | |
457 | DESCRIPTION: Player 1: ... | |
458 | ||
459 | URL:http://www1.foretees.com/governorsclub | |
460 | END:VEVENT | |
461 | END:VCALENDAR</code></pre> | |
462 | <p>When you press <em>Finish</em>, the entry will be converted to etm format</p> | |
463 | <pre><code>^ 8:00 AM Tennis Reservation @s 2014-06-30 8am | |
464 | @d Player 1: ... | |
465 | @z US/Eastern</code></pre> | |
466 | <p>and you can choose the file to hold it.</p> | |
467 | <p>The following etm and iCalendar item types are supported:</p> | |
468 | <ul> | |
469 | <li><p>export from etm:</p> | |
470 | <ul> | |
471 | <li>occasion to VEVENT without end time</li> | |
472 | <li>event (with or without extent) to VEVENT</li> | |
473 | <li>action to VJOURNAL</li> | |
474 | <li>note to VJOURNAL</li> | |
475 | <li>task to VTODO</li> | |
476 | <li>delegated task to VTODO</li> | |
477 | <li>task group to VTODO (one for each job)</li> | |
478 | </ul></li> | |
479 | <li><p>import from iCalendar</p> | |
480 | <ul> | |
481 | <li>VEVENT without end time to occasion</li> | |
482 | <li>VEVENT with end time to event</li> | |
483 | <li>VJOURNAL to note</li> | |
484 | <li>VTODO to task</li> | |
485 | </ul></li> | |
486 | </ul> | |
487 | <h2 id="tools"><a href="#tools">Tools</a></h2> | |
488 | <h3 id="date-and-time-calculator"><a href="#date-and-time-calculator">Date and time calculator</a></h3> | |
489 | <p>Enter an expression of the form <code>x [+-] y</code> where <code>x</code> is a date and <code>y</code> is either a date or a time period if <code>-</code> is used and a time period if <code>+</code> is used. Both <code>x</code> and <code>y</code> can be followed by timezones, e.g.,</p> | |
490 | <pre><code> 4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai: | |
491 | ||
492 | 14h25m</code></pre> | |
493 | <p>or</p> | |
494 | <pre><code> 4/20 4:50p Asia/Shanghai + 14h25m US/Central: | |
495 | ||
496 | 2014-04-20 18:15-0500</code></pre> | |
497 | <p>The local timezone is assumed when none is given.</p> | |
498 | <h3 id="available-dates-calculator"><a href="#available-dates-calculator">Available dates calculator</a></h3> | |
499 | <p>Enter an expression of the form</p> | |
500 | <pre><code>start; end; busy</code></pre> | |
501 | <p>where start and end are dates and busy is comma separated list of busy dates or busy intervals. E.g., entering</p> | |
502 | <pre><code>6/1; 6/30; 6/2, 6/14-6/22, 6/5-6/9, 6/11-6/15, 6/17-6/29</code></pre> | |
503 | <p>would give:</p> | |
504 | <pre><code>Sun Jun 01 | |
505 | Tue Jun 03 | |
506 | Wed Jun 04 | |
507 | Tue Jun 10 | |
508 | Mon Jun 30</code></pre> | |
509 | <h3 id="yearly-calendar"><a href="#yearly-calendar">Yearly calendar</a></h3> | |
510 | <p>Gives a display such as</p> | |
511 | <pre><code> January 2014 February 2014 March 2014 | |
512 | Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su | |
513 | 1 2 3 4 5 1 2 1 2 | |
514 | 6 7 8 9 10 11 12 3 4 5 6 7 8 9 3 4 5 6 7 8 9 | |
515 | 13 14 15 16 17 18 19 10 11 12 13 14 15 16 10 11 12 13 14 15 16 | |
516 | 20 21 22 23 24 25 26 17 18 19 20 21 22 23 17 18 19 20 21 22 23 | |
517 | 27 28 29 30 31 24 25 26 27 28 24 25 26 27 28 29 30 | |
518 | 31 | |
519 | ||
520 | April 2014 May 2014 June 2014 | |
521 | Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su | |
522 | 1 2 3 4 5 6 1 2 3 4 1 | |
523 | 7 8 9 10 11 12 13 5 6 7 8 9 10 11 2 3 4 5 6 7 8 | |
524 | 14 15 16 17 18 19 20 12 13 14 15 16 17 18 9 10 11 12 13 14 15 | |
525 | 21 22 23 24 25 26 27 19 20 21 22 23 24 25 16 17 18 19 20 21 22 | |
526 | 28 29 30 26 27 28 29 30 31 23 24 25 26 27 28 29 | |
527 | 30 | |
528 | ||
529 | July 2014 August 2014 September 2014 | |
530 | Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su | |
531 | 1 2 3 4 5 6 1 2 3 1 2 3 4 5 6 7 | |
532 | 7 8 9 10 11 12 13 4 5 6 7 8 9 10 8 9 10 11 12 13 14 | |
533 | 14 15 16 17 18 19 20 11 12 13 14 15 16 17 15 16 17 18 19 20 21 | |
534 | 21 22 23 24 25 26 27 18 19 20 21 22 23 24 22 23 24 25 26 27 28 | |
535 | 28 29 30 31 25 26 27 28 29 30 31 29 30 | |
536 | ||
537 | October 2014 November 2014 December 2014 | |
538 | Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su | |
539 | 1 2 3 4 5 1 2 1 2 3 4 5 6 7 | |
540 | 6 7 8 9 10 11 12 3 4 5 6 7 8 9 8 9 10 11 12 13 14 | |
541 | 13 14 15 16 17 18 19 10 11 12 13 14 15 16 15 16 17 18 19 20 21 | |
542 | 20 21 22 23 24 25 26 17 18 19 20 21 22 23 22 23 24 25 26 27 28 | |
543 | 27 28 29 30 31 24 25 26 27 28 29 30 29 30 31</code></pre> | |
544 | <p>Left and right cursor keys move backward and forward a year at a time, respectively, and pressing the spacebar returns to the current year.</p> | |
545 | <h3 id="history-of-changes"><a href="#history-of-changes">History of changes</a></h3> | |
546 | <p>This requires that either <em>git</em> or <em>mercurial</em> is installed. If an item is selected show a history of changes to the file that contains the item. Otherwise show a history of changes for all etm data files. In either case, choose an integer number of the most recent changes to show or choose 0 to show all changes.</p> | |
547 | <h2 id="data-organization-and-calendars"><a href="#data-organization-and-calendars">Data Organization and Calendars</a></h2> | |
548 | <p><em>etm</em> offers two hierarchical ways of organizing your data: by keyword and file path. There are no hard and fast rules about how to use these hierarchies but the goal is a system that makes complementary uses of folder and keyword and fits your needs. As with any filing system, planning and consistency are paramount.</p> | |
549 | <p>For example, one pattern of use for a business would be to use folders for people and keywords for client-project-category.</p> | |
550 | <p>Similarly, a family could use folders to separate personal and shared items for family members, for example:</p> | |
551 | <pre><code>root etm data directory | |
552 | personal | |
553 | dag | |
554 | erp | |
555 | shared | |
556 | holidays | |
557 | birthdays | |
558 | events</code></pre> | |
559 | <p>Here</p> | |
560 | <pre><code>~dag/.etm/etm.cfg | |
561 | ~erp/.etm/etm.cfg</code></pre> | |
562 | <p>would both contain <code>datadir</code> entries specifying the common root data directory. Additionally, if these configuration files contained, respectively, the entries</p> | |
563 | <pre><code>~dag/.etm/etm.cfg | |
564 | calendars | |
565 | - [dag, true, personal/dag] | |
566 | - [erp, false, personal/erp] | |
567 | - [shared, true, shared]</code></pre> | |
568 | <p>and</p> | |
569 | <pre><code>~erp/.etm/etm.cfg | |
570 | calendars | |
571 | - [erp, true, personal/erp] | |
572 | - [dag, false, personal/dag] | |
573 | - [shared, true, shared]</code></pre> | |
574 | <p>then, by default, both dag and erp would see the entries from their personal files as well as the shared entries and each could optionally view the entries from the other's personal files as well. See the <a href="#preferences">Preferences</a> for details on the <code>calendars</code> entry.</p> | |
575 | <p>Note for Windows users. The path separator needs to be "escaped" in the calendar paths, e.g., you should enter</p> | |
576 | <pre><code> - [dag, true, personal\\dag]</code></pre> | |
577 | <p>instead of</p> | |
578 | <pre><code> - [dag, true, personal\dag]</code></pre> | |
579 | <h1 id="item-types"><a href="#item-types">Item types</a></h1> | |
580 | <p>There are several types of items in etm. Each item begins with a type character such as an asterisk (event) and continues on one or more lines either until the end of the file is reached or another line is found that begins with a type character. The type character for each item is followed by the item summary and then, perhaps, by one or more <code>@key value</code> pairs - see <a href="#keys">@-Keys</a> for details. The order in which such pairs are entered does not matter.</p> | |
581 | <h2 id="action"><a href="#action">~ Action</a></h2> | |
582 | <p>A record of the expenditure of time (<code>@e</code>) and/or money (<code>@x</code>). Actions are not reminders, they are instead records of how time and/or money was actually spent. Action lines begin with a tilde, <code>~</code>.</p> | |
583 | <pre><code> ~ picked up lumber and paint @s mon 3p @e 1h15m @x 127.32</code></pre> | |
584 | <p>Entries such as <code>@s mon 3p</code>, <code>@e 1h15m</code> and <code>@x 127.32</code> are discussed below under <em>Item details</em>. Action entries form the basis for time and expense billing using action reports - see <a href="#reports">Reports</a> for details.</p> | |
585 | <p>Tip: You can use either path or keyword or a combination of the two to organize your actions.</p> | |
586 | <h2 id="event"><a href="#event">* Event</a></h2> | |
587 | <p>Something that will happen on particular day(s) and time(s). Event lines begin with an asterick, <code>*</code>.</p> | |
588 | <pre><code> * dinner with Karen and Al @s sat 7p @e 3h</code></pre> | |
589 | <p>Events have a starting datetime, <code>@s</code> and an extent, <code>@e</code>. The ending datetime is given implicitly as the sum of the starting datetime and the extent. Events that span more than one day are possible, e.g.,</p> | |
590 | <pre><code> * Sales conference @s 9a wed @e 2d8h</code></pre> | |
591 | <p>begins at 9am on Wednesday and ends at 5pm on Friday.</p> | |
592 | <p>An event without an <code>@e</code> entry or with <code>@e 0</code> is regarded as a <em>reminder</em> and, since there is no extent, will not be displayed in <em>busy times</em>.</p> | |
593 | <h2 id="occasion"><a href="#occasion">^ Occasion</a></h2> | |
594 | <p>Holidays, anniversaries, birthdays and such. Similar to an event with a date but no starting time and no extent. Occasions begin with a caret sign, <code>^</code>.</p> | |
595 | <pre><code> ^ The !1776! Independence Day @s 2010-07-04 @r y &M 7 &m 4</code></pre> | |
596 | <p>On July 4, 2013, this would appear as <code>The 237th Independence Day</code>. Here !1776!` is an example of an <em>anniversary substitution</em> - see <a href="#dates">Dates</a> for details.</p> | |
597 | <h2 id="note"><a href="#note">! Note</a></h2> | |
598 | <p>A record of some useful information. Note lines begin with an exclamation point, <code>!</code>.</p> | |
599 | <pre><code>! xyz software @k software:passwords @d user: dnlg, pw: abc123def</code></pre> | |
600 | <p>Tip: Since both the GUI and CLI note views group and sort by keyword, it is a good idea to use keywords to organize your notes.</p> | |
601 | <h2 id="task"><a href="#task">- Task</a></h2> | |
602 | <p>Something that needs to be done. It may or may not have a due date. Task lines begin with a minus sign, <code>-</code>.</p> | |
603 | <pre><code>- pay bills @s Oct 25</code></pre> | |
604 | <p>A task with an <code>@s</code> entry becomes due on that date and past due when that date has passed. If the task also has an <code>@b</code> begin-by entry, then advance warnings of the task will begin appearing the specified number of days before the task is due. An <code>@e</code> entry in a task is interpreted as an estimate of the time required to finish the task.</p> | |
605 | <h2 id="delegated-task"><a href="#delegated-task">% Delegated task</a></h2> | |
606 | <p>A task that is assigned to someone else, usually the person designated in an <code>@u</code> entry. Delegated tasks begin with a percent sign, <code>%</code>.</p> | |
607 | <pre><code> % make reservations for trip @u joe @s fri</code></pre> | |
608 | <h2 id="task-group"><a href="#task-group">+ Task group</a></h2> | |
609 | <p>A collection of related tasks, some of which may be prerequisite for others. Task groups begin with a plus sign, <code>+</code>.</p> | |
610 | <pre><code> + dog house | |
611 | @j pickup lumber and paint &q 1 | |
612 | @j cut pieces &q 2 | |
613 | @j assemble &q 3 | |
614 | @j paint &q 4</code></pre> | |
615 | <p>Note that a task group is a single item and is treated as such. E.g., if any job is selected for editing then the entire group is displayed.</p> | |
616 | <p>Individual jobs are given by the <code>@j</code> entries. The <em>queue</em> entries, <code>&q</code>, set the order --- tasks with smaller &q values are prerequisites for subsequent tasks with larger &q values. In the example above, neither "pickup lumber" nor "pickup paint" have any prerequisites. "Pickup lumber", however, is a prerequisite for "cut pieces" which, in turn, is a prerequisite for "assemble". Both "assemble" and "pickup paint" are prerequisites for "paint".</p> | |
617 | <h2 id="in-basket"><a href="#in-basket">$ In basket</a></h2> | |
618 | <p>A quick, don't worry about the details item to be edited later when you have the time. In basket entries begin with a dollar sign, <code>$</code>.</p> | |
619 | <pre><code> $ joe 919 123-4567</code></pre> | |
620 | <p>If you create an item using <em>etm</em> and forget to provide a type character, an <code>$</code> will automatically be inserted.</p> | |
621 | <h2 id="someday-maybe"><a href="#someday-maybe">? Someday maybe</a></h2> | |
622 | <p>Something are you don't want to forget about altogether but don't want to appear on your next or scheduled lists. Someday maybe items begin with a question mark, <code>?</code>.</p> | |
623 | <pre><code> ? lose weight and exercise more</code></pre> | |
624 | <h2 id="hidden"><a href="#hidden"># Hidden</a></h2> | |
625 | <p>Hidden items begin with a hash mark, <code>#</code>. Such items are ignored by etm save for appearing in the folder view. Stick a hash mark in front of any item that you don't want to delete but don't want to see in your other views.</p> | |
626 | <h2 id="defaults"><a href="#defaults">= Defaults</a></h2> | |
627 | <p>Default entries begin with an equal sign, <code>=</code>. These entries consist of <code>@key value</code> pairs which then become the defaults for subsequent entries in the same file until another <code>=</code> entry is reached.</p> | |
628 | <p>Suppose, for example, that a particular file contains items relating to "project_a" for "client_1". Then entering</p> | |
629 | <pre><code>= @k client_1:project_a</code></pre> | |
630 | <p>on the first line of the file and</p> | |
631 | <pre><code>=</code></pre> | |
632 | <p>on the twentieth line of the file would set the default keyword for entries between the first and twentieth line in the file.</p> | |
633 | <h1 id="keys"><a href="#keys">@Keys</a></h1> | |
634 | <h2 id="a-alert"><a href="#a-alert"><span class="citation">@a</span> alert</a></h2> | |
635 | <p>The specification of the alert(s) to use with the item. One or more alerts can be specified in an item. E.g.,</p> | |
636 | <pre><code>@a 10m, 5m | |
637 | @a 1h: s</code></pre> | |
638 | <p>would trigger the alert(s) specified by <code>default_alert</code> in your <code>etm.cfg</code> at 10 and 5 minutes before the starting time and a (s)ound alert one hour before the starting time.</p> | |
639 | <p>The alert</p> | |
640 | <pre><code>@a 2d: e; who@what.com, where2@when.org; filepath1, filepath2</code></pre> | |
641 | <p>would send an email to the two listed recipients exactly 2 days (48 hours) before the starting time of the item with the item summary as the subject, with file1 and file2 as attachments and with the body of the message composed using <code>email_template</code> from your <code>etm.cfg</code>.</p> | |
642 | <p>Similarly, the alert</p> | |
643 | <pre><code>@a 10m: t; 9191234567@vtext.com, 9197654321@txt.att.net</code></pre> | |
644 | <p>would send a text message 10 minutes before the starting time of the item to the two mobile phones listed (using 10 digit area code and carrier mms extension) together with the settings for <code>sms</code> in <code>etm.cfg</code>. If no numbers are given, the number and mms extension specified in <code>sms.phone</code> will be used. Here are the mms extensions for the major US carriers:</p> | |
645 | <pre><code>Alltel @message.alltel.com | |
646 | AT&T @txt.att.net | |
647 | Nextel @messaging.nextel.com | |
648 | Sprint @messaging.sprintpcs.com | |
649 | SunCom @tms.suncom.com | |
650 | T-mobile @tmomail.net | |
651 | VoiceStream @voicestream.net | |
652 | Verizon @vtext.com</code></pre> | |
653 | <p>Finally,</p> | |
654 | <pre><code>@a 0: p; program_path</code></pre> | |
655 | <p>would execute <code>program_path</code> at the starting time of the item.</p> | |
656 | <p>The format for each of these:</p> | |
657 | <pre><code>@a <trigger times> [: action [; arguments]]</code></pre> | |
658 | <p>In addition to the default action used when the optional <code>: action</code> is not given, there are the following possible values for <code>action</code>:</p> | |
659 | <pre><code>d Execute alert_displaycmd in etm.cfg. | |
660 | ||
661 | e; recipients[;attachments] Send an email to recipients (a comma separated list of email addresses) optionally attaching attachments (a comma separated list of file paths). The item summary is used as the subject of the email and the expanded value of email_template from etm.cfg as the body. | |
662 | ||
663 | m Display an internal etm message box using alert_template. | |
664 | ||
665 | p; process Execute the command given by process. | |
666 | ||
667 | s Execute alert_soundcmd in etm.cfg. | |
668 | ||
669 | t [; phonenumbers] Send text messages to phonenumbers (a comma separated list of 10 digit phone numbers with the sms extension of the carrier appended) with the expanded value of sms.message as the text message. | |
670 | ||
671 | v Execute alert_voicecmd in etm.cfg.</code></pre> | |
672 | <p>Note: either <code>e</code> or <code>p</code> can be combined with other actions in a single alert but not with one another.</p> | |
673 | <h2 id="b-beginby"><a href="#b-beginby"><span class="citation">@b</span> beginby</a></h2> | |
674 | <p>An integer number of days before the starting date time at which to begin displaying <em>begin by</em> notices. When notices are displayed they will be sorted by the item's starting datetime and then by the item's priority, if any.</p> | |
675 | <h2 id="c-context"><a href="#c-context"><span class="citation">@c</span> context</a></h2> | |
676 | <p>Intended primarily for tasks to indicate the context in which the task can be completed. Common contexts include home, office, phone, computer and errands. The "next view" supports this usage by showing undated tasks, grouped by context. If you're about to run errands, for example, you can open the "next view", look under "errands" and be sure that you will have no "wish I had remembered" regrets.</p> | |
677 | <h2 id="d-description"><a href="#d-description"><span class="citation">@d</span> description</a></h2> | |
678 | <p>An elaboration of the details of the item to complement the summary.</p> | |
679 | <h2 id="e-extent"><a href="#e-extent"><span class="citation">@e</span> extent</a></h2> | |
680 | <p>A time period string such as <code>1d2h</code> (1 day 2 hours). For an action, this would be the elapsed time. For a task, this could be an estimate of the time required for completion. For an event, this would be the duration. The ending time of the event would be this much later than the starting datetime.</p> | |
681 | <p>Tip. Need to determine the appropriate value for <code>@e</code> for a flight when you have the departure and arrival datetimes but the timezones are different? The date calculator (shortcut F5) will accept timezone information so that, e.g., entering the arrival time minus the departure time</p> | |
682 | <pre><code>4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai</code></pre> | |
683 | <p>into the calculator would give</p> | |
684 | <pre><code>14h25m</code></pre> | |
685 | <p>as the flight time.</p> | |
686 | <h2 id="f-done-due"><a href="#f-done-due"><span class="citation">@f</span> done[; due]</a></h2> | |
687 | <p>Datetimes; tasks, delegated tasks and task groups only. When a task is completed an <code>@f done</code> entry is added to the task. When the task has a due date, <code>; due</code> is appended to the entry. Similarly, when a job from a task group is completed in etm, an <code>&f done</code> or <code>&f done; due</code> entry is appended to the job and it is removed from the list of prerequisites for the other jobs. In both cases <code>done</code> is the completion datetime and <code>due</code>, if added, is the datetime that the task or job was due. The completed task or job is shown as finished on the completion date. When the last job in a task group is finished an <code>@f done</code> or <code>@f done; due</code> entry is added to the task group itself reflecting the datetime that the last job was done and, if the task group is repeating, the <code>&f</code> entries are removed from the individual jobs.</p> | |
688 | <p>Another step is taken for repeating task groups. When the first job in a task group is completed, the <code>@s</code> entry is updated using the setting for <code>@o</code> (above) to show the next datetime the task group is due and the <code>@f</code> entry is removed from the task group. This means when some, but not all of the jobs for the current repetition have been completed, only these job completions will be displayed. Otherwise, when none of the jobs for the current repetition have been completed, then only that last completion of the task group itself will be displayed.</p> | |
689 | <p>Consider, for example, the following repeating task group which repeats monthly on the last weekday on or before the 25th.</p> | |
690 | <pre><code>+ pay bills @s 11/23 @f 10/24;10/25 | |
691 | @r m &w MO,TU,WE,TH,FR &m 23,24,25 &s -1 | |
692 | @j organize bills &q 1 | |
693 | @j pay on-line bills &q 3 | |
694 | @j get stamps, envelopes, checkbook &q 1 | |
695 | @j write checks &q 2 | |
696 | @j mail checks &q 3</code></pre> | |
697 | <p>Here "organize bills" and "get stamps, envelopes, checkbook" have no prerequisites. "Organize bills", however, is a prerequisite for "pay on-line bills" and both "organize bills" and "get stamps, envelops, checkbook" are prerequisites for "write checks" which, in turn, is a prerequisite for "mail checks".</p> | |
698 | <p>The repetition that was due on 10/25 was completed on 10/24. The next repetition was due on 11/23 and, since none of the jobs for this repetition have been completed, the completion of the group on 10/24 and the list of jobs due on 11/23 will be displayed initially. The following sequence of screen shots show the effect of completing the jobs for the 11/23 repetition one by one on 11/27.</p> | |
699 | <h2 id="g-goto"><a href="#g-goto"><span class="citation">@g</span> goto</a></h2> | |
700 | <p>The path to a file or a URL to be opened using the system default application when the user presses <em>G</em> in the GUI. E.g., here's a task to join the etm discussion group with the URL of the group as the link. In this case, pressing <em>G</em> would open the URL in your default browser.</p> | |
701 | <pre><code>- join the etm discussion group @s +1/1 | |
702 | @g http://groups.google.com/group/eventandtaskmanager/topics</code></pre> | |
703 | <p>Tip. Have a pdf file with the agenda for a meeting? Stick an <span class="citation">@g</span> entry with the path to the file in the event you create for the meeting. Then whenever the meeting is selected, <em>G</em> will bring up the agenda.</p> | |
704 | <h2 id="h-history"><a href="#h-history"><span class="citation">@h</span> history</a></h2> | |
705 | <p>Used internally with task groups to track completion done;due pairs.</p> | |
706 | <h2 id="j-job"><a href="#j-job"><span class="citation">@j</span> job</a></h2> | |
707 | <p>Component tasks or jobs within a task group are given by <code>@j job</code> entries. <code>@key value</code> entries prior to the first <code>@j</code> become the defaults for the jobs that follow. <code>&key value</code> entries given in jobs use <code>&</code> rather than <code>@</code> and apply only to the specific job.</p> | |
708 | <p>Many key-value pairs can be given either in the group task using <code>@</code> or in the component jobs using <code>&</code>:</p> | |
709 | <pre><code>@c or &c context | |
710 | @d or &d description | |
711 | @e or &e extent | |
712 | @f or &f done[; due] datetime | |
713 | @k or &k keyword | |
714 | @l or &l location | |
715 | @u or &u user</code></pre> | |
716 | <p>The key-value pair <code>&h</code> is used internally to track job done;due completions in task groups.</p> | |
717 | <p>The key-value pair <code>&q</code> (queue position) can <em>only</em> be given in component jobs where it is required. Key-values other than <code>&q</code> and those listed above, can <em>only</em> be given in the initial group task entry and their values are inherited by the component jobs.</p> | |
718 | <h2 id="k-keyword"><a href="#k-keyword"><span class="citation">@k</span> keyword</a></h2> | |
719 | <p>A heirarchical classifier for the item. Intended for actions to support time billing where a common format would be <code>client:job:category</code>. <em>etm</em> treats such a keyword as a heirarchy so that an action report grouped by month and then keyword might appear as follows</p> | |
720 | <pre><code> 27.5h) Client 1 (3) | |
721 | 4.9h) Project A (1) | |
722 | 15h) Project B (1) | |
723 | 7.6h) Project C (1) | |
724 | 24.2h) Client 2 (3) | |
725 | 3.1h) Project D (1) | |
726 | 21.1h) Project E (2) | |
727 | 5.1h) Category a (1) | |
728 | 16h) Category b (1) | |
729 | 4.2h) Client 3 (1) | |
730 | 8.7h) Client 4 (2) | |
731 | 2.1h) Project F (1) | |
732 | 6.6h) Project G (1)</code></pre> | |
733 | <p>An arbitrary number of heirarchical levels in keywords is supported.</p> | |
734 | <h2 id="l-location"><a href="#l-location"><span class="citation">@l</span> location</a></h2> | |
735 | <p>The location at which, for example, an event will take place.</p> | |
736 | <h2 id="m-memo"><a href="#m-memo"><span class="citation">@m</span> memo</a></h2> | |
737 | <p>Further information about the item not included in the summary or the description. Since the summary is used as the subject of an email alert and the descripton is commonly included in the body of an email alert, this field could be used for information not to be included in the email.</p> | |
738 | <h2 id="o-overdue"><a href="#o-overdue"><span class="citation">@o</span> overdue</a></h2> | |
739 | <p>Repeating tasks only. One of the following choices: k) keep, r) restart, or s) skip. Details below.</p> | |
740 | <h2 id="p-priority"><a href="#p-priority"><span class="citation">@p</span> priority</a></h2> | |
741 | <p>Either 0 (no priority) or an intger between 1 (highest priority) and 9 (lowest priority). Primarily used with undated tasks.</p> | |
742 | <h2 id="r-repetition-rule"><a href="#r-repetition-rule"><span class="citation">@r</span> repetition rule</a></h2> | |
743 | <p>The specification of how an item is to repeat. Repeating items <strong>must</strong> have an <code>@s</code> entry as well as one or more <code>@r</code> entries. Generated datetimes are those satisfying any of the <code>@r</code> entries and falling <strong>on or after</strong> the datetime given in <code>@s</code>. Note that the datetime given in <code>@s</code> will only be included if it matches one of the datetimes generated by the <code>@r</code> entry.</p> | |
744 | <p>A repetition rule begins with</p> | |
745 | <pre><code>@r frequency</code></pre> | |
746 | <p>where <code>frequency</code> is one of the following characters:</p> | |
747 | <pre><code>y yearly | |
748 | m monthly | |
749 | w weekly | |
750 | d daily | |
751 | h hourly | |
752 | n minutely | |
753 | l list (a list of datetimes will be provided using @+)</code></pre> | |
754 | <p>The <code>@r frequency</code> entry can, optionally, be followed by one or more <code>&key value</code> pairs:</p> | |
755 | <pre><code>&i: interval (positive integer, default = 1) E.g, with frequency w, interval 3 would repeat every three weeks. | |
756 | &t: total (positive integer) Include no more than this total number of repetitions. | |
757 | &s: bysetpos (integer). When multiple dates satisfy the rule, take the date from this position in the list, e.g, &s 1 would choose the first element and &s -1 the last. See the payday example below for an illustration of bysetpos. | |
758 | &u: until (datetime) Only include repetitions falling **before** (not including) this datetime. | |
759 | &M: bymonth (1, 2, ..., 12) | |
760 | &m: bymonthday (1, 2, ..., 31) Use, e.g., -1 for the last day of the month. | |
761 | &W: byweekno (1, 2, ..., 53) | |
762 | &w: byweekday (*English* weekday abbreviation SU ... SA). Use, e.g., 3WE for the 3rd Wednesday or -1FR, for the last Friday in the month. | |
763 | &h: byhour (0 ... 23) | |
764 | &n: byminute (0 ... 59) | |
765 | &E: byeaster (integer number of days before, < 0, or after, > 0, Easter)</code></pre> | |
766 | <p>Repetition examples:</p> | |
767 | <ul> | |
768 | <li><p>1st and 3rd Wednesdays of each month.</p> | |
769 | <pre><code>^ 1st and 3rd Wednesdays | |
770 | @r m &w 1WE, 3WE</code></pre></li> | |
771 | <li><p>Payday (an occasion) on the last week day of each month. (The <code>&s -1</code> entry extracts the last date which is both a weekday and falls within the last three days of the month.)</p> | |
772 | <pre><code>^ payday @s 2010-07-01 | |
773 | @r m &w MO, TU, WE, TH, FR &m -1, -2, -3 &s -1</code></pre></li> | |
774 | <li><p>Take a prescribed medication daily (an event) from the 23rd through the 27th of the current month at 10am, 2pm, 6pm and 10pm and trigger an alert zero minutes before each event.</p> | |
775 | <pre><code>* take Rx @d 10a 23 @r d &u 11p 27 &h 10, 14 18, 22 @a 0</code></pre></li> | |
776 | <li><p>Vote for president (an occasion) every four years on the first Tuesday after a Monday in November. (The <code>&m range(2,9)</code> requires the month day to fall within 2 ... 8 and thus, combined with <code>&w TU</code> to be the first Tuesday following a Monday.)</p> | |
777 | <pre><code>^ Vote for president @s 2012-11-06 | |
778 | @r y &i 4 &M 11 &m range(2,9) &w TU</code></pre></li> | |
779 | <li><p>Ash Wednesday (an occasion) that occurs 46 days before Easter each year.</p> | |
780 | <p>^ Ash Wednesday 2010-01-01 <span class="citation">@r</span> y &E -46</p></li> | |
781 | <li><p>Easter Sunday (an occasion).</p> | |
782 | <p>^ Easter Sunday 2010-01-01 <span class="citation">@r</span> y &E 0</p></li> | |
783 | </ul> | |
784 | <p>Optionally, <code>@+</code> and <code>@-</code> entries can be given.</p> | |
785 | <ul> | |
786 | <li><code>@+</code>: include (comma separated list to datetimes to be <em>added</em> to those generated by the <code>@r</code> entries)</li> | |
787 | <li><code>@-</code>: exclude (comma separated list to datetimes to be <em>removed</em> from those generated by the <code>@r</code> entries)</li> | |
788 | </ul> | |
789 | <p>A repeating <em>task</em> may optionally also include an <code>@o <k|s|r></code> entry (default = k):</p> | |
790 | <ul> | |
791 | <li><p><code>@o k</code>: Keep the current due date if it becomes overdue and use the next due date from the recurrence rule if it is finished early. This would be appropriate, for example, for the task 'file tax return'. The return due April 15, 2009 must still be filed even if it is overdue and the 2010 return won't be due until April 15, 2010 even if the 2009 return is finished early.</p></li> | |
792 | <li><p><code>@o s</code>: Skip overdue due dates and set the due date for the next repetition to the first due date from the recurrence rule on or after the current date. This would be appropriate, for example, for the task 'put out the trash' since there is no point in putting it out on Tuesday if it's picked up on Mondays. You might just as well wait until the next Monday to put it out. There's also no point in being reminded until the next Monday.</p></li> | |
793 | <li><p><code>@o r</code>: Restart the repetitions based on the last completion date. Suppose you want to mow the grass once every ten days and that when you mowed yesterday, you were already nine days past due. Then you want the next due date to be ten days from yesterday and not today. Similarly, if you were one day early when you mowed yesterday, then you would want the next due date to be ten days from yesterday and not ten days from today.</p></li> | |
794 | </ul> | |
795 | <h2 id="s-starting-datetime"><a href="#s-starting-datetime"><span class="citation">@s</span> starting datetime</a></h2> | |
796 | <p>When an action is started, an event begins or a task is due.</p> | |
797 | <h2 id="t-tags"><a href="#t-tags"><span class="citation">@t</span> tags</a></h2> | |
798 | <p>A tag or list of tags for the item.</p> | |
799 | <h2 id="u-user"><a href="#u-user"><span class="citation">@u</span> user</a></h2> | |
800 | <p>Intended to specify the person to whom a delegated task is assigned. Could also be used in actions to indicate the person performing the action.</p> | |
801 | <h2 id="v-action_rates-key"><a href="#v-action_rates-key"><span class="citation">@v</span> action_rates key</a></h2> | |
802 | <p>Actions only. A key from <code>action_rates</code> in your etm.cft to apply to the value of <code>@e</code>. Used in actions to apply a billing rate to time spent in an action. E.g., with</p> | |
803 | <pre><code> minutes: 6 | |
804 | action_rates: | |
805 | br1: 45.0 | |
806 | br2: 60.0</code></pre> | |
807 | <p>then entries of <code>@v br1</code> and <code>@e 2h25m</code> in an action would entail a value of <code>45.0 * 2.5 = 112.50</code>.</p> | |
808 | <h2 id="w-action_markups-key"><a href="#w-action_markups-key"><span class="citation">@w</span> action_markups key</a></h2> | |
809 | <p>A key from <code>action_markups</code> in your <code>etm.cfg</code> to apply to the value of <code>@x</code>. Used in actions to apply a markup rate to expense in an action. E.g., with</p> | |
810 | <pre><code> weights: | |
811 | mr1: 1.5 | |
812 | mr2: 10.0</code></pre> | |
813 | <p>then entries of <code>@w mr1</code> and <code>@x 27.50</code> in an action would entail a value of <code>27.50 * 1.5 = 41.25</code>.</p> | |
814 | <h2 id="x-expense"><a href="#x-expense"><span class="citation">@x</span> expense</a></h2> | |
815 | <p>Actions only. A currency amount such as <code>27.50</code>. Used in conjunction with <span class="citation">@w</span> above to bill for action expenditures.</p> | |
816 | <h2 id="z-time-zone"><a href="#z-time-zone"><span class="citation">@z</span> time zone</a></h2> | |
817 | <p>The time zone of the item, e.g., US/Eastern. The starting and other datetimes in the item will be interpreted as belonging to this time zone.</p> | |
818 | <p>Tip. You live in the US/Eastern time zone but a flight that departs Sydney on April 20 at 9pm bound for New York with a flight duration of 14 hours and 30 minutes. The hard way is to convert this to US/Eastern time and enter the flight using that time zone. The easy way is to use Australia/Sydney and skip the conversion:</p> | |
819 | <pre><code>* Sydney to New York @s 2014-04-23 9pm @e 14h30m @z Australia/Sydney</code></pre> | |
820 | <p>This flight will be displayed while you're in the Australia/Sydney time zone as extending from 9pm on April 23 until 11:30am on April 24, but in the US/Eastern time zone it will be displayed as extending from 7am until 9:30pm on April 23.</p> | |
821 | <h2 id="include"><a href="#include">@+ include</a></h2> | |
822 | <p>A datetime or list of datetimes to be added to the repetitions generated by the <code>@r rrule</code> entry. If only a date is provided, 12:00am is assumed.</p> | |
823 | <h2 id="exclude"><a href="#exclude">@- exclude</a></h2> | |
824 | <p>A datetime or list of datetimes to be removed from the repetitions generated by the <code>@r rrule</code> entry. If only a date is provided, 12:00am is assumed.</p> | |
825 | <p>Note that to exclude a datetime from the recurrence rule, the @- datetime <em>must exactly match both the date and time</em> generated by the recurrence rule.</p> | |
826 | <h1 id="dates"><a href="#dates">Dates</a></h1> | |
827 | <h2 id="fuzzy-dates"><a href="#fuzzy-dates">Fuzzy dates</a></h2> | |
828 | <p>When either a <em>datetime</em> or an <em>time period</em> is to be entered, special formats are used in <em>etm</em>. Examples include entering a starting datetime for an item using <code>@s</code>, jumping to a date using Ctrl-J and calculating a date using F5.</p> | |
829 | <p>Suppose, for example, that it is currently 8:30am on Friday, February 15, 2013. Then, <em>fuzzy dates</em> would expand into the values illustrated below.</p> | |
830 | <pre><code> mon 2p or mon 14h 2:00pm Monday, February 19 | |
831 | fri 12:00am Friday, February 15 | |
832 | 9a -1/1 or 9h -1/1 9:00am Tuesday, January 1 | |
833 | +2/15 12:00am Monday, April 15 2013 | |
834 | 8p +7 or 20h +7 8:00pm Friday, February 22 | |
835 | -14 12:00am Friday, February 1 | |
836 | now 8:30am Friday, February 15</code></pre> | |
837 | <p>Note that 12am is the default time when a time is not explicity entered. E.g., <code>+2/15</code> in the examples above gives 12:00am on April 15.</p> | |
838 | <p>To avoid ambiguity, always append either 'a', 'p' or 'h' when entering an hourly time, e.g., use <code>1p</code> or <code>13h</code>.</p> | |
839 | <h2 id="time-periods"><a href="#time-periods">Time periods</a></h2> | |
840 | <p>Time periods are entered using the format <code>DdHhMm</code> where D, H and M are integers and d, h and m refer to days, hours and minutes respectively. For example:</p> | |
841 | <pre><code> 2h30m 2 hours, 30 minutes | |
842 | 7d 7 days | |
843 | 45m 45 minutes</code></pre> | |
844 | <p>As an example, if it is currently 8:50am on Friday February 15, 2013, then entering <code>now + 2d4h30m</code> into the date calculator would give <code>2013-02-17 1:20pm</code>.</p> | |
845 | <h2 id="time-zones"><a href="#time-zones">Time zones</a></h2> | |
846 | <p>Dates and times are always stored in <em>etm</em> data files as times in the time zone given by the entry for <code>@z</code>. On the other hand, dates and times are always displayed in <em>etm</em> using the local time zone of the system.</p> | |
847 | <p>For example, if it is currently 8:50am EST on Friday February 15, 2013, and an item is saved on a system in the <code>US/Eastern</code> time zone containing the entry</p> | |
848 | <pre><code>@s now @z Australia/Sydney</code></pre> | |
849 | <p>then the data file would contain</p> | |
850 | <pre><code>@s 2013-02-16 12:50am @z Australia/Sydney</code></pre> | |
851 | <p>but this item would be displayed as starting at <code>8:50am 2013-02-15</code> on the system in the <code>US/Eastern</code> time zone.</p> | |
852 | <p>Tip. Need to determine the flight time when the departing timezone is different that the arriving timezone? The date calculator (shortcut F5) will accept timezone information so that, e.g., entering the arrival time minus the departure time</p> | |
853 | <pre><code>4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai</code></pre> | |
854 | <p>into the calculator would give</p> | |
855 | <pre><code>14h25m</code></pre> | |
856 | <p>as the flight time.</p> | |
857 | <h2 id="anniversary-substitutions"><a href="#anniversary-substitutions">Anniversary substitutions</a></h2> | |
858 | <p>An anniversary substitution is an expression of the form <code>!YYYY!</code> that appears in an item summary. Consider, for example, the occassion</p> | |
859 | <pre><code>^ !2010! anniversary @s 2011-02-20 @r y</code></pre> | |
860 | <p>This would appear on Feb 20 of 2011, 2012, 2013 and 2014, respectively, as <em>1st anniversary</em>, <em>2nd anniversary</em>, <em>3rd anniversary</em> and <em>4th anniversary</em>. The suffixes, <em>st</em>, <em>nd</em> and so forth, depend upon the translation file for the locale.</p> | |
861 | <h2 id="easter"><a href="#easter">Easter</a></h2> | |
862 | <p>An expression of the form <code>easter(yyyy)</code> can be used as a date specification in <code>@s</code> entries and in the datetime calculator. E.g.</p> | |
863 | <pre><code>@s easter(2014) 4p</code></pre> | |
864 | <p>would expand to <code>2014-04-20 4pm</code>. Similarly, in the date calculator</p> | |
865 | <pre><code>easter(2014) - 48d</code></pre> | |
866 | <p>(Rose Monday) would return <code>2014-03-03</code>. In repeating items <code>easter(yyyy)</code> is replaced by <code>&E</code>, e.g.,</p> | |
867 | <pre><code>^ Easter Sunday @s 2010-01-01 @r y &E 0 | |
868 | ^ Ash Wednesday @s 2010-01-01 @r y &E -46 | |
869 | ^ Rose Monday @s 2010-01-01 @r y &E -48</code></pre> | |
870 | <h1 id="preferences"><a href="#preferences">Preferences</a></h1> | |
871 | <p>Configuration options are stored in a file named <code>etmtk.cfg</code> which, by default, belongs to the folder <code>.etm</code> in your home directory. When this file is edited in <em>etm</em> (Shift Ctrl-P), your changes become effective as soon as they are saved --- you do not need to restart <em>etm</em>. These options are listed below with illustrative entries and brief descriptions.</p> | |
872 | <h2 id="template-expansions"><a href="#template-expansions">Template expansions</a></h2> | |
873 | <p>The following template expansions can be used in <code>alert_displaycmd</code>, <code>alert_template</code>, <code>alert_voicecmd</code>, <code>email_template</code>, <code>sms_message</code> and <code>sms_subject</code> below.</p> | |
874 | <h3 id="summary"><a href="#summary"><code>!summary!</code></a></h3> | |
875 | <p>the item's summary (this will be used as the subject of email and message alerts)</p> | |
876 | <h3 id="start_date"><a href="#start_date"><code>!start_date!</code></a></h3> | |
877 | <p>the starting date of the event</p> | |
878 | <h3 id="start_time"><a href="#start_time"><code>!start_time!</code></a></h3> | |
879 | <p>the starting time of the event</p> | |
880 | <h3 id="time_span"><a href="#time_span"><code>!time_span!</code></a></h3> | |
881 | <p>the time span of the event (see below)</p> | |
882 | <h3 id="alert_time"><a href="#alert_time"><code>!alert_time!</code></a></h3> | |
883 | <p>the time the alert is triggered</p> | |
884 | <h3 id="time_left"><a href="#time_left"><code>!time_left!</code></a></h3> | |
885 | <p>the time remaining until the event starts</p> | |
886 | <h3 id="when"><a href="#when"><code>!when!</code></a></h3> | |
887 | <p>the time remaining until the event starts as a sentence (see below)</p> | |
888 | <h3 id="d"><a href="#d"><code>!d!</code></a></h3> | |
889 | <p>the item's <code>@d</code> (description)</p> | |
890 | <h3 id="l"><a href="#l"><code>!l!</code></a></h3> | |
891 | <p>the item's <code>@l</code> (location)</p> | |
892 | <p>The value of <code>!time_span!</code> depends on the starting and ending datetimes. Here are some examples:</p> | |
893 | <ul> | |
894 | <li><p>if the start and end <em>datetimes</em> are the same (zero extent): <code>10am Wed, Aug 4</code></p></li> | |
895 | <li><p>else if the times are different but the <em>dates</em> are the same: <code>10am - 2pm Wed, Aug 4</code></p></li> | |
896 | <li><p>else if the dates are different: <code>10am Wed, Aug 4 - 9am Thu, Aug 5</code></p></li> | |
897 | <li><p>additionally, the year is appended if a date falls outside the current year:</p> | |
898 | <pre><code>10am - 2pm Thu, Jan 3 2013 | |
899 | 10am Mon, Dec 31 - 2pm Thu, Jan 3 2013</code></pre></li> | |
900 | </ul> | |
901 | <p>Here are values of <code>!time_left!</code> and <code>!when!</code> for some illustrative periods:</p> | |
902 | <ul> | |
903 | <li><p><code>2d3h15m</code></p> | |
904 | <pre><code>time_left : '2 days 3 hours 15 minutes' | |
905 | when : '2 days 3 hours 15 minutes from now'</code></pre></li> | |
906 | <li><p><code>20m</code></p> | |
907 | <pre><code>time_left : '20 minutes' | |
908 | when : '20 minutes from now'</code></pre></li> | |
909 | <li><p><code>0m</code></p> | |
910 | <pre><code>time_left : '' | |
911 | when : 'now'</code></pre></li> | |
912 | </ul> | |
913 | <p>Note that 'now', 'from now', 'days', 'day', 'hours' and so forth are determined by the translation file in use.</p> | |
914 | <h2 id="options"><a href="#options">Options</a></h2> | |
915 | <h3 id="action_interval"><a href="#action_interval">action_interval</a></h3> | |
916 | <pre><code>action_interval: 1</code></pre> | |
917 | <p>Every <code>action_interval</code> minutes, execute <code>action_timercmd</code> when the timer is running and <code>action_pausecmd</code> when the timer is paused. Choose zero to disable executing these commands.</p> | |
918 | <h3 id="action_markups"><a href="#action_markups">action_markups</a></h3> | |
919 | <pre><code>action_markups: | |
920 | default: 1.0 | |
921 | mu1: 1.5 | |
922 | mu2: 2.0</code></pre> | |
923 | <p>Possible markup rates to use for <code>@x</code> expenses in actions. An arbitrary number of rates can be entered using whatever labels you like. These labels can then be used in actions in the <code>@w</code> field so that, e.g.,</p> | |
924 | <pre><code>... @x 25.80 @w mu1 ...</code></pre> | |
925 | <p>in an action would give this expansion in an action template:</p> | |
926 | <pre><code>!expense! = 25.80 | |
927 | !charge! = 38.70</code></pre> | |
928 | <h3 id="action_minutes"><a href="#action_minutes">action_minutes</a></h3> | |
929 | <pre><code>action_minutes: 6</code></pre> | |
930 | <p>Round action times up to the nearest <code>action_minutes</code> in action reports. Possible choices are 1, 6, 12, 15, 30 and 60. With 1, no rounding is done and times are reported as integer minutes. Otherwise, the prescribed rounding is done and times are reported as floating point hours.</p> | |
931 | <h3 id="action_rates"><a href="#action_rates">action_rates</a></h3> | |
932 | <pre><code>action_rates: | |
933 | default: 30.0 | |
934 | br1: 45.0 | |
935 | br2: 60.0</code></pre> | |
936 | <p>Possible billing rates to use for <code>@e</code> times in actions. An arbitrary number of rates can be entered using whatever labels you like. These labels can then be used in the <code>@v</code> field in actions so that, e.g., with <code>action_minutes: 6</code> then:</p> | |
937 | <pre><code>... @e 75m @v br1 ...</code></pre> | |
938 | <p>in an action would give these expansions in an action template:</p> | |
939 | <pre><code>!hours! = 1.3 | |
940 | !value! = 58.50</code></pre> | |
941 | <p>If the label <code>default</code> is used, the corresponding rate will be used when <code>@v</code> is not specified in an action.</p> | |
942 | <p>Note that etm accumulates group totals from the <code>time</code> and <code>value</code> of individual actions. Thus</p> | |
943 | <pre><code>... @e 75m @v br1 ... | |
944 | ... @e 60m @v br2 ...</code></pre> | |
945 | <p>would aggregate to</p> | |
946 | <pre><code>!hours! = 2.3 (= 1.3 + 1) | |
947 | !value! = 118.50 (= 1.3 * 45.0 + 1 * 60.0)</code></pre> | |
948 | <h3 id="action_template"><a href="#action_template">action_template</a></h3> | |
949 | <pre><code>action_template: '!hours!h) !label! (!count!)'</code></pre> | |
950 | <p>Used for action reports. With the above settings for <code>action_minutes</code> and <code>action_template</code>, a report might appear as follows:</p> | |
951 | <pre><code>27.5h) Client 1 (3) | |
952 | 4.9h) Project A (1) | |
953 | 15h) Project B (1) | |
954 | 7.6h) Project C (1) | |
955 | 24.2h) Client 2 (3) | |
956 | 3.1h) Project D (1) | |
957 | 21.1h) Project E (2) | |
958 | 5.1h) Category a (1) | |
959 | 16h) Category b (1) | |
960 | 4.2h) Client 3 (1) | |
961 | 8.7h) Client 4 (2) | |
962 | 2.1h) Project F (1) | |
963 | 6.6h) Project G (1)</code></pre> | |
964 | <p>Available template expansions for <code>action_template</code> include:</p> | |
965 | <ul> | |
966 | <li><p><code>!label!</code>: the item or group label.</p></li> | |
967 | <li><p><code>!count!</code>: the number of children represented in the reported item or group.</p></li> | |
968 | <li><p><code>!minutes!:</code> the total time from <code>@e</code> entries in minutes rounded up using the setting for <code>action_minutes</code>.</p></li> | |
969 | <li><p><code>!hours!</code>: if action_minutes = 1, the time in hours and minutes. Otherwise, the time in floating point hours.</p></li> | |
970 | <li><p><code>!value!</code>: the billing value of the rounded total time. Requires an action entry such as <code>@v br1</code> and a setting for <code>action_rates</code>.</p></li> | |
971 | <li><p><code>!expense!</code>: the total expense from <code>@x</code> entries.</p></li> | |
972 | <li><p><code>!charge!</code>: the billing value of the total expense. Requires an action entry such as <code>@w mu1</code> and a setting for <code>action_markups</code>.</p></li> | |
973 | <li><p><code>!total!</code>: the sum of <code>!value!</code> and <code>!charge!</code>.</p></li> | |
974 | </ul> | |
975 | <p>Note: when aggregating amounts in action reports, billing and markup rates are applied first to times and expenses for individual actions and the resulting amounts are then aggregated. Similarly, when times are rounded up, the rounding is done for individual actions and the results are then aggregated.</p> | |
976 | <h3 id="action_timer"><a href="#action_timer">action_timer</a></h3> | |
977 | <pre><code>action_timer: | |
978 | paused: 'play ~/.etm/sounds/timer_paused.wav' | |
979 | running: 'play ~/.etm/sounds/timer_running.wav' | |
980 | idle: 'play ~/.etm/sounds/timer_idle.wav'</code></pre> | |
981 | <p>The command <code>running</code> is executed every <code>action_interval</code> minutes whenever the action timer is running and <code>paused</code> every minute when the action timer is paused. The command <code>idle</code> is executed every <code>action_interval</code> minutes when the idle timer is running and the action timer is neither running nor paused.</p> | |
982 | <h3 id="agenda"><a href="#agenda">agenda</a></h3> | |
983 | <pre><code>agenda_days: 4, | |
984 | agenda_colors: 2, | |
985 | agenda_indent: 2, | |
986 | agenda_width1: 43, | |
987 | agenda_width2: 17,</code></pre> | |
988 | <p>Sets the number of days with scheduled items to display in agenda view and other parameters affecting the display in the CLI.</p> | |
989 | <h3 id="alert_default"><a href="#alert_default">alert_default</a></h3> | |
990 | <pre><code>alert_default: [m]</code></pre> | |
991 | <p>The alert or list of alerts to be used when an alert is specified for an item but the type is not given. Possible values for the list include: - d: display (requires <code>alert_displaycmd</code>) - m: message (using <code>alert_template</code>) - s: sound (requires <code>alert_soundcmd</code>) - v: voice (requires <code>alert_voicecmd</code>)</p> | |
992 | <h3 id="alert_displaycmd"><a href="#alert_displaycmd">alert_displaycmd</a></h3> | |
993 | <pre><code>alert_displaycmd: growlnotify -t !summary! -m '!time_span!'</code></pre> | |
994 | <p>The command to be executed when <code>d</code> is included in an alert. Possible template expansions are discussed at the beginning of this tab.</p> | |
995 | <h3 id="alert_soundcmd"><a href="#alert_soundcmd">alert_soundcmd</a></h3> | |
996 | <pre><code>alert_soundcmd: 'play ~/.etm/sounds/etm_alert.wav'</code></pre> | |
997 | <p>The command to execute when <code>s</code> is included in an alert. Possible template expansions are discussed at the beginning of this tab.</p> | |
998 | <h3 id="alert_template"><a href="#alert_template">alert_template</a></h3> | |
999 | <pre><code>alert_template: '!time_span!\n!l!\n\n!d!'</code></pre> | |
1000 | <p>The template to use for the body of <code>m</code> (message) alerts. See the discussion of template expansions at the beginning of this tab for other possible expansion items.</p> | |
1001 | <h3 id="alert_voicecmd"><a href="#alert_voicecmd">alert_voicecmd</a></h3> | |
1002 | <pre><code>alert_voicecmd: say -v 'Alex' '!summary! begins !when!.'</code></pre> | |
1003 | <p>The command to be executed when <code>v</code> is included in an alert. Possible expansions are are discussed at the beginning of this tab.</p> | |
1004 | <h3 id="alert_wakecmd"><a href="#alert_wakecmd">alert_wakecmd</a></h3> | |
1005 | <pre><code>alert_wakecmd: ~/bin/SleepDisplay -w</code></pre> | |
1006 | <p>If given, this command will be issued to "wake up the display" before executing <code>alert_displaycmd</code>.</p> | |
1007 | <h3 id="ampm"><a href="#ampm">ampm</a></h3> | |
1008 | <pre><code>ampm: true</code></pre> | |
1009 | <p>Use ampm times if true and twenty-four hour times if false. E.g., 2:30pm (true) or 14:30 (false).</p> | |
1010 | <h3 id="completions_width"><a href="#completions_width">completions_width</a></h3> | |
1011 | <pre><code>completions_width: 36</code></pre> | |
1012 | <p>The width in characters of the auto completions popup window.</p> | |
1013 | <h3 id="calendars"><a href="#calendars">calendars</a></h3> | |
1014 | <pre><code>calendars: | |
1015 | - [dag, true, personal/dag] | |
1016 | - [erp, false, personal/erp] | |
1017 | - [shared, true, shared]</code></pre> | |
1018 | <p>These are (label, default, path relative to <code>datadir</code>) tuples to be interpreted as separate calendars. Those for which default is <code>true</code> will be displayed as default calendars. E.g., with the <code>datadir</code> below, <code>dag</code> would be a default calendar and would correspond to the absolute path <code>/Users/dag/.etm/data/personal/dag</code>. With this setting, the calendar selection dialog would appear as follows:</p> | |
1019 | <p>When non-default calendars are selected, busy times in the "week view" will appear in one color for events from default calendars and in another color for events from non-default calendars.</p> | |
1020 | <p><strong>Only data files that belong to one of the calendar directories or their subdirectories will be accessible within etm.</strong></p> | |
1021 | <h3 id="cfg_files"><a href="#cfg_files">cfg_files</a></h3> | |
1022 | <pre><code>cfg_files: | |
1023 | - completions: [] | |
1024 | - reports: [] | |
1025 | - users: []</code></pre> | |
1026 | <p>Each of the three list brackets can contain one or more comma separated <em>absolute</em> file paths. Additionally, paths corresponding to active calendars in the <code>datadir</code> directory are searched for files named <code>completions.cfg</code>, <code>reports.cfg</code> and <code>users.cfg</code> and these are processed in addition to the ones from <code>cfg_files</code>.</p> | |
1027 | <p>Note. Windows users should place each absolute path in quotes and escape backslashes, i.e., use <code>\\</code> anywhere <code>\</code> appears in a path.</p> | |
1028 | <ul> | |
1029 | <li><p>Completions</p> | |
1030 | <p>Each line in a completions file provides a possible completion when using the editor. E.g. with these completions</p> | |
1031 | <pre><code>@c computer | |
1032 | @c home | |
1033 | @c errands | |
1034 | @c office | |
1035 | @c phone | |
1036 | @z US/Eastern | |
1037 | @z US/Central | |
1038 | @z US/Mountain | |
1039 | @z US/Pacific | |
1040 | dnlgrhm@gmail.com</code></pre> | |
1041 | <p>entering, for example, "<span class="citation">@c</span>" in the editor and pressing Ctrl-Space, would popup a list of possible completions. Choosing the one you want and pressing <em>Return</em> would insert it and close the popup.</p> | |
1042 | <p>Up and down arrow keys change the selection and either <em>Tab</em> or <em>Return</em> inserts the selection.</p></li> | |
1043 | <li><p>Reports</p> | |
1044 | <p>Each line in a reports file provides a possible reports specification. These are available when using the CLI <code>m</code> command and in the GUI custom view. See <a href="#reports">Reports</a> for details.</p></li> | |
1045 | <li><p>Users</p> | |
1046 | <p>User files contain user (contact) information in a free form, text database. Each entry begins with a unique key for the person and is followed by detail lines each of which begins with a minus sign and contains some detail about the person that you want to record. Any detail line containing a colon should be quoted, e.g.,</p> | |
1047 | <pre><code>jbrown: | |
1048 | - Brown, Joe | |
1049 | - jbr@whatever.com | |
1050 | - 'home: 123 456-7890' | |
1051 | - 'birthday: 1978-12-14' | |
1052 | dcharles: | |
1053 | - Charles, Debbie | |
1054 | - dch@sometime.com | |
1055 | - 'cell: 456 789-0123' | |
1056 | - 'spouse: Rebecca'</code></pre> | |
1057 | <p>Keys from this file are added to auto-completions so that if you type, say, <code>@u jb</code> and press <em>Ctrl-Space</em>, then <code>@u jbrown</code> would be offered for completion.</p> | |
1058 | <p>If an item with the entry <code>@u jbrown</code> is selected in the GUI, you can press "u" to see a popup with the details:</p> | |
1059 | <pre><code>Brown, Joe | |
1060 | jbr@whatever.com | |
1061 | home: 123 456-7890 | |
1062 | birthday: 1978-12-14</code></pre></li> | |
1063 | </ul> | |
1064 | <h3 id="current-files"><a href="#current-files">current files</a></h3> | |
1065 | <pre><code>current_htmlfile: '' | |
1066 | current_textfile: '' | |
1067 | current_icsfolder: '' | |
1068 | current_indent: 3 | |
1069 | current_opts: '' | |
1070 | current_width1: 40 | |
1071 | current_width2: 17</code></pre> | |
1072 | <p>If absolute file paths are entered for <code>current_textfile</code> and/or <code>current_htmlfile</code>, then these files will be created and automatically updated by etm as as plain text or html files, respectively. If <code>current_opts</code> is given then the file will contain a report using these options; otherwise the file will contain an agenda. Indent and widths are taken from these setting with other settings, including color, from <em>report</em> or <em>agenda</em>, respectively.</p> | |
1073 | <p>If an absolute path is entered for <code>current_icsfolder</code>, then ics files corresponding to the entries in <code>calendars</code> will be created in this folder and updated as necessary. If there are no entries in calendars, then a single file, <code>all.ics</code>, will be created in this folder and updated as necessary.</p> | |
1074 | <p>Hint: fans of geektool can use the shell command <code>cat <current_textfile></code> to have the current agenda displayed on their desktops.</p> | |
1075 | <h3 id="datadir"><a href="#datadir">datadir</a></h3> | |
1076 | <pre><code>datadir: ~/.etm/data</code></pre> | |
1077 | <p>All etm data files are in this directory.</p> | |
1078 | <h3 id="dayfirst"><a href="#dayfirst">dayfirst</a></h3> | |
1079 | <pre><code>dayfirst: false</code></pre> | |
1080 | <p>If dayfirst is False, the MM-DD-YYYY format will have precedence over DD-MM-YYYY in an ambiguous date. See also <code>yearfirst</code>.</p> | |
1081 | <h3 id="details_rows"><a href="#details_rows">details_rows</a></h3> | |
1082 | <pre><code>details_rows: 4</code></pre> | |
1083 | <p>The number of rows to display in the bottom, details panel of the main window.</p> | |
1084 | <h3 id="edit_cmd"><a href="#edit_cmd">edit_cmd</a></h3> | |
1085 | <pre><code>edit_cmd: ~/bin/vim !file! +!line!</code></pre> | |
1086 | <p>This command is used in the command line version of etm to create and edit items. When the command is expanded, <code>!file!</code> will be replaced with the complete path of the file to be edited and <code>!line!</code> with the starting line number in the file. If the editor will open a new window, be sure to include the command to wait for the file to be closed before returning, e.g., with vim:</p> | |
1087 | <pre><code>edit_cmd: ~/bin/gvim -f !file! +!line!</code></pre> | |
1088 | <p>or with sublime text:</p> | |
1089 | <pre><code>edit_cmd: ~/bin/subl -n -w !file!:!line!</code></pre> | |
1090 | <h3 id="email_template"><a href="#email_template">email_template</a></h3> | |
1091 | <pre><code>email_template: 'Time: !time_span! | |
1092 | Locaton: !l! | |
1093 | ||
1094 | ||
1095 | !d!'</code></pre> | |
1096 | <p>Note that two newlines are required to get one empty line when the template is expanded. This template might expand as follows:</p> | |
1097 | <pre><code> Time: 1pm - 2:30pm Wed, Aug 4 | |
1098 | Location: Conference Room | |
1099 | ||
1100 | <contents of @d></code></pre> | |
1101 | <p>See the discussion of template expansions at the beginning of this tab for other possible expansion items.</p> | |
1102 | <h3 id="etmdir"><a href="#etmdir">etmdir</a></h3> | |
1103 | <pre><code>etmdir: ~/.etm</code></pre> | |
1104 | <p>Absolute path to the directory for etm.cfg and other etm configuration files.</p> | |
1105 | <h3 id="encoding"><a href="#encoding">encoding</a></h3> | |
1106 | <pre><code>encoding: {file: utf-8, gui: utf-8, term: utf-8}</code></pre> | |
1107 | <p>The encodings to be used for file IO, the GUI and terminal IO.</p> | |
1108 | <h3 id="filechange_alert"><a href="#filechange_alert">filechange_alert</a></h3> | |
1109 | <pre><code>filechange_alert: 'play ~/.etm/sounds/etm_alert.wav'</code></pre> | |
1110 | <p>The command to be executed when etm detects an external change in any of its data files. Leave this command empty to disable the notification.</p> | |
1111 | <h3 id="fontsize_fixed"><a href="#fontsize_fixed">fontsize_fixed</a></h3> | |
1112 | <pre><code>fontsize_fixed: 0</code></pre> | |
1113 | <p>Use this font size in the details panel, editor and reports. Use 0 to keep the system default.</p> | |
1114 | <h3 id="fontsize_tree"><a href="#fontsize_tree">fontsize_tree</a></h3> | |
1115 | <pre><code>fontsize_tree: 0</code></pre> | |
1116 | <p>Use this font size in the gui treeviews. Use 0 to keep the system default.</p> | |
1117 | <p>Tip: Leave the font sizes set to 0 and run etm with logging level 2 to see the system default sizes.</p> | |
1118 | <h3 id="freetimes"><a href="#freetimes">freetimes</a></h3> | |
1119 | <pre><code>freetimes: | |
1120 | opening: 480 # 8*60 minutes after midnight = 8am | |
1121 | closing: 1020 # 17*60 minutes after midnight = 5pm | |
1122 | minimum: 30 # 30 minutes | |
1123 | buffer: 15 # 15 minutes</code></pre> | |
1124 | <p>Only display free periods between <em>opening</em> and <em>closing</em> that last at least <em>minimum</em> minutes and preserve at least <em>buffer</em> minutes between events. Note that each of these settings must be an <em>interger</em> number of minutes.</p> | |
1125 | <p>E.g., with the above settings and these busy periods:</p> | |
1126 | <pre><code>Busy periods in Week 16: Apr 14 - 20, 2014 | |
1127 | ------------------------------------------ | |
1128 | Mon 14: 10:30am-11:00am; 12:00pm-1:00pm; 5:00pm-6:00pm | |
1129 | Tue 15: 9:00am-10:00am | |
1130 | Wed 16: 8:30am-9:30am; 2:00pm-3:00pm; 5:00pm-6:00pm | |
1131 | Thu 17: 11:00am-12:00pm; 6:00pm-7:00pm; 7:00pm-9:00pm | |
1132 | Fri 18: 3:00pm-4:00pm; 5:00pm-6:00pm | |
1133 | Sat 19: 9:00am-10:30am; 7:30pm-10:00pm</code></pre> | |
1134 | <p>This would be the corresponding list of free periods:</p> | |
1135 | <pre><code>Free periods in Week 16: Apr 14 - 20, 2014 | |
1136 | ------------------------------------------ | |
1137 | Mon 14: 8:00am-10:15am; 11:15am-11:45am; 1:15pm-4:45pm | |
1138 | Tue 15: 8:00am-8:45am; 10:15am-5:00pm | |
1139 | Wed 16: 9:45am-1:45pm; 3:15pm-4:45pm | |
1140 | Thu 17: 8:00am-10:45am; 12:15pm-5:00pm | |
1141 | Fri 18: 8:00am-2:45pm; 4:15pm-4:45pm | |
1142 | Sat 19: 8:00am-8:45am; 10:45am-5:00pm | |
1143 | Sun 20: 8:00am-5:00pm | |
1144 | ---------------------------------------- | |
1145 | Only periods of at least 30 minutes are displayed.</code></pre> | |
1146 | <p>When displaying free times in week view you will be prompted for the shortest period to display using the setting for <em>minimum</em> as the default.</p> | |
1147 | <p>Tip: Need to tell someone when you're free in a given week? Jump to that week in week view, press <em>Ctrl-F</em>, set the minimum period and then copy and paste the resulting list into an email.</p> | |
1148 | <h3 id="icalendar-settings"><a href="#icalendar-settings">iCalendar settings</a></h3> | |
1149 | <h4 id="icscal_file"><a href="#icscal_file">icscal_file</a></h4> | |
1150 | <p>If an item is not selected, pressing Shift-X in the gui will export the active calendars in iCalendar format to this file.</p> | |
1151 | <pre><code>icscal_file: ~/.etm/etmcal.ics</code></pre> | |
1152 | <h4 id="icsitem_file"><a href="#icsitem_file">icsitem_file</a></h4> | |
1153 | <p>If an item is selected, pressing Shift-X in the gui will export the selected item in iCalendar format to this file.</p> | |
1154 | <pre><code>icsitem_file: ~/.etm/etmitem.ics</code></pre> | |
1155 | <h4 id="icssync_folder"><a href="#icssync_folder">icssync_folder</a></h4> | |
1156 | <pre><code>icssync_folder: ''</code></pre> | |
1157 | <p>A relative path from <code>etmdata</code> to a folder. If given, files in this folder with the extension <code>.txt</code> and <code>.ics</code> will automatically kept concurrent using export to iCalendar and import from iCalendar. I.e., if the <code>.txt</code> file is more recent than than the <code>.ics</code> then the <code>.txt</code> file will be exported to the <code>.ics</code> file. On the other hand, if the <code>.ics</code> file is more recent then it will be imported to the <code>.txt</code> file. In either case, the contents of the file to be updated will be overwritten with the new content and the last acess/modified times for both will be set to the current time.</p> | |
1158 | <p>Note that the calendar application you use to modify the <code>.ics</code> file will impose restrictions on the subsequent content of the <code>.txt</code> file. E.g., if the <code>.txt</code> file has a note entry, then this note will be exported by etm as a VJOURNAL entry to the <code>.ics</code> file. But VJOURNAL entries are not be recognized by many (most) calendar apps. When importing this file to such an application, the note will be omitted and thus will be missing from the <code>.ics</code> file after the next export from the application. The note will then be missing from the <code>.txt</code> file as well after the next automatic update. Restricting the content to events should be safe with with any calendar application.</p> | |
1159 | <p>Additionally, if an absolute path is entered for <code>current_icsfolder</code>, then ics files corresponding to the entries in <code>calendars</code> will be created in this folder and updated as necessary. If there are no entries in calendars, then a single file, <code>all.ics</code>, will be created in this folder and updated as necessary.</p> | |
1160 | <h4 id="ics_subscriptions"><a href="#ics_subscriptions">ics_subscriptions</a></h4> | |
1161 | <pre><code>ics_subscriptions: []</code></pre> | |
1162 | <p>A list of (URL, path) tuples for automatic updates. The URL is a calendar subscription, e.g., for a Google Calendar subscription the entry might be something like:</p> | |
1163 | <pre><code>ics_subscriptions: | |
1164 | - ['https://www.google.com/calendar/ical/.../basic.ics', 'personal/dag/google.txt'] | |
1165 | </code></pre> | |
1166 | <p>With this entry, pressing Shift-M in the gui would import the calendar from the URL, convert it from ics to etm format and then write the result to <code>personal/google.txt</code> in the etm data directory. Note that this data file should be regarded as read-only since any changes made to it will be lost with the next subscription update.</p> | |
1167 | <h3 id="idle_minutes"><a href="#idle_minutes">idle_minutes</a></h3> | |
1168 | <pre><code>idle_minutes: 10</code></pre> | |
1169 | <p>When the idle timer is running and an action timer is started or restarted, only open the dialog to assign idle time if the current idle time is at least this many minutes.</p> | |
1170 | <h3 id="local_timezone"><a href="#local_timezone">local_timezone</a></h3> | |
1171 | <pre><code>local_timezone: US/Eastern</code></pre> | |
1172 | <p>This timezone will be used as the default when a value for <code>@z</code> is not given in an item.</p> | |
1173 | <h3 id="monthly"><a href="#monthly">monthly</a></h3> | |
1174 | <pre><code>monthly: monthly</code></pre> | |
1175 | <p>Relative path from <code>datadir</code>. With the settings above and for <code>datadir</code> the suggested location for saving new items in, say, October 2012, would be the file:</p> | |
1176 | <pre><code>~/.etm/data/monthly/2012/10.txt</code></pre> | |
1177 | <p>The directories <code>monthly</code> and <code>2012</code> and the file <code>10.txt</code> would, if necessary, be created. The user could either accept this default or choose a different file.</p> | |
1178 | <h3 id="outline_depth"><a href="#outline_depth">outline_depth</a></h3> | |
1179 | <pre><code>outline_depth: 2</code></pre> | |
1180 | <p>The default outline depth to use when opening keyword, note, path or tag view. Once any view is opened, use Ctrl-O to change the depth for that view.</p> | |
1181 | <h3 id="prefix"><a href="#prefix">prefix</a></h3> | |
1182 | <pre><code>prefix: "\n " | |
1183 | prefix_uses: "rj+-tldm"</code></pre> | |
1184 | <p>Apply <code>prefix</code> (whitespace only) to the <span class="citation">@keys</span> in <code>prefix_uses</code> when displaying and saving items. The default would cause the selected elements to begin on a newline and indented by two spaces. E.g.,</p> | |
1185 | <pre><code>+ summary @s 2014-05-09 12am @z US/Eastern | |
1186 | @m memo | |
1187 | @j job 1 &f 20140510T1411;20140509T0000 &q 1 | |
1188 | @j job 2 &f 20140510T1412;20140509T0000 &q 2 | |
1189 | @j job 3 &q 3 | |
1190 | @d description</code></pre> | |
1191 | <h3 id="report"><a href="#report">report</a></h3> | |
1192 | <pre><code>report_begin: '1' | |
1193 | report_end: '+1/1' | |
1194 | report_colors: 2 | |
1195 | report_width1: 61 | |
1196 | report_width2: 19</code></pre> | |
1197 | <p>Report begin and end are fuzzy parsed dates specifying the default period for reports that group by dates. Each line in the file specified by <code>report_specifications</code> provides a possible specification for a report. E.g.</p> | |
1198 | <pre><code>a MMM yyyy; k[0]; k[1:] -b -1/1 -e 1 | |
1199 | a k, MMM yyyy -b -1/1 -e 1 | |
1200 | c ddd MMM d yyyy | |
1201 | c f</code></pre> | |
1202 | <p>In custom view these appear in the report specifications pop-up list. A specification from the list can be selected and, perhaps, modified or an entirely new specification can be entered. See <a href="#reports">Reports</a> for details. See also the <a href="#agenda">agenda</a> settings above.</p> | |
1203 | <h3 id="retain_ids"><a href="#retain_ids">retain_ids</a></h3> | |
1204 | <pre><code>retain_ids: false</code></pre> | |
1205 | <p>If true, the unique ids that etm associates with items will be written to the data files and retained between sessions. If false, new ids will be generated for each session.</p> | |
1206 | <p>Retaining ids enables etm to avoid duplicates when importing and exporting iCalendar files.</p> | |
1207 | <h3 id="show_finished"><a href="#show_finished">show_finished</a></h3> | |
1208 | <pre><code>show_finished: 1</code></pre> | |
1209 | <p>Show this many of the most recent completions of repeated tasks or, if 0, show all completions.</p> | |
1210 | <h3 id="smtp"><a href="#smtp">smtp</a></h3> | |
1211 | <pre><code>smtp_from: dnlgrhm@gmail.com | |
1212 | smtp_id: dnlgrhm | |
1213 | smtp_pw: ********** | |
1214 | smtp_server: smtp.gmail.com</code></pre> | |
1215 | <p>Required settings for the smtp server to be used for email alerts.</p> | |
1216 | <h3 id="sms"><a href="#sms">sms</a></h3> | |
1217 | <pre><code>sms_message: '!summary!' | |
1218 | sms_subject: '!time_span!' | |
1219 | sms_from: dnlgrhm@gmail.com | |
1220 | sms_pw: ********** | |
1221 | sms_phone: 0123456789@vtext.com | |
1222 | sms_server: smtp.gmail.com:587</code></pre> | |
1223 | <p>Required settings for text messaging in alerts. Enter the 10-digit area code and number and mms extension for the mobile phone to receive the text message when no numbers are specified in the alert. The illustrated phone number is for Verizon. Here are the mms extensions for the major carriers:</p> | |
1224 | <pre><code>Alltel @message.alltel.com | |
1225 | AT&T @txt.att.net | |
1226 | Nextel @messaging.nextel.com | |
1227 | Sprint @messaging.sprintpcs.com | |
1228 | SunCom @tms.suncom.com | |
1229 | T-mobile @tmomail.net | |
1230 | VoiceStream @voicestream.net | |
1231 | Verizon @vtext.com</code></pre> | |
1232 | <h3 id="sundayfirst"><a href="#sundayfirst">sundayfirst</a></h3> | |
1233 | <pre><code>sundayfirst: false</code></pre> | |
1234 | <p>The setting affects only the twelve month calendar display.</p> | |
1235 | <h3 id="update_minutes"><a href="#update_minutes">update_minutes</a></h3> | |
1236 | <pre><code>update_minutes: 15</code></pre> | |
1237 | <p>Update <code>current_html</code>, <code>current_text</code> and the files in <code>icssync_folder</code> when the number of minutes past the hour modulo <code>update_minutes</code> is equal to zero. I.e. with the default, the update would occur on the hour and at 15, 30 and 45 minutes past the hour. Acceptable settings are integers between 1 and 59. Note that with a setting greater than or equal to 30, the update will occur only twice each hour.</p> | |
1238 | <h3 id="vcs_settings"><a href="#vcs_settings">vcs_settings</a></h3> | |
1239 | <pre><code>vcs_settings: | |
1240 | command: '' | |
1241 | commit: '' | |
1242 | dir: '' | |
1243 | file: '' | |
1244 | history: '' | |
1245 | init: '' | |
1246 | limit: ''</code></pre> | |
1247 | <p>These settings are ignored unless the setting for <code>vcs_system</code> below is either <code>git</code> or <code>mercurial</code>.</p> | |
1248 | <p>Default values will be provided for these settings based on the choice of <code>vcs_system</code> below. Any of the settings that you define here will overrule the defaults.</p> | |
1249 | <p>Here, for example, are the default values of these settings for git under OS X:</p> | |
1250 | <pre><code>vcs_settings: | |
1251 | command: '/usr/bin/git --git-dir {repo} --work-dir {work}' | |
1252 | commit: '/usr/bin/git --git-dir {repo} --work-dir {work} add */\*.txt | |
1253 | && /usr/bin/git --git-dir {repo} --work-dir {work} commit -a -m "{mesg}"' | |
1254 | dir: '.git' | |
1255 | file: '' | |
1256 | history: '/usr/bin/git -git-dir {repo} --work-dir {work} log | |
1257 | --pretty=format:"- %ar: %an%n%w(70,0,4)%s" -U1 {numchanges} | |
1258 | {file}' | |
1259 | init: '/usr/bin/git init {work}; /usr/bin/git -git-dir {repo} | |
1260 | --work-dir {work} add */\*.txt; /usr/bin/git-git-dir {repo} | |
1261 | --work-dir {work} commit -a -m "{mesg}"' | |
1262 | limit: '-n'</code></pre> | |
1263 | <p>In these settings, <code>{mesg}</code> will be replaced with an internally generated commit message, <code>{numchanges}</code> with an expression that depends upon <code>limit</code> that determines how many changes to show and, when a file is selected, <code>{file}</code> with the corresponding path. If <code>~/.etm/data</code> is your etm datadir, the <code>{repo}</code> would be replaced with <code>~/.etm/data/.git</code> and {work} with <code>~/.etm/data</code>.</p> | |
1264 | <p>Leave these settings empty to use the defaults.</p> | |
1265 | <h3 id="vcs_system"><a href="#vcs_system">vcs_system</a></h3> | |
1266 | <pre><code>vcs_system: ''</code></pre> | |
1267 | <p>This setting must be either <code>''</code> or <code>git</code> or <code>mercurial</code>.</p> | |
1268 | <p>If you specify either git or mercurial here (and have it installed on your system), then etm will automatically commit any changes you make to any of your data files. The history of these changes is available in the GUI with the show changes command (<em>Ctrl-H</em>) and you can, of course, use any git or mercurial commands in your terminal to, for example, restore a previous version of a file.</p> | |
1269 | <h3 id="weeks_after"><a href="#weeks_after">weeks_after</a></h3> | |
1270 | <pre><code>weeks_after: 52</code></pre> | |
1271 | <p>In the day view, all non-repeating, dated items are shown. Additionally all repetitions of repeating items with a finite number of repetitions are shown. This includes 'list-only' repeating items and items with <code>&u</code> (until) or <code>&t</code> (total number of repetitions) entries. For repeating items with an infinite number of repetitions, those repetitions that occur within the first <code>weeks_after</code> weeks after the current week are displayed along with the first repetition after this interval. This assures that for infrequently repeating items such as voting for president, at least one repetition will be displayed.</p> | |
1272 | <h3 id="yearfirst"><a href="#yearfirst">yearfirst</a></h3> | |
1273 | <pre><code>yearfirst: true</code></pre> | |
1274 | <p>If yearfirst is true, the YY-MM-DD format will have precedence over MM-DD-YY in an ambiguous date. See also <code>dayfirst</code>.</p> | |
1275 | <h1 id="reports"><a href="#reports">Reports</a></h1> | |
1276 | <p>To create a report open the custom view in the GUI. If you have entries in your report specifications file, <code>~./etm/reports.cfg</code> by default, you can choose one of them in the selection box at the bottom of the window.</p> | |
1277 | <p>You can also add report specifications to the list by selecting any item from the list and then replacing the content with anything you like. Press <em>Return</em> to <em>add</em> your specification temporarily to the list. <em>Note that the original entry will not be affected.</em> When you leave the custom view you will have an opportunity to save the additions you have made. If you choose a file, your additions will be inserted into the list and it will be opened for editing.</p> | |
1278 | <p>When you have selected a report specification, press <em>Return</em> to generate the report and display it.</p> | |
1279 | <p>A <em>report specification</em> is created by entering a report <em>type character</em>, either "a" or "c", followed by a <em>groupby setting</em> and, perhaps, by one or more <em>report options</em>:</p> | |
1280 | <pre><code><a|c> <groupby setting> [options]</code></pre> | |
1281 | <p>Together, the type character, groupby setting and options determine which items will appear in the report and how they will be organized and displayed.</p> | |
1282 | <h2 id="report-type-characters"><a href="#report-type-characters">Report type characters</a></h2> | |
1283 | <ul> | |
1284 | <li><p><strong>a</strong>: action report.</p> | |
1285 | <p>A report of expenditures of time and money recorded in <em>actions</em> with output formatted using <code>action_template</code> computations and expansions. See <a href="#preferences">Preferences</a> for further details about the role of <code>action_template</code> in formatting action report output.</p></li> | |
1286 | <li><p><strong>c</strong>: composite report.</p> | |
1287 | <p>Any item types, including actions, but without <code>action_template</code> computations and expansions. Note that only unfinished tasks and unfinished instances of repeating tasks will be displayed.</p></li> | |
1288 | </ul> | |
1289 | <h2 id="groupby-setting"><a href="#groupby-setting">Groupby setting</a></h2> | |
1290 | <p>A semicolon separated list that determines how items will be grouped and sorted. Possible elements include <em>date specifications</em> and elements from</p> | |
1291 | <ul> | |
1292 | <li><p>c: context</p></li> | |
1293 | <li><p>f: file path</p></li> | |
1294 | <li><p>k: keyword</p></li> | |
1295 | <li><p>t: tag</p></li> | |
1296 | <li><p>u: user</p></li> | |
1297 | </ul> | |
1298 | <p>A <em>date specification</em> is either</p> | |
1299 | <ul> | |
1300 | <li>w: week number</li> | |
1301 | </ul> | |
1302 | <p>or a combination of one or more of the following:</p> | |
1303 | <ul> | |
1304 | <li><p>yy: 2-digit year</p></li> | |
1305 | <li><p>yyyy: 4-digit year</p></li> | |
1306 | <li><p>MM: month: 01 - 12</p></li> | |
1307 | <li><p>MMM: locale specific abbreviated month name: Jan - Dec</p></li> | |
1308 | <li><p>MMMM: locale specific month name: January - December</p></li> | |
1309 | <li><p>dd: month day: 01 - 31</p></li> | |
1310 | <li><p>ddd: locale specific abbreviated week day: Mon - Sun</p></li> | |
1311 | <li><p>dddd: locale specific week day: Monday - Sunday</p></li> | |
1312 | </ul> | |
1313 | <p>For example, the report specification <code>c ddd, MMM dd yyyy</code> would group by year, month and day together to give output such as</p> | |
1314 | <pre><code>Fri, Apr 1 2011 | |
1315 | items for April 1 | |
1316 | Sat, Apr 2 2011 | |
1317 | items for April 2 | |
1318 | ...</code></pre> | |
1319 | <p>On the other hand, the report specificaton <code>a w; u; k[0]; k[1:]</code> would group by week number, user and keywords to give output such as</p> | |
1320 | <pre><code>13.1) 2014 Week 14: Mar 31 - Apr 6 | |
1321 | 6.3) agent 1 | |
1322 | 1.3) client 1 | |
1323 | 1.3) project 2 | |
1324 | 1.3) Activity (12) | |
1325 | 5) client 2 | |
1326 | 4.5) project 1 | |
1327 | 4.5) Activity (21) | |
1328 | 0.5) project 2 | |
1329 | 0.5) Activity (22) | |
1330 | 6.8) agent 2 | |
1331 | 2.2) client 1 | |
1332 | 2.2) project 2 | |
1333 | 2.2) Activity (13) | |
1334 | 4.6) client 2 | |
1335 | 3.9) project 1 | |
1336 | 3.9) Activity (23) | |
1337 | 0.7) project 2 | |
1338 | 0.7) Activity (23)</code></pre> | |
1339 | <p>With the heirarchial elements, file path and keyword, it is possible to use parts of the element as well as the whole. Consider, for example, the file path <code>A/B/C</code> with the components <code>[A, B, C]</code>. Then for this file path:</p> | |
1340 | <pre><code>f[0] = A | |
1341 | f[:2] = A/B | |
1342 | f[2:] = C | |
1343 | f = A/B/C</code></pre> | |
1344 | <p>Suppose that keywords have the format <code>client:project</code>. Then grouping by year and month, then client and finally project to give output such as</p> | |
1345 | <pre><code>report: a MMM yyyy; u; k[0]; k[1] -b 1 -e +1/1 | |
1346 | ||
1347 | 13.1) Feb 2014 | |
1348 | 6.3) agent 1 | |
1349 | 1.3) client 1 | |
1350 | 1.3) project 2 | |
1351 | 1.3) Activity 12 | |
1352 | 5) client 2 | |
1353 | 4.5) project 1 | |
1354 | 4.5) Activity 21 | |
1355 | 0.5) project 2 | |
1356 | 0.5) Activity 22 | |
1357 | 6.8) agent 2 | |
1358 | 2.2) client 1 | |
1359 | 2.2) project 2 | |
1360 | 2.2) Activity 13 | |
1361 | 4.6) client 2 | |
1362 | 3.9) project 1 | |
1363 | 3.9) Activity 23 | |
1364 | 0.7) project 2 | |
1365 | 0.7) Activity 23</code></pre> | |
1366 | <p>Items that are missing an element specified in <code>groupby</code> will be omitted from the output. E.g., undated tasks and notes will be omitted when a date specification is included, items without keywords will be omitted when <code>k</code> is included and so forth.</p> | |
1367 | <p>When a date specification is not included in the groupby setting, undated notes and tasks will be potentially included, but only those instances of dated items that correspond to the <em>relevant datetime</em> of the item of the item will be included, where the <em>relevant datetime</em> is the past due date for any past due tasks, the starting datetime for any non-repeating item and the datetime of the next instance for any repeating item.</p> | |
1368 | <p>Within groups, items are automatically sorted by date, type and time.</p> | |
1369 | <h2 id="options-1"><a href="#options-1">Options</a></h2> | |
1370 | <p>Report options are listed below. Report types <code>c</code> supports all options except <code>-d</code>. Report type <code>a</code> supports all options except <code>-o</code> and <code>-h</code>.</p> | |
1371 | <h3 id="b-begin_date"><a href="#b-begin_date">-b BEGIN_DATE</a></h3> | |
1372 | <p>Fuzzy parsed date. Limit the display of dated items to those with datetimes falling <em>on or after</em> this datetime. Relative day and month expressions can also be used so that, for example, <code>-b -14</code> would begin 14 days before the current date and <code>-b -1/1</code> would begin on the first day of the previous month. It is also possible to add (or subtract) a time period from the fuzzy date, e.g., <code>-b mon + 7d</code> would begin with the second Monday falling on or after today. Default: None.</p> | |
1373 | <h3 id="c-context-1"><a href="#c-context-1">-c CONTEXT</a></h3> | |
1374 | <p>Regular expression. Limit the display to items with contexts matching CONTEXT (ignoring case). Prepend an exclamation mark, i.e., use !CONTEXT rather than CONTEXT, to limit the display to items which do NOT have contexts matching CONTEXT.</p> | |
1375 | <h3 id="d-depth"><a href="#d-depth">-d DEPTH</a></h3> | |
1376 | <p>CLI only. In the GUI use <em>View/Set outline depth</em>. The default, <code>-d 0</code>, includes all outline levels. Use <code>-d 1</code> to include only level 1, <code>-d 2</code> to include levels 1 and 2 and so forth. This setting applies to the CLI only. In the GUI use the command <em>set outline depth</em>.</p> | |
1377 | <p>For example, modifying the report above by adding <code>-d 3</code> would give the following:</p> | |
1378 | <pre><code>report: a MMM yyyy; u; k[0]; k[1] -b 1 -e +1/1 -d 3 | |
1379 | ||
1380 | 13.1) Feb 2014 | |
1381 | 6.3) agent 1 | |
1382 | 1.3) client 1 | |
1383 | 5) client 2 | |
1384 | 6.8) agent 2 | |
1385 | 2.2) client 1 | |
1386 | 4.6) client 2</code></pre> | |
1387 | <h3 id="e-end_date"><a href="#e-end_date">-e END_DATE</a></h3> | |
1388 | <p>Fuzzy parsed date. Limit the display of dated items to those with datetimes falling <em>before</em> this datetime. As with BEGIN_DATE relative month expressions can be used so that, for example, <code>-b -1/1 -e 1</code> would include all items from the previous month. As with <code>-b</code>, period strings can be appended, e.g., <code>-b mon -e mon + 7d</code> would include items from the week that begins with the first Monday falling on or after today. Default: None.</p> | |
1389 | <h3 id="f-file"><a href="#f-file">-f FILE</a></h3> | |
1390 | <p>Regular expression. Limit the display to items from files whose paths match FILE (ignoring case). Prepend an exclamation mark, i.e., use !FILE rather than FILE, to limit the display to items from files whose path does NOT match FILE.</p> | |
1391 | <h3 id="k-keyword-1"><a href="#k-keyword-1">-k KEYWORD</a></h3> | |
1392 | <p>Regular expression. Limit the display to items with contexts matching KEYWORD (ignoring case). Prepend an exclamation mark, i.e., use !KEYWORD rather than KEYWORD, to limit the display to items which do NOT have keywords matching KEYWORD.</p> | |
1393 | <h3 id="l-location-1"><a href="#l-location-1">-l LOCATION</a></h3> | |
1394 | <p>Regular expression. Limit the display to items with location matching LOCATION (ignoring case). Prepend an exclamation mark, i.e., use !LOCATION rather than LOCATION, to limit the display to items which do NOT have a location that matches LOCATION.</p> | |
1395 | <h3 id="o-omit"><a href="#o-omit">-o OMIT</a></h3> | |
1396 | <p>String. Composite reports only. Show/hide a)ctions, d)elegated tasks, e)vents, g)roup tasks, n)otes, o)ccasions and/or other t)asks. For example, <code>-o on</code> would show everything except occasions and notes and <code>-o !on</code> would show only occasions and notes.</p> | |
1397 | <h3 id="s-summary"><a href="#s-summary">-s SUMMARY</a></h3> | |
1398 | <p>Regular expression. Limit the display to items containing SUMMARY (ignoring case) in the item summary. Prepend an exclamation mark, i.e., use !SUMMARY rather than SUMMARY, to limit the display to items which do NOT contain SUMMARY in the summary.</p> | |
1399 | <h3 id="s-search"><a href="#s-search">-S SEARCH</a></h3> | |
1400 | <p>Regular expression. Composite reports only. Limit the display to items containing SEARCH (ignoring case) anywhere in the item or its file path. Prepend an exclamation mark, i.e., use !SEARCH rather than SEARCH, to limit the display to items which do NOT contain SEARCH in the item or its file path.</p> | |
1401 | <h3 id="t-tags-1"><a href="#t-tags-1">-t TAGS</a></h3> | |
1402 | <p>Comma separated list of case insensitive regular expressions. E.g., use</p> | |
1403 | <pre><code>-t tag1, !tag2</code></pre> | |
1404 | <p>or</p> | |
1405 | <pre><code>-t tag1, -t !tag2</code></pre> | |
1406 | <p>to display items with one or more tags that match 'tag1' but none that match 'tag2'.</p> | |
1407 | <h3 id="u-user-1"><a href="#u-user-1">-u USER</a></h3> | |
1408 | <p>Regular expression. Limit the display to items with user matching USER (ignoring case). Prepend an exclamation mark, i.e., use !USER rather than USER, to limit the display to items which do NOT have a user that matches USER.</p> | |
1409 | <h1 id="shortcuts"><a href="#shortcuts">Shortcuts</a></h1> | |
1410 | <h2 id="menubar"><a href="#menubar">Menubar</a></h2> | |
1411 | <pre><code>File | |
1412 | New | |
1413 | Item N | |
1414 | File Shift-N | |
1415 | Begin/Pause Action Timer T | |
1416 | Finish Action Timer Shift-T | |
1417 | Start/Resolve Idle Timer I | |
1418 | Stop Idle Timer Shift-I | |
1419 | Open | |
1420 | Data file ... Shift-F | |
1421 | Configuration file ... Shift-C | |
1422 | preferences Shift-P | |
1423 | scratchpad Shift-S | |
1424 | ---- | |
1425 | Quit Ctrl-Q | |
1426 | View | |
1427 | Home Space | |
1428 | Show remaining alerts for today A | |
1429 | Jump to date J | |
1430 | ---- | |
1431 | Next sibling Control-Down | |
1432 | Previous sibling Control-Up | |
1433 | Set outline filter Ctrl-F | |
1434 | Clear outline filter Shift-Ctrl-F | |
1435 | Toggle displaying labels column L | |
1436 | Set outline depth O | |
1437 | Show outline as text S | |
1438 | Print outline P | |
1439 | Toggle displaying finished X | |
1440 | ---- | |
1441 | Previous week/month Left | |
1442 | Next week/month Right | |
1443 | Previous item/day in week/month Up | |
1444 | Next item/day in week/month Down | |
1445 | List busy times in week/month B | |
1446 | List free times in week/month F | |
1447 | Item | |
1448 | Copy C | |
1449 | Delete BackSpace | |
1450 | Edit E | |
1451 | Edit file Shift-E | |
1452 | Finish F | |
1453 | Move M | |
1454 | Reschedule R | |
1455 | Schedule new Shift-R | |
1456 | Open link G | |
1457 | Show user details U | |
1458 | Tools | |
1459 | Date and time calculator Shift-D | |
1460 | Available dates calculator Shift-A | |
1461 | Yearly calendar Shift-Y | |
1462 | History of changes Shift-H | |
1463 | Export to iCal Shift-X | |
1464 | Update calendar subscriptions Shift-M | |
1465 | Reload data from files Shift-L | |
1466 | Custom | |
1467 | Create and display selected report Return | |
1468 | Export report in text format ... Ctrl-T | |
1469 | Export report in csv format ... Ctrl-X | |
1470 | Save changes to report specifications Ctrl-W | |
1471 | Expand report list Down | |
1472 | Help | |
1473 | Search | |
1474 | Shortcuts ? | |
1475 | User manual F1 | |
1476 | About F2 | |
1477 | Check for update F3 </code></pre> | |
1478 | <h2 id="main"><a href="#main">Main</a></h2> | |
1479 | <pre><code>Views | |
1480 | Agenda Ctrl-A | |
1481 | ---- | |
1482 | Day Ctrl-D | |
1483 | Week Ctrl-W | |
1484 | Month Ctrl-M | |
1485 | ---- | |
1486 | Tag Ctrl-T | |
1487 | Keyword Ctrl-K | |
1488 | Path Ctrl-P | |
1489 | ---- | |
1490 | Note Ctrl-N | |
1491 | Custom Ctrl-C </code></pre> | |
1492 | <h2 id="edit"><a href="#edit">Edit</a></h2> | |
1493 | <pre><code>Show completions Ctrl-Space | |
1494 | Cancel Escape | |
1495 | Finish ... Ctrl-W </code></pre> | |
1496 | </body> | |
1497 | </html>⏎ |
0 | # Changes in the 4 weeks preceding Mon Sep 15 12:22:49 EDT 2014: | |
1 | - 2014-09-15 12:22:51 -0400 (HEAD, tag: 3.0.40, master): Dan Graham | |
2 | tagged version 3.0.40 | |
3 | - 2014-09-13 10:31:26 -0400 (origin/master): Dan Graham | |
4 | When importing ics, use UTC if a time zone is not provided. | |
5 | - 2014-09-12 13:42:11 -0400 (tag: 3.0.39): Dan Graham | |
6 | tagged version 3.0.39 | |
7 | - 2014-09-12 13:41:46 -0400: Dan Graham | |
8 | Added unicode to edit.py and view.py. | |
9 | - 2014-09-10 08:44:54 -0400: Dan Graham | |
10 | Put the message window last in the alert sequence to avoid the | |
11 | modal window blocking other alerts. | |
12 | - 2014-09-09 17:35:12 -0400: Dan Graham | |
13 | Updated monthly images | |
14 | - 2014-09-09 09:56:27 -0400 (tag: 3.0.38): Dan Graham | |
15 | tagged version 3.0.38 | |
16 | - 2014-09-09 09:55:54 -0400: Dan Graham | |
17 | Use %X and %x to format non-ascii datetimes for python2. | |
18 | - 2014-09-09 09:19:30 -0400: Dan Graham | |
19 | Fixed bugs with non-integer screen distances in week and month | |
20 | view. | |
21 | - 2014-09-09 09:10:11 -0400: Dan Graham | |
22 | Added early and late warning lines to week view days with events | |
23 | falling either before or after the displayed interval. Fixed bug in | |
24 | displaying events starting after display range in week view. | |
25 | - 2014-09-09 07:20:37 -0400 (monthbars): Dan Graham | |
26 | Replaced day coloring in month view with an "active times" border. | |
27 | Added encode to subprocess call in alert_displaycmd. | |
28 | - 2014-09-08 09:15:10 -0400 (origin/monthbars): Dan Graham | |
29 | Test work on showing event details in month view. |
0 | #!/usr/bin/env python3 | |
1 | # -*- coding: utf-8 -*- | |
2 | from __future__ import (absolute_import, division, print_function, | |
3 | unicode_literals) | |
4 | import os | |
5 | import os.path | |
6 | # import pwd | |
7 | from copy import deepcopy | |
8 | from textwrap import wrap | |
9 | import platform | |
10 | ||
11 | import logging | |
12 | import logging.config | |
13 | logger = logging.getLogger() | |
14 | ||
15 | ||
16 | def setup_logging(level, etmdir=None): | |
17 | """ | |
18 | Setup logging configuration. Override root:level in | |
19 | logging.yaml with default_level. | |
20 | """ | |
21 | if etmdir: | |
22 | etmdir = os.path.normpath(etmdir) | |
23 | else: | |
24 | etmdir = os.path.normpath(os.path.join(os.path.expanduser("~/.etm"))) | |
25 | log_levels = { | |
26 | '1': logging.DEBUG, | |
27 | '2': logging.INFO, | |
28 | '3': logging.WARN, | |
29 | '4': logging.ERROR, | |
30 | '5': logging.CRITICAL | |
31 | } | |
32 | ||
33 | if level in log_levels: | |
34 | loglevel = log_levels[level] | |
35 | else: | |
36 | loglevel = log_levels['3'] | |
37 | ||
38 | if os.path.isdir(etmdir): | |
39 | logfile = os.path.normpath(os.path.abspath(os.path.join(etmdir, "etmtk_log.txt"))) | |
40 | if not os.path.isfile(logfile): | |
41 | open(logfile, 'a').close() | |
42 | ||
43 | config = {'disable_existing_loggers': False, | |
44 | 'formatters': {'simple': { | |
45 | 'format': '--- %(asctime)s - %(levelname)s - %(module)s.%(funcName)s\n %(message)s'}}, | |
46 | 'handlers': {'console': {'class': 'logging.StreamHandler', | |
47 | 'formatter': 'simple', | |
48 | 'level': loglevel, | |
49 | 'stream': 'ext://sys.stdout'}, | |
50 | 'file': {'backupCount': 5, | |
51 | 'class': 'logging.handlers.RotatingFileHandler', | |
52 | 'encoding': 'utf8', | |
53 | 'filename': logfile, | |
54 | 'formatter': 'simple', | |
55 | 'level': 'WARN', | |
56 | 'maxBytes': 1048576}}, | |
57 | 'loggers': {'etmtk': {'handlers': ['console'], | |
58 | 'level': 'DEBUG', | |
59 | 'propagate': False}}, | |
60 | 'root': {'handlers': ['console', 'file'], 'level': 'DEBUG'}, | |
61 | 'version': 1} | |
62 | logging.config.dictConfig(config) | |
63 | logger.info('logging at level: {0}\n writing exceptions to: {1}'.format(loglevel, logfile)) | |
64 | else: # no etmdir - first use | |
65 | config = {'disable_existing_loggers': False, | |
66 | 'formatters': {'simple': { | |
67 | 'format': '--- %(asctime)s - %(levelname)s - %(module)s.%(funcName)s\n %(message)s'}}, | |
68 | 'handlers': {'console': {'class': 'logging.StreamHandler', | |
69 | 'formatter': 'simple', | |
70 | 'level': loglevel, | |
71 | 'stream': 'ext://sys.stdout'}}, | |
72 | 'loggers': {'etmtk': {'handlers': ['console'], | |
73 | 'level': 'DEBUG', | |
74 | 'propagate': False}}, | |
75 | 'root': {'handlers': ['console'], 'level': 'DEBUG'}, | |
76 | 'version': 1} | |
77 | logging.config.dictConfig(config) | |
78 | logger.info('logging at level: {0}'.format(loglevel)) | |
79 | ||
80 | import subprocess | |
81 | ||
82 | import gettext | |
83 | ||
84 | if platform.python_version() >= '3': | |
85 | python_version = 3 | |
86 | python_version2 = False | |
87 | from io import StringIO | |
88 | from gettext import gettext as _ | |
89 | unicode = str | |
90 | u = lambda x: x | |
91 | raw_input = input | |
92 | else: | |
93 | python_version = 2 | |
94 | python_version2 = True | |
95 | from cStringIO import StringIO | |
96 | _ = gettext.lgettext | |
97 | ||
98 | ||
99 | from random import random | |
100 | from math import log | |
101 | ||
102 | ||
103 | class Node(object): | |
104 | __slots__ = 'value', 'next', 'width' | |
105 | ||
106 | def __init__(self, value, next, width): | |
107 | self.value, self.next, self.width = value, next, width | |
108 | ||
109 | ||
110 | class End(object): | |
111 | """ | |
112 | Sentinel object that always compares greater than another object. | |
113 | Replaced __cmp__ to work with python3.x | |
114 | """ | |
115 | ||
116 | def __eq__(self, other): | |
117 | return 0 | |
118 | ||
119 | def __ne__(self, other): | |
120 | return 1 | |
121 | ||
122 | def __gt__(self, other): | |
123 | return 1 | |
124 | ||
125 | def __ge__(self, other): | |
126 | return 1 | |
127 | ||
128 | def __le__(self, other): | |
129 | return 0 | |
130 | ||
131 | def __lt__(self, other): | |
132 | return 0 | |
133 | ||
134 | # Singleton terminator node | |
135 | NIL = Node(End(), [], []) | |
136 | ||
137 | # default for items without a tag or keyword entry | |
138 | NONE = '~~~' | |
139 | ||
140 | ||
141 | class IndexableSkiplist: | |
142 | """Sorted collection supporting O(lg n) insertion, removal, and lookup by rank.""" | |
143 | ||
144 | def __init__(self, expected_size=100, type=""): | |
145 | self.size = 0 | |
146 | self.type = type | |
147 | self.maxlevels = int(1 + log(expected_size, 2)) | |
148 | self.head = Node('HEAD', [NIL] * self.maxlevels, [1] * self.maxlevels) | |
149 | ||
150 | def __len__(self): | |
151 | return self.size | |
152 | ||
153 | def __getitem__(self, i): | |
154 | node = self.head | |
155 | i += 1 | |
156 | for level in reversed(range(self.maxlevels)): | |
157 | while node.width[level] <= i: | |
158 | i -= node.width[level] | |
159 | node = node.next[level] | |
160 | return node.value | |
161 | ||
162 | def insert(self, value): | |
163 | # find first node on each level where node.next[levels].value > value | |
164 | chain = [None] * self.maxlevels | |
165 | steps_at_level = [0] * self.maxlevels | |
166 | node = self.head | |
167 | for level in reversed(range(self.maxlevels)): | |
168 | try: | |
169 | while node.next[level].value <= value: | |
170 | steps_at_level[level] += node.width[level] | |
171 | node = node.next[level] | |
172 | chain[level] = node | |
173 | except: | |
174 | logger.exception('Error comparing {0}:\n {1}\n with the value to be inserted\n {2}'.format(self.type, node.next[level].value, value)) | |
175 | return | |
176 | ||
177 | # insert a link to the newnode at each level | |
178 | d = min(self.maxlevels, 1 - int(log(random(), 2.0))) | |
179 | newnode = Node(value, [None] * d, [None] * d) | |
180 | steps = 0 | |
181 | for level in range(d): | |
182 | prevnode = chain[level] | |
183 | newnode.next[level] = prevnode.next[level] | |
184 | prevnode.next[level] = newnode | |
185 | newnode.width[level] = prevnode.width[level] - steps | |
186 | prevnode.width[level] = steps + 1 | |
187 | steps += steps_at_level[level] | |
188 | for level in range(d, self.maxlevels): | |
189 | chain[level].width[level] += 1 | |
190 | self.size += 1 | |
191 | ||
192 | def remove(self, value): | |
193 | # find first node on each level where node.next[levels].value >= value | |
194 | chain = [None] * self.maxlevels | |
195 | node = self.head | |
196 | for level in reversed(range(self.maxlevels)): | |
197 | try: | |
198 | while node.next[level].value < value: | |
199 | node = node.next[level] | |
200 | chain[level] = node | |
201 | except: | |
202 | logger.exception('Error comparing {0}:\n {1}\n with the value to be removed\n {2}'.format(self.type, node.next[level].value, value)) | |
203 | if value != chain[0].next[0].value: | |
204 | raise KeyError('Not Found') | |
205 | ||
206 | # remove one link at each level | |
207 | d = len(chain[0].next[0].next) | |
208 | for level in range(d): | |
209 | prevnode = chain[level] | |
210 | prevnode.width[level] += prevnode.next[level].width[level] - 1 | |
211 | prevnode.next[level] = prevnode.next[level].next[level] | |
212 | for level in range(d, self.maxlevels): | |
213 | chain[level].width[level] -= 1 | |
214 | self.size -= 1 | |
215 | ||
216 | def __iter__(self): | |
217 | 'Iterate over values in sorted order' | |
218 | node = self.head.next[0] | |
219 | while node is not NIL: | |
220 | yield node.value | |
221 | node = node.next[0] | |
222 | ||
223 | # initial instances | |
224 | ||
225 | itemsSL = IndexableSkiplist(5000, "items") | |
226 | alertsSL = IndexableSkiplist(100, "alerts") | |
227 | datetimesSL = IndexableSkiplist(1000, "datetimes") | |
228 | datesSL = IndexableSkiplist(1000, "dates") | |
229 | busytimesSL = {} | |
230 | occasionsSL = {} | |
231 | items = [] | |
232 | alerts = [] | |
233 | datetimes = [] | |
234 | busytimes = {} | |
235 | occasions = {} | |
236 | file2uuids = {} | |
237 | uuid2hash = {} | |
238 | file2data = {} | |
239 | ||
240 | name2list = { | |
241 | "items": items, | |
242 | "alerts": alerts, | |
243 | "datetimes": datetimes | |
244 | } | |
245 | name2SL = { | |
246 | "items": itemsSL, | |
247 | "alerts": alertsSL, | |
248 | "datetimes": datetimesSL | |
249 | } | |
250 | ||
251 | ||
252 | def clear_all_data(): | |
253 | global itemsSL, alertsSL, datetimesSL, datesSL, busytimesSL, occasionsSL, items, alerts, datetimes, busytimes, occasions, file2uuids, uuid2hash, file2data, name2list, name2SL | |
254 | itemsSL = IndexableSkiplist(5000, "items") | |
255 | alertsSL = IndexableSkiplist(100, "alerts") | |
256 | datetimesSL = IndexableSkiplist(1000, "datetimes") | |
257 | datesSL = IndexableSkiplist(1000, "dates") | |
258 | busytimesSL = {} | |
259 | occasionsSL = {} | |
260 | items = [] | |
261 | alerts = [] | |
262 | datetimes = [] | |
263 | busytimes = {} | |
264 | occasions = {} | |
265 | file2uuids = {} | |
266 | uuid2hash = {} | |
267 | file2data = {} | |
268 | ||
269 | name2list = { | |
270 | "items": items, | |
271 | "alerts": alerts, | |
272 | "datetimes": datetimes | |
273 | } | |
274 | name2SL = { | |
275 | "items": itemsSL, | |
276 | "alerts": alertsSL, | |
277 | "datetimes": datetimesSL | |
278 | } | |
279 | ||
280 | ||
281 | dayfirst = False | |
282 | yearfirst = True | |
283 | # bgclr = "#e9e9e9" | |
284 | # BGCOLOR = "#ebebeb" | |
285 | ||
286 | FINISH = _("Finish ...") | |
287 | ||
288 | IGNORE = """\ | |
289 | syntax: glob | |
290 | .* | |
291 | """ | |
292 | ||
293 | from datetime import datetime, timedelta, time | |
294 | from dateutil.tz import (tzlocal, tzutc) | |
295 | from dateutil.easter import easter | |
296 | ||
297 | ||
298 | def get_current_time(): | |
299 | return datetime.now(tzlocal()) | |
300 | ||
301 | # this task will be created for first time users | |
302 | SAMPLE = """\ | |
303 | # Sample entries - edit or delete at your pleasure | |
304 | = @t sample, tasks | |
305 | ? lose weight and exercise more | |
306 | - milk and eggs @c errands | |
307 | - reservation for Saturday dinner @c phone | |
308 | - hair cut @s -1 @r w &i 2 &o r @c errands | |
309 | - put out trash @s 1 @r w &w MO @o s | |
310 | ||
311 | = @t sample, occasions | |
312 | ^ etm's !2009! birthday @s 2010-02-27 @r y @d initial release 2009-02-27 | |
313 | ^ payday @s 1/1 @r m &w MO, TU, WE, TH, FR &m -1, -2, -3 &s -1 @d the last weekday of each month | |
314 | ||
315 | = @t sample, events | |
316 | * sales meeting @s +7 9a @e 1h @a 5 @a 2d: e; who@when.com, what@where.org @u jsmith | |
317 | * stationary bike @s 1 5:30p @e 30 @r d @a 0 | |
318 | * Tête-à-têtes @s 1 3p @e 90 @r w &w fri @l conference room @t meetings | |
319 | * Book club @s -1/1 7pm @e 2h @z US/Eastern @r w &w TH | |
320 | * Tennis @s -1/1 9am @e 1h30m @z US/Eastern @r w &w SA | |
321 | * Dinner @s -1/1 7:30pm @e 2h30m @z US/Eastern @a 1h, 40m: m @u dag @r w &w SA | |
322 | * Appt with Dr Burns @s 2014-05-15 10am @e 1h @r m &i 9 &w 1TU &t 2 | |
323 | """ | |
324 | ||
325 | HOLIDAYS = """\ | |
326 | ^ Martin Luther King Day @s 2010-01-18 @r y &w 3MO &M 1 | |
327 | ^ Valentine's Day @s 2010-02-14 @r y &M 2 &m 14 | |
328 | ^ President's Day @s 2010-02-15 @c holiday @r y &w 3MO &M 2 | |
329 | ^ Daylight saving time begins @s 2010-03-14 @r y &w 2SU &M 3 | |
330 | ^ St Patrick's Day @s 2010-03-17 @r y &M 3 &m 17 | |
331 | ^ Easter Sunday @s 2010-01-01 @r y &E 0 | |
332 | ^ Mother's Day @s 2010-05-09 @r y &w 2SU &M 5 | |
333 | ^ Memorial Day @s 2010-05-31 @r y &w -1MO &M 5 | |
334 | ^ Father's Day @s 2010-06-20 @r y &w 3SU &M 6 | |
335 | ^ The !1776! Independence Day @s 2010-07-04 @r y &M 7 &m 4 | |
336 | ^ Labor Day @s 2010-09-06 @r y &w 1MO &M 9 | |
337 | ^ Daylight saving time ends @s 2010-11-01 @r y &w 1SU &M 11 | |
338 | ^ Thanksgiving @s 2010-11-26 @r y &w 4TH &M 11 | |
339 | ^ Christmas @s 2010-12-25 @r y &M 12 &m 25 | |
340 | ^ Presidential election day @s 2004-11-01 12am @r y &i 4 &m 2, 3, 4, 5, 6, 7, 8 &M 11 &w TU | |
341 | """ | |
342 | ||
343 | JOIN = "- join the etm discussion group @s +14 @b 10 @c computer @g http://groups.google.com/group/eventandtaskmanager/topics" | |
344 | ||
345 | COMPETIONS = """\ | |
346 | # put completion phrases here, one per line. E.g.: | |
347 | @z US/Eastern | |
348 | @z US/Central | |
349 | @z US/Mountain | |
350 | @z US/Pacific | |
351 | ||
352 | @c errands | |
353 | @c phone | |
354 | @c home | |
355 | @c office | |
356 | ||
357 | # empty lines and lines that begin with '#' are ignored. | |
358 | """ | |
359 | ||
360 | USERS = """\ | |
361 | jsmith: | |
362 | - Smith, John | |
363 | - jsmth@whatever.com | |
364 | - wife Rebecca | |
365 | - children Tom, Dick and Harry | |
366 | """ | |
367 | ||
368 | REPORTS = """\ | |
369 | # put report specifications here, one per line. E.g.: | |
370 | ||
371 | # scheduled items this week: | |
372 | c ddd, MMM dd yyyy -b mon - 7d -e +7 | |
373 | ||
374 | # this and next week: | |
375 | c ddd, MMM dd yyyy -b mon - 7d -e +14 | |
376 | ||
377 | # this month: | |
378 | c ddd, MMM dd yyyy -b 1 -e +1/1 | |
379 | ||
380 | # this and next month: | |
381 | c ddd, MMM dd yyyy -b 1 -e +2/1 | |
382 | ||
383 | # last month's actions: | |
384 | a MMM yyyy; u; k[0]; k[1:] -b -1/1 -e 1 | |
385 | ||
386 | # this month's actions: | |
387 | a MMM yyyy; u; k[0]; k[1:] -b 1 -e +1/1 | |
388 | ||
389 | # this week's actions: | |
390 | a w; u; k[0]; k[1:] -b sun - 6d -e sun | |
391 | ||
392 | # all items by folder: | |
393 | c f | |
394 | ||
395 | # all items by keyword: | |
396 | c k | |
397 | ||
398 | # all items by tag: | |
399 | c t | |
400 | ||
401 | # all items by user: | |
402 | c u | |
403 | ||
404 | # empty lines and lines that begin with '#' are ignored. | |
405 | """ | |
406 | ||
407 | # command line usage | |
408 | USAGE = """\ | |
409 | Usage: | |
410 | ||
411 | etm [logging level] [path] [?] [acmsv] | |
412 | ||
413 | With no arguments, etm will set logging level 3 (warn), use settings from | |
414 | the configuration file ~/.etm/etmtk.cfg, and open the GUI. | |
415 | ||
416 | If the first argument is an integer not less than 1 (debug) and not greater | |
417 | than 5 (critical), then set that logging level and remove the argument. | |
418 | ||
419 | If the first (remaining) argument is the path to a directory that contains | |
420 | a file named etmtk.cfg, then use that configuration file and remove the | |
421 | argument. | |
422 | ||
423 | If the first (remaining) argument is one of the commands listed below, then | |
424 | execute the remaining arguments without opening the GUI. | |
425 | ||
426 | a ARG display the agenda view using ARG, if given, as a filter. | |
427 | c ARGS display a custom view using the remaining arguments as the | |
428 | specification. (Enclose ARGS in single quotes to prevent shell | |
429 | expansion.) | |
430 | d ARG display the day view using ARG, if given, as a filter. | |
431 | k ARG display the keywords view using ARG, if given, as a filter. | |
432 | m INT display a report using the remaining argument, which must be a | |
433 | positive integer, to display a report using the corresponding | |
434 | entry from the file given by report_specifications in etmtk.cfg. | |
435 | Use ? m to display the numbered list of entries from this file. | |
436 | n ARG display the notes view using ARG, if given, as a filter. | |
437 | N ARGS Create a new item using the remaining arguments as the item | |
438 | specification. (Enclose ARGS in single quotes to prevent shell | |
439 | expansion.) | |
440 | p ARG display the path view using ARG, if given, as a filter. | |
441 | t ARG display the tags view using ARG, if given, as a filter. | |
442 | v display information about etm and the operating system. | |
443 | ? ARG display (this) command line help information if ARGS = '' or, | |
444 | if ARGS = X where X is one of the above commands, then display | |
445 | details about command X. 'X ?' is equivalent to '? X'.\ | |
446 | """ | |
447 | ||
448 | import re | |
449 | import sys | |
450 | import locale | |
451 | ||
452 | # term_encoding = locale.getdefaultlocale()[1] | |
453 | term_locale = locale.getdefaultlocale()[0] | |
454 | ||
455 | qt2dt = [ | |
456 | ('a', '%p'), | |
457 | ('dddd', '%A'), | |
458 | ('ddd', '%a'), | |
459 | ('dd', '%d'), | |
460 | ('MMMM', '%B'), | |
461 | ('MMM', '%b'), | |
462 | ('MM', '%m'), | |
463 | ('yyyy', '%Y'), | |
464 | ('yy', '%y'), | |
465 | ('hh', '%H'), | |
466 | ('h', '%I'), | |
467 | ('mm', '%M'), | |
468 | ('w', 'WEEK') | |
469 | ] | |
470 | ||
471 | ||
472 | def commandShortcut(s): | |
473 | """ | |
474 | Produce label, command pairs from s based on Command for OSX | |
475 | and Control otherwise. | |
476 | """ | |
477 | if s.upper() == s and s.lower() != s: | |
478 | shift = "Shift-" | |
479 | else: | |
480 | shift = "" | |
481 | if mac: | |
482 | # return "{0}Cmd-{1}".format(shift, s), "<{0}Command-{1}>".format(shift, s) | |
483 | return "{0}Ctrl-{1}".format(shift, s.upper()), "<{0}Control-{1}>".format(shift, s) | |
484 | else: | |
485 | return "{0}Ctrl-{1}".format(shift, s.upper()), "<{0}Control-{1}>".format(shift, s) | |
486 | ||
487 | ||
488 | def optionShortcut(s): | |
489 | """ | |
490 | Produce label, command pairs from s based on Command for OSX | |
491 | and Control otherwise. | |
492 | """ | |
493 | if s.upper() == s and s.lower() != s: | |
494 | shift = "Shift-" | |
495 | else: | |
496 | shift = "" | |
497 | if mac: | |
498 | return "{0}Alt-{1}".format(shift, s.upper()), "<{0}Option-{1}>".format(shift, s) | |
499 | else: | |
500 | return "{0}Alt-{1}".format(shift, s.upper()), "<{0}Alt-{1}>".format(shift, s) | |
501 | ||
502 | ||
503 | def init_localization(): | |
504 | """prepare l10n""" | |
505 | locale.setlocale(locale.LC_ALL, '') # use user's preferred locale | |
506 | # take first two characters of country code | |
507 | trans = gettext.NullTranslations() | |
508 | trans.install() | |
509 | ||
510 | ||
511 | def d_to_str(d, s): | |
512 | for key, val in qt2dt: | |
513 | s = s.replace(key, val) | |
514 | ret = s2or3(d.strftime(s)) | |
515 | if 'WEEK' in ret: | |
516 | theweek = get_week(d) | |
517 | ret = ret.replace('WEEK', theweek) | |
518 | return ret | |
519 | ||
520 | ||
521 | def dt_to_str(dt, s): | |
522 | for key, val in qt2dt: | |
523 | s = s.replace(key, val) | |
524 | ret = s2or3(dt.strftime(s)) | |
525 | if 'WEEK' in ret: | |
526 | theweek = get_week(dt) | |
527 | ret = ret.replace('WEEK', theweek) | |
528 | return ret | |
529 | ||
530 | ||
531 | def get_week(dt): | |
532 | yn, wn, dn = dt.isocalendar() | |
533 | if dn > 1: | |
534 | days = dn - 1 | |
535 | else: | |
536 | days = 0 | |
537 | weekbeg = dt - days * oneday | |
538 | weekend = dt + (6 - days) * oneday | |
539 | ybeg = weekbeg.year | |
540 | yend = weekend.year | |
541 | mbeg = weekbeg.month | |
542 | mend = weekend.month | |
543 | if mbeg == mend: | |
544 | header = "{0} - {1}".format( | |
545 | fmt_dt(weekbeg, '%b %d'), fmt_dt(weekend, '%d')) | |
546 | elif ybeg == yend: | |
547 | header = "{0} - {1}".format( | |
548 | fmt_dt(weekbeg, '%b %d'), fmt_dt(weekend, '%b %d')) | |
549 | else: | |
550 | header = "{0} - {1}".format( | |
551 | fmt_dt(weekbeg, '%b %d, %Y'), fmt_dt(weekend, '%b %d, %Y')) | |
552 | header = leadingzero.sub('', header) | |
553 | theweek = "{0} {1}: {2}".format(_("{0} Week".format(yn)), "{0:02d}".format(wn), header) | |
554 | return theweek | |
555 | ||
556 | ||
557 | from etmTk.v import version | |
558 | from etmTk.version import version as fullversion | |
559 | ||
560 | last_version = version | |
561 | ||
562 | from re import split as rsplit | |
563 | ||
564 | sys_platform = platform.system() | |
565 | if sys_platform in ('Windows', 'Microsoft'): | |
566 | windoz = True | |
567 | from time import clock as timer | |
568 | else: | |
569 | windoz = False | |
570 | from time import time as timer | |
571 | ||
572 | if sys.platform == 'darwin': | |
573 | mac = True | |
574 | CMD = "Command" | |
575 | else: | |
576 | mac = False | |
577 | CMD = "Control" | |
578 | ||
579 | # used in hack to prevent dialog from hanging under os x | |
580 | if mac: | |
581 | AFTER = 200 | |
582 | else: | |
583 | AFTER = 1 | |
584 | ||
585 | ||
586 | class TimeIt(object): | |
587 | def __init__(self, loglevel=1, label=""): | |
588 | self.loglevel = loglevel | |
589 | self.label = label | |
590 | msg = "{0} timer started".format(self.label) | |
591 | if self.loglevel == 1: | |
592 | logger.debug(msg) | |
593 | elif self.loglevel == 2: | |
594 | logger.info(msg) | |
595 | self.start = timer() | |
596 | ||
597 | def stop(self, *args): | |
598 | self.end = timer() | |
599 | self.secs = self.end - self.start | |
600 | self.msecs = self.secs * 1000 # millisecs | |
601 | msg = "{0} timer stopped; elapsed time: {1} milliseconds".format(self.label, self.msecs) | |
602 | if self.loglevel == 1: | |
603 | logger.debug(msg) | |
604 | elif self.loglevel == 2: | |
605 | logger.info(msg) | |
606 | ||
607 | has_icalendar = False | |
608 | try: | |
609 | from icalendar import Calendar, Event, Todo, Journal | |
610 | from icalendar.caselessdict import CaselessDict | |
611 | from icalendar.prop import vDate, vDatetime | |
612 | has_icalendar = True | |
613 | import pytz | |
614 | except ImportError: | |
615 | if has_icalendar: | |
616 | logger.info('Could not import pytz') | |
617 | else: | |
618 | logger.info('Could not import icalendar and/or pytz') | |
619 | has_icalendar = False | |
620 | ||
621 | from time import sleep | |
622 | import dateutil.rrule as dtR | |
623 | from dateutil.parser import parse as dparse | |
624 | from dateutil import __version__ as dateutil_version | |
625 | # noinspection PyPep8Naming | |
626 | from dateutil.tz import gettz as getTz | |
627 | ||
628 | ||
629 | def memoize(fn): | |
630 | memo = {} | |
631 | ||
632 | def memoizer(*param_tuple, **kwds_dict): | |
633 | if kwds_dict: | |
634 | memoizer.namedargs += 1 | |
635 | return fn(*param_tuple, **kwds_dict) | |
636 | try: | |
637 | memoizer.cacheable += 1 | |
638 | try: | |
639 | return memo[param_tuple] | |
640 | except KeyError: | |
641 | memoizer.misses += 1 | |
642 | memo[param_tuple] = result = fn(*param_tuple) | |
643 | return result | |
644 | except TypeError: | |
645 | memoizer.cacheable -= 1 | |
646 | memoizer.noncacheable += 1 | |
647 | return fn(*param_tuple) | |
648 | ||
649 | memoizer.namedargs = memoizer.cacheable = memoizer.noncacheable = 0 | |
650 | memoizer.misses = 0 | |
651 | return memoizer | |
652 | ||
653 | ||
654 | @memoize | |
655 | def gettz(z=None): | |
656 | return getTz(z) | |
657 | ||
658 | ||
659 | import calendar | |
660 | ||
661 | import yaml | |
662 | from itertools import groupby | |
663 | # from dateutil.rrule import * | |
664 | from dateutil.rrule import (DAILY, rrule) | |
665 | ||
666 | import bisect | |
667 | import uuid | |
668 | import codecs | |
669 | import shutil | |
670 | import fnmatch | |
671 | ||
672 | ||
673 | def s2or3(s): | |
674 | """ | |
675 | ||
676 | :rtype : str | |
677 | """ | |
678 | if python_version == 2: | |
679 | if type(s) is unicode: | |
680 | return s | |
681 | elif type(s) is str: | |
682 | try: | |
683 | return unicode(s, term_encoding) | |
684 | except ValueError: | |
685 | logger.error('s2or3 exception: {0}'.format(s)) | |
686 | else: | |
687 | return s.toUtf8() | |
688 | else: | |
689 | return s | |
690 | ||
691 | ||
692 | def term_print(s): | |
693 | if python_version2: | |
694 | try: | |
695 | print(unicode(s).encode(term_encoding)) | |
696 | except Exception: | |
697 | logger.exception("error printing: '{0}', {1}".format(s, type(s))) | |
698 | else: | |
699 | print(s) | |
700 | ||
701 | parse = None | |
702 | ||
703 | ||
704 | def setup_parse(day_first, year_first): | |
705 | """ | |
706 | ||
707 | :param day_first: bool | |
708 | :param year_first: bool | |
709 | :return: func | |
710 | """ | |
711 | global parse | |
712 | ||
713 | # noinspection PyRedeclaration | |
714 | def parse(s): | |
715 | try: | |
716 | res = dparse(str(s), dayfirst=day_first, yearfirst=year_first) | |
717 | except: | |
718 | logger.exception('Could not parse: {0}, {1}, {2}'.format(s, day_first, year_first)) | |
719 | return False | |
720 | return res | |
721 | ||
722 | ||
723 | try: | |
724 | from os.path import relpath | |
725 | except ImportError: # python < 2.6 | |
726 | from os.path import curdir, abspath, sep, commonprefix, pardir, join | |
727 | ||
728 | def relpath(path, start=curdir): | |
729 | """Return a relative version of a path""" | |
730 | if not path: | |
731 | raise ValueError("no path specified") | |
732 | start_list = abspath(start).split(sep) | |
733 | path_list = abspath(path).split(sep) | |
734 | # Work out how much of the filepath is shared by start and path. | |
735 | i = len(commonprefix([start_list, path_list])) | |
736 | rel_list = [pardir] * (len(start_list) - i) + path_list[i:] | |
737 | if not rel_list: | |
738 | return curdir | |
739 | return join(*rel_list) | |
740 | ||
741 | cwd = os.getcwd() | |
742 | ||
743 | ||
744 | def pathSearch(filename): | |
745 | search_path = os.getenv('PATH').split(os.pathsep) | |
746 | for path in search_path: | |
747 | candidate = os.path.normpath(os.path.join(path, filename)) | |
748 | # logger.debug('checking for: {0}'.format(candidate)) | |
749 | if os.path.isfile(candidate): | |
750 | # return os.path.abspath(candidate) | |
751 | return candidate | |
752 | return '' | |
753 | ||
754 | ||
755 | def getMercurial(): | |
756 | if windoz: | |
757 | hg = pathSearch('hg.exe') | |
758 | else: | |
759 | hg = pathSearch('hg') | |
760 | if hg: | |
761 | logger.debug('found hg: {0}'.format(hg)) | |
762 | base_command = "hg -R {work}" | |
763 | history_command = 'hg log --style compact --template "{desc}\\n" -R {work} -p {numchanges} {file}' | |
764 | commit_command = 'hg commit -q -A -R {work} -m "{mesg}"' | |
765 | init = 'hg init {work}' | |
766 | init_command = "%s && %s" % (init, commit_command) | |
767 | logger.debug('hg base_command: {0}; history_command: {1}; commit_command: {2}; init_command: {3}'.format(base_command, history_command, commit_command, init_command)) | |
768 | else: | |
769 | logger.debug('could not find hg in path') | |
770 | base_command = history_command = commit_command = init_command = '' | |
771 | return base_command, history_command, commit_command, init_command | |
772 | ||
773 | ||
774 | def getGit(): | |
775 | if windoz: | |
776 | git = pathSearch('git.exe') | |
777 | else: | |
778 | git = pathSearch('git') | |
779 | if git: | |
780 | logger.debug('found git: {0}'.format(git)) | |
781 | base_command = "git --git-dir {repo} --work-tree {work}" | |
782 | history_command = "git --git-dir {repo} --work-tree {work} log --pretty=format:'- %ai %an: %s' -U0 {numchanges} {file}" | |
783 | init = 'git init {work}' | |
784 | add = 'git --git-dir {repo} --work-tree {work} add */\*.txt > /dev/null' | |
785 | commit = 'git --git-dir {repo} --work-tree {work} commit -a -m "{mesg}" > /dev/null' | |
786 | commit_command = '%s && %s' % (add, commit) | |
787 | init_command = '%s && %s && %s' % (init, add, commit) | |
788 | logger.debug('git base_command: {0}; history_command: {1}; commit_command: {2}; init_command: {3}'.format(base_command, history_command, commit_command, init_command)) | |
789 | else: | |
790 | logger.debug('could not find git in path') | |
791 | base_command = history_command = commit_command = init_command = '' | |
792 | return base_command, history_command, commit_command, init_command | |
793 | ||
794 | ||
795 | zonelist = [ | |
796 | 'Africa/Cairo', | |
797 | 'Africa/Casablanca', | |
798 | 'Africa/Johannesburg', | |
799 | 'Africa/Mogadishu', | |
800 | 'Africa/Nairobi', | |
801 | 'America/Belize', | |
802 | 'America/Buenos_Aires', | |
803 | 'America/Edmonton', | |
804 | 'America/Mexico_City', | |
805 | 'America/Monterrey', | |
806 | 'America/Montreal', | |
807 | 'America/Toronto', | |
808 | 'America/Vancouver', | |
809 | 'America/Winnipeg', | |
810 | 'Asia/Baghdad', | |
811 | 'Asia/Bahrain', | |
812 | 'Asia/Calcutta', | |
813 | 'Asia/Damascus', | |
814 | 'Asia/Dubai', | |
815 | 'Asia/Hong_Kong', | |
816 | 'Asia/Istanbul', | |
817 | 'Asia/Jakarta', | |
818 | 'Asia/Jerusalem', | |
819 | 'Asia/Katmandu', | |
820 | 'Asia/Kuwait', | |
821 | 'Asia/Macao', | |
822 | 'Asia/Pyongyang', | |
823 | 'Asia/Saigon', | |
824 | 'Asia/Seoul', | |
825 | 'Asia/Shanghai', | |
826 | 'Asia/Singapore', | |
827 | 'Asia/Tehran', | |
828 | 'Asia/Tokyo', | |
829 | 'Asia/Vladivostok', | |
830 | 'Atlantic/Azores', | |
831 | 'Atlantic/Bermuda', | |
832 | 'Atlantic/Reykjavik', | |
833 | 'Australia/Sydney', | |
834 | 'Europe/Amsterdam', | |
835 | 'Europe/Berlin', | |
836 | 'Europe/Lisbon', | |
837 | 'Europe/London', | |
838 | 'Europe/Madrid', | |
839 | 'Europe/Minsk', | |
840 | 'Europe/Monaco', | |
841 | 'Europe/Moscow', | |
842 | 'Europe/Oslo', | |
843 | 'Europe/Paris', | |
844 | 'Europe/Prague', | |
845 | 'Europe/Rome', | |
846 | 'Europe/Stockholm', | |
847 | 'Europe/Vienna', | |
848 | 'Pacific/Auckland', | |
849 | 'Pacific/Fiji', | |
850 | 'Pacific/Samoa', | |
851 | 'Pacific/Tahiti', | |
852 | 'Turkey', | |
853 | 'US/Alaska', | |
854 | 'US/Aleutian', | |
855 | 'US/Arizona', | |
856 | 'US/Central', | |
857 | 'US/East-Indiana', | |
858 | 'US/Eastern', | |
859 | 'US/Hawaii', | |
860 | 'US/Indiana-Starke', | |
861 | 'US/Michigan', | |
862 | 'US/Mountain', | |
863 | 'US/Pacific'] | |
864 | ||
865 | ||
866 | def get_localtz(zones=zonelist): | |
867 | """ | |
868 | ||
869 | :param zones: list of timezone strings | |
870 | :return: timezone string | |
871 | """ | |
872 | linfo = gettz() | |
873 | now = get_current_time() | |
874 | # get the abbreviation for the local timezone, e.g, EDT | |
875 | possible = [] | |
876 | # try the zone list first unless windows system | |
877 | if not windoz: | |
878 | for i in range(len(zones)): | |
879 | z = zones[i] | |
880 | zinfo = gettz(z) | |
881 | if zinfo and zinfo == linfo: | |
882 | possible.append(i) | |
883 | break | |
884 | if not possible: | |
885 | for i in range(len(zones)): | |
886 | z = zones[i] | |
887 | zinfo = gettz(z) | |
888 | if zinfo and zinfo.utcoffset(now) == linfo.utcoffset(now): | |
889 | possible.append(i) | |
890 | if not possible: | |
891 | # the local zone needs to be added to timezones | |
892 | return [''] | |
893 | return [zonelist[i] for i in possible] | |
894 | ||
895 | ||
896 | def calyear(advance=0, options=None): | |
897 | """ | |
898 | """ | |
899 | if not options: | |
900 | options = {} | |
901 | lcl = options['lcl'] | |
902 | if 'sundayfirst' in options and options['sundayfirst']: | |
903 | week_begin = 6 | |
904 | else: | |
905 | week_begin = 0 | |
906 | # hack to set locale on darwin, windoz and linux | |
907 | try: | |
908 | if mac: | |
909 | # locale test | |
910 | c = calendar.LocaleTextCalendar(week_begin, lcl[0]) | |
911 | elif windoz: | |
912 | locale.setlocale(locale.LC_ALL, '') | |
913 | lcl = locale.getlocale() | |
914 | c = calendar.LocaleTextCalendar(week_begin, lcl) | |
915 | else: | |
916 | lcl = locale.getdefaultlocale() | |
917 | c = calendar.LocaleTextCalendar(week_begin, lcl) | |
918 | except: | |
919 | logger.exception('Could not set locale: {0}'.format(lcl)) | |
920 | c = calendar.LocaleTextCalendar(week_begin) | |
921 | cal = [] | |
922 | y = int(today.strftime("%Y")) | |
923 | m = 1 | |
924 | # d = 1 | |
925 | y += advance | |
926 | for i in range(12): | |
927 | cal.append(c.formatmonth(y, m).split('\n')) | |
928 | m += 1 | |
929 | if m > 12: | |
930 | y += 1 | |
931 | m = 1 | |
932 | ret = [] | |
933 | for r in range(0, 12, 3): | |
934 | l = max(len(cal[r]), len(cal[r + 1]), len(cal[r + 2])) | |
935 | for i in range(3): | |
936 | if len(cal[r + i]) < l: | |
937 | for j in range(len(cal[r + i]), l + 1): | |
938 | cal[r + i].append('') | |
939 | for j in range(l): | |
940 | if python_version2: | |
941 | ret.append(s2or3(u' %-20s %-20s %-20s' % (cal[r][j], cal[r + 1][j], cal[r + 2][j]))) | |
942 | else: | |
943 | ret.append((u' %-20s %-20s %-20s' % (cal[r][j], cal[r + 1][j], cal[r + 2][j]))) | |
944 | return ret | |
945 | ||
946 | ||
947 | def date_calculator(s, options=None): | |
948 | """ | |
949 | x [+-] y | |
950 | where x is a datetime and y is either a datetime or a timeperiod | |
951 | :param s: | |
952 | """ | |
953 | estr = estr_regex.search(s) | |
954 | if estr: | |
955 | y = estr.group(1) | |
956 | e = easter(int(y)) | |
957 | E = e.strftime("%Y-%m-%d") | |
958 | s = estr_regex.sub(E, s) | |
959 | ||
960 | m = date_calc_regex.match(s) | |
961 | if not m: | |
962 | return 'Could not parse "%s"' % s | |
963 | x, pm, y = [z.strip() for z in m.groups()] | |
964 | xzs = '' | |
965 | nx = timezone_regex.match(x) | |
966 | if nx: | |
967 | x, xzs = nx.groups() | |
968 | yz = tzlocal() | |
969 | yzs = '' | |
970 | ny = timezone_regex.match(y) | |
971 | if ny: | |
972 | y, yzs = ny.groups() | |
973 | yz = gettz(yzs) | |
974 | windoz_epoch = _("Warning: under Windows with dates prior to 1970,\nany timezone information is ignored.") | |
975 | warn = "" | |
976 | try: | |
977 | dt_x = parse(parse_dtstr(x, timezone=xzs)) | |
978 | pmy = "%s%s" % (pm, y) | |
979 | if period_string_regex.match(pmy): | |
980 | dt = (dt_x + parse_period(pmy)) | |
981 | if windoz and (dt_x.year < 1970 or dt.year < 1970): | |
982 | warn = "\n\n{0}".format(windoz_epoch) | |
983 | else: | |
984 | dt.astimezone(yz) | |
985 | ||
986 | res = dt.strftime("%Y-%m-%d %H:%M%z") | |
987 | prompt = "{0}:\n\n{1}{2}".format(s.strip(), res.strip(), warn) | |
988 | return prompt | |
989 | else: | |
990 | dt_y = parse(parse_dtstr(y, timezone=yzs)) | |
991 | if windoz and (dt_x.year < 1970 or dt_y.year < 1970): | |
992 | warn = "\n\n{0}".format(windoz_epoch) | |
993 | dt_x = dt_x.replace(tzinfo=None) | |
994 | dt_y = dt_y.replace(tzinfo=None) | |
995 | if pm == '-': | |
996 | res = fmt_period(dt_x - dt_y) | |
997 | prompt = "{0}:\n\n{1}{2}".format(s.strip(), res.strip(), warn) | |
998 | return prompt | |
999 | else: | |
1000 | return 'error: datetimes cannot be added' | |
1001 | except ValueError: | |
1002 | return 'error parsing "%s"' % s | |
1003 | ||
1004 | ||
1005 | def mail_report(message, smtp_from=None, smtp_server=None, | |
1006 | smtp_id=None, smtp_pw=None, smtp_to=None): | |
1007 | """ | |
1008 | """ | |
1009 | import smtplib | |
1010 | from email.MIMEMultipart import MIMEMultipart | |
1011 | from email.MIMEText import MIMEText | |
1012 | from email.Utils import COMMASPACE, formatdate | |
1013 | # from email import Encoders | |
1014 | ||
1015 | assert type(smtp_to) == list | |
1016 | ||
1017 | msg = MIMEMultipart() | |
1018 | msg['From'] = smtp_from | |
1019 | msg['To'] = COMMASPACE.join(smtp_to) | |
1020 | msg['Date'] = formatdate(localtime=True) | |
1021 | msg['Subject'] = "etm agenda" | |
1022 | ||
1023 | msg.attach(MIMEText(message, 'html')) | |
1024 | ||
1025 | smtp = smtplib.SMTP_SSL(smtp_server) | |
1026 | smtp.login(smtp_id, smtp_pw) | |
1027 | smtp.sendmail(smtp_from, smtp_to, msg.as_string()) | |
1028 | smtp.close() | |
1029 | ||
1030 | ||
1031 | def send_mail(smtp_to, subject, message, files=None, smtp_from=None, smtp_server=None, | |
1032 | smtp_id=None, smtp_pw=None): | |
1033 | """ | |
1034 | """ | |
1035 | if not files: | |
1036 | files = [] | |
1037 | import smtplib | |
1038 | if windoz: | |
1039 | from email.mime.multipart import MIMEMultipart | |
1040 | from email.mime.base import MIMEBase | |
1041 | from email.mime.text import MIMEText | |
1042 | from email.utils import COMMASPACE, formatdate | |
1043 | from email import encoders as Encoders | |
1044 | else: | |
1045 | from email.MIMEMultipart import MIMEMultipart | |
1046 | from email.MIMEBase import MIMEBase | |
1047 | from email.MIMEText import MIMEText | |
1048 | from email.Utils import COMMASPACE, formatdate | |
1049 | from email import Encoders | |
1050 | assert type(smtp_to) == list | |
1051 | assert type(files) == list | |
1052 | msg = MIMEMultipart() | |
1053 | msg['From'] = smtp_from | |
1054 | msg['To'] = COMMASPACE.join(smtp_to) | |
1055 | msg['Date'] = formatdate(localtime=True) | |
1056 | msg['Subject'] = subject | |
1057 | msg.attach(MIMEText(message)) | |
1058 | for f in files: | |
1059 | part = MIMEBase('application', "octet-stream") | |
1060 | part.set_payload(open(f, "rb").read()) | |
1061 | Encoders.encode_base64(part) | |
1062 | part.add_header( | |
1063 | 'Content-Disposition', | |
1064 | 'attachment; filename="%s"' % os.path.basename(f)) | |
1065 | msg.attach(part) | |
1066 | smtp = smtplib.SMTP_SSL(smtp_server) | |
1067 | smtp.login(smtp_id, smtp_pw) | |
1068 | smtp.sendmail(smtp_from, smtp_to, msg.as_string()) | |
1069 | smtp.close() | |
1070 | ||
1071 | ||
1072 | def send_text(sms_phone, subject, message, sms_from, sms_server, sms_pw): | |
1073 | sms_phone = "%s" % sms_phone | |
1074 | import smtplib | |
1075 | from email.mime.text import MIMEText | |
1076 | ||
1077 | sms = smtplib.SMTP(sms_server) | |
1078 | sms.starttls() | |
1079 | sms.login(sms_from, sms_pw) | |
1080 | for num in sms_phone.split(','): | |
1081 | msg = MIMEText(message) | |
1082 | msg["From"] = sms_from | |
1083 | msg["Subject"] = subject | |
1084 | msg['To'] = num | |
1085 | sms.sendmail(sms_from, sms_phone, msg.as_string()) | |
1086 | sms.quit() | |
1087 | ||
1088 | ||
1089 | item_regex = re.compile(r'^([\$\^\*~!%\?#=\+\-])\s') | |
1090 | email_regex = re.compile('([\w\-\.]+@(\w[\w\-]+\.)+[\w\-]+)') | |
1091 | sign_regex = re.compile(r'(^\s*([+-])?)') | |
1092 | week_regex = re.compile(r'[+-]?(\d+)w', flags=re.I) | |
1093 | estr_regex = re.compile(r'easter\((\d{4,4})\)', flags=re.I) | |
1094 | day_regex = re.compile(r'[+-]?(\d+)d', flags=re.I) | |
1095 | hour_regex = re.compile(r'[+-]?(\d+)h', flags=re.I) | |
1096 | minute_regex = re.compile(r'[+-]?(\d+)m', flags=re.I) | |
1097 | date_calc_regex = re.compile(r'^\s*(.+)\s+([+-])\s+(.+)\s*$') | |
1098 | period_string_regex = re.compile(r'^\s*([+-]?(\d+[wWdDhHmM]?)+\s*$)') | |
1099 | timezone_regex = re.compile(r'^(.+)\s+([A-Za-z]+/[A-Za-z]+)$') | |
1100 | int_regex = re.compile(r'^\s*([+-]?\d+)\s*$') | |
1101 | leadingzero = re.compile(r'(?<!(:|\d|-))0+(?=\d)') | |
1102 | trailingzeros = re.compile(r'(:00)') | |
1103 | at_regex = re.compile(r'\s+@', re.MULTILINE) | |
1104 | minus_regex = re.compile(r'\s+\-(?=[a-zA-Z])') | |
1105 | amp_regex = re.compile(r'\s+&') | |
1106 | comma_regex = re.compile(r',\s*') | |
1107 | range_regex = re.compile(r'range\((\d+)(\s*,\s*(\d+))?\)') | |
1108 | id_regex = re.compile(r'^\s*@i') | |
1109 | anniversary_regex = re.compile(r'!(\d{4})!') | |
1110 | group_regex = re.compile(r'^\s*(.*)\s+(\d+)/(\d+):\s*(.*)') | |
1111 | groupdate_regex = re.compile(r'\by{2}\b|\by{4}\b|\b[dM]{1,4}\b|\bw\b') | |
1112 | options_regex = re.compile(r'^\s*(!?[fk](\[[:\d]+\])?)|(!?[clostu])\s*$') | |
1113 | # completion_regex = re.compile(r'(?:^.*?)((?:\@[a-zA-Z] ?)?\b\S*)$') | |
1114 | completion_regex = re.compile(r'((?:[\@\&][a-zA-Z]? ?)?(?:\b[a-zA-Z0-9_/:]+)?)$') | |
1115 | ||
1116 | # what about other languages? | |
1117 | # lun mar mer jeu ven sam dim | |
1118 | # we'll use this to reduce abbrevs to 2 letters for weekdays in rrule | |
1119 | threeday_regex = re.compile(r'(MON|TUE|WED|THU|FRI|SAT|SUN)', | |
1120 | re.IGNORECASE) | |
1121 | ||
1122 | oneminute = timedelta(minutes=1) | |
1123 | onehour = timedelta(hours=1) | |
1124 | oneday = timedelta(days=1) | |
1125 | oneweek = timedelta(weeks=1) | |
1126 | ||
1127 | rel_date_regex = re.compile(r'(?<![0-9])([-+][0-9]+)') | |
1128 | rel_month_regex = re.compile(r'(?<![0-9])([-+][0-9]+)/([0-9]+)') | |
1129 | ||
1130 | fmt = "%a %Y-%m-%d %H:%M %Z" | |
1131 | ||
1132 | rfmt = "%Y-%m-%d %H:%M%z" | |
1133 | efmt = "%H:%M %a %b %d" | |
1134 | ||
1135 | sfmt = "%Y%m%dT%H%M" | |
1136 | ||
1137 | # finish and due dates | |
1138 | zfmt = "%Y%m%dT%H%M" | |
1139 | ||
1140 | sortdatefmt = "%Y%m%d" | |
1141 | reprdatefmt = "%a %b %d, %Y" | |
1142 | shortdatefmt = "%a %b %d %Y" | |
1143 | shortyearlessfmt = "%a %b %d" | |
1144 | weekdayfmt = "%a %d" | |
1145 | sorttimefmt = "%H%M" | |
1146 | etmdatefmt = "%Y-%m-%d" | |
1147 | etmtimefmt = "%H:%M" | |
1148 | rrulefmt = "%a %b %d %Y %H:%M %Z %z" | |
1149 | ||
1150 | today = datetime.now(tzlocal()).replace( | |
1151 | hour=0, minute=0, second=0, microsecond=0) | |
1152 | yesterday = today - oneday | |
1153 | tomorrow = today + oneday | |
1154 | ||
1155 | day_begin = time(0, 0) | |
1156 | day_end = time(23, 59) | |
1157 | day_end_minutes = 23 * 60 + 59 | |
1158 | actions = ["s", "d", "e", "p", "v"] | |
1159 | ||
1160 | ||
1161 | def setConfig(options): | |
1162 | dfile_encoding = options['encoding']['file'] | |
1163 | cal_regex = None | |
1164 | if 'calendars' in options: | |
1165 | cal_pattern = r'^%s' % '|'.join( | |
1166 | [x[2] for x in options['calendars'] if x[1]]) | |
1167 | cal_regex = re.compile(cal_pattern) | |
1168 | ||
1169 | options['user_data'] = {} | |
1170 | options['completions'] = [] | |
1171 | options['reports'] = [] | |
1172 | completions = set([]) | |
1173 | reports = set([]) | |
1174 | completion_files = [] | |
1175 | report_files = [] | |
1176 | user_files = [] | |
1177 | ||
1178 | # get info from files in datadir | |
1179 | prefix, filelist = getFiles(options['datadir'], include=r'*.cfg') | |
1180 | for rp in ['completions.cfg', 'users.cfg', 'reports.cfg']: | |
1181 | fp = os.path.join(options['etmdir'], rp) | |
1182 | if os.path.isfile(fp): | |
1183 | filelist.append((fp, rp)) | |
1184 | logger.info('prefix: {0}; files: {1}'.format(prefix, filelist)) | |
1185 | for fp, rp in filelist: | |
1186 | if os.path.split(rp)[0] and cal_regex and not cal_regex.match(rp): | |
1187 | continue | |
1188 | np = relpath(fp, options['etmdir']) | |
1189 | drive, parts = os_path_splitall(fp) | |
1190 | n, e = os.path.splitext(parts[-1]) | |
1191 | # skip etmtk and any other .cfg files other than the following | |
1192 | if n == "completions": | |
1193 | completion_files.append((np, fp, False)) | |
1194 | with codecs.open(fp, 'r', dfile_encoding) as fo: | |
1195 | for x in fo.readlines(): | |
1196 | x = x.rstrip() | |
1197 | if x and x[0] != "#": | |
1198 | completions.add(x) | |
1199 | ||
1200 | elif n == "reports": | |
1201 | report_files.append((np, fp, False)) | |
1202 | with codecs.open(fp, 'r', dfile_encoding) as fo: | |
1203 | for x in fo.readlines(): | |
1204 | x = x.rstrip() | |
1205 | if x and x[0] != "#": | |
1206 | reports.add(x) | |
1207 | ||
1208 | elif n == "users": | |
1209 | user_files.append((np, fp, False)) | |
1210 | fo = codecs.open(fp, 'r', dfile_encoding) | |
1211 | tmp = yaml.load(fo) | |
1212 | fo.close() | |
1213 | try: | |
1214 | # if a key already exists, use the tmp value | |
1215 | options['user_data'].update(tmp) | |
1216 | for x in tmp.keys(): | |
1217 | completions.add("@u {0}".format(x)) | |
1218 | completions.add("&u {0}".format(x)) | |
1219 | except: | |
1220 | logger.exception("Error loading {0}".format(fp)) | |
1221 | ||
1222 | # get info from cfg_files | |
1223 | if 'cfg_files' in options and options['cfg_files']: | |
1224 | if 'completions' in options['cfg_files'] and options['cfg_files']['completions']: | |
1225 | for fp in options['cfg_files']['completions']: | |
1226 | completion_files.append((relpath(fp, options['etmdir']), fp, False)) | |
1227 | with codecs.open(fp, 'r', dfile_encoding) as fo: | |
1228 | for x in fo.readlines(): | |
1229 | x = x.rstrip() | |
1230 | if x and x[0] != "#": | |
1231 | completions.add(x) | |
1232 | if 'reports' in options['cfg_files'] and options['cfg_files']['reports']: | |
1233 | for fp in options['cfg_files']['reports']: | |
1234 | report_files.append((relpath(fp, options['etmdir']), fp, False)) | |
1235 | with codecs.open(fp, 'r', dfile_encoding) as fo: | |
1236 | for x in fo.readlines(): | |
1237 | x = x.rstrip() | |
1238 | if x and x[0] != "#": | |
1239 | reports.add(x) | |
1240 | if 'users' in options['cfg_files'] and options['cfg_files']['users']: | |
1241 | for fp in options['cfg_files']['users']: | |
1242 | user_files.append((relpath(fp, options['etmdir']), fp, False)) | |
1243 | fo = codecs.open(fp, 'r', dfile_encoding) | |
1244 | tmp = yaml.load(fo) | |
1245 | fo.close() | |
1246 | # if a key already exists, use this value | |
1247 | options['user_data'].update(tmp) | |
1248 | for x in tmp.keys(): | |
1249 | completions.add("@u {0}".format(x)) | |
1250 | completions.add("&u {0}".format(x)) | |
1251 | ||
1252 | if completions: | |
1253 | completions = list(completions) | |
1254 | completions.sort() | |
1255 | options['completions'] = completions | |
1256 | options['keywords'] = [x[3:] for x in completions if x.startswith('@k')] | |
1257 | else: | |
1258 | logger.info('no completions') | |
1259 | if reports: | |
1260 | reports = list(reports) | |
1261 | reports.sort() | |
1262 | options['reports'] = reports | |
1263 | else: | |
1264 | logger.info('no reports') | |
1265 | ||
1266 | options['completion_files'] = completion_files | |
1267 | options['report_files'] = report_files | |
1268 | options['user_files'] = user_files | |
1269 | ||
1270 | ||
1271 | # noinspection PyGlobalUndefined | |
1272 | term_encoding = None | |
1273 | file_encoding = None | |
1274 | local_timezone = None | |
1275 | ||
1276 | ||
1277 | def get_options(d=''): | |
1278 | """ | |
1279 | """ | |
1280 | logger.debug('starting get_options with directory: "{0}"'.format(d)) | |
1281 | global parse, s2or3, term_encoding, file_encoding, local_timezone | |
1282 | from locale import getpreferredencoding | |
1283 | from sys import stdout | |
1284 | try: | |
1285 | dterm_encoding = stdout.term_encoding | |
1286 | except AttributeError: | |
1287 | dterm_encoding = None | |
1288 | if not dterm_encoding: | |
1289 | dterm_encoding = getpreferredencoding() | |
1290 | ||
1291 | dterm_encoding = dfile_encoding = codecs.lookup(dterm_encoding).name | |
1292 | ||
1293 | use_locale = () | |
1294 | etmdir = '' | |
1295 | NEWCFG = "etmtk.cfg" | |
1296 | OLDCFG = "etm.cfg" | |
1297 | using_oldcfg = False | |
1298 | if d and os.path.isdir(d): | |
1299 | etmdir = os.path.abspath(d) | |
1300 | else: | |
1301 | homedir = os.path.expanduser("~") | |
1302 | etmdir = os.path.normpath(os.path.join(homedir, ".etm")) | |
1303 | newconfig = os.path.normpath(os.path.join(etmdir, NEWCFG)) | |
1304 | oldconfig = os.path.normpath(os.path.join(etmdir, OLDCFG)) | |
1305 | datafile = os.path.join(etmdir, ".etmtkdata.pkl") | |
1306 | default_datadir = os.path.normpath(os.path.join(etmdir, 'data')) | |
1307 | logger.debug('checking first for: {0}; then: {1}'.format(newconfig, oldconfig)) | |
1308 | ||
1309 | locale_cfg = os.path.normpath(os.path.join(etmdir, 'locale.cfg')) | |
1310 | if os.path.isfile(locale_cfg): | |
1311 | logger.info('using locale file: {0}'.format(locale_cfg)) | |
1312 | fo = codecs.open(locale_cfg, 'r', dfile_encoding) | |
1313 | use_locale = yaml.load(fo) | |
1314 | fo.close() | |
1315 | if use_locale: | |
1316 | dgui_encoding = use_locale[0][1] | |
1317 | else: | |
1318 | use_locale = () | |
1319 | tmp = locale.getdefaultlocale() | |
1320 | dgui_encoding = tmp[1] | |
1321 | else: | |
1322 | use_locale = () | |
1323 | tmp = locale.getdefaultlocale() | |
1324 | dgui_encoding = tmp[1] | |
1325 | ||
1326 | try: | |
1327 | dgui_encoding = codecs.lookup(dgui_encoding).name | |
1328 | except (TypeError, LookupError): | |
1329 | dgui_encoding = codecs.lookup(locale.getpreferredencoding()).name | |
1330 | ||
1331 | time_zone = get_localtz()[0] | |
1332 | ||
1333 | default_freetimes = {'opening': 8 * 60, 'closing': 17 * 60, 'minimum': 30, 'buffer': 15} | |
1334 | ||
1335 | git_command, git_history, git_commit, git_init = getGit() | |
1336 | hg_command, hg_history, hg_commit, hg_init = getMercurial() | |
1337 | ||
1338 | default_vcs = '' | |
1339 | ||
1340 | default_options = { | |
1341 | 'action_markups': {'default': 1.0, }, | |
1342 | 'action_minutes': 6, | |
1343 | 'action_interval': 1, | |
1344 | 'action_timer': {'running': '', 'paused': '', 'idle': ''}, | |
1345 | 'action_rates': {'default': 100.0, }, | |
1346 | 'action_template': '!hours!h $!value!) !label! (!count!)', | |
1347 | ||
1348 | 'agenda_colors': 2, | |
1349 | 'agenda_days': 4, | |
1350 | 'agenda_indent': 3, | |
1351 | 'agenda_width1': 32, | |
1352 | 'agenda_width2': 18, | |
1353 | ||
1354 | 'alert_default': ['m'], | |
1355 | 'alert_displaycmd': '', | |
1356 | 'alert_soundcmd': '', | |
1357 | 'alert_template': '!time_span!\n!l!\n\n!d!', | |
1358 | 'alert_voicecmd': '', | |
1359 | 'alert_wakecmd': '', | |
1360 | ||
1361 | 'ampm': True, | |
1362 | 'completions_width': 36, | |
1363 | ||
1364 | 'calendars': [], | |
1365 | ||
1366 | 'cfg_files': {'completions': [], 'reports': [], 'users': []}, | |
1367 | ||
1368 | 'current_textfile': '', | |
1369 | 'current_htmlfile': '', | |
1370 | 'current_icsfolder': '', | |
1371 | 'current_indent': 3, | |
1372 | 'current_opts': '', | |
1373 | 'current_width1': 48, | |
1374 | 'current_width2': 18, | |
1375 | ||
1376 | 'datadir': default_datadir, | |
1377 | 'dayfirst': dayfirst, | |
1378 | ||
1379 | 'details_rows': 4, | |
1380 | ||
1381 | 'edit_cmd': '', | |
1382 | 'email_template': "!time_span!\n!l!\n\n!d!", | |
1383 | 'etmdir': etmdir, | |
1384 | 'encoding': {'file': dfile_encoding, 'gui': dgui_encoding, | |
1385 | 'term': dterm_encoding}, | |
1386 | 'filechange_alert': '', | |
1387 | 'fontsize_fixed': 0, | |
1388 | 'fontsize_tree': 0, | |
1389 | 'freetimes': default_freetimes, | |
1390 | 'icscal_file': os.path.normpath(os.path.join(etmdir, 'etmcal.ics')), | |
1391 | 'icsitem_file': os.path.normpath(os.path.join(etmdir, 'etmitem.ics')), | |
1392 | 'icssync_folder': '', | |
1393 | 'ics_subscriptions': [], | |
1394 | 'idle_minutes': 10, | |
1395 | ||
1396 | 'local_timezone': time_zone, | |
1397 | ||
1398 | # 'monthly': os.path.join('personal', 'monthly'), | |
1399 | 'monthly': os.path.join('personal', 'monthly'), | |
1400 | 'outline_depth': 0, | |
1401 | 'prefix': "\n ", | |
1402 | 'prefix_uses': 'dfjlmrtz+-', | |
1403 | 'report_begin': '1', | |
1404 | 'report_end': '+1/1', | |
1405 | 'report_colors': 2, | |
1406 | 'report_indent': 3, | |
1407 | 'report_width1': 43, | |
1408 | 'report_width2': 17, | |
1409 | ||
1410 | 'show_finished': 1, | |
1411 | ||
1412 | 'smtp_from': '', | |
1413 | 'smtp_id': '', | |
1414 | 'smtp_pw': '', | |
1415 | 'smtp_server': '', | |
1416 | 'smtp_to': '', | |
1417 | ||
1418 | 'sms_from': '', | |
1419 | 'sms_message': '!summary!', | |
1420 | 'sms_phone': '', | |
1421 | 'sms_pw': '', | |
1422 | 'sms_server': '', | |
1423 | 'sms_subject': '!time_span!', | |
1424 | ||
1425 | 'sundayfirst': False, | |
1426 | 'update_minutes': 15, | |
1427 | 'vcs_system': default_vcs, | |
1428 | 'vcs_settings': {'command': '', 'commit': '', 'dir': '', 'file': '', 'history': '', 'init': '', 'limit': ''}, | |
1429 | 'weeks_after': 52, | |
1430 | 'yearfirst': yearfirst} | |
1431 | ||
1432 | if not os.path.isdir(etmdir): | |
1433 | # first etm use, no etmdir | |
1434 | os.makedirs(etmdir) | |
1435 | logfile = os.path.normpath(os.path.abspath(os.path.join(etmdir, "etmtk_log.txt"))) | |
1436 | if not os.path.isfile(logfile): | |
1437 | fo = codecs.open(logfile, 'w', dfile_encoding) | |
1438 | fo.write("") | |
1439 | fo.close() | |
1440 | ||
1441 | if os.path.isfile(newconfig): | |
1442 | try: | |
1443 | logger.info('user options: {0}'.format(newconfig)) | |
1444 | fo = codecs.open(newconfig, 'r', dfile_encoding) | |
1445 | user_options = yaml.load(fo) | |
1446 | fo.close() | |
1447 | except yaml.parser.ParserError: | |
1448 | logger.exception( | |
1449 | 'Exception loading {0}. Using default options.'.format(newconfig)) | |
1450 | user_options = {} | |
1451 | elif os.path.isfile(oldconfig): | |
1452 | try: | |
1453 | using_oldcfg = True | |
1454 | logger.info('user options: {0}'.format(oldconfig)) | |
1455 | fo = codecs.open(oldconfig, 'r', dfile_encoding) | |
1456 | user_options = yaml.load(fo) | |
1457 | fo.close() | |
1458 | except yaml.parser.ParserError: | |
1459 | logger.exception( | |
1460 | 'Exception loading {0}. Using default options.'.format(oldconfig)) | |
1461 | user_options = {} | |
1462 | else: | |
1463 | logger.info('using default options') | |
1464 | user_options = {'datadir': default_datadir} | |
1465 | fo = codecs.open(newconfig, 'w', dfile_encoding) | |
1466 | yaml.safe_dump(user_options, fo) | |
1467 | fo.close() | |
1468 | ||
1469 | options = deepcopy(default_options) | |
1470 | changed = False | |
1471 | if user_options: | |
1472 | if ('actions_timercmd' in user_options and | |
1473 | user_options['actions_timercmd']): | |
1474 | user_options['action_timer']['running'] = \ | |
1475 | user_options['actions_timercmd'] | |
1476 | del user_options['actions_timercmd'] | |
1477 | changed = True | |
1478 | options.update(user_options) | |
1479 | else: | |
1480 | user_options = {} | |
1481 | # logger.debug("user_options: {0}".format(user_options)) | |
1482 | ||
1483 | for key in default_options: | |
1484 | if key in ['show_finished', 'fontsize_busy', 'fontsize_fixed', 'fontsize_tree', 'outline_depth', 'prefix', 'prefix_uses', 'icssyc_folder', 'ics_subscriptions']: | |
1485 | if key not in user_options: | |
1486 | # we want to allow 0 as an entry | |
1487 | options[key] = default_options[key] | |
1488 | changed = True | |
1489 | elif key in ['ampm', 'dayfirst', 'yearfirst', 'retain_ids']: | |
1490 | if key not in user_options: | |
1491 | # we want to allow False as an entry | |
1492 | options[key] = default_options[key] | |
1493 | changed = True | |
1494 | ||
1495 | elif default_options[key] and (key not in user_options or not user_options[key]): | |
1496 | options[key] = default_options[key] | |
1497 | changed = True | |
1498 | ||
1499 | if type(options['update_minutes']) is not int or options['update_minutes'] <= 0 or options['update_minutes'] > 59: | |
1500 | options['update_minutes'] = default_options['update_minutes'] | |
1501 | ||
1502 | remove_keys = [] | |
1503 | for key in options: | |
1504 | if key not in default_options: | |
1505 | remove_keys.append(key) | |
1506 | changed = True | |
1507 | for key in remove_keys: | |
1508 | del options[key] | |
1509 | ||
1510 | # check freetimes | |
1511 | for key in default_freetimes: | |
1512 | if key not in options['freetimes']: | |
1513 | options['freetimes'][key] = default_freetimes[key] | |
1514 | logger.warn('A value was not provided for freetimes[{0}] - using the default value.'.format(key)) | |
1515 | changed = True | |
1516 | else: | |
1517 | if type(options['freetimes'][key]) is not int: | |
1518 | changed = True | |
1519 | try: | |
1520 | options['freetimes'][key] = int(eval(options['freetimes'][key])) | |
1521 | except: | |
1522 | logger.warn('The value provided for freetimes[{0}], "{1}", could not be converted to an integer - using the default value instead.'.format(key, options['freetimes'][key])) | |
1523 | options['freetimes'][key] = default_freetimes[key] | |
1524 | ||
1525 | free_keys = [x for x in options['freetimes'].keys()] | |
1526 | for key in free_keys: | |
1527 | if key not in default_freetimes: | |
1528 | del options['freetimes'][key] | |
1529 | logger.warn('A value was provided for freetimes[{0}], but this is an invalid option and has been deleted.'.format(key)) | |
1530 | changed = True | |
1531 | ||
1532 | if not os.path.isdir(options['datadir']): | |
1533 | """ | |
1534 | <datadir> | |
1535 | personal/ | |
1536 | monthly/ | |
1537 | sample/ | |
1538 | completions.cfg | |
1539 | reports.cfg | |
1540 | sample.txt | |
1541 | users.cfg | |
1542 | shared/ | |
1543 | holidays.txt | |
1544 | ||
1545 | etmtk.cfg | |
1546 | calendars: | |
1547 | - - personal | |
1548 | - true | |
1549 | - personal | |
1550 | - - sample | |
1551 | - true | |
1552 | - sample | |
1553 | - - shared | |
1554 | - true | |
1555 | - shared | |
1556 | """ | |
1557 | changed = True | |
1558 | term_print('creating datadir: {0}'.format(options['datadir'])) | |
1559 | # first use of this datadir - first use of new etm? | |
1560 | os.makedirs(options['datadir']) | |
1561 | # create one task for new users to join the etm discussion group | |
1562 | currfile = ensureMonthly(options) | |
1563 | with open(currfile, 'w') as fo: | |
1564 | fo.write(JOIN) | |
1565 | sample = os.path.normpath(os.path.join(options['datadir'], 'sample')) | |
1566 | os.makedirs(sample) | |
1567 | with open(os.path.join(sample, 'sample.txt'), 'w') as fo: | |
1568 | fo.write(SAMPLE) | |
1569 | holidays = os.path.normpath(os.path.join(options['datadir'], 'shared')) | |
1570 | os.makedirs(holidays) | |
1571 | with open(os.path.join(holidays, 'holidays.txt'), 'w') as fo: | |
1572 | fo.write(HOLIDAYS) | |
1573 | with open(os.path.join(options['datadir'], 'sample', 'completions.cfg'), 'w') as fo: | |
1574 | fo.write(COMPETIONS) | |
1575 | with open(os.path.join(options['datadir'], 'sample', 'reports.cfg'), 'w') as fo: | |
1576 | fo.write(REPORTS) | |
1577 | with open(os.path.join(options['datadir'], 'sample', 'users.cfg'), 'w') as fo: | |
1578 | fo.write(USERS) | |
1579 | if not options['calendars']: | |
1580 | options['calendars'] = [['personal', True, 'personal'], ['sample', True, 'sample'], ['shared', True, 'shared']] | |
1581 | logger.info('using datadir: {0}'.format(options['datadir'])) | |
1582 | logger.debug('changed: {0}; user: {1}; options: {2}'.format(changed, (user_options != default_options), (options != default_options))) | |
1583 | if changed or using_oldcfg: | |
1584 | # save options to newconfig even if user options came from oldconfig | |
1585 | logger.debug('Writing etmtk.cfg changes to {0}'.format(newconfig)) | |
1586 | fo = codecs.open(newconfig, 'w', options['encoding']['file']) | |
1587 | yaml.safe_dump(options, fo, default_flow_style=False) | |
1588 | fo.close() | |
1589 | ||
1590 | # add derived options | |
1591 | if options['vcs_system'] == 'git': | |
1592 | if git_command: | |
1593 | options['vcs'] = {'command': git_command, 'history': git_history, 'commit': git_commit, 'init': git_init, 'dir': '.git', 'limit': '-n', 'file': ""} | |
1594 | repo = os.path.normpath(os.path.join(options['datadir'], options['vcs']['dir'])) | |
1595 | work = options['datadir'] | |
1596 | # logger.debug('{0} options: {1}'.format(options['vcs_system'], options['vcs'])) | |
1597 | else: | |
1598 | logger.warn('could not setup "git" vcs') | |
1599 | options['vcs'] = {} | |
1600 | options['vcs_system'] = '' | |
1601 | elif options['vcs_system'] == 'mercurial': | |
1602 | if hg_command: | |
1603 | options['vcs'] = {'command': hg_command, 'history': hg_history, 'commit': hg_commit, 'init': hg_init, 'dir': '.hg', 'limit': '-l', 'file': ''} | |
1604 | repo = os.path.normpath(os.path.join(options['datadir'], options['vcs']['dir'])) | |
1605 | work = options['datadir'] | |
1606 | # logger.debug('{0} options: {1}'.format(options['vcs_system'], options['vcs'])) | |
1607 | else: | |
1608 | logger.warn('could not setup "mercurial" vcs') | |
1609 | options['vcs'] = {} | |
1610 | options['vcs_system'] = '' | |
1611 | else: | |
1612 | options['vcs_system'] = '' | |
1613 | options['vcs'] = {} | |
1614 | ||
1615 | # overrule the defaults if any custom settings are given | |
1616 | if options['vcs_system']: | |
1617 | if options['vcs_settings']: | |
1618 | # update any settings with custom modifications | |
1619 | for key in options['vcs_settings']: | |
1620 | if options['vcs_settings'][key]: | |
1621 | options['vcs'][key] = options['vcs_settings'][key] | |
1622 | # add the derived options | |
1623 | options['vcs']['repo'] = repo | |
1624 | options['vcs']['work'] = work | |
1625 | ||
1626 | if options['vcs']: | |
1627 | vcs_lst = [] | |
1628 | keys = [x for x in options['vcs'].keys()] | |
1629 | keys.sort() | |
1630 | for key in keys: | |
1631 | vcs_lst.append("{0}: {1}".format(key, options['vcs'][key])) | |
1632 | vcs_str = "\n ".join(vcs_lst) | |
1633 | else: | |
1634 | vcs_str = "" | |
1635 | logger.info('using vcs {0}; options:\n {1}'.format(options['vcs_system'], vcs_str)) | |
1636 | ||
1637 | (options['daybegin_fmt'], options['dayend_fmt'], options['reprtimefmt'], | |
1638 | options['reprdatetimefmt'], options['etmdatetimefmt'], | |
1639 | options['rfmt'], options['efmt']) = get_fmts(options) | |
1640 | options['config'] = newconfig | |
1641 | options['datafile'] = datafile | |
1642 | options['scratchpad'] = os.path.normpath(os.path.join(options['etmdir'], _("scratchpad"))) | |
1643 | ||
1644 | if options['action_minutes'] not in [1, 6, 12, 15, 30, 60]: | |
1645 | term_print( | |
1646 | "Invalid action_minutes setting: %s. Reset to 1." % | |
1647 | options['action_minutes']) | |
1648 | options['action_minutes'] = 1 | |
1649 | ||
1650 | setConfig(options) | |
1651 | ||
1652 | z = gettz(options['local_timezone']) | |
1653 | if z is None: | |
1654 | term_print( | |
1655 | "Error: bad entry for local_timezone in etmtk.cfg: '%s'" % | |
1656 | options['local_timezone']) | |
1657 | options['local_timezone'] = '' | |
1658 | ||
1659 | if 'vcs_system' in options and options['vcs_system']: | |
1660 | logger.debug('vcs_system: {0}'.format(options['vcs_system'])) | |
1661 | f = '' | |
1662 | if options['vcs_system'] == 'mercurial': | |
1663 | f = os.path.normpath(os.path.join(options['datadir'], '.hgignore')) | |
1664 | elif options['vcs_system'] == 'git': | |
1665 | f = os.path.normpath(os.path.join(options['datadir'], '.gitignore')) | |
1666 | if f and not os.path.isfile(f): | |
1667 | fo = open(f, 'w') | |
1668 | fo.write(IGNORE) | |
1669 | fo.close() | |
1670 | logger.info('created: {0}'.format(f)) | |
1671 | logger.debug('checking for {0}'.format(options['vcs']['repo'])) | |
1672 | if not os.path.isdir(options['vcs']['repo']): | |
1673 | init = options['vcs']['init'] | |
1674 | # work = (options['vcs']['work']) | |
1675 | command = init.format(work=options['vcs']['work'], repo=options['vcs']['repo'], mesg="initial commit") | |
1676 | logger.debug('initializing vcs: {0}'.format(command)) | |
1677 | # run_cmd(command) | |
1678 | subprocess.call(command, shell=True) | |
1679 | ||
1680 | if options['current_icsfolder']: | |
1681 | if not os.path.isdir(options['current_icsfolder']): | |
1682 | os.makedirs(options['current_icsfolder']) | |
1683 | ||
1684 | if use_locale: | |
1685 | locale.setlocale(locale.LC_ALL, map(str, use_locale[0])) | |
1686 | lcl = locale.getlocale() | |
1687 | else: | |
1688 | lcl = locale.getdefaultlocale() | |
1689 | ||
1690 | options['lcl'] = lcl | |
1691 | logger.info('using lcl: {0}'.format(lcl)) | |
1692 | options['hide_finished'] = False | |
1693 | # define parse using dayfirst and yearfirst | |
1694 | setup_parse(options['dayfirst'], options['yearfirst']) | |
1695 | term_encoding = options['encoding']['term'] | |
1696 | file_encoding = options['encoding']['file'] | |
1697 | local_timezone = options['local_timezone'] | |
1698 | logger.debug("ending get_options") | |
1699 | return user_options, options, use_locale | |
1700 | ||
1701 | ||
1702 | def get_fmts(options): | |
1703 | global rfmt, efmt | |
1704 | df = "%x" | |
1705 | ef = "%a %b %d" | |
1706 | if 'ampm' in options and options['ampm']: | |
1707 | reprtimefmt = "%I:%M%p" | |
1708 | daybegin_fmt = "12am" | |
1709 | dayend_fmt = "11:59pm" | |
1710 | rfmt = "{0} %I:%M%p %z".format(df) | |
1711 | efmt = "%I:%M%p {0}".format(ef) | |
1712 | ||
1713 | else: | |
1714 | reprtimefmt = "%H:%M" | |
1715 | daybegin_fmt = "0:00" | |
1716 | dayend_fmt = "23:59" | |
1717 | rfmt = "{0} %H:%M%z".format(df) | |
1718 | efmt = "%H:%M {0}".format(ef) | |
1719 | ||
1720 | reprdatetimefmt = "%s %s %%Z" % (reprdatefmt, reprtimefmt) | |
1721 | etmdatetimefmt = "%s %s" % (etmdatefmt, reprtimefmt) | |
1722 | return (daybegin_fmt, dayend_fmt, reprtimefmt, reprdatetimefmt, | |
1723 | etmdatetimefmt, rfmt, efmt) | |
1724 | ||
1725 | ||
1726 | def checkForNewerVersion(): | |
1727 | global python_version2 | |
1728 | import socket | |
1729 | ||
1730 | timeout = 10 | |
1731 | socket.setdefaulttimeout(timeout) | |
1732 | if platform.python_version() >= '3': | |
1733 | python_version2 = False | |
1734 | from urllib.request import urlopen | |
1735 | from urllib.error import URLError | |
1736 | # from urllib.parse import urlencode | |
1737 | else: | |
1738 | python_version2 = True | |
1739 | from urllib2 import urlopen, URLError | |
1740 | ||
1741 | url = "http://people.duke.edu/~dgraham/etmtk/version.txt" | |
1742 | try: | |
1743 | response = urlopen(url) | |
1744 | except URLError as e: | |
1745 | if hasattr(e, 'reason'): | |
1746 | msg = """\ | |
1747 | The latest version could not be determined. | |
1748 | Reason: %s.""" % e.reason | |
1749 | elif hasattr(e, 'code'): | |
1750 | msg = """\ | |
1751 | The server couldn\'t fulfill the request. | |
1752 | Error code: %s.""" % e.code | |
1753 | return 0, msg | |
1754 | else: | |
1755 | # everything is fine | |
1756 | if python_version2: | |
1757 | res = response.read() | |
1758 | vstr = rsplit('\s+', res)[0] | |
1759 | else: | |
1760 | res = response.read().decode(term_encoding) | |
1761 | vstr = rsplit('\s+', res)[0] | |
1762 | ||
1763 | if version < vstr: | |
1764 | return (1, """\ | |
1765 | A newer version of etm, %s, is available at \ | |
1766 | people.duke.edu/~dgraham/etmtk.""" % vstr) | |
1767 | else: | |
1768 | return 1, 'You are using the latest version.' | |
1769 | ||
1770 | ||
1771 | type_keys = [x for x in '=^*-+%~$?!#'] | |
1772 | ||
1773 | type2Str = { | |
1774 | '$': "ib", | |
1775 | '^': "oc", | |
1776 | '*': "ev", | |
1777 | '~': "ac", | |
1778 | '!': "nu", # undated only appear in folders | |
1779 | '-': "un", # for next view | |
1780 | '+': "un", # for next view | |
1781 | '%': "du", | |
1782 | '?': "so", | |
1783 | '#': "dl"} | |
1784 | ||
1785 | id2Type = { | |
1786 | # TStr TNum Forground Color Icon view | |
1787 | "ac": '~', | |
1788 | "av": '-', | |
1789 | "by": '>', | |
1790 | "cs": '+', # job | |
1791 | "cu": '+', # job with unfinished prereqs | |
1792 | "dl": '#', | |
1793 | "ds": '%', | |
1794 | "du": '%', | |
1795 | "ev": '*', | |
1796 | "fn": u"\u2713", | |
1797 | "ib": '$', | |
1798 | "ns": '!', | |
1799 | "nu": '!', | |
1800 | "oc": '^', | |
1801 | "pc": '+', # job pastdue | |
1802 | "pu": '+', # job pastdue with unfinished prereqs | |
1803 | "pd": '%', | |
1804 | "pt": '-', | |
1805 | "rm": '*', | |
1806 | "so": '?', | |
1807 | "un": '-', | |
1808 | } | |
1809 | ||
1810 | # named colors: aliceblue antiquewhite aqua aquamarine azure beige | |
1811 | # bisque black blanchedalmond blue blueviolet brown burlywood | |
1812 | # cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson | |
1813 | # cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey | |
1814 | # darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred | |
1815 | # darksalmon darkseagreen darkslateblue darkslategray darkslategrey | |
1816 | # darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey | |
1817 | # dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro | |
1818 | # ghostwhite gold goldenrod gray green greenyellow grey honeydew | |
1819 | # hotpink indianred indigo ivory khaki lavender lavenderblush | |
1820 | # lawngreen lemonchiffon lightblue lightcoral lightcyan | |
1821 | # lightgoldenrodyellow lightgray lightgreen lightgrey lightpink | |
1822 | # lightsalmon lightseagreen lightskyblue lightslategray lightslategrey | |
1823 | # lightsteelblue lightyellow lime limegreen linen magenta maroon | |
1824 | # mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen | |
1825 | # mediumslateblue mediumspringgreen mediumturquoise mediumvioletred | |
1826 | # midnightblue mintcream mistyrose moccasin navajowhite navy oldlace | |
1827 | # olive olivedrab orange orangered orchid palegoldenrod palegreen | |
1828 | # paleturquoise palevioletred papayawhip peachpuff peru pink plum | |
1829 | # powderblue purple red rosybrown royalblue saddlebrown salmon | |
1830 | # sandybrown seagreen seashell sienna silver skyblue slateblue | |
1831 | # slategray slategrey snow springgreen steelblue tan teal thistle | |
1832 | # tomato transparent turquoise violet wheat white whitesmoke yellow | |
1833 | # yellowgreen | |
1834 | ||
1835 | # type string to Sort Color Icon | |
1836 | tstr2SCI = { | |
1837 | # TStr TNum Forground Color Icon view | |
1838 | "ac": [23, "darkorchid", "action", "day"], | |
1839 | "av": [16, "slateblue2", "task", "day"], | |
1840 | "by": [19, "gold3", "beginby", "now"], | |
1841 | "cs": [18, "slateblue2", "child", "day"], | |
1842 | "cu": [22, "gray65", "child", "day"], | |
1843 | "dl": [28, "gray70", "delete", "folder"], | |
1844 | "ds": [17, "darkslategray", "delegated", "day"], | |
1845 | "du": [21, "darkslategrey", "delegated", "day"], | |
1846 | # "ev": [12, "forestgreen", "event", "day"], | |
1847 | "ev": [12, "springgreen4", "event", "day"], | |
1848 | "fn": [27, "gray70", "finished", "day"], | |
1849 | ||
1850 | "ib": [10, "firebrick3", "inbox", "now"], | |
1851 | "ns": [24, "saddlebrown", "note", "day"], | |
1852 | "nu": [25, "saddlebrown", "note", "day"], | |
1853 | "oc": [11, "peachpuff4", "occasion", "day"], | |
1854 | "pc": [15, "orangered", "child", "now"], | |
1855 | ||
1856 | "pu": [15, "firebrick3", "child", "now"], | |
1857 | ||
1858 | "pd": [14, "orangered", "delegated", "now"], | |
1859 | "pt": [13, "orangered", "task", "now"], | |
1860 | "rm": [12, "seagreen", "reminder", "day"], | |
1861 | "so": [26, "slateblue1", "someday", "now"], | |
1862 | "un": [20, "slateblue2", "task", "next"], | |
1863 | } | |
1864 | ||
1865 | ||
1866 | def fmt_period(td, parent=None): | |
1867 | # logger.debug('td: {0}, {1}'.format(td, type(td))) | |
1868 | if td < oneminute * 0: | |
1869 | return '0m' | |
1870 | if td == oneminute * 0: | |
1871 | return '0m' | |
1872 | until = [] | |
1873 | td_days = td.days | |
1874 | td_hours = td.seconds // (60 * 60) | |
1875 | td_minutes = (td.seconds % (60 * 60)) // 60 | |
1876 | ||
1877 | if td_days: | |
1878 | until.append("%dd" % td_days) | |
1879 | if td_hours: | |
1880 | until.append("%dh" % td_hours) | |
1881 | if td_minutes: | |
1882 | until.append("%dm" % td_minutes) | |
1883 | if not until: | |
1884 | until = "0m" | |
1885 | return "".join(until) | |
1886 | ||
1887 | ||
1888 | def fmt_time(dt, omitMidnight=False, options=None): | |
1889 | # fmt time, omit leading zeros and, if ampm, convert to lowercase | |
1890 | # and omit trailing m's | |
1891 | if not options: | |
1892 | options = {} | |
1893 | if omitMidnight and dt.hour == 0 and dt.minute == 0: | |
1894 | return u'' | |
1895 | # logger.debug('dt before fmt: {0}'.format(dt)) | |
1896 | dt_fmt = dt.strftime(options['reprtimefmt']) | |
1897 | # logger.debug('dt dt_fmt: {0}'.format(dt_fmt)) | |
1898 | if dt_fmt[0] == "0": | |
1899 | dt_fmt = dt_fmt[1:] | |
1900 | # The 3rd test is for Poland where am, pm = '' | |
1901 | if 'ampm' in options and options['ampm'] and not dt_fmt[-1].isdigit(): | |
1902 | # dt_fmt = dt_fmt.lower()[:-1] | |
1903 | dt_fmt = dt_fmt.lower() | |
1904 | dt_fmt = leadingzero.sub('', dt_fmt) | |
1905 | dt_fmt = trailingzeros.sub('', dt_fmt) | |
1906 | return s2or3(dt_fmt) | |
1907 | ||
1908 | ||
1909 | def fmt_date(dt, short=False): | |
1910 | if type(dt) in [str, unicode]: | |
1911 | return unicode(dt) | |
1912 | if short: | |
1913 | tdy = datetime.today() | |
1914 | if dt.date() == tdy.date(): | |
1915 | dt_fmt = "%s" % _('today') | |
1916 | elif dt.year == tdy.year: | |
1917 | dt_fmt = dt.strftime(shortyearlessfmt) | |
1918 | else: | |
1919 | dt_fmt = dt.strftime(shortdatefmt) | |
1920 | else: | |
1921 | dt_fmt = dt.strftime(reprdatefmt) | |
1922 | return s2or3(dt_fmt) | |
1923 | ||
1924 | ||
1925 | def fmt_shortdatetime(dt, options=None): | |
1926 | if not options: | |
1927 | options = {} | |
1928 | if type(dt) in [str, unicode]: | |
1929 | return unicode(dt) | |
1930 | tdy = datetime.today() | |
1931 | if dt.date() == tdy.date(): | |
1932 | dt_fmt = "%s %s" % (fmt_time(dt, options=options), _('today')) | |
1933 | elif dt.year == tdy.year: | |
1934 | try: | |
1935 | x1 = unicode(fmt_time(dt, options=options)) | |
1936 | x2 = unicode(dt.strftime(shortyearlessfmt)) | |
1937 | dt_fmt = "%s %s" % (x1, x2) | |
1938 | except: | |
1939 | dt_fmt = dt.strftime("%X %x") | |
1940 | else: | |
1941 | try: | |
1942 | dt_fmt = dt.strftime(shortdatefmt) | |
1943 | dt_fmt = leadingzero.sub('', dt_fmt) | |
1944 | except: | |
1945 | dt_fmt = dt.strftime("%X %x") | |
1946 | return s2or3(dt_fmt) | |
1947 | ||
1948 | ||
1949 | def fmt_datetime(dt, options=None): | |
1950 | if not options: | |
1951 | options = {} | |
1952 | t_fmt = fmt_time(dt, options=options) | |
1953 | dt_fmt = "%s %s" % (dt.strftime(etmdatefmt), t_fmt) | |
1954 | return s2or3(dt_fmt) | |
1955 | ||
1956 | ||
1957 | def fmt_weekday(dt): | |
1958 | return fmt_dt(dt, weekdayfmt) | |
1959 | ||
1960 | ||
1961 | def fmt_dt(dt, f): | |
1962 | dt_fmt = dt.strftime(f) | |
1963 | return s2or3(dt_fmt) | |
1964 | ||
1965 | ||
1966 | rrule_hsh = { | |
1967 | 'f': 'FREQUENCY', # unicode | |
1968 | 'i': 'INTERVAL', # positive integer | |
1969 | 't': 'COUNT', # total count positive integer | |
1970 | 's': 'BYSETPOS', # integer | |
1971 | 'u': 'UNTIL', # unicode | |
1972 | 'M': 'BYMONTH', # integer 1...12 | |
1973 | 'm': 'BYMONTHDAY', # positive integer | |
1974 | 'W': 'BYWEEKNO', # positive integer | |
1975 | 'w': 'BYWEEKDAY', # integer 0 (SU) ... 6 (SA) | |
1976 | 'h': 'BYHOUR', # positive integer | |
1977 | 'n': 'BYMINUTE', # positive integer | |
1978 | 'E': 'BYEASTER', # non-negative integer number of days after easter | |
1979 | } | |
1980 | ||
1981 | # for icalendar export we need BYDAY instead of BYWEEKDAY | |
1982 | ical_hsh = deepcopy(rrule_hsh) | |
1983 | ical_hsh['w'] = 'BYDAY' | |
1984 | ical_hsh['f'] = 'FREQ' | |
1985 | ||
1986 | ical_rrule_hsh = { | |
1987 | 'FREQ': 'r', # unicode | |
1988 | 'INTERVAL': 'i', # positive integer | |
1989 | 'COUNT': 't', # total count positive integer | |
1990 | 'BYSETPOS': 's', # integer | |
1991 | 'UNTIL': 'u', # unicode | |
1992 | 'BYMONTH': 'M', # integer 1...12 | |
1993 | 'BYMONTHDAY': 'm', # positive integer | |
1994 | 'BYWEEKNO': 'W', # positive integer | |
1995 | 'BYDAY': 'w', # integer 0 (SU) ... 6 (SA) | |
1996 | # 'BYWEEKDAY': 'w', # integer 0 (SU) ... 6 (SA) | |
1997 | 'BYHOUR': 'h', # positive integer | |
1998 | 'BYMINUTE': 'n', # positive integer | |
1999 | 'BYEASTER': 'E', # non negative integer number of days after easter | |
2000 | } | |
2001 | ||
2002 | # don't add f and u - they require special processing in get_rrulestr | |
2003 | rrule_keys = ['i', 'm', 'M', 'w', 'W', 'h', 'n', 't', 's', 'E'] | |
2004 | ical_rrule_keys = ['f', 'i', 'm', 'M', 'w', 'W', 'h', 'n', 't', 's', 'u'] | |
2005 | ||
2006 | # ^ Presidential election day @s 2004-11-01 12am | |
2007 | # @r y &i 4 &m 2, 3, 4, 5, 6, 7, 8 &M 11 &w TU | |
2008 | ||
2009 | # don't add l (list) - handeled separately | |
2010 | freq_hsh = { | |
2011 | 'y': 'YEARLY', | |
2012 | 'm': 'MONTHLY', | |
2013 | 'w': 'WEEKLY', | |
2014 | 'd': 'DAILY', | |
2015 | 'h': 'HOURLY', | |
2016 | 'n': 'MINUTELY', | |
2017 | 'E': 'EASTERLY', | |
2018 | } | |
2019 | ||
2020 | ical_freq_hsh = { | |
2021 | 'YEARLY': 'y', | |
2022 | 'MONTHLY': 'm', | |
2023 | 'WEEKLY': 'w', | |
2024 | 'DAILY': 'd', | |
2025 | 'HOURLY': 'h', | |
2026 | 'MINUTELY': 'n', | |
2027 | # 'EASTERLY': 'e' | |
2028 | } | |
2029 | ||
2030 | amp_hsh = { | |
2031 | 'r': 'f', # the starting value for an @r entry is frequency | |
2032 | 'a': 't' # the starting value for an @a enotry is *triggers* | |
2033 | } | |
2034 | ||
2035 | at_keys = [ | |
2036 | 's', # start datetime | |
2037 | 'e', # extent time spent | |
2038 | 'x', # expense money spent | |
2039 | 'a', # alert | |
2040 | 'b', # begin | |
2041 | 'c', # context | |
2042 | 'k', # keyword | |
2043 | 't', # tags | |
2044 | 'l', # location | |
2045 | 'u', # user | |
2046 | 'f', # finish date | |
2047 | 'h', # history (task group) | |
2048 | 'g', # goto | |
2049 | 'j', # job | |
2050 | 'p', # priority | |
2051 | 'r', # repetition rule | |
2052 | '+', # include | |
2053 | '-', # exclude | |
2054 | 'o', # overdue | |
2055 | 'd', # description | |
2056 | 'm', # memo | |
2057 | 'z', # time zone | |
2058 | 'i', # id', | |
2059 | 'v', # action rate key | |
2060 | 'w', # expense markup key | |
2061 | ] | |
2062 | ||
2063 | all_keys = at_keys + ['entry', 'fileinfo', 'itemtype', 'rrule', '_summary', '_group_summary', '_a', '_j', '_p', '_r', 'prereqs'] | |
2064 | ||
2065 | label_keys = [ | |
2066 | # 'f', # finish date | |
2067 | '_a', # alert | |
2068 | 'b', # begin | |
2069 | 'c', # context | |
2070 | 'd', # description | |
2071 | 'g', # goto | |
2072 | 'k', # keyword | |
2073 | 'l', # location | |
2074 | 'm', # memo | |
2075 | 'p', # priority | |
2076 | '_r', # repetition rule | |
2077 | 't', # tags | |
2078 | 'u', # user | |
2079 | ] | |
2080 | ||
2081 | amp_keys = { | |
2082 | 'r': [ | |
2083 | u'f', # r frequency | |
2084 | u'i', # r interval | |
2085 | u'm', # r monthday | |
2086 | u'M', # r month | |
2087 | u'w', # r weekday | |
2088 | u'W', # r week | |
2089 | u'h', # r hour | |
2090 | u'n', # r minute | |
2091 | u'E', # r easter | |
2092 | u't', # r total (dateutil COUNT) (c is context in j) | |
2093 | u'u', # r until | |
2094 | u's'], # r set position | |
2095 | 'j': [ | |
2096 | u'j', # j job summary | |
2097 | u'b', # j beginby | |
2098 | u'c', # j context | |
2099 | u'd', # j description | |
2100 | u'e', # e extent | |
2101 | u'f', # j finish | |
2102 | u'h', # h history (task group jobs) | |
2103 | u'p', # j priority | |
2104 | u'u', # user | |
2105 | u'q'], # j queue position | |
2106 | } | |
2107 | ||
2108 | ||
2109 | @memoize | |
2110 | def makeTree(tree_rows, view=None, calendars=None, sort=True, fltr=None, hide_finished=False): | |
2111 | tree = {} | |
2112 | lofl = [] | |
2113 | root = '_' | |
2114 | empty = True | |
2115 | cal_regex = None | |
2116 | if calendars: | |
2117 | cal_pattern = r'^%s' % '|'.join([x[2] for x in calendars if x[1]]) | |
2118 | cal_regex = re.compile(cal_pattern) | |
2119 | if fltr is not None: | |
2120 | mtch = True | |
2121 | if fltr[0] == '!': | |
2122 | mtch = False | |
2123 | fltr = fltr[1:] | |
2124 | filter_regex = re.compile(r'{0}'.format(fltr), re.IGNORECASE) | |
2125 | logger.debug('filter: {0} ({1})'.format(fltr, mtch)) | |
2126 | else: | |
2127 | filter_regex = None | |
2128 | for pc in tree_rows: | |
2129 | if hide_finished and pc[-1][1] == 'fn': | |
2130 | continue | |
2131 | if cal_regex and not cal_regex.match(pc[0][-1]): | |
2132 | continue | |
2133 | if view and pc[0][0] != view: | |
2134 | continue | |
2135 | if filter_regex is not None: | |
2136 | s = "{0} {1}".format(pc[-1][2], " ".join(pc[1:-1])) | |
2137 | # logger.debug('looking in "{0}"'.format(s)) | |
2138 | m = filter_regex.search(s) | |
2139 | if not ((mtch and m) or (not mtch and not m)): | |
2140 | continue | |
2141 | root_key = tuple(["", root]) | |
2142 | tree.setdefault(root_key, []) | |
2143 | if sort: | |
2144 | pc.pop(0) | |
2145 | empty = False | |
2146 | key = tuple([root, pc[0]]) | |
2147 | if key not in tree[root_key]: | |
2148 | tree[root_key].append(key) | |
2149 | # logger.debug('key: {0}'.format(key)) | |
2150 | lofl.append(pc) | |
2151 | for i in range(len(pc) - 1): | |
2152 | if pc[:i]: | |
2153 | parent_key = tuple([":".join(pc[:i]), pc[i]]) | |
2154 | else: | |
2155 | parent_key = tuple([root, pc[i]]) | |
2156 | child_key = tuple([":".join(pc[:i + 1]), pc[i + 1]]) | |
2157 | # logger.debug('parent: {0}; child: {1}'.format(parent_key, child_key)) | |
2158 | if pc[:i + 1] not in lofl: | |
2159 | lofl.append(pc[:i + 1]) | |
2160 | tree.setdefault(parent_key, []) | |
2161 | if child_key not in tree[parent_key]: | |
2162 | tree[parent_key].append(child_key) | |
2163 | if empty: | |
2164 | return {} | |
2165 | return tree | |
2166 | ||
2167 | ||
2168 | def truncate(s, l): | |
2169 | if len(s) > l: | |
2170 | if re.search(' ~ ', s): | |
2171 | s = s.split(' ~ ')[0] | |
2172 | s = "%s.." % s[:l - 2] | |
2173 | return s | |
2174 | ||
2175 | ||
2176 | def tree2Html(tree, indent=2, width1=54, width2=20, colors=2): | |
2177 | global html_lst | |
2178 | html_lst = [] | |
2179 | if colors: | |
2180 | e_c = "</font>" | |
2181 | else: | |
2182 | e_c = "" | |
2183 | tab = " " * indent | |
2184 | ||
2185 | def t2H(tree_hsh, node=('', '_'), level=0): | |
2186 | if type(node) == tuple: | |
2187 | if type(node[1]) == tuple: | |
2188 | t = id2Type[node[1][1]] | |
2189 | col2 = "{0:^{width}}".format( | |
2190 | truncate(node[1][3], width2), width=width2) | |
2191 | if colors == 2: | |
2192 | s_c = '<font color="%s">' % tstr2SCI[node[1][1]][1] | |
2193 | elif colors == 1: | |
2194 | if node[1][1][0] == 'p': | |
2195 | # past due | |
2196 | s_c = '<font color="%s">' % tstr2SCI[node[1][1]][1] | |
2197 | else: | |
2198 | s_c = '<font color="black">' | |
2199 | else: | |
2200 | s_c = '' | |
2201 | rmlft = width1 - indent * level | |
2202 | s = "%s%s%s %-*s %s%s" % (tab * level, s_c, unicode(t), | |
2203 | rmlft, unicode(truncate(node[1][2], rmlft)), | |
2204 | col2, e_c) | |
2205 | html_lst.append(s) | |
2206 | else: | |
2207 | html_lst.append("%s%s" % (tab * level, node[1])) | |
2208 | else: | |
2209 | html_lst.append("%s%s" % (tab * level, node)) | |
2210 | if node not in tree_hsh: | |
2211 | return () | |
2212 | level += 1 | |
2213 | nodes = tree_hsh[node] | |
2214 | for n in nodes: | |
2215 | t2H(tree_hsh, n, level) | |
2216 | t2H(tree) | |
2217 | return [x[indent:] for x in html_lst] | |
2218 | ||
2219 | ||
2220 | def tree2Rst(tree, indent=2, width1=54, width2=14, colors=0, | |
2221 | number=False, count=0, count2id=None): | |
2222 | global text_lst | |
2223 | args = [count, count2id] | |
2224 | text_lst = [] | |
2225 | if colors: | |
2226 | e_c = "" | |
2227 | else: | |
2228 | e_c = "" | |
2229 | tab = " " * indent | |
2230 | ||
2231 | def t2H(tree_hsh, node=('', '_'), level=0): | |
2232 | if args[1] is None: | |
2233 | args[1] = {} | |
2234 | if type(node) == tuple: | |
2235 | if type(node[1]) == tuple: | |
2236 | args[0] += 1 | |
2237 | # join the uuid and the datetime of the instance | |
2238 | args[1][args[0]] = "{0}::{1}".format(node[-1][0], node[-1][-1]) | |
2239 | t = id2Type[node[1][1]] | |
2240 | s_c = '' | |
2241 | col2 = "{0:^{width}}".format( | |
2242 | truncate(node[1][3], width2), width=width2) | |
2243 | if number: | |
2244 | rmlft = width1 - indent * level - 2 - len(str(args[0])) | |
2245 | s = "%s\%s%s [%s] %-*s %s%s" % ( | |
2246 | tab * (level - 1), s_c, unicode(t), | |
2247 | args[0], rmlft, | |
2248 | unicode(truncate(node[1][2], rmlft)), | |
2249 | col2, e_c) | |
2250 | else: | |
2251 | rmlft = width1 - indent * level | |
2252 | s = "%s\%s%s %-*s %s%s" % (tab * (level - 1), s_c, unicode(t), | |
2253 | rmlft, | |
2254 | unicode(truncate(node[1][2], rmlft)), | |
2255 | col2, e_c) | |
2256 | text_lst.append(s) | |
2257 | else: | |
2258 | if node[1].strip() != '_': | |
2259 | text_lst.append("%s[b]%s[/b]" % (tab * (level - 1), node[1])) | |
2260 | else: | |
2261 | text_lst.append("%s%s" % (tab * (level - 1), node)) | |
2262 | if node not in tree_hsh: | |
2263 | return () | |
2264 | level += 1 | |
2265 | nodes = tree_hsh[node] | |
2266 | for n in nodes: | |
2267 | t2H(tree_hsh, n, level) | |
2268 | ||
2269 | t2H(tree) | |
2270 | return [x for x in text_lst], args[0], args[1] | |
2271 | ||
2272 | ||
2273 | def tree2Text(tree, indent=4, width1=43, width2=20, colors=0, | |
2274 | number=False, count=0, count2id=None, depth=0): | |
2275 | global text_lst | |
2276 | args = [count, count2id] | |
2277 | text_lst = [] | |
2278 | if colors: | |
2279 | e_c = "" | |
2280 | else: | |
2281 | e_c = "" | |
2282 | tab = " " * indent | |
2283 | ||
2284 | def t2H(tree_hsh, node=('', '_'), level=0): | |
2285 | if depth and level > depth: | |
2286 | return | |
2287 | if args[1] is None: | |
2288 | args[1] = {} | |
2289 | if type(node) == tuple: | |
2290 | if type(node[1]) == tuple: | |
2291 | args[0] += 1 | |
2292 | # join the uuid and the datetime of the instance | |
2293 | args[1][args[0]] = "{0}::{1}".format(node[-1][0], node[-1][-1]) | |
2294 | t = id2Type[node[1][1]] | |
2295 | s_c = '' | |
2296 | # logger.debug("node13: {0}; width2: {1}".format(node[1][3], width2)) | |
2297 | if node[1][3]: | |
2298 | col2 = "{0:^{width}}".format( | |
2299 | truncate(node[1][3], width2), width=width2) | |
2300 | else: | |
2301 | col2 = "" | |
2302 | if number: | |
2303 | rmlft = width1 - indent * level - 2 - len(str(args[0])) | |
2304 | s = u"{0:s}{1:s}{2:s} [{3:s}] {4:<*s} {5:s}{6:s}".format( | |
2305 | tab * level, | |
2306 | s_c, | |
2307 | unicode(t), | |
2308 | args[0], | |
2309 | rmlft, | |
2310 | unicode(truncate(node[1][2], rmlft)), | |
2311 | col2, e_c) | |
2312 | else: | |
2313 | rmlft = width1 - indent * level | |
2314 | s = "%s%s%s %-*s %s%s" % (tab * level, s_c, unicode(t), rmlft, unicode(truncate(node[1][2], rmlft)), col2, e_c) | |
2315 | text_lst.append(s) | |
2316 | else: | |
2317 | aug = "%s%s" % (tab * level, node[1]) | |
2318 | text_lst.append(aug.split('!!')[0]) | |
2319 | else: | |
2320 | text_lst.append("%s%s" % (tab * level, node)) | |
2321 | if node not in tree_hsh: | |
2322 | return () | |
2323 | level += 1 | |
2324 | nodes = tree_hsh[node] | |
2325 | for n in nodes: | |
2326 | t2H(tree_hsh, n, level) | |
2327 | ||
2328 | t2H(tree) | |
2329 | return [x[indent:] for x in text_lst], args[0], args[1] | |
2330 | ||
2331 | ||
2332 | lst = None | |
2333 | rows = None | |
2334 | row = None | |
2335 | ||
2336 | ||
2337 | def tallyByGroup(list_of_tuples, max_level=0, indnt=3, options=None, export=False): | |
2338 | """ | |
2339 | list_of_tuples should already be sorted and the last component | |
2340 | in each tuple should be a tuple (minutes, value, expense, charge) | |
2341 | to be tallied. | |
2342 | ||
2343 | ('Scotland', 'Glasgow', 'North', 'summary sgn', (306, 10, 20.00, 30.00)), | |
2344 | ('Scotland', 'Glasgow', 'South', 'summary sgs', (960, 10, 45.00, 60.00)), | |
2345 | ('Wales', 'Cardiff', 'summary wc', (396, 10, 22.50, 30.00)), | |
2346 | ('Wales', 'Bangor', 'summary wb', (126, 10, 37.00, 37.00)), | |
2347 | ||
2348 | Recursively process groups and accumulate the totals. | |
2349 | """ | |
2350 | if not options: | |
2351 | options = {} | |
2352 | if not max_level: | |
2353 | max_level = len(list_of_tuples[0]) - 1 | |
2354 | level = -1 | |
2355 | global lst | |
2356 | global head | |
2357 | global auglst | |
2358 | head = [] | |
2359 | auglst = [] | |
2360 | lst = [] | |
2361 | if 'action_template' in options: | |
2362 | action_template = options['action_template'] | |
2363 | else: | |
2364 | action_template = "!hours! $!value!) !label! (!count!)" | |
2365 | ||
2366 | action_template = "!indent!%s" % action_template | |
2367 | ||
2368 | if 'action_minutes' in options and options['action_minutes'] in [6, 12, 15, 30, 60]: | |
2369 | # floating point hours | |
2370 | m = options['action_minutes'] | |
2371 | ||
2372 | tab = " " * indnt | |
2373 | ||
2374 | global rows, row | |
2375 | rows = [] | |
2376 | row = ['' for i in range(max_level + 1)] | |
2377 | ||
2378 | def doLeaf(tup, lvl): | |
2379 | global row, rows, head, auglst | |
2380 | if len(tup) < 2: | |
2381 | rows.append(deepcopy(row)) | |
2382 | return () | |
2383 | k = tup[0] | |
2384 | g = tup[1:] | |
2385 | t = tup[-1] | |
2386 | lvl += 1 | |
2387 | row[lvl] = k | |
2388 | row[-1] = t | |
2389 | hsh = {} | |
2390 | if max_level and lvl > max_level - 1: | |
2391 | rows.append(deepcopy(row)) | |
2392 | return () | |
2393 | indent = " " * indnt | |
2394 | hsh['indent'] = indent * lvl | |
2395 | hsh['count'] = 1 | |
2396 | hsh['minutes'] = t[0] | |
2397 | hsh['value'] = "%.2f" % t[1] # only 2 digits after the decimal point | |
2398 | hsh['expense'] = t[2] | |
2399 | hsh['charge'] = t[3] | |
2400 | hsh['total'] = t[1] + t[3] | |
2401 | if options['action_minutes'] in [6, 12, 15, 30, 60]: | |
2402 | # floating point hours | |
2403 | hsh['hours'] = "{0:n}".format( | |
2404 | ((t[0] // m + (t[0] % m > 0)) * m) / 60.0) | |
2405 | else: | |
2406 | # hours and minutes | |
2407 | hsh['hours'] = "%d:%02d" % (t[0] // 60, t[0] % 60) | |
2408 | hsh['label'] = k | |
2409 | lst.append(expand_template(action_template, hsh, complain=True)) | |
2410 | head.append(lst[-1].lstrip()) | |
2411 | auglst.append(head) | |
2412 | ||
2413 | if len(g) >= 1: | |
2414 | doLeaf(g, lvl) | |
2415 | ||
2416 | def doGroups(tuple_list, lvl): | |
2417 | global row, rows, head, auglst | |
2418 | hsh = {} | |
2419 | lvl += 1 | |
2420 | if max_level and lvl > max_level - 1: | |
2421 | rows.append(deepcopy(row)) | |
2422 | return | |
2423 | hsh['indent'] = tab * lvl | |
2424 | for k, g, t in group_sort(tuple_list): | |
2425 | head = head[:lvl] | |
2426 | row[lvl] = k[-1] | |
2427 | row[-1] = t | |
2428 | hsh['count'] = len(g) | |
2429 | hsh['minutes'] = t[0] # only 2 digits after the decimal point | |
2430 | hsh['value'] = "%.2f" % t[1] | |
2431 | hsh['expense'] = t[2] | |
2432 | hsh['charge'] = t[3] | |
2433 | hsh['total'] = t[1] + t[3] | |
2434 | if options['action_minutes'] in [6, 12, 15, 30, 60]: | |
2435 | # hours and tenths | |
2436 | hsh['hours'] = "{0:n}".format( | |
2437 | ((t[0] // m + (t[0] % m > 0)) * m) / 60.0) | |
2438 | else: | |
2439 | # hours and minutes | |
2440 | hsh['hours'] = "%d:%02d" % (t[0] // 60, t[0] % 60) | |
2441 | ||
2442 | hsh['label'] = k[-1] | |
2443 | lst.append(expand_template(action_template, hsh, complain=True)) | |
2444 | head.append(lst[-1].lstrip()) | |
2445 | if len(head) == max_level: | |
2446 | auglst.append(head) | |
2447 | if len(g) > 1: | |
2448 | doGroups(g, lvl) | |
2449 | else: | |
2450 | doLeaf(g[0], lvl) | |
2451 | ||
2452 | doGroups(list_of_tuples, level) | |
2453 | ||
2454 | for i in range(len(auglst)): | |
2455 | if type(auglst[i][-1]) is str: | |
2456 | summary, uid, trailing = auglst[i][-1].split('!!') | |
2457 | auglst[i][-1] = tuple((uid, 'ac', summary, '')) | |
2458 | res = makeTree(auglst, sort=False) | |
2459 | ||
2460 | if export: | |
2461 | for i in range(len(rows)): | |
2462 | # remove the uuid from the summary | |
2463 | summary = rows[i][-2].split('!!')[0] | |
2464 | rows[i][-2] = summary | |
2465 | return rows | |
2466 | else: | |
2467 | return res | |
2468 | ||
2469 | ||
2470 | def group_sort(row_lst): | |
2471 | # last element of each list component is a (minutes, value, | |
2472 | # expense, charge) tuple. | |
2473 | # next to last element is a summary string. | |
2474 | key = lambda cols: [cols[0]] | |
2475 | for k, group in groupby(row_lst, key): | |
2476 | t = [] | |
2477 | g = [] | |
2478 | for x in group: | |
2479 | t.append(x[-1]) | |
2480 | g.append(x[1:]) | |
2481 | s = tupleSum(t) | |
2482 | yield k, g, s | |
2483 | ||
2484 | ||
2485 | def uniqueId(): | |
2486 | # return unicode(thistime.strftime("%Y%m%dT%H%M%S@etmtk")) | |
2487 | return unicode("{0}etm".format(uuid.uuid4().hex)) | |
2488 | ||
2489 | ||
2490 | def nowAsUTC(): | |
2491 | return datetime.now(tzlocal()).astimezone(tzutc()).replace(tzinfo=None) | |
2492 | ||
2493 | ||
2494 | def datetime2minutes(dt): | |
2495 | if type(dt) != datetime: | |
2496 | return () | |
2497 | t = dt.time() | |
2498 | return t.hour * 60 + t.minute | |
2499 | ||
2500 | ||
2501 | def parse_datetime(dt, timezone='', f=rfmt): | |
2502 | # relative date and month parsing for user input | |
2503 | # logger.debug('dt: {0}, tz: {1}, f: {2}'.format(dt, timezone, f)) | |
2504 | if not dt: | |
2505 | return '' | |
2506 | if type(dt) is datetime: | |
2507 | return parse_dtstr(dt, timezone=timezone, f=f) | |
2508 | ||
2509 | now = datetime.now() | |
2510 | new_y = now.year | |
2511 | now_m = new_m = now.month | |
2512 | new_d = now.day | |
2513 | # easter | |
2514 | estr = estr_regex.search(dt) | |
2515 | if estr: | |
2516 | y = estr.group(1) | |
2517 | e = easter(int(y)) | |
2518 | E = e.strftime("%Y-%m-%d") | |
2519 | dt = estr_regex.sub(E, dt) | |
2520 | try: | |
2521 | rel_mnth = rel_month_regex.search(dt) | |
2522 | if rel_mnth: | |
2523 | mnth, day = map(int, rel_mnth.groups()) | |
2524 | new_m = now_m + mnth | |
2525 | new_d = day | |
2526 | if new_m <= 0: | |
2527 | new_y -= 1 | |
2528 | new_m += 12 | |
2529 | elif new_m > 12: | |
2530 | new_y += 1 | |
2531 | new_m -= 12 | |
2532 | new_date = "%s-%02d-%02d" % (new_y, new_m, new_d) | |
2533 | new_dt = rel_month_regex.sub(new_date, dt) | |
2534 | return parse_dtstr(new_dt, timezone=timezone, f=f) | |
2535 | rel_date = rel_date_regex.search(dt) | |
2536 | if rel_date: | |
2537 | days = int(rel_date.group(0)) | |
2538 | new_date = (now + days * oneday).strftime("%Y-%m-%d") | |
2539 | new_dt = rel_date_regex.sub(new_date, dt) | |
2540 | return parse_dtstr(new_dt, timezone=timezone, f=f) | |
2541 | ||
2542 | return parse_dtstr(dt, timezone=timezone, f=f) | |
2543 | ||
2544 | except Exception: | |
2545 | logger.exception('Could not parse "{0}"'.format(dt)) | |
2546 | return None | |
2547 | ||
2548 | ||
2549 | def parse_dtstr(dtstr, timezone="", f=rfmt): | |
2550 | """ | |
2551 | Take a string and a time zone and return a formatted datetime | |
2552 | string. E.g., ('2/5/12', 'US/Pacific') => "20120205T0000-0800" | |
2553 | """ | |
2554 | msg = "" | |
2555 | if type(dtstr) in [str, unicode]: | |
2556 | if dtstr == 'now': | |
2557 | if timezone: | |
2558 | dt = datetime.now().replace( | |
2559 | tzinfo=tzlocal()).astimezone( | |
2560 | gettz(timezone)).replace(tzinfo=None) | |
2561 | else: | |
2562 | dt = datetime.now() | |
2563 | else: | |
2564 | try: | |
2565 | dt = parse(dtstr) | |
2566 | except: | |
2567 | msg = _("Could not parse: {0}".format(dtstr)) | |
2568 | logger.exception(msg) | |
2569 | return msg | |
2570 | elif dtstr.utcoffset() is None: | |
2571 | dt = dtstr.replace(tzinfo=tzlocal()) | |
2572 | else: | |
2573 | dt = dtstr | |
2574 | if timezone: | |
2575 | dtz = dt.replace(tzinfo=gettz(timezone)) | |
2576 | else: | |
2577 | dtz = dt.replace(tzinfo=tzlocal()) | |
2578 | if windoz and dtz.year < 1970: | |
2579 | y = dtz.year | |
2580 | m = dtz.month | |
2581 | d = dtz.day | |
2582 | H = dtz.hour | |
2583 | M = dtz.minute | |
2584 | dtz = datetime(y, m, d, H, M, 0, 0) | |
2585 | epoch = datetime(1970, 1, 1, 0, 0, 0, 0) | |
2586 | ||
2587 | # dtz.replace(tzinfo=None) | |
2588 | td = epoch - dtz | |
2589 | seconds = td.days * 24 * 60 * 60 + td.seconds | |
2590 | dtz = epoch - timedelta(seconds=seconds) | |
2591 | ||
2592 | return dtz.strftime(f) | |
2593 | ||
2594 | ||
2595 | def parse_dt(s, timezone='', f=rfmt): | |
2596 | dt = parse(parse_datetime(s, timezone, )) | |
2597 | return(dt) | |
2598 | ||
2599 | ||
2600 | def parse_date_period(s): | |
2601 | """ | |
2602 | fuzzy_date [ (+|-) period string] | |
2603 | e.g. mon + 7d: the 2nd Monday on or after today | |
2604 | """ | |
2605 | parts = [x.strip() for x in rsplit(' [+-] ', s)] | |
2606 | try: | |
2607 | dt = parse(parse_datetime(parts[0])) | |
2608 | except Exception: | |
2609 | return 'error: could not parse date "{0}"'.format(parts[0]) | |
2610 | if len(parts) > 1: | |
2611 | try: | |
2612 | pr = parse_period(parts[1]) | |
2613 | except Exception: | |
2614 | return 'error: could not parse period "{0}"'.format(parts[1]) | |
2615 | if ' + ' in s: | |
2616 | return dt + pr | |
2617 | else: | |
2618 | return dt - pr | |
2619 | else: | |
2620 | return dt | |
2621 | ||
2622 | ||
2623 | def parse_period(s, minutes=True): | |
2624 | """\ | |
2625 | Take a case-insensitive period string and return a corresponding timedelta. | |
2626 | Examples: | |
2627 | parse_period('-2W3D4H5M')= -timedelta(weeks=2,days=3,hours=4,minutes=5) | |
2628 | parse_period('1h30m') = timedelta(hours=1, minutes=30) | |
2629 | parse_period('-10') = timedelta(minutes= 10) | |
2630 | where: | |
2631 | W or w: weeks | |
2632 | D or d: days | |
2633 | H or h: hours | |
2634 | M or m: minutes | |
2635 | If an integer is passed or a string that can be converted to an | |
2636 | integer, then return a timedelta corresponding to this number of | |
2637 | minutes if 'minutes = True', and this number of days otherwise. | |
2638 | Minutes will be True for alerts and False for beginbys. | |
2639 | """ | |
2640 | td = timedelta(seconds=0) | |
2641 | if minutes: | |
2642 | unitperiod = oneminute | |
2643 | else: | |
2644 | unitperiod = oneday | |
2645 | try: | |
2646 | m = int(s) | |
2647 | return m * unitperiod | |
2648 | except Exception: | |
2649 | m = int_regex.match(s) | |
2650 | if m: | |
2651 | return td + int(m.group(1)) * unitperiod, "" | |
2652 | # if we get here we should have a period string | |
2653 | m = period_string_regex.match(s) | |
2654 | if not m: | |
2655 | logger.error("Invalid period string: '{0}'".format(s)) | |
2656 | return "Invalid period string: '{0}'".format(s) | |
2657 | m = week_regex.search(s) | |
2658 | if m: | |
2659 | td += int(m.group(1)) * oneweek | |
2660 | m = day_regex.search(s) | |
2661 | if m: | |
2662 | td += int(m.group(1)) * oneday | |
2663 | m = hour_regex.search(s) | |
2664 | if m: | |
2665 | td += int(m.group(1)) * onehour | |
2666 | m = minute_regex.search(s) | |
2667 | if m: | |
2668 | td += int(m.group(1)) * oneminute | |
2669 | m = sign_regex.match(s) | |
2670 | if m and m.group(1) == '-': | |
2671 | return -1 * td | |
2672 | else: | |
2673 | return td | |
2674 | ||
2675 | ||
2676 | def year2string(startyear, endyear): | |
2677 | """compute difference and append suffix""" | |
2678 | diff = int(endyear) - int(startyear) | |
2679 | suffix = 'th' | |
2680 | if diff < 4 or diff > 20: | |
2681 | if diff % 10 == 1: | |
2682 | suffix = 'st' | |
2683 | elif diff % 10 == 2: | |
2684 | suffix = 'nd' | |
2685 | elif diff % 10 == 3: | |
2686 | suffix = 'rd' | |
2687 | return "%d%s" % (diff, suffix) | |
2688 | ||
2689 | ||
2690 | def lst2str(l): | |
2691 | if type(l) != list: | |
2692 | return l | |
2693 | tmp = [] | |
2694 | for item in l: | |
2695 | if type(item) in [datetime]: | |
2696 | tmp.append(parse_dtstr(item, f=zfmt)) | |
2697 | elif type(item) in [timedelta]: | |
2698 | tmp.append(timedelta2Str(item)) | |
2699 | else: # type(i) in [unicode, str]: | |
2700 | tmp.append(str(item)) | |
2701 | return ", ".join(tmp) | |
2702 | ||
2703 | ||
2704 | def hsh2str(hsh, options=None, include_uid=False): | |
2705 | """ | |
2706 | For editing one or more, but not all, instances of an item. Needed: | |
2707 | 1. Add @+ datetime to orig and make copy sans all repeating info and | |
2708 | with @s datetime. | |
2709 | 2. Add &r datetime - ONEMINUTE to each _r in orig and make copy with | |
2710 | @s datetime | |
2711 | 3. Add &f datetime to selected job. | |
2712 | """ | |
2713 | if not options: | |
2714 | options = {} | |
2715 | msg = [] | |
2716 | if '_summary' not in hsh: | |
2717 | hsh['_summary'] = '' | |
2718 | if '_group_summary' in hsh: | |
2719 | sl = ["%s %s" % (hsh['itemtype'], hsh['_group_summary'])] | |
2720 | if 'i' in hsh: | |
2721 | # fix the item index | |
2722 | hsh['i'] = hsh['i'].split(':')[0] | |
2723 | else: | |
2724 | sl = ["%s %s" % (hsh['itemtype'], hsh['_summary'])] | |
2725 | if 'i' not in hsh or not hsh['i']: | |
2726 | hsh['i'] = uniqueId() | |
2727 | bad_keys = [x for x in hsh.keys() if x not in all_keys] | |
2728 | if bad_keys: | |
2729 | omitted = [] | |
2730 | for key in bad_keys: | |
2731 | omitted.append('@{0} {1}'.format(key, hsh[key])) | |
2732 | msg.append("unrecogized entries: {0}".format(", ".join(omitted))) | |
2733 | for key in at_keys: | |
2734 | amp_key = None | |
2735 | if hsh['itemtype'] == "=": | |
2736 | prefix = "" | |
2737 | elif key in options['prefix_uses']: | |
2738 | prefix = options['prefix'] | |
2739 | else: | |
2740 | prefix = "" | |
2741 | if key == 'a' and '_a' in hsh: | |
2742 | alerts = [] | |
2743 | for alert in hsh["_a"]: | |
2744 | triggers, acts, arguments = alert | |
2745 | _ = "@a %s" % ", ".join([fmt_period(x) for x in triggers]) | |
2746 | if acts: | |
2747 | _ += ": %s" % ", ".join(acts) | |
2748 | if arguments: | |
2749 | arg_strings = [] | |
2750 | for arg in arguments: | |
2751 | arg_strings.append(", ".join(arg)) | |
2752 | _ += "; %s" % "; ".join(arg_strings) | |
2753 | alerts.append(_) | |
2754 | sl.extend(alerts) | |
2755 | elif key in ['r', 'j']: | |
2756 | at_key = key | |
2757 | keys = amp_keys[key] | |
2758 | key = "_%s" % key | |
2759 | elif key in ['+', '-']: | |
2760 | keys = [] | |
2761 | elif key in ['t', 'l', 'd']: | |
2762 | keys = [] | |
2763 | else: | |
2764 | keys = [] | |
2765 | ||
2766 | if key in hsh and hsh[key]: | |
2767 | # since r and j can repeat, value will be a list | |
2768 | value = hsh[key] | |
2769 | if keys: | |
2770 | # @r or @j --- value will be a list of hashes or | |
2771 | # possibly, in the case of @a, a list of lists. f | |
2772 | # will be the first key for @r and t will be the | |
2773 | # first for @a | |
2774 | omitted = [] | |
2775 | for v in value: | |
2776 | for k in v.keys(): | |
2777 | if k not in keys: | |
2778 | omitted.append('&{0} {1}'.format(k, v[k])) | |
2779 | if omitted: | |
2780 | msg.append("unrecogized entries: {0}".format(", ".join(omitted))) | |
2781 | ||
2782 | tmp = [] | |
2783 | for h in value: | |
2784 | if unicode(keys[0]) not in h: | |
2785 | logger.warning("{0} not in {1}".format(keys[0], h)) | |
2786 | continue | |
2787 | tmp.append('%s@%s %s' % (prefix, at_key, | |
2788 | lst2str(h[unicode(keys[0])]))) | |
2789 | for amp_key in keys[1:]: | |
2790 | if amp_key in h: | |
2791 | if at_key == 'j' and amp_key == 'f': | |
2792 | pairs = [] | |
2793 | for pair in h['f']: | |
2794 | pairs.append(";".join([ | |
2795 | x.strftime(zfmt) for x in pair if x])) | |
2796 | v = (', '.join(pairs)) | |
2797 | elif at_key == 'j' and amp_key == 'h': | |
2798 | pairs = [] | |
2799 | for pair in h['h']: | |
2800 | pairs.append(";".join([ | |
2801 | x.strftime(zfmt) for x in pair if x])) | |
2802 | v = (', '.join(pairs)) | |
2803 | elif amp_key == 'e': | |
2804 | try: | |
2805 | v = fmt_period(h['e']) | |
2806 | except Exception: | |
2807 | v = h['e'] | |
2808 | logger.error( | |
2809 | "error: could not parse h['e']: '{0}'".format( | |
2810 | h['e'])) | |
2811 | else: | |
2812 | v = lst2str(h[amp_key]) | |
2813 | tmp.append('&%s %s' % (amp_key, v)) | |
2814 | if tmp: | |
2815 | sl.append(" ".join(tmp)) | |
2816 | elif key == 's': | |
2817 | try: | |
2818 | sl.append("%s@%s %s" % (prefix, key, fmt_datetime(value, options=options))) | |
2819 | except: | |
2820 | msg.append("problem with @{0}: {1}".format(key, value)) | |
2821 | elif key == 'e': | |
2822 | try: | |
2823 | sl.append("%s@%s %s" % (prefix, key, fmt_period(value))) | |
2824 | except: | |
2825 | msg.append("problem with @{0}: {1}".format(key, value)) | |
2826 | elif key == 'f': | |
2827 | tmp = [] | |
2828 | for pair in hsh['f']: | |
2829 | tmp.append(";".join([x.strftime(zfmt) for x in pair if x])) | |
2830 | sl.append("%s@f %s" % (prefix, ", {0}".format(prefix).join(tmp))) | |
2831 | elif key == 'i': | |
2832 | if include_uid and hsh['itemtype'] != "=": | |
2833 | sl.append("prefix@i {0}".format(prefix, value)) | |
2834 | elif key == 'h': | |
2835 | tmp = [] | |
2836 | for pair in hsh['h']: | |
2837 | tmp.append(";".join([x.strftime(zfmt) for x in pair if x])) | |
2838 | sl.append("%s@h %s" % (prefix, ", {0}".format(prefix).join(tmp))) | |
2839 | else: | |
2840 | sl.append("%s@%s %s" % (prefix, key, lst2str(value))) | |
2841 | return " ".join(sl), msg | |
2842 | ||
2843 | ||
2844 | def process_all_datafiles(options): | |
2845 | prefix, filelist = getFiles(options['datadir']) | |
2846 | return process_data_file_list(filelist, options=options) | |
2847 | ||
2848 | ||
2849 | def process_data_file_list(filelist, options=None): | |
2850 | if not options: | |
2851 | options = {} | |
2852 | messages = [] | |
2853 | file2lastmodified = {} | |
2854 | bad_datafiles = {} | |
2855 | file2uuids = {} | |
2856 | uuid2hashes = {} | |
2857 | uuid2labels = {} | |
2858 | for f, r in filelist: | |
2859 | file2lastmodified[(f, r)] = os.path.getmtime(f) | |
2860 | msg, hashes, u2l = process_one_file(f, r, options) | |
2861 | uuid2labels.update(u2l) | |
2862 | if msg: | |
2863 | messages.append("errors loading %s:" % r) | |
2864 | messages.extend(msg) | |
2865 | try: | |
2866 | for hsh in hashes: | |
2867 | if hsh['itemtype'] == '=': | |
2868 | continue | |
2869 | uid = hsh['i'] | |
2870 | uuid2hashes[uid] = hsh | |
2871 | file2uuids.setdefault(r, []).append(uid) | |
2872 | except Exception: | |
2873 | fio = StringIO() | |
2874 | msg = fio.getvalue() | |
2875 | bad_datafiles[r] = msg | |
2876 | logger.error('Error processing: {0}\n{1}'.format(r, msg)) | |
2877 | return uuid2hashes, uuid2labels, file2uuids, file2lastmodified, bad_datafiles, messages | |
2878 | ||
2879 | ||
2880 | def process_one_file(full_filename, rel_filename, options=None): | |
2881 | if not options: | |
2882 | options = {} | |
2883 | file_items = getFileItems(full_filename, rel_filename) | |
2884 | return items2Hashes(file_items, options) | |
2885 | ||
2886 | ||
2887 | def getFiles(root, include=r'*.txt', exclude=r'.*', other=[]): | |
2888 | """ | |
2889 | Return the common prefix and a list of full paths from root | |
2890 | :param root: directory | |
2891 | :return: common prefix of files and a list of full file paths | |
2892 | """ | |
2893 | # includes = r'*.txt' | |
2894 | # excludes = r'.*' | |
2895 | paths = [root] | |
2896 | filelist = [] | |
2897 | other.sort() | |
2898 | for path in other: | |
2899 | paths.append(path) | |
2900 | common_prefix = os.path.commonprefix(paths) | |
2901 | for path in other: | |
2902 | rel_path = relpath(path, common_prefix) | |
2903 | filelist.append((path, rel_path)) | |
2904 | for path, dirs, files in os.walk(root): | |
2905 | # exclude dirs | |
2906 | dirs[:] = [os.path.join(path, d) for d in dirs | |
2907 | if not fnmatch.fnmatch(d, exclude)] | |
2908 | ||
2909 | # exclude/include files | |
2910 | files = [os.path.join(path, f) for f in files | |
2911 | if not fnmatch.fnmatch(f, exclude)] | |
2912 | files = [os.path.normpath(f) for f in files if fnmatch.fnmatch(f, include)] | |
2913 | ||
2914 | for fname in files: | |
2915 | rel_path = relpath(fname, common_prefix) | |
2916 | filelist.append((fname, rel_path)) | |
2917 | return common_prefix, filelist | |
2918 | ||
2919 | ||
2920 | def getAllFiles(root, include=r'*', exclude=r'.*', other=[]): | |
2921 | """ | |
2922 | Return the common prefix and a list of full paths from root | |
2923 | :param root: directory | |
2924 | :return: common prefix of files and a list of full file paths | |
2925 | """ | |
2926 | paths = [root] | |
2927 | filelist = [] | |
2928 | for path in other: | |
2929 | paths.append(path) | |
2930 | other.sort() | |
2931 | common_prefix = os.path.commonprefix(paths) | |
2932 | for path in other: | |
2933 | rel_path = relpath(path, common_prefix) | |
2934 | filelist.append((path, rel_path)) | |
2935 | for path, dirs, files in os.walk(root): | |
2936 | # exclude dirs | |
2937 | dirs[:] = [os.path.join(path, d) for d in dirs | |
2938 | if not fnmatch.fnmatch(d, exclude)] | |
2939 | # exclude/include files | |
2940 | files = [os.path.join(path, f) for f in files | |
2941 | if not fnmatch.fnmatch(f, exclude)] | |
2942 | files = [os.path.normpath(f) for f in files if fnmatch.fnmatch(f, include)] | |
2943 | for fname in files: | |
2944 | rel_path = relpath(fname, common_prefix) | |
2945 | filelist.append((fname, rel_path)) | |
2946 | if not (dirs or files): | |
2947 | # empty | |
2948 | rel_path = relpath(path, common_prefix) | |
2949 | filelist.append((path, rel_path)) | |
2950 | return common_prefix, filelist | |
2951 | ||
2952 | ||
2953 | def getFileTuples(root, include=r'*.txt', exclude=r'.*', all=False, other=[]): | |
2954 | if all: | |
2955 | common_prefix, filelist = getAllFiles(root, include, exclude, other=other) | |
2956 | else: | |
2957 | common_prefix, filelist = getFiles(root, include, exclude, other=other) | |
2958 | lst = [] | |
2959 | prior = [] | |
2960 | for fp, rp in filelist: | |
2961 | drive, tup = os_path_splitall(rp) | |
2962 | for i in range(0, len(tup)): | |
2963 | if len(prior) > i and tup[i] == prior[i]: | |
2964 | continue | |
2965 | prior = tup[:i] | |
2966 | disable = (i < len(tup) - 1) or os.path.isdir(fp) | |
2967 | lst.append(("{0}{1}".format(" " * 6 * i, tup[i]), rp, disable)) | |
2968 | return common_prefix, lst | |
2969 | ||
2970 | ||
2971 | def os_path_splitall(path, debug=False): | |
2972 | parts = [] | |
2973 | drive, path = os.path.splitdrive(path) | |
2974 | while True: | |
2975 | newpath, tail = os.path.split(path) | |
2976 | if newpath == path: | |
2977 | assert not tail | |
2978 | if path: | |
2979 | parts.append(path) | |
2980 | break | |
2981 | parts.append(tail) | |
2982 | path = newpath | |
2983 | parts.reverse() | |
2984 | return drive, parts | |
2985 | ||
2986 | ||
2987 | def lines2Items(lines): | |
2988 | """ | |
2989 | Group the lines into logical items and return them. | |
2990 | """ | |
2991 | # make sure we have a trailing new-line. Yes, we really need this. | |
2992 | lines.append('\n') | |
2993 | linenum = 0 | |
2994 | linenums = [] | |
2995 | logical_line = [] | |
2996 | for line in lines: | |
2997 | linenums.append(linenum) | |
2998 | linenum += 1 | |
2999 | # preserve new lines and leading whitespace within logical lines | |
3000 | stripped = line.rstrip() | |
3001 | m = item_regex.match(stripped) | |
3002 | if m: | |
3003 | if logical_line: | |
3004 | yield (''.join(logical_line)) | |
3005 | logical_line = [] | |
3006 | linenums = [] | |
3007 | logical_line.append("%s\n" % line.rstrip()) | |
3008 | elif stripped: | |
3009 | # a line which does not continue, end of logical line | |
3010 | logical_line.append("%s\n" % line.rstrip()) | |
3011 | elif logical_line: | |
3012 | # preserve interior empty lines | |
3013 | logical_line.append("\n") | |
3014 | if logical_line: | |
3015 | # end of sequence implies end of last logical line | |
3016 | yield (''.join(logical_line)) | |
3017 | ||
3018 | ||
3019 | def getFileItems(full_name, rel_name, append_newline=True): | |
3020 | """ | |
3021 | Group the lines in file f into logical items and return them. | |
3022 | :param full_name: including datadir | |
3023 | :param rel_name: from datadir | |
3024 | :param append_newline: bool, default True | |
3025 | """ | |
3026 | fo = codecs.open(full_name, 'r', file_encoding) | |
3027 | lines = fo.readlines() | |
3028 | fo.close() | |
3029 | # make sure we have a trailing new-line. Yes, we really need this. | |
3030 | if append_newline: | |
3031 | lines.append('\n') | |
3032 | linenum = 0 | |
3033 | linenums = [] | |
3034 | logical_line = [] | |
3035 | for line in lines: | |
3036 | linenums.append(linenum) | |
3037 | linenum += 1 | |
3038 | # preserve new lines and leading whitespace within logical lines | |
3039 | stripped = line.rstrip() | |
3040 | m = item_regex.match(stripped) | |
3041 | if m: | |
3042 | if logical_line: | |
3043 | yield (''.join(logical_line), rel_name, linenums) | |
3044 | logical_line = [] | |
3045 | linenums = [] | |
3046 | logical_line.append("%s\n" % line.rstrip()) | |
3047 | elif stripped: | |
3048 | # a line which does not continue, end of logical line | |
3049 | logical_line.append("%s\n" % line.rstrip()) | |
3050 | elif logical_line: | |
3051 | # preserve interior empty lines | |
3052 | logical_line.append("\n") | |
3053 | if logical_line: | |
3054 | # end of sequence implies end of last logical line | |
3055 | yield (''.join(logical_line), rel_name, linenums) | |
3056 | ||
3057 | ||
3058 | def items2Hashes(list_of_items, options=None): | |
3059 | """ | |
3060 | Return a list of messages and a list of hashes corresponding to items in | |
3061 | list_of_items. | |
3062 | """ | |
3063 | if not options: | |
3064 | options = {} | |
3065 | messages = [] | |
3066 | hashes = [] | |
3067 | uuid2labels = {} | |
3068 | defaults = {} | |
3069 | # in_task_group = False | |
3070 | for item, rel_name, linenums in list_of_items: | |
3071 | hsh, msg = str2hsh(item, options=options) | |
3072 | tmp_hsh = {} | |
3073 | tmp_hsh.update(defaults) | |
3074 | tmp_hsh.update(hsh) | |
3075 | hsh = tmp_hsh | |
3076 | try: | |
3077 | hsh['fileinfo'] = (rel_name, linenums[0], linenums[-1]) | |
3078 | except: | |
3079 | raise ValueError("exception in fileinfo:", | |
3080 | rel_name, linenums, "\n", hsh) | |
3081 | if msg: | |
3082 | lines = [] | |
3083 | item = item.strip() | |
3084 | if len(item) > 56: | |
3085 | lines.extend(wrap(item, 56)) | |
3086 | else: | |
3087 | lines.append(item) | |
3088 | for line in lines: | |
3089 | messages.append(" %s" % line) | |
3090 | for m in msg: | |
3091 | messages.append(' %s' % m) | |
3092 | ||
3093 | # put the bad item in the inbox for repairs | |
3094 | hsh['_summary'] = "{0} {1}".format(hsh['itemtype'], hsh['_summary']) | |
3095 | hsh['itemtype'] = "$" | |
3096 | hsh['i'] = uniqueId() | |
3097 | hsh['errors'] = "\n".join(msg) | |
3098 | logger.warn("hsh errors: {0}".format(hsh['errors'])) | |
3099 | # no more processing | |
3100 | # ('hsh:', hsh) | |
3101 | hashes.append(hsh) | |
3102 | continue | |
3103 | ||
3104 | itemtype = hsh['itemtype'] | |
3105 | if itemtype == '$': | |
3106 | # inbasket item | |
3107 | hashes.append(hsh) | |
3108 | elif itemtype == '#': | |
3109 | # deleted item | |
3110 | # yield this so that hidden entries are in file2uuids | |
3111 | hashes.append(hsh) | |
3112 | elif itemtype == '=': | |
3113 | # set group defaults | |
3114 | # hashes.append(this so that default entries are in file2uuids | |
3115 | defaults = hsh | |
3116 | hashes.append(hsh) | |
3117 | elif itemtype == '+': | |
3118 | # needed for task group: | |
3119 | # the original hsh with the summary adjusted to show | |
3120 | # the number of tasks and type changed to '-' and the | |
3121 | # date updated to refect the due (keep) due date | |
3122 | # a non-repeating hash with type '+' for each job | |
3123 | # with current due date for unfinished jobs and | |
3124 | # otherwise the finished date. These will appear | |
3125 | # in days but not folders | |
3126 | # '+' items will be not be added to folders | |
3127 | # Finishing a group task should be handled separately | |
3128 | # when the last job is finished and 'f' is updated. | |
3129 | # Here we assume that one or more jobs are unfinished. | |
3130 | queue_hsh = {} | |
3131 | tmp_hsh = {} | |
3132 | tmp_hsh.update(defaults) | |
3133 | tmp_hsh.update(hsh) | |
3134 | group_defaults = tmp_hsh | |
3135 | group_task = deepcopy(group_defaults) | |
3136 | done, due, following = getDoneAndTwo(group_task) | |
3137 | if 'f' in group_defaults and due: | |
3138 | del group_defaults['f'] | |
3139 | group_defaults['s'] = due | |
3140 | if 'rrule' in group_defaults: | |
3141 | del group_defaults['rrule'] | |
3142 | prereqs = [] | |
3143 | last_level = 1 | |
3144 | uid = hsh['i'] | |
3145 | summary = hsh['_summary'] | |
3146 | if 'j' not in hsh: | |
3147 | continue | |
3148 | job_num = 0 | |
3149 | jobs = [x for x in hsh['j']] | |
3150 | completed = [] | |
3151 | num_jobs = len(jobs) | |
3152 | del group_defaults['j'] | |
3153 | if following: | |
3154 | del group_task['j'] | |
3155 | # group_task['s'] = following | |
3156 | group_task['s'] = following | |
3157 | group_task['_summary'] = "%s [%s jobs]" % ( | |
3158 | summary, len(jobs)) | |
3159 | hashes.append(group_task) | |
3160 | for job in jobs: | |
3161 | tmp_hsh = {} | |
3162 | tmp_hsh.update(group_defaults) | |
3163 | tmp_hsh.update(job) | |
3164 | job = tmp_hsh | |
3165 | job['itemtype'] = '+' | |
3166 | job_num += 1 | |
3167 | current_id = "%s:%02d" % (uid, job_num) | |
3168 | if 'f' in job: | |
3169 | # this will be a done:due pair with the due | |
3170 | # of the current group task | |
3171 | completed.append(current_id) | |
3172 | job["_summary"] = "%s %d/%d: %s" % ( | |
3173 | summary, job_num, num_jobs, job['j']) | |
3174 | del job['j'] | |
3175 | if 'q' not in job: | |
3176 | logger.warn('error: q missing from job') | |
3177 | continue | |
3178 | try: | |
3179 | current_level = int(job['q']) | |
3180 | except: | |
3181 | logger.warn('error: bad value for q', job['q']) | |
3182 | continue | |
3183 | job['i'] = current_id | |
3184 | ||
3185 | queue_hsh.setdefault(current_level, set([])).add(current_id) | |
3186 | ||
3187 | if current_level < last_level: | |
3188 | prereqs = [] | |
3189 | for k in queue_hsh: | |
3190 | if k > current_level: | |
3191 | queue_hsh[k] = set([]) | |
3192 | for k in queue_hsh: | |
3193 | if k < current_level: | |
3194 | prereqs.extend(list(queue_hsh[k])) | |
3195 | job['prereqs'] = [x for x in prereqs if x not in completed] | |
3196 | ||
3197 | last_level = current_level | |
3198 | try: | |
3199 | job['fileinfo'] = (rel_name, linenums[0], linenums[-1]) | |
3200 | except: | |
3201 | logger.exception("fileinfo: {0}.{1}".format(rel_name, linenums)) | |
3202 | logger.debug('appending job: {0}'.format(job)) | |
3203 | hashes.append(job) | |
3204 | else: | |
3205 | tmp_hsh = {} | |
3206 | tmp_hsh.update(defaults) | |
3207 | tmp_hsh.update(hsh) | |
3208 | hsh = tmp_hsh | |
3209 | try: | |
3210 | hsh['fileinfo'] = (rel_name, linenums[0], linenums[-1]) | |
3211 | except: | |
3212 | raise ValueError("exception in fileinfo:", | |
3213 | rel_name, linenums, "\n", hsh) | |
3214 | hashes.append(hsh) | |
3215 | if itemtype not in ['=', '$']: | |
3216 | tmp = [' '] | |
3217 | for key in label_keys: | |
3218 | if key in hsh and hsh[key]: | |
3219 | # dump the '_' | |
3220 | key = key[-1] | |
3221 | tmp.append(key) | |
3222 | # else: | |
3223 | # tmp.append(' ') | |
3224 | uuid2labels[hsh['i']] = "".join(tmp) | |
3225 | return messages, hashes, uuid2labels | |
3226 | ||
3227 | ||
3228 | def get_reps(bef, hsh): | |
3229 | if hsh['itemtype'] in ['+', '-', '%']: | |
3230 | done, due, following = getDoneAndTwo(hsh) | |
3231 | if hsh['itemtype'] == '+': | |
3232 | if done and following: | |
3233 | start = following | |
3234 | elif due: | |
3235 | start = due | |
3236 | elif due: | |
3237 | start = due | |
3238 | else: | |
3239 | start = done | |
3240 | else: | |
3241 | start = parse(parse_dtstr(hsh['s'])).replace(tzinfo=None) | |
3242 | tmp = [] | |
3243 | if not start: | |
3244 | return False, [] | |
3245 | for hsh_r in hsh['_r']: | |
3246 | tests = [ | |
3247 | u'f' in hsh_r and hsh_r['f'] == 'l', | |
3248 | u't' in hsh_r, | |
3249 | u'u' in hsh_r | |
3250 | ] | |
3251 | for test in tests: | |
3252 | passed = False | |
3253 | if test: | |
3254 | passed = True | |
3255 | break | |
3256 | if not passed: | |
3257 | break | |
3258 | ||
3259 | if passed: | |
3260 | # finite, get instances after start | |
3261 | try: | |
3262 | tmp.extend([x for x in hsh['rrule'] if x >= start]) | |
3263 | except: | |
3264 | logger.exception('done: {0}; due: {1}; following: {2}; start: {3}; rrule: {4}'.format(done, due, following, start, hsh['rrule'])) | |
3265 | else: | |
3266 | tmp.extend(list(hsh['rrule'].between(start, bef, inc=True))) | |
3267 | tmp.append(hsh['rrule'].after(bef, inc=False)) | |
3268 | ||
3269 | if windoz: | |
3270 | ret = [] | |
3271 | epoch = datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=None) | |
3272 | for i in tmp: | |
3273 | if not i: | |
3274 | continue | |
3275 | # i.replace(tzinfo=gettz(hsh['z'])) | |
3276 | if i.year < 1970: | |
3277 | # i.replace(tzinfo=gettz(hsh['z'])) | |
3278 | td = epoch - i | |
3279 | i = epoch - td | |
3280 | else: | |
3281 | i.replace(tzinfo=gettz(hsh['z'])).astimezone(tzlocal()).replace(tzinfo=None) | |
3282 | ret.append(i) | |
3283 | return passed, ret | |
3284 | ||
3285 | return passed, [j.replace(tzinfo=gettz(hsh['z'])).astimezone(tzlocal()).replace(tzinfo=None) for j in tmp if j] | |
3286 | ||
3287 | ||
3288 | def get_rrulestr(hsh, key_hsh=rrule_hsh): | |
3289 | """ | |
3290 | Parse the rrule relevant information in hsh and return a | |
3291 | corresponding RRULE string. | |
3292 | """ | |
3293 | if 'r' not in hsh: | |
3294 | return () | |
3295 | try: | |
3296 | lofh = hsh['r'] | |
3297 | except: | |
3298 | raise ValueError("Could not load rrule:", hsh['r']) | |
3299 | ret = [] | |
3300 | l = [] | |
3301 | if type(lofh) == dict: | |
3302 | lofh = [lofh] | |
3303 | for h in lofh: | |
3304 | if 'f' in h and h['f'] == 'l': | |
3305 | # list only | |
3306 | l = [] | |
3307 | else: | |
3308 | try: | |
3309 | l = ["RRULE:FREQ=%s" % freq_hsh[h['f']]] | |
3310 | except: | |
3311 | logger.exception("bad rrule: {0}, {1}, {2}\n{3}".format(rrule, "\nh:", h, hsh)) | |
3312 | ||
3313 | for k in rrule_keys: | |
3314 | if k in h and h[k]: | |
3315 | v = h[k] | |
3316 | if type(v) == list: | |
3317 | v = ",".join(map(str, v)) | |
3318 | if k == 'w': | |
3319 | # make weekdays upper case | |
3320 | v = v.upper() | |
3321 | m = threeday_regex.search(v) | |
3322 | while m: | |
3323 | v = threeday_regex.sub("%s" % m.group(1)[:2], | |
3324 | v, count=1) | |
3325 | m = threeday_regex.search(v) | |
3326 | l.append("%s=%s" % (rrule_hsh[k], v)) | |
3327 | if 'u' in h: | |
3328 | dt = parse(parse_dtstr( | |
3329 | h['u'], hsh['z'])).replace(tzinfo=None) | |
3330 | l.append("UNTIL=%s" % dt.strftime(sfmt)) | |
3331 | ret.append(";".join(l)) | |
3332 | return "\n".join(ret) | |
3333 | ||
3334 | ||
3335 | def get_rrule(hsh): | |
3336 | """ | |
3337 | Used to process the rulestr entry. Dates and times in *rstr* | |
3338 | will be datetimes with offsets. Parameters *aft* and *bef* are | |
3339 | UTC datetimes. Datetimes from *rule* will be returned as local | |
3340 | times. | |
3341 | :param hsh: item hash | |
3342 | """ | |
3343 | rlst = [] | |
3344 | warn = [] | |
3345 | if 'z' not in hsh: | |
3346 | hsh['z'] = local_timezone | |
3347 | if 'o' in hsh and hsh['o'] == 'r' and 'f' in hsh: | |
3348 | # restart | |
3349 | dtstart = hsh['f'][-1][0].replace(tzinfo=gettz(hsh['z'])) | |
3350 | elif 's' in hsh: | |
3351 | dtstart = parse(parse_dtstr( | |
3352 | hsh['s'], hsh['z'])).replace(tzinfo=None) | |
3353 | else: | |
3354 | dtstart = datetime.now() | |
3355 | if 'r' in hsh: | |
3356 | if hsh['r']: | |
3357 | rlst.append(hsh['r']) | |
3358 | if dtstart: | |
3359 | rlst.insert(0, "DTSTART:%s" % dtstart.strftime(sfmt)) | |
3360 | if '+' in hsh: | |
3361 | parts = hsh['+'] | |
3362 | if type(parts) != list: | |
3363 | parts = [parts] | |
3364 | if parts: | |
3365 | for part in map(str, parts): | |
3366 | # rlst.append("RDATE:%s" % parse(part).strftime(sfmt)) | |
3367 | rlst.append("RDATE:%s" % parse_datetime( | |
3368 | part, f=sfmt)) | |
3369 | if '-' in hsh: | |
3370 | tmprule = dtR.rrulestr("\n".join(rlst)) | |
3371 | parts = hsh['-'] | |
3372 | if type(parts) != list: | |
3373 | parts = [parts] | |
3374 | if parts: | |
3375 | for part in map(str, parts): | |
3376 | thisdatetime = parse(parse_datetime(part, f=sfmt)) | |
3377 | beforedatetime = tmprule.before(thisdatetime, inc=True) | |
3378 | if beforedatetime != thisdatetime: | |
3379 | warn.append(_( | |
3380 | "{0} is listed in @- but doesn't match any datetimes generated by @r.").format( | |
3381 | thisdatetime.strftime(rfmt))) | |
3382 | rlst.append("EXDATE:%s" % parse_datetime( | |
3383 | part, f=sfmt)) | |
3384 | rulestr = "\n".join(rlst) | |
3385 | try: | |
3386 | rule = dtR.rrulestr(rulestr) | |
3387 | except: | |
3388 | raise ValueError("could not create rule from", rulestr) | |
3389 | return rulestr, rule, warn | |
3390 | ||
3391 | # checks | |
3392 | # all require @i | |
3393 | # * -> @s | |
3394 | # % -> @u | |
3395 | # @a, @r -> @s | |
3396 | # @+, @- -> @r | |
3397 | ||
3398 | ||
3399 | def checkhsh(hsh): | |
3400 | messages = [] | |
3401 | if hsh['itemtype'] in ['*', '~', '^'] and 's' not in hsh: | |
3402 | messages.append( | |
3403 | "An entry for @s is required for events, actions and occasions.") | |
3404 | elif hsh['itemtype'] in ['~'] and 'e' not in hsh and 'x' not in hsh: | |
3405 | messages.append("An entry for either @e or @x is required for actions.") | |
3406 | if ('a' in hsh or 'r' in hsh) and 's' not in hsh: | |
3407 | messages.append( | |
3408 | "An entry for @s is required for items with either @a or @r entries.") | |
3409 | if ('+' in hsh or '-' in hsh) and 'r' not in hsh: | |
3410 | messages.extend( | |
3411 | ["An entry for @r is required for items with", | |
3412 | "either @+ or @- entries."]) | |
3413 | return messages | |
3414 | ||
3415 | ||
3416 | def str2opts(s, options=None, cli=True): | |
3417 | if not options: | |
3418 | options = {} | |
3419 | filters = {} | |
3420 | if 'calendars' in options: | |
3421 | cal_pattern = r'^%s' % '|'.join( | |
3422 | [x[2] for x in options['calendars'] if x[1]]) | |
3423 | filters['cal_regex'] = re.compile(cal_pattern) | |
3424 | s = str(s) | |
3425 | op_str = s.split('#')[0] | |
3426 | parts = minus_regex.split(op_str) | |
3427 | head = parts.pop(0) | |
3428 | report = head[0] | |
3429 | groupbystr = head[1:].strip() | |
3430 | if not report or report not in ['c', 'a'] or not groupbystr: | |
3431 | return {} | |
3432 | grpby = {'report': report} | |
3433 | filters['dates'] = False | |
3434 | dated = {'grpby': False} | |
3435 | filters['report'] = unicode(report) | |
3436 | filters['omit'] = [True, []] | |
3437 | filters['neg_fields'] = [] | |
3438 | filters['pos_fields'] = [] | |
3439 | groupbylst = [unicode(x.strip()) for x in groupbystr.split(';')] | |
3440 | grpby['lst'] = groupbylst | |
3441 | for part in groupbylst: | |
3442 | if groupdate_regex.search(part): | |
3443 | dated['grpby'] = True | |
3444 | filters['dates'] = True | |
3445 | elif part not in ['c', 'u'] and part[0] not in ['k', 'f', 't']: | |
3446 | term_print( | |
3447 | str(_('Ignoring invalid grpby part: "{0}"'.format(part)))) | |
3448 | groupbylst.remove(part) | |
3449 | if not groupbylst: | |
3450 | return '', '', '' | |
3451 | # we'll split cols on :: after applying fmts to the string | |
3452 | grpby['cols'] = "::".join(["{%d}" % i for i in range(len(groupbylst))]) | |
3453 | grpby['fmts'] = [] | |
3454 | grpby['tuples'] = [] | |
3455 | filters['grpby'] = ['_summary'] | |
3456 | # include = {'y', 'm', 'w', 'd'} | |
3457 | include = {'y', 'm', 'd'} | |
3458 | for group in groupbylst: | |
3459 | d_lst = [] | |
3460 | if groupdate_regex.search(group): | |
3461 | if 'w' in group: | |
3462 | # groupby week or some other date spec, not both | |
3463 | group = "w" | |
3464 | d_lst.append('w') | |
3465 | include.discard('w') | |
3466 | if 'y' in group: | |
3467 | include.discard('y') | |
3468 | if 'M' in group: | |
3469 | include.discard('m') | |
3470 | if 'd' in group: | |
3471 | include.discard('d') | |
3472 | else: | |
3473 | if 'y' in group: | |
3474 | d_lst.append('yyyy') | |
3475 | include.discard('y') | |
3476 | if 'M' in group: | |
3477 | d_lst.append('MM') | |
3478 | include.discard('m') | |
3479 | if 'd' in group: | |
3480 | d_lst.append('dd') | |
3481 | include.discard('d') | |
3482 | grpby['tuples'].append(" ".join(d_lst)) | |
3483 | grpby['fmts'].append( | |
3484 | "d_to_str(tup[-3], '%s')" % group) | |
3485 | ||
3486 | elif '[' in group: | |
3487 | if group[0] == 'f': | |
3488 | if ':' in group: | |
3489 | grpby['fmts'].append( | |
3490 | "'/'.join(rsplit('/', hsh['fileinfo'][0])%s)" % | |
3491 | (group[1:])) | |
3492 | grpby['tuples'].append( | |
3493 | "'/'.join(rsplit('/', hsh['fileinfo'][0])%s)" % | |
3494 | (group[1:])) | |
3495 | else: | |
3496 | grpby['fmts'].append( | |
3497 | "rsplit('/', hsh['fileinfo'][0])%s" % (group[1:])) | |
3498 | grpby['tuples'].append( | |
3499 | "rsplit('/', hsh['fileinfo'][0])%s" % (group[1:])) | |
3500 | elif group[0] == 'k': | |
3501 | if ':' in group: | |
3502 | grpby['fmts'].append( | |
3503 | "':'.join(rsplit(':', hsh['%s'])%s)" % | |
3504 | (group[0], group[1:])) | |
3505 | grpby['tuples'].append( | |
3506 | "':'.join(rsplit(':', hsh['%s'])%s)" % | |
3507 | (group[0], group[1:])) | |
3508 | else: | |
3509 | grpby['fmts'].append( | |
3510 | "rsplit(':', hsh['%s'])%s" % (group[0], group[1:])) | |
3511 | grpby['tuples'].append( | |
3512 | "rsplit(':', hsh['%s'])%s" % (group[0], group[1:])) | |
3513 | filters['grpby'].append(group[0]) | |
3514 | else: | |
3515 | if 'f' in group: | |
3516 | grpby['fmts'].append("hsh['fileinfo'][0]") | |
3517 | grpby['tuples'].append("hsh['fileinfo'][0]") | |
3518 | else: | |
3519 | grpby['fmts'].append("hsh['%s']" % group.strip()) | |
3520 | grpby['tuples'].append("hsh['%s']" % group.strip()) | |
3521 | filters['grpby'].append(group[0]) | |
3522 | if include: | |
3523 | if include == {'y', 'm', 'd'}: | |
3524 | grpby['include'] = "yyyy-MM-dd" | |
3525 | elif include == {'m', 'd'}: | |
3526 | grpby['include'] = "MMM d" | |
3527 | elif include == {'y', 'd'}: | |
3528 | grpby['include'] = "yyyy-MM-dd" | |
3529 | elif include == set(['y', 'w']): | |
3530 | groupby['include'] = "w" | |
3531 | elif include == {'d'}: | |
3532 | grpby['include'] = "MMM dd" | |
3533 | elif include == set(['w']): | |
3534 | grpby['include'] = "w" | |
3535 | else: | |
3536 | grpby['include'] = "" | |
3537 | else: | |
3538 | grpby['include'] = "" | |
3539 | logger.debug('grpby final: {0}'.format(grpby)) | |
3540 | ||
3541 | for part in parts: | |
3542 | key = unicode(part[0]) | |
3543 | if key in ['b', 'e']: | |
3544 | dt = parse_date_period(part[1:]) | |
3545 | dated[key] = dt.replace(tzinfo=None) | |
3546 | ||
3547 | elif key == 'f': | |
3548 | value = unicode(part[1:].strip()) | |
3549 | if value[0] == '!': | |
3550 | filters['folder'] = (False, re.compile(r'%s' % value[1:], | |
3551 | re.IGNORECASE)) | |
3552 | else: | |
3553 | filters['folder'] = (True, re.compile(r'%s' % value, | |
3554 | re.IGNORECASE)) | |
3555 | elif key == 's': | |
3556 | value = unicode(part[1:].strip()) | |
3557 | if value[0] == '!': | |
3558 | filters['search'] = (False, re.compile(r'%s' % value[1:], | |
3559 | re.IGNORECASE)) | |
3560 | else: | |
3561 | filters['search'] = (True, re.compile(r'%s' % value, | |
3562 | re.IGNORECASE)) | |
3563 | elif key == 'S': | |
3564 | value = unicode(part[1:].strip()) | |
3565 | if value[0] == '!': | |
3566 | filters['search-all'] = (False, re.compile(r'%s' % value[1:], re.IGNORECASE | re.DOTALL)) | |
3567 | else: | |
3568 | filters['search-all'] = (True, re.compile(r'%s' % value, re.IGNORECASE | re.DOTALL)) | |
3569 | elif key == 'd': | |
3570 | if cli: | |
3571 | if grpby['report'] == 'a': | |
3572 | d = int(part[1:]) | |
3573 | if d: | |
3574 | d += 1 | |
3575 | grpby['depth'] = d | |
3576 | else: | |
3577 | pass | |
3578 | ||
3579 | elif key == 't': | |
3580 | value = [x.strip() for x in part[1:].split(',')] | |
3581 | for t in value: | |
3582 | if t[0] == '!': | |
3583 | filters['neg_fields'].append(( | |
3584 | 't', re.compile(r'%s' % t[1:], re.IGNORECASE))) | |
3585 | else: | |
3586 | filters['pos_fields'].append(( | |
3587 | 't', re.compile(r'%s' % t, re.IGNORECASE))) | |
3588 | elif key == 'o': | |
3589 | value = unicode(part[1:].strip()) | |
3590 | if value[0] == '!': | |
3591 | filters['omit'][0] = False | |
3592 | filters['omit'][1] = [x for x in value[1:]] | |
3593 | else: | |
3594 | filters['omit'][0] = True | |
3595 | filters['omit'][1] = [x for x in value] | |
3596 | elif key == 'h': | |
3597 | grpby['colors'] = int(part[1:]) | |
3598 | elif key == 'w': | |
3599 | grpby['width1'] = int(part[1:]) | |
3600 | elif key == 'W': | |
3601 | grpby['width2'] = int(part[1:]) | |
3602 | else: | |
3603 | value = unicode(part[1:].strip()) | |
3604 | if value[0] == '!': | |
3605 | filters['neg_fields'].append(( | |
3606 | key, re.compile(r'%s' % value[1:], re.IGNORECASE))) | |
3607 | else: | |
3608 | filters['pos_fields'].append(( | |
3609 | key, re.compile(r'%s' % value, re.IGNORECASE))) | |
3610 | if 'b' not in dated: | |
3611 | dated['b'] = parse( | |
3612 | parse_datetime(options['report_begin'])).replace(tzinfo=None) | |
3613 | if 'e' not in dated: | |
3614 | dated['e'] = parse( | |
3615 | parse_datetime(options['report_end'])).replace(tzinfo=None) | |
3616 | if 'colors' not in grpby or grpby['colors'] not in [0, 1, 2]: | |
3617 | grpby['colors'] = options['report_colors'] | |
3618 | if 'width1' not in grpby: | |
3619 | grpby['width1'] = options['report_width1'] | |
3620 | if 'width2' not in grpby: | |
3621 | grpby['width2'] = options['report_width2'] | |
3622 | grpby['lst'].append(u'summary') | |
3623 | logger.debug('grpby: {0}; dated: {1}; filters: {2}'.format(grpby, dated, filters)) | |
3624 | return grpby, dated, filters | |
3625 | ||
3626 | ||
3627 | def applyFilters(file2uuids, uuid2hash, filters): | |
3628 | """ | |
3629 | Apply all filters except begin and end and return a list of | |
3630 | the uid's of the passing hashes. | |
3631 | ||
3632 | TODO: memoize? | |
3633 | """ | |
3634 | ||
3635 | typeHsh = { | |
3636 | 'a': '~', | |
3637 | 'd': '%', | |
3638 | 'e': '*', | |
3639 | 'g': '+', | |
3640 | 'o': '^', | |
3641 | 'n': '!', | |
3642 | 't': '-' | |
3643 | } | |
3644 | uuids = [] | |
3645 | ||
3646 | omit = None | |
3647 | if 'omit' in filters: | |
3648 | omit, omit_types = filters['omit'] | |
3649 | omit_chars = [typeHsh[x] for x in omit_types] | |
3650 | ||
3651 | for f in file2uuids: | |
3652 | if 'cal_regex' in filters and not filters['cal_regex'].match(f): | |
3653 | continue | |
3654 | if 'folder' in filters: | |
3655 | tf, folder_regex = filters['folder'] | |
3656 | if tf and not folder_regex.search(f): | |
3657 | continue | |
3658 | if not tf and folder_regex.search(f): | |
3659 | continue | |
3660 | for uid in file2uuids[f]: | |
3661 | hsh = uuid2hash[uid] | |
3662 | skip = False | |
3663 | type_char = hsh['itemtype'] | |
3664 | if type_char in ['=', '#', '$', '?']: | |
3665 | # omit defaults, hidden, inbox and someday | |
3666 | continue | |
3667 | if filters['dates'] and 's' not in hsh: | |
3668 | # groupby includes a date specification and this item is undated | |
3669 | continue | |
3670 | if filters['report'] == 'a' and type_char != '~': | |
3671 | continue | |
3672 | if filters['report'] == 'c' and omit is not None: | |
3673 | if omit and type_char in omit_chars: | |
3674 | # we're omitting this type | |
3675 | continue | |
3676 | if not omit and type_char not in omit_chars: | |
3677 | # we're not showing this type | |
3678 | continue | |
3679 | if 'search' in filters: | |
3680 | tf, rx = filters['search'] | |
3681 | l = [] | |
3682 | for g in filters['grpby']: | |
3683 | # search over the leaf summary and the branch | |
3684 | for t in ['_summary', u'c', u'k', u'f', u'u']: | |
3685 | if t not in g: | |
3686 | continue | |
3687 | if t == 'f': | |
3688 | v = hsh['fileinfo'][0] | |
3689 | elif t in hsh: | |
3690 | v = hsh[t] | |
3691 | else: | |
3692 | continue | |
3693 | # add v to l | |
3694 | l.append(v) | |
3695 | s = ' '.join(l) | |
3696 | res = rx.search(s) | |
3697 | if tf and not res: | |
3698 | skip = True | |
3699 | if not tf and res: | |
3700 | skip = True | |
3701 | if 'search-all' in filters: | |
3702 | tf, rx = filters['search-all'] | |
3703 | # search over the entire entry and the file path | |
3704 | l = [hsh['entry'], hsh['fileinfo'][0]] | |
3705 | s = ' '.join(l) | |
3706 | res = rx.search(s) | |
3707 | if tf and not res: | |
3708 | skip = True | |
3709 | if not tf and res: | |
3710 | skip = True | |
3711 | for t in ['c', 'k', 'u']: | |
3712 | if t in filters['grpby'] and t not in hsh: | |
3713 | # t is missing from hsh | |
3714 | skip = True | |
3715 | break | |
3716 | if skip: | |
3717 | # try the next uid | |
3718 | continue | |
3719 | for flt, rgx in filters['pos_fields']: | |
3720 | if flt == 't': | |
3721 | if 't' not in hsh or not rgx.search(" ".join(hsh['t'])): | |
3722 | skip = True | |
3723 | break | |
3724 | elif flt not in hsh or not rgx.search(hsh[flt]): | |
3725 | skip = True | |
3726 | break | |
3727 | if skip: | |
3728 | # try the next uid | |
3729 | continue | |
3730 | for flt, rgx in filters['neg_fields']: | |
3731 | if flt == 't': | |
3732 | if 't' in hsh and rgx.search(" ".join(hsh['t'])): | |
3733 | skip = True | |
3734 | break | |
3735 | elif flt in hsh and rgx.search(hsh[flt]): | |
3736 | skip = True | |
3737 | break | |
3738 | if skip: | |
3739 | # try the next uid | |
3740 | continue | |
3741 | # passed all tests | |
3742 | uuids.append(uid) | |
3743 | return uuids | |
3744 | ||
3745 | ||
3746 | def reportDT(dt, include, options=None): | |
3747 | # include will be something like "MMM d yyyy" | |
3748 | if not options: | |
3749 | options = {} | |
3750 | res = '' | |
3751 | if dt.hour == 0 and dt.minute == 0: | |
3752 | if not include: | |
3753 | return '' | |
3754 | return d_to_str(dt, "yyyy-MM-dd") | |
3755 | else: | |
3756 | if options['ampm']: | |
3757 | if include: | |
3758 | res = dt_to_str(dt, "%s h:mma" % include) | |
3759 | else: | |
3760 | res = dt_to_str(dt, "h:mma") | |
3761 | else: | |
3762 | if include: | |
3763 | res = dt_to_str(dt, "%s hh:mm" % include) | |
3764 | else: | |
3765 | res = dt_to_str(dt, "hh:mm") | |
3766 | return leadingzero.sub('', res.lower()) | |
3767 | ||
3768 | ||
3769 | # noinspection PyChainedComparisons | |
3770 | def makeReportTuples(uuids, uuid2hash, grpby, dated, options=None): | |
3771 | """ | |
3772 | Using filtered uuids, and dates: grpby, b and e, return a sorted | |
3773 | list of tuples | |
3774 | (sort1, sort2, ... typenum, dt or '', uid) | |
3775 | using dt takes care of time when need or date and time when | |
3776 | grpby has no date specification | |
3777 | """ | |
3778 | if not options: | |
3779 | options = {} | |
3780 | today_datetime = datetime.now().replace( | |
3781 | hour=0, minute=0, second=0, microsecond=0) | |
3782 | today_date = datetime.now().date() | |
3783 | tups = [] | |
3784 | for uid in uuids: | |
3785 | try: | |
3786 | hsh = {} | |
3787 | for k, v in uuid2hash[uid].items(): | |
3788 | hsh[k] = v | |
3789 | # we'll make anniversary subs to a copy later | |
3790 | hsh['summary'] = hsh['_summary'] | |
3791 | tchr = hsh['itemtype'] | |
3792 | tstr = type2Str[tchr] | |
3793 | if 't' not in hsh: | |
3794 | hsh['t'] = [] | |
3795 | if dated['grpby']: | |
3796 | dates = [] | |
3797 | if 'f' in hsh and hsh['f']: | |
3798 | next = getDoneAndTwo(hsh)[1] | |
3799 | if next: | |
3800 | start = next | |
3801 | else: | |
3802 | start = parse(parse_dtstr(hsh['s'], hsh['z'])).astimezone(tzlocal()).replace(tzinfo=None) | |
3803 | if 'rrule' in hsh: | |
3804 | if dated['b'] > start: | |
3805 | start = dated['b'] | |
3806 | for date in hsh['rrule'].between(start, dated['e'], inc=True): | |
3807 | # on or after start but before 'e' | |
3808 | if date < dated['e']: | |
3809 | bisect.insort(dates, date) | |
3810 | elif 's' in hsh and hsh['s'] and 'f' not in hsh: | |
3811 | if hsh['s'] < dated['e'] and hsh['s'] >= dated['b']: | |
3812 | bisect.insort(dates, start) | |
3813 | # datesSL.insert(start) | |
3814 | if 'f' in hsh and hsh['f']: | |
3815 | dt = parse(parse_dtstr( | |
3816 | hsh['f'][-1][0], hsh['z'])).astimezone( | |
3817 | tzlocal()).replace(tzinfo=None) | |
3818 | if dt <= dated['e'] and dt >= dated['b']: | |
3819 | bisect.insort(dates, dt) | |
3820 | for dt in dates: | |
3821 | item = [] | |
3822 | # ('dt', type(dt), dt) | |
3823 | for g in grpby['tuples']: | |
3824 | if groupdate_regex.search(g): | |
3825 | item.append(d_to_str(dt, g)) | |
3826 | elif g in ['c', 'u']: | |
3827 | item.append(hsh[g]) | |
3828 | else: # should be f or k | |
3829 | item.append(eval(g)) | |
3830 | item.extend([ | |
3831 | tstr2SCI[tstr][0], | |
3832 | tstr, | |
3833 | dt, | |
3834 | reportDT(dt, grpby['include'], options), | |
3835 | uid]) | |
3836 | bisect.insort(tups, tuple(item)) | |
3837 | ||
3838 | else: # no date spec in grpby | |
3839 | item = [] | |
3840 | dt = '' | |
3841 | if hsh['itemtype'] in [u'+', u'-', u'%']: | |
3842 | # task type | |
3843 | done, due, following = getDoneAndTwo(hsh) | |
3844 | if due: | |
3845 | # add a due entry | |
3846 | if due.date() < today_date: | |
3847 | if tchr == '+': | |
3848 | tstr = 'pc' | |
3849 | elif tchr == '-': | |
3850 | tstr = 'pt' | |
3851 | elif tchr == '%': | |
3852 | tstr = 'pd' | |
3853 | dt = due | |
3854 | elif done: | |
3855 | dt = done | |
3856 | else: | |
3857 | # not a task type | |
3858 | if 's' in hsh: | |
3859 | if 'rrule' in hsh: | |
3860 | if tchr in ['^', '*', '~']: | |
3861 | dt = (hsh['rrule'].after(today_datetime, inc=True) or hsh['rrule'].before(today_datetime, inc=True)) | |
3862 | if dt is None: | |
3863 | logger.warning('No valid datetimes for {0}, {1}'.format(hsh['_summary'], hsh['fileinfo'])) | |
3864 | continue | |
3865 | else: | |
3866 | dt = hsh['rrule'].after(hsh['s'], inc=True) | |
3867 | else: | |
3868 | dt = parse( | |
3869 | parse_dtstr(hsh['s'], hsh['z'])).replace(tzinfo=None) | |
3870 | else: | |
3871 | # undated | |
3872 | dt = '' | |
3873 | for g in grpby['tuples']: | |
3874 | if groupdate_regex.search(g): | |
3875 | item.append(dt_to_str(dt, g)) | |
3876 | else: | |
3877 | try: | |
3878 | res = eval(g) | |
3879 | item.append(res) | |
3880 | except: | |
3881 | pass | |
3882 | if type(dt) == datetime: | |
3883 | dtstr = reportDT(dt, grpby['include'], options) | |
3884 | dt = dt.strftime(etmdatefmt) | |
3885 | else: | |
3886 | dtstr = dt | |
3887 | item.extend([ | |
3888 | tstr2SCI[tstr][0], | |
3889 | tstr, | |
3890 | dt, | |
3891 | dtstr, | |
3892 | uid]) | |
3893 | bisect.insort(tups, tuple(item)) | |
3894 | except: | |
3895 | logger.exception('Error processing: {0}, {1}'.format(hsh['_summary'], hsh['fileinfo'])) | |
3896 | return tups | |
3897 | ||
3898 | ||
3899 | def getAgenda(allrows, colors=2, days=4, indent=2, width1=54, | |
3900 | width2=14, calendars=None, mode='html', fltr=None): | |
3901 | if not calendars: | |
3902 | calendars = [] | |
3903 | items = deepcopy(allrows) | |
3904 | day = [] | |
3905 | inbasket = [] | |
3906 | now = [] | |
3907 | next = [] | |
3908 | someday = [] | |
3909 | if colors and mode == 'html': | |
3910 | bb = "<b>" | |
3911 | eb = "</b>" | |
3912 | else: | |
3913 | bb = "" | |
3914 | eb = "" | |
3915 | beg = datetime.today() | |
3916 | beg_fmt = beg.strftime("%Y%m%d") | |
3917 | beg + days * oneday | |
3918 | day_count = 0 | |
3919 | last_day = '' | |
3920 | if not items: | |
3921 | return "no output" | |
3922 | for item in items: | |
3923 | if item[0][0] == 'day': | |
3924 | if item[0][1] >= beg_fmt and day_count <= days + 1: | |
3925 | # process day items until we get to days+1 so that all items | |
3926 | # from days are included | |
3927 | if item[2][1] in ['fn', 'ac', 'ns']: | |
3928 | # skip finished tasks, actions and notes | |
3929 | continue | |
3930 | if item[0][1] != last_day: | |
3931 | last_day = item[0][1] | |
3932 | day_count += 1 | |
3933 | if day_count <= days: | |
3934 | day.append(item) | |
3935 | elif item[0][0] == 'inbasket': | |
3936 | item.insert(1, "%sIn Basket%s" % (bb, eb)) | |
3937 | inbasket.append(item) | |
3938 | elif item[0][0] == 'now': | |
3939 | item.insert(1, "%sNow%s" % (bb, eb)) | |
3940 | now.append(item) | |
3941 | elif item[0][0] == 'next': | |
3942 | item.insert(1, "%sNext%s" % (bb, eb)) | |
3943 | next.append(item) | |
3944 | elif item[0][0] == 'someday': | |
3945 | item.insert(1, "%sSomeday%s" % (bb, eb)) | |
3946 | someday.append(item) | |
3947 | tree = {} | |
3948 | nv = 0 | |
3949 | for l in [day, inbasket, now, next, someday]: | |
3950 | if l: | |
3951 | nv += 1 | |
3952 | update = makeTree(l, calendars=calendars, fltr=fltr) | |
3953 | for key in update.keys(): | |
3954 | tree.setdefault(key, []).extend(update[key]) | |
3955 | logger.debug("called makeTree for {0} views".format(nv)) | |
3956 | return tree | |
3957 | ||
3958 | ||
3959 | # @memoize | |
3960 | def getReportData(s, file2uuids, uuid2hash, options=None, export=False, | |
3961 | colors=None, cli=True): | |
3962 | """ | |
3963 | getViewData returns items with the format: | |
3964 | [(view, (sort)), node1, node2, ..., | |
3965 | (uuid, typestr, summary, col_2, dt_sort_str) ] | |
3966 | pop item[0] after sort leaving | |
3967 | [node1, node2, ... (xxx) ] | |
3968 | ||
3969 | for actions (tallyByGroup) we need | |
3970 | (node1, node2, ... (minutes, value, expense, charge)) | |
3971 | """ | |
3972 | if not options: | |
3973 | options = {} | |
3974 | try: | |
3975 | grpby, dated, filters = str2opts(s, options, cli) | |
3976 | except: | |
3977 | e = _("Could not process: {0}").format(s) | |
3978 | logger.exception(e) | |
3979 | return e | |
3980 | if not grpby: | |
3981 | return [str(_('invalid grpby setting'))] | |
3982 | uuids = applyFilters(file2uuids, uuid2hash, filters) | |
3983 | tups = makeReportTuples(uuids, uuid2hash, grpby, dated, options) | |
3984 | items = [] | |
3985 | cols = grpby['cols'] | |
3986 | fmts = grpby['fmts'] | |
3987 | for tup in tups: | |
3988 | uuid = tup[-1] | |
3989 | hsh = uuid2hash[tup[-1]] | |
3990 | ||
3991 | # for eval we need to be sure that t is in hsh | |
3992 | if 't' not in hsh: | |
3993 | hsh['t'] = [] | |
3994 | ||
3995 | try: | |
3996 | # for eval: {} is the global namespace | |
3997 | # and {'tup' ... dt_to_str} is the local namespace | |
3998 | eval_fmts = [ | |
3999 | eval(x, {}, | |
4000 | {'tup': tup, 'hsh': hsh, 'rsplit': rsplit, | |
4001 | 'd_to_str': d_to_str, 'dt_to_str': dt_to_str}) | |
4002 | for x in fmts] | |
4003 | except Exception: | |
4004 | logger.exception('fmts: {0}'.format(fmts)) | |
4005 | continue | |
4006 | if filters['dates']: | |
4007 | dt = reportDT(tup[-3], grpby['include'], options) | |
4008 | if dt == '00:00': | |
4009 | dt = '' | |
4010 | dtl = None | |
4011 | else: | |
4012 | dtl = tup[-3] | |
4013 | else: | |
4014 | # the datetime (sort string) will be in tup[-3], | |
4015 | # the display string in tup[-2] | |
4016 | dt = tup[-2] | |
4017 | dtl = tup[-3] | |
4018 | if dtl: | |
4019 | etmdt = parse_datetime(dtl, hsh['z']) | |
4020 | if etmdt is None: | |
4021 | etmdt = "" | |
4022 | else: | |
4023 | etmdt = '' | |
4024 | ||
4025 | try: | |
4026 | item = (cols.format(*eval_fmts)).split('::') | |
4027 | except: | |
4028 | logger.exception("eval_fmts: {0}".format(*eval_fmts)) | |
4029 | ||
4030 | if grpby['report'] == 'c': | |
4031 | if fmts.count(u"hsh['t']"): | |
4032 | position = fmts.index(u"hsh['t']") | |
4033 | for tag in hsh['t']: | |
4034 | rowcpy = deepcopy(item) | |
4035 | rowcpy[position] = tag | |
4036 | rowcpy.append( | |
4037 | (tup[-1], tup[-4], setSummary(hsh, parse(dtl)), dt, etmdt)) | |
4038 | items.append(rowcpy) | |
4039 | else: | |
4040 | item.append((tup[-1], tup[-4], setSummary(hsh, parse(dtl)), dt, etmdt)) | |
4041 | items.append(item) | |
4042 | else: # action report | |
4043 | summary = format(setSummary(hsh, parse(dt))) | |
4044 | item.append("{0}!!{1}!!".format(summary, uuid)) | |
4045 | temp = [] | |
4046 | temp.extend(timeValue(hsh, options)) | |
4047 | temp.extend(expenseCharge(hsh, options)) | |
4048 | item.append(temp) | |
4049 | items.append(item) | |
4050 | if grpby['report'] == 'c' and not export: | |
4051 | tree = makeTree(items, sort=False) | |
4052 | return tree | |
4053 | else: | |
4054 | if grpby['report'] == 'a' and 'depth' in grpby and grpby['depth']: | |
4055 | depth = min(grpby['depth'], len(grpby['lst'])) | |
4056 | else: | |
4057 | depth = len(grpby['lst']) | |
4058 | logger.debug('using depth: {0}'.format(depth)) | |
4059 | if export: | |
4060 | data = [] | |
4061 | head = [x for x in grpby['lst'][:depth]] | |
4062 | logger.debug('head: {0}\nlst: {1}\ndepth: {2}'.format(head, grpby['lst'], depth)) | |
4063 | if grpby['report'] == 'c': | |
4064 | for row in items: | |
4065 | tup = ['"{0}"'.format(x) for x in row.pop(-1)[2:6]] | |
4066 | row.extend(tup) | |
4067 | data.append(row) | |
4068 | else: | |
4069 | head.extend(['minutes', 'value', 'expense', 'charge']) | |
4070 | data.append(head) | |
4071 | lst = tallyByGroup( | |
4072 | items, max_level=depth, options=options, export=True) | |
4073 | for row in lst: | |
4074 | tup = [x for x in list(row.pop(-1))] | |
4075 | row.extend(tup) | |
4076 | data.append(row) | |
4077 | return data | |
4078 | else: | |
4079 | res = tallyByGroup(items, max_level=depth, options=options) | |
4080 | return res | |
4081 | ||
4082 | ||
4083 | def str2hsh(s, uid=None, options=None): | |
4084 | if not options: | |
4085 | options = {} | |
4086 | msg = [] | |
4087 | try: | |
4088 | hsh = {} | |
4089 | alerts = [] | |
4090 | at_parts = at_regex.split(s) | |
4091 | # logger.debug('at_parts: {0}'.format(at_parts)) | |
4092 | head = at_parts.pop(0).strip() | |
4093 | if head and head[0] in type_keys: | |
4094 | itemtype = unicode(head[0]) | |
4095 | summary = head[1:].strip() | |
4096 | else: | |
4097 | # in basket | |
4098 | itemtype = u'$' | |
4099 | summary = head | |
4100 | hsh['itemtype'] = itemtype | |
4101 | hsh['_summary'] = summary | |
4102 | if uid: | |
4103 | hsh['i'] = uid | |
4104 | if itemtype == u'+': | |
4105 | hsh['_group_summary'] = summary | |
4106 | # drop the @i line | |
4107 | lines = [x for x in s.split('\n') if not x.startswith('@i')] | |
4108 | hsh['entry'] = "\n".join(lines) | |
4109 | for at_part in at_parts: | |
4110 | at_key = unicode(at_part[0]) | |
4111 | at_val = at_part[1:].strip() | |
4112 | if at_key == 'a': | |
4113 | actns = options['alert_default'] | |
4114 | arguments = [] | |
4115 | # alert_parts = at_val.split(':', maxsplit=1) | |
4116 | alert_parts = re.split(':', at_val, maxsplit=1) | |
4117 | t_lst = alert_parts.pop(0).split(',') | |
4118 | periods = tuple([parse_period(x) for x in t_lst]) | |
4119 | triggers = [x for x in periods] | |
4120 | if alert_parts: | |
4121 | action_parts = [ | |
4122 | x.strip() for x in alert_parts[0].split(';')] | |
4123 | actns = [ | |
4124 | unicode(x.strip()) for x in | |
4125 | action_parts.pop(0).split(',')] | |
4126 | if action_parts: | |
4127 | arguments = [] | |
4128 | for action_part in action_parts: | |
4129 | tmp = action_part.split(',') | |
4130 | arguments.append(tmp) | |
4131 | alerts.append([triggers, actns, arguments]) | |
4132 | elif at_key in ['+', '-']: | |
4133 | parts = comma_regex.split(at_val) | |
4134 | tmp = [] | |
4135 | for part in parts: | |
4136 | tmp.append(part) | |
4137 | hsh[at_key] = tmp | |
4138 | elif at_key in ['r', 'j']: | |
4139 | amp_parts = amp_regex.split(at_val) | |
4140 | part_hsh = {} | |
4141 | this_key = unicode(amp_hsh.get(at_key, at_key)) | |
4142 | amp_0 = amp_parts.pop(0) | |
4143 | part_hsh[this_key] = amp_0 | |
4144 | for amp_part in amp_parts: | |
4145 | amp_key = unicode(amp_part[0]) | |
4146 | amp_val = amp_part[1:].strip() | |
4147 | if amp_key in ['q', 'i', 't']: | |
4148 | try: | |
4149 | part_hsh[amp_key] = int(amp_val) | |
4150 | except: | |
4151 | msg.append('Bad entry "{0}" given for "&{1}". An integer is required.'.format(amp_val, amp_key)) | |
4152 | logger.exception('Bad entry "{0}" given for "&{1}" in "{2}". An integer is required.'.format(amp_val, amp_key, hsh['entry'])) | |
4153 | elif amp_key == 'e': | |
4154 | part_hsh['e'] = parse_period(amp_val) | |
4155 | else: | |
4156 | m = range_regex.search(amp_val) | |
4157 | if m: | |
4158 | if m.group(3): | |
4159 | part_hsh[amp_key] = [ | |
4160 | x for x in range( | |
4161 | int(m.group(1)), | |
4162 | int(m.group(3)))] | |
4163 | else: | |
4164 | part_hsh[amp_key] = range(int(m.group(1))) | |
4165 | # value will be a scalar or list | |
4166 | elif comma_regex.search(amp_val): | |
4167 | part_hsh[amp_key] = comma_regex.split(amp_val) | |
4168 | else: | |
4169 | part_hsh[amp_key] = amp_val | |
4170 | try: | |
4171 | hsh.setdefault("%s" % at_key, []).append(part_hsh) | |
4172 | except: | |
4173 | msg.append("error appending '%s' to hsh[%s]" % | |
4174 | (part_hsh, at_key)) | |
4175 | else: | |
4176 | # value will be a scalar or list | |
4177 | if at_key in ['a', 't']: | |
4178 | if comma_regex.search(at_val): | |
4179 | hsh[at_key] = [ | |
4180 | x for x in comma_regex.split(at_val) if x] | |
4181 | else: | |
4182 | hsh[at_key] = [at_val] | |
4183 | elif at_key == 's': | |
4184 | # we'll parse this after we get the timezone | |
4185 | hsh['s'] = at_val | |
4186 | elif at_key == 'k': | |
4187 | hsh['k'] = ":".join([x.strip() for x in at_val.split(':')]) | |
4188 | elif at_key == 'e': | |
4189 | hsh['e'] = parse_period(at_val) | |
4190 | elif at_key == 'p': | |
4191 | hsh['p'] = int(at_val) | |
4192 | if hsh['p'] <= 0 or hsh['p'] >= 10: | |
4193 | hsh['p'] = 10 | |
4194 | else: | |
4195 | hsh[at_key] = at_val | |
4196 | if alerts: | |
4197 | hsh['_a'] = alerts | |
4198 | if 'z' not in hsh: | |
4199 | if 's' in hsh or 'f' in hsh: | |
4200 | hsh['z'] = options['local_timezone'] | |
4201 | if 'z' in hsh: | |
4202 | z = gettz(hsh['z']) | |
4203 | if z is None: | |
4204 | msg.append("error: bad timezone: '%s'" % hsh['z']) | |
4205 | hsh['z'] = '' | |
4206 | if 's' in hsh: | |
4207 | try: | |
4208 | hsh['s'] = parse( | |
4209 | parse_datetime( | |
4210 | hsh['s'], hsh['z'])).replace(tzinfo=None) | |
4211 | except: | |
4212 | err = "error: could not parse '@s {0}'".format(hsh['s']) | |
4213 | msg.append(err) | |
4214 | if '+' in hsh: | |
4215 | tmp = [] | |
4216 | for part in hsh['+']: | |
4217 | tmp.append(parse(parse_datetime(part, f=sfmt))) | |
4218 | hsh['+'] = tmp | |
4219 | if '-' in hsh: | |
4220 | tmp = [] | |
4221 | for part in hsh['-']: | |
4222 | tmp.append(parse(parse_datetime(part, f=sfmt))) | |
4223 | hsh['-'] = tmp | |
4224 | if 'b' in hsh: | |
4225 | try: | |
4226 | hsh['b'] = int(hsh['b']) | |
4227 | except: | |
4228 | msg.append( | |
4229 | "the value of @b should be an integer: '@b {0}'".format( | |
4230 | hsh['b'])) | |
4231 | if 'f' in hsh: | |
4232 | # this will be a list of done:due pairs | |
4233 | # 20120201T1325;20120202T1400, ... | |
4234 | # logger.debug('hsh["f"]: {0}'.format(hsh['f'])) | |
4235 | pairs = [x.strip() for x in hsh['f'].split(',') if x.strip()] | |
4236 | # logger.debug('pairs: {0}'.format(pairs)) | |
4237 | hsh['f'] = [] | |
4238 | for pair in pairs: | |
4239 | pair = pair.split(';') | |
4240 | done = parse( | |
4241 | parse_datetime( | |
4242 | pair[0], hsh['z'])).replace(tzinfo=None) | |
4243 | if len(pair) > 1: | |
4244 | due = parse( | |
4245 | parse_datetime( | |
4246 | pair[1], hsh['z'])).replace(tzinfo=None) | |
4247 | else: | |
4248 | due = done | |
4249 | # logger.debug("appending {0} to {1}".format(done, hsh['entry'])) | |
4250 | hsh['f'].append((done, due)) | |
4251 | if 'h' in hsh: | |
4252 | # this will be a list of done:due pairs | |
4253 | # 20120201T1325;20120202T1400, ... | |
4254 | # logger.debug('hsh["f"]: {0}'.format(hsh['f'])) | |
4255 | pairs = [x.strip() for x in hsh['h'].split(',') if x.strip()] | |
4256 | # logger.debug('pairs: {0}'.format(pairs)) | |
4257 | hsh['h'] = [] | |
4258 | for pair in pairs: | |
4259 | pair = pair.split(';') | |
4260 | done = parse( | |
4261 | parse_datetime( | |
4262 | pair[0], hsh['z'])).replace(tzinfo=None) | |
4263 | if len(pair) > 1: | |
4264 | due = parse( | |
4265 | parse_datetime( | |
4266 | pair[1], hsh['z'])).replace(tzinfo=None) | |
4267 | else: | |
4268 | due = done | |
4269 | # logger.debug("appending {0} to {1}".format(done, hsh['entry'])) | |
4270 | hsh['h'].append((done, due)) | |
4271 | if 'j' in hsh: | |
4272 | for i in range(len(hsh['j'])): | |
4273 | job = hsh['j'][i] | |
4274 | if 'q' not in job: | |
4275 | msg.append("@j: %s" % job['j']) | |
4276 | msg.append("an &q entry is required for jobs") | |
4277 | if 'f' in job: | |
4278 | if 'z' not in hsh: | |
4279 | hsh['z'] = options['local_timezone'] | |
4280 | ||
4281 | pair = job['f'].split(';') | |
4282 | done = parse( | |
4283 | parse_datetime( | |
4284 | pair[0], hsh['z'])).replace(tzinfo=None) | |
4285 | if len(pair) > 1: | |
4286 | due = parse( | |
4287 | parse_datetime( | |
4288 | pair[1], hsh['z'])).replace(tzinfo=None) | |
4289 | else: | |
4290 | due = '' | |
4291 | ||
4292 | job['f'] = [(done, due)] | |
4293 | ||
4294 | if 'h' in job: | |
4295 | # this will be a list of done:due pairs | |
4296 | # 20120201T1325;20120202T1400, ... | |
4297 | logger.debug("job['h']: {0}, {1}".format(job['h'], type(job['h']))) | |
4298 | if type(job['h']) is str: | |
4299 | pairs = job['h'].split(',') | |
4300 | else: | |
4301 | pairs = job['h'] | |
4302 | logger.debug('starting pairs: {0}, {1}'.format(pairs, type(pairs))) | |
4303 | job['h'] = [] | |
4304 | # if type(pairs) in [unicode, str]: | |
4305 | if type(pairs) not in [list]: | |
4306 | pairs = [pairs] | |
4307 | for pair in pairs: | |
4308 | logger.debug('splitting pair: {0}'.format(pair)) | |
4309 | pair = pair.split(';') | |
4310 | logger.debug('processing done, due: {0}'.format(pair)) | |
4311 | done = parse( | |
4312 | parse_datetime( | |
4313 | pair[0], hsh['z'])).replace(tzinfo=None) | |
4314 | if len(pair) > 1: | |
4315 | logger.debug('parsing due: {0}, {1}'.format(pair[1], type(pair[1]))) | |
4316 | due = parse( | |
4317 | parse_datetime( | |
4318 | pair[1], hsh['z'])).replace(tzinfo=None) | |
4319 | else: | |
4320 | due = done | |
4321 | logger.debug("appending ({0}, {1}) to {2} ".format(done, due, job['j'])) | |
4322 | job['h'].append((done, due)) | |
4323 | logger.debug("job['h']: {0}".format(job['h'])) | |
4324 | # put the modified job back in the hash | |
4325 | hsh['j'][i] = job | |
4326 | for k, v in hsh.items(): | |
4327 | if type(v) in [datetime, timedelta]: | |
4328 | pass | |
4329 | elif k == 's': | |
4330 | pass | |
4331 | elif type(v) in [list, int, tuple]: | |
4332 | hsh[k] = v | |
4333 | else: | |
4334 | hsh[k] = v.strip() | |
4335 | if 'r' in hsh: | |
4336 | if hsh['r'] == 'l': | |
4337 | # list only with no '&' fields | |
4338 | hsh['r'] = {'f': 'l'} | |
4339 | # skip one time and handle with finished, begin and pastdue | |
4340 | msg.extend(checkhsh(hsh)) | |
4341 | if msg: | |
4342 | return hsh, msg | |
4343 | if 'p' in hsh: | |
4344 | hsh['_p'] = hsh['p'] | |
4345 | else: | |
4346 | hsh['_p'] = 10 | |
4347 | if 'a' in hsh: | |
4348 | hsh['_a'] = hsh['a'] | |
4349 | if 'j' in hsh: | |
4350 | hsh['_j'] = hsh['j'] | |
4351 | if 'r' in hsh: | |
4352 | hsh['_r'] = hsh['r'] | |
4353 | try: | |
4354 | hsh['r'] = get_rrulestr(hsh) | |
4355 | except: | |
4356 | msg.append("exception processing rulestring: %s" % hsh['_r']) | |
4357 | try: | |
4358 | hsh['r'], hsh['rrule'], warn = get_rrule(hsh) | |
4359 | if warn: | |
4360 | msg.extend(warn) | |
4361 | except: | |
4362 | logger.exception("exception processing rrule: {0}".format(hsh['_r'])) | |
4363 | if 'i' not in hsh: | |
4364 | hsh['i'] = uniqueId() | |
4365 | ||
4366 | except: | |
4367 | logger.exception('exception processing "{0}"'.format(s)) | |
4368 | msg.append('exception processing "{0}"'.format(s)) | |
4369 | return hsh, msg | |
4370 | ||
4371 | ||
4372 | def expand_template(template, hsh, lbls=None, complain=False): | |
4373 | if not lbls: | |
4374 | lbls = {} | |
4375 | marker = '!' | |
4376 | ||
4377 | def lookup(w): | |
4378 | if w == '': | |
4379 | return marker | |
4380 | l1, l2 = lbls.get(w, ('', '')) | |
4381 | v = hsh.get(w, None) | |
4382 | if v is None: | |
4383 | if complain: | |
4384 | return w | |
4385 | else: | |
4386 | return '' | |
4387 | if type(v) in [str, unicode]: | |
4388 | return "%s%s%s" % (l1, v, l2) | |
4389 | if type(v) == datetime: | |
4390 | return "%s%s%s" % (l1, v.strftime("%a %b %d, %Y %H:%M"), l2) | |
4391 | return "%s%s%s" % (l1, repr(v), l2) | |
4392 | ||
4393 | parts = template.split(marker) | |
4394 | parts[1::2] = map(lookup, parts[1::2]) | |
4395 | return ''.join(parts) | |
4396 | ||
4397 | ||
4398 | def getToday(): | |
4399 | return datetime.today().strftime(sortdatefmt) | |
4400 | ||
4401 | ||
4402 | def getCurrentDate(): | |
4403 | return datetime.today().strftime(reprdatefmt) | |
4404 | ||
4405 | ||
4406 | last_added = None | |
4407 | ||
4408 | ||
4409 | def add2list(l, item, expand=True): | |
4410 | """Add item to l if not already present using bisect to maintain order.""" | |
4411 | global last_added | |
4412 | if expand and len(item) == 3 and type(item[1]) is tuple: | |
4413 | # this is a tree entry, so we need to expand the middle tuple | |
4414 | # for makeTree | |
4415 | try: | |
4416 | entry = [item[0]] | |
4417 | entry.extend(list(item[1])) | |
4418 | entry.append(item[2]) | |
4419 | item = entry | |
4420 | except: | |
4421 | logger.exception('error expanding: {0}'.formt(item)) | |
4422 | return () | |
4423 | try: | |
4424 | # i = bisect.bisect_left(name2list[l], item) | |
4425 | name2SL[l].insert(item) | |
4426 | except: | |
4427 | logger.exception("error adding:\n{0}\n\n last added:\n{1}".format(item, last_added)) | |
4428 | return () | |
4429 | ||
4430 | return True | |
4431 | ||
4432 | ||
4433 | def removeFromlist(l, item, expand=True): | |
4434 | """Add item to l if not already present using bisect to maintain order.""" | |
4435 | global last_added | |
4436 | if expand and len(item) == 3 and type(item[1]) is tuple: | |
4437 | # this is a tree entry, so we need to expand the middle tuple | |
4438 | # for makeTree | |
4439 | try: | |
4440 | entry = [item[0]] | |
4441 | entry.extend(list(item[1])) | |
4442 | entry.append(item[2]) | |
4443 | item = entry | |
4444 | except: | |
4445 | logger.exception('error expanding: {0}'.formt(item)) | |
4446 | return () | |
4447 | try: | |
4448 | name2SL[l].remove(item) | |
4449 | except: | |
4450 | logger.exception("error adding:\n{0}\n\n last added:\n{1}".format(item, last_added)) | |
4451 | return () | |
4452 | return True | |
4453 | ||
4454 | ||
4455 | def getPrevNext(l, cal_regex): | |
4456 | result = [] | |
4457 | seen = [] | |
4458 | # remove duplicates | |
4459 | for xx in l: | |
4460 | if cal_regex and not cal_regex.match(xx[1]): | |
4461 | continue | |
4462 | x = xx[0].date() | |
4463 | i = bisect.bisect_left(seen, x) | |
4464 | if i == len(seen) or seen[i] != x: | |
4465 | seen.insert(i, x) | |
4466 | result.append(x) | |
4467 | l = result | |
4468 | ||
4469 | prevnext = {} | |
4470 | if not l: | |
4471 | return {} | |
4472 | aft = l[0] | |
4473 | bef = l[-1] | |
4474 | d = aft | |
4475 | prev = 0 | |
4476 | nxt = len(l) - 1 | |
4477 | last_prev = 0 | |
4478 | while d <= bef: | |
4479 | i = bisect.bisect_left(l, d) | |
4480 | j = bisect.bisect_right(l, d) | |
4481 | if i != len(l) and l[i] == d: | |
4482 | # d is in the list | |
4483 | last_prev = i | |
4484 | curr = i | |
4485 | prev = max(0, i - 1) | |
4486 | nxt = min(len(l) - 1, j) | |
4487 | else: | |
4488 | # d is not in the list | |
4489 | curr = last_prev | |
4490 | prev = last_prev | |
4491 | prevnext[d] = [l[prev], l[curr], l[nxt]] | |
4492 | d += oneday | |
4493 | return prevnext | |
4494 | ||
4495 | ||
4496 | def get_changes(options, file2lastmodified): | |
4497 | new = [] | |
4498 | deleted = [] | |
4499 | modified = [] | |
4500 | prefix, filelist = getFiles(options['datadir']) | |
4501 | for f, r in filelist: | |
4502 | if (f, r) not in file2lastmodified: | |
4503 | new.append((f, r)) | |
4504 | elif os.path.getmtime(f) != file2lastmodified[(f, r)]: | |
4505 | logger.debug('mtime: {0}; lastmodified: {1}'.format(os.path.getmtime(f), file2lastmodified[(f, r)])) | |
4506 | modified.append((f, r)) | |
4507 | for (f, r) in file2lastmodified: | |
4508 | if (f, r) not in filelist: | |
4509 | deleted.append((f, r)) | |
4510 | return new, modified, deleted | |
4511 | ||
4512 | ||
4513 | def get_data(options=None): | |
4514 | if not options: | |
4515 | options = {} | |
4516 | bad_datafiles = [] | |
4517 | (uuid2hash, uuid2labels, file2uuids, file2lastmodified, bad_datafiles, messages) = process_all_datafiles(options) | |
4518 | if bad_datafiles: | |
4519 | logger.warn("bad data files: {0}".format(bad_datafiles)) | |
4520 | return uuid2hash, uuid2labels, file2uuids, file2lastmodified, bad_datafiles, messages | |
4521 | ||
4522 | ||
4523 | def expandPath(path): | |
4524 | path, ext = os.path.splitext(path) | |
4525 | folders = [] | |
4526 | while 1: | |
4527 | path, folder = os.path.split(path) | |
4528 | if folder != "": | |
4529 | folders.append(folder) | |
4530 | else: | |
4531 | if path != "": | |
4532 | folders.append(path) | |
4533 | break | |
4534 | folders.reverse() | |
4535 | return folders | |
4536 | ||
4537 | ||
4538 | # noinspection PyArgumentList | |
4539 | def getDoneAndTwo(hsh, keep=False): | |
4540 | if hsh['itemtype'] not in ['+', '-', '%']: | |
4541 | return | |
4542 | done = None | |
4543 | nxt = None | |
4544 | following = None | |
4545 | if 'z' in hsh: | |
4546 | today_datetime = datetime.now(gettz(hsh['z'])).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) | |
4547 | else: | |
4548 | today_datetime = get_current_time() | |
4549 | if 'f' in hsh and hsh['f']: | |
4550 | if type(hsh['f']) in [str, unicode]: | |
4551 | parts = str(hsh['f']).split(';') | |
4552 | done = parse( | |
4553 | parse_dtstr( | |
4554 | parts[0], hsh['z']).replace(tzinfo=None)) | |
4555 | if len(parts) < 2: | |
4556 | due = done | |
4557 | else: | |
4558 | due = parse( | |
4559 | parse_dtstr( | |
4560 | parts[1], hsh['z']).replace(tzinfo=None)) | |
4561 | elif type(hsh['f'][-1]) in [list, tuple]: | |
4562 | done, due = hsh['f'][-1] | |
4563 | else: | |
4564 | done = hsh['f'][-1] | |
4565 | due = done | |
4566 | k_aft = due | |
4567 | k_inc = False | |
4568 | r_aft = done | |
4569 | r_inc = False | |
4570 | if due and due < today_datetime: | |
4571 | s_aft = today_datetime | |
4572 | s_inc = True | |
4573 | else: | |
4574 | s_aft = due | |
4575 | s_inc = False | |
4576 | else: | |
4577 | if 's' in hsh: | |
4578 | k_aft = r_aft = hsh['s'] | |
4579 | else: | |
4580 | k_aft = r_aft = today_datetime | |
4581 | k_inc = r_inc = True | |
4582 | s_aft = today_datetime | |
4583 | s_inc = True | |
4584 | ||
4585 | if 'rrule' in hsh: | |
4586 | nxt = None | |
4587 | if keep or 'o' not in hsh or hsh['o'] == 'k': | |
4588 | # keep | |
4589 | if k_aft: | |
4590 | nxt = hsh['rrule'].after(k_aft, k_inc) | |
4591 | elif hsh['o'] == 'r': | |
4592 | # restart | |
4593 | if r_aft: | |
4594 | nxt = hsh['rrule'].after(r_aft, r_inc) | |
4595 | elif hsh['o'] == 's': | |
4596 | # skip | |
4597 | if s_aft: | |
4598 | nxt = hsh['rrule'].after(s_aft, s_inc) | |
4599 | if nxt: | |
4600 | following = hsh['rrule'].after(nxt, False) | |
4601 | elif 's' in hsh and hsh['s']: | |
4602 | if 'f' in hsh: | |
4603 | nxt = None | |
4604 | else: | |
4605 | nxt = parse( | |
4606 | parse_dtstr( | |
4607 | hsh['s'], hsh['z'])).replace(tzinfo=None) | |
4608 | return done, nxt, following | |
4609 | ||
4610 | ||
4611 | def timeValue(hsh, options): | |
4612 | """ | |
4613 | Return rounded integer minutes and float value. | |
4614 | """ | |
4615 | minutes = value = 0 | |
4616 | if 'e' not in hsh or hsh['e'] <= oneminute * 0: | |
4617 | return 0, 0.0 | |
4618 | td_minutes = hsh['e'].seconds // 60 + (hsh['e'].seconds % 60 > 0) | |
4619 | ||
4620 | a_m = int(options['action_minutes']) | |
4621 | if a_m not in [1, 6, 12, 15, 30, 60]: | |
4622 | a_m = 1 | |
4623 | minutes = ((td_minutes // a_m + (td_minutes % a_m > 0)) * a_m) | |
4624 | ||
4625 | if 'action_rates' in options: | |
4626 | if 'v' in hsh and hsh['v'] in options['action_rates']: | |
4627 | rate = float(options['action_rates'][hsh['v']]) | |
4628 | elif 'default' in options['action_rates']: | |
4629 | rate = float(options['action_rates']['default']) | |
4630 | else: | |
4631 | rate = 0.0 | |
4632 | value = rate * (minutes / 60.0) | |
4633 | else: | |
4634 | value = 0.0 | |
4635 | return minutes, value | |
4636 | ||
4637 | ||
4638 | def expenseCharge(hsh, options): | |
4639 | expense = charge = 0.0 | |
4640 | rate = 1.0 | |
4641 | if 'x' in hsh: | |
4642 | expense = charge = float(hsh['x']) | |
4643 | if 'action_markups' in options: | |
4644 | if 'w' in hsh and hsh['w'] in options['action_markups']: | |
4645 | rate = float(options['action_markups'][hsh['w']]) | |
4646 | elif 'default' in options['action_markups']: | |
4647 | rate = float(options['action_markups']['default']) | |
4648 | else: | |
4649 | rate = 1.0 | |
4650 | charge = rate * expense | |
4651 | return expense, charge | |
4652 | ||
4653 | ||
4654 | def timedelta2Str(td, short=False): | |
4655 | """ | |
4656 | """ | |
4657 | if td <= oneminute * 0: | |
4658 | return 'none' | |
4659 | until = [] | |
4660 | td_days = td.days | |
4661 | td_hours = td.seconds // (60 * 60) | |
4662 | td_minutes = (td.seconds % (60 * 60)) // 60 | |
4663 | if short: | |
4664 | # drop the seconds part | |
4665 | return "+%s" % str(td)[:-3] | |
4666 | ||
4667 | if td_days: | |
4668 | if td_days == 1: | |
4669 | days = _("day") | |
4670 | else: | |
4671 | days = _("days") | |
4672 | until.append("%d %s" % (td_days, days)) | |
4673 | if td_hours: | |
4674 | if td_hours == 1: | |
4675 | hours = _("hour") | |
4676 | else: | |
4677 | hours = _("hours") | |
4678 | until.append("%d %s" % (td_hours, hours)) | |
4679 | if td_minutes: | |
4680 | if td_minutes == 1: | |
4681 | minutes = _("minute") | |
4682 | else: | |
4683 | minutes = _("minutes") | |
4684 | until.append("%d %s" % (td_minutes, minutes)) | |
4685 | return " ".join(until) | |
4686 | ||
4687 | ||
4688 | def timedelta2Sentence(td): | |
4689 | string = timedelta2Str(td) | |
4690 | if string == 'none': | |
4691 | return str(_("now")) | |
4692 | else: | |
4693 | return str(_("{0} from now")).format(string) | |
4694 | ||
4695 | ||
4696 | def add_busytime(key, sm, em, evnt_summary, uid, rpth): | |
4697 | """ | |
4698 | key = (year, weeknum, weekdaynum with Monday=1, Sunday=7) | |
4699 | value = [minute_total, list of (uid, start_minute, end_minute)] | |
4700 | """ | |
4701 | # key = tuple(sd.isocalendar()) # year, weeknum, weekdaynum | |
4702 | entry = (sm, em, evnt_summary, uid, rpth) | |
4703 | # logger.debug("adding busytime: {0}; {1}".format(key, evnt_summary)) | |
4704 | busytimesSL.setdefault(key, IndexableSkiplist(2000, "busytimes")).insert(entry) | |
4705 | ||
4706 | ||
4707 | def remove_busytime(key, bt): | |
4708 | """ | |
4709 | key = (year, weeknum, weekdaynum with Monday=1, Sunday=7) | |
4710 | value = [minute_total, list of (uid, start_minute, end_minute)] | |
4711 | """ | |
4712 | # sm, em, uid, evnt_summary, rpth = bt | |
4713 | # timekey = sd.isocalendar() # year, weeknum, weekdaynum | |
4714 | # daykey = sd | |
4715 | busytimesSL[key].remove(bt) | |
4716 | ||
4717 | ||
4718 | def add_occasion(key, evnt_summary, uid, f): | |
4719 | # key = tuple(sd.isocalendar()) # year, weeknum, weekdaynum | |
4720 | # logger.debug("adding occasion: {0}; {1}".format(key, evnt_summary)) | |
4721 | occasionsSL.setdefault(key, IndexableSkiplist(1000, "occasions")).insert((evnt_summary, uid, f)) | |
4722 | ||
4723 | ||
4724 | def remove_occasion(key, oc): # sd, evnt_summary, uid, f): | |
4725 | # logger.debug("removing occasion: {0}, {1}".format(key, oc)) | |
4726 | occasionsSL[key].remove(oc) | |
4727 | ||
4728 | ||
4729 | def setSummary(hsh, dt): | |
4730 | if not dt: | |
4731 | return hsh['_summary'] | |
4732 | # logger.debug("dt: {0}".format(dt)) | |
4733 | mtch = anniversary_regex.search(hsh['_summary']) | |
4734 | retval = hsh['_summary'] | |
4735 | if mtch: | |
4736 | startyear = mtch.group(1) | |
4737 | numyrs = year2string(startyear, dt.year) | |
4738 | retval = anniversary_regex.sub(numyrs, hsh['_summary']) | |
4739 | return retval | |
4740 | ||
4741 | ||
4742 | def setItemPeriod(hsh, start, end, short=False, options=None): | |
4743 | if not options: | |
4744 | options = {} | |
4745 | sy = start.year | |
4746 | ey = end.year | |
4747 | sm = start.month | |
4748 | em = end.month | |
4749 | sd = start.day | |
4750 | ed = end.day | |
4751 | if start == end: # same time - zero extent | |
4752 | if short: | |
4753 | period = "%s" % fmt_time(start, options=options) | |
4754 | else: | |
4755 | period = "%s %s" % ( | |
4756 | fmt_time(start, options=options), fmt_date(start, True)) | |
4757 | elif (sy, sm, sd) == (ey, em, ed): # same day | |
4758 | if short: | |
4759 | period = "%s - %s" % ( | |
4760 | fmt_time(start, options=options), | |
4761 | fmt_time(end, options=options)) | |
4762 | else: | |
4763 | period = "%s - %s %s" % ( | |
4764 | fmt_time(start, options=options), | |
4765 | fmt_time(end, options=options), | |
4766 | fmt_date(end, True)) | |
4767 | else: | |
4768 | period = "%s %s - %s %s" % ( | |
4769 | fmt_time(start, options=options), fmt_date(start, True), | |
4770 | fmt_time(end, options=options), fmt_date(end, True)) | |
4771 | return period | |
4772 | ||
4773 | ||
4774 | def getDataFromFile(f, file2data, bef, file2uuids=None, uuid2hash=None, options=None): | |
4775 | if not options: | |
4776 | options = {} | |
4777 | if not file2uuids: | |
4778 | file2uuids = {} | |
4779 | if not uuid2hash: | |
4780 | uuid2hash = {} | |
4781 | today_datetime = datetime.now().replace( | |
4782 | hour=0, minute=0, second=0, microsecond=0) | |
4783 | today_date = datetime.now().date() | |
4784 | yearnum, weeknum, daynum = today_date.isocalendar() | |
4785 | items = [] # [(view, sort(3|4), fn), (branches), (leaf)] | |
4786 | datetimes = [] | |
4787 | busytimes = [] | |
4788 | occasions = [] | |
4789 | alerts = [] | |
4790 | alert_minutes = {} | |
4791 | folders = expandPath(f) | |
4792 | for uid in file2uuids[f]: | |
4793 | # this will give the items in file order! | |
4794 | if uuid2hash[uid]['itemtype'] in ['=']: | |
4795 | continue | |
4796 | sdt = "" | |
4797 | hsh = {} | |
4798 | for k, v in uuid2hash[uid].items(): | |
4799 | hsh[k] = v | |
4800 | # we'll make anniversary subs to a copy later | |
4801 | hsh['summary'] = hsh['_summary'] | |
4802 | typ = type2Str[hsh['itemtype']] | |
4803 | # we need a context for due view and a keyword for keyword view | |
4804 | ||
4805 | if hsh['itemtype'] != '#': | |
4806 | if 'c' not in hsh: | |
4807 | if 's' not in hsh and hsh['itemtype'] in [u'+', u'-', u'%']: | |
4808 | # undated task | |
4809 | hsh['c'] = NONE | |
4810 | else: | |
4811 | hsh['c'] = NONE | |
4812 | ||
4813 | if 'k' not in hsh: | |
4814 | hsh['k'] = NONE | |
4815 | ||
4816 | if 't' not in hsh: | |
4817 | hsh['t'] = [NONE] | |
4818 | ||
4819 | # make task entries for day, keyword and folder view | |
4820 | if hsh['itemtype'] in [u'+', u'-', u'%']: | |
4821 | done, due, following = getDoneAndTwo(hsh) | |
4822 | hist_key = 'f' | |
4823 | if hsh['itemtype'] == '+' and 'h' in hsh: | |
4824 | hist_key = 'h' | |
4825 | if done: | |
4826 | # add the last show_finished completions to day and keywords | |
4827 | # dts = done.strftime(sortdatefmt) | |
4828 | # sdt = fmt_date(hsh['f'][-1][0], True) | |
4829 | sdt = fmt_date(hsh['f'][-1][0], True) | |
4830 | typ = 'fn' | |
4831 | # add a finished entry to day view | |
4832 | # only show the last 'show_finished' completions | |
4833 | for d0, d1 in hsh[hist_key][-options['show_finished']:]: | |
4834 | item = [ | |
4835 | ('day', d0.strftime(sortdatefmt), | |
4836 | tstr2SCI[typ][0], hsh['_p'], '', f), | |
4837 | (fmt_date(d0),), | |
4838 | (uid, typ, setSummary(hsh, d0), '', d0)] | |
4839 | items.append(item) | |
4840 | # add2Dates(datetimes, (d0, f)) | |
4841 | # add2list("datetimes", (d0, f)) | |
4842 | datetimes.append((d0, f)) | |
4843 | # datetimes.append((d0, f)) | |
4844 | if 'k' in hsh: | |
4845 | keywords = [x.strip() for x in hsh['k'].split(':')] | |
4846 | item = [ | |
4847 | ('keyword', (hsh['k'], tstr2SCI[typ][0]), | |
4848 | d0, hsh['_summary'], f), tuple(keywords), | |
4849 | (uid, typ, | |
4850 | setSummary(hsh, d0), fmt_date(d0, True), d0)] | |
4851 | items.append(item) | |
4852 | ||
4853 | if not due: | |
4854 | # add the last completion to folder view | |
4855 | item = [ | |
4856 | ('folder', (f, tstr2SCI[typ][0]), done, | |
4857 | hsh['_summary'], f), tuple(folders), | |
4858 | (uid, typ, setSummary(hsh, done), sdt, done)] | |
4859 | items.append(item) | |
4860 | ||
4861 | if due: | |
4862 | # add a due entry to folder view | |
4863 | dtl = due | |
4864 | # dts = due.strftime(sortdatefmt) | |
4865 | sdt = fmt_date(due, True) | |
4866 | time_diff = (due - today_datetime).days | |
4867 | if time_diff >= 0: | |
4868 | time_str = sdt | |
4869 | pastdue = False | |
4870 | else: | |
4871 | if time_diff > -99: | |
4872 | time_str = '%s: %dd' % (sdt, time_diff) | |
4873 | else: | |
4874 | time_str = sdt | |
4875 | pastdue = True | |
4876 | time_str = leadingzero.sub('', time_str) | |
4877 | ||
4878 | if hsh['itemtype'] == '%': | |
4879 | if pastdue: | |
4880 | typ = 'pd' | |
4881 | else: | |
4882 | typ = 'ds' | |
4883 | elif hsh['itemtype'] == '-': | |
4884 | if pastdue: | |
4885 | typ = 'pt' | |
4886 | else: | |
4887 | typ = 'av' | |
4888 | else: | |
4889 | # group | |
4890 | if 'prereqs' in hsh and hsh['prereqs']: | |
4891 | if pastdue: | |
4892 | typ = 'pu' | |
4893 | else: | |
4894 | typ = 'cu' | |
4895 | else: | |
4896 | if pastdue: | |
4897 | typ = 'pc' | |
4898 | else: | |
4899 | typ = 'cs' | |
4900 | item = [ | |
4901 | ('folder', (f, tstr2SCI[typ][0]), due, | |
4902 | hsh['_summary'], f), tuple(folders), | |
4903 | (uid, typ, setSummary(hsh, due), time_str, dtl)] | |
4904 | items.append(item) | |
4905 | if 'k' in hsh and hsh['itemtype'] != '#': | |
4906 | keywords = [x.strip() for x in hsh['k'].split(':')] | |
4907 | item = [ | |
4908 | ('keyword', (hsh['k'], tstr2SCI[typ][0]), | |
4909 | due, hsh['_summary'], f), tuple(keywords), | |
4910 | (uid, typ, | |
4911 | setSummary(hsh, due), time_str, dtl)] | |
4912 | items.append(item) | |
4913 | if 't' in hsh and hsh['itemtype'] != "#": | |
4914 | for tag in hsh['t']: | |
4915 | item = [ | |
4916 | ('tag', (tag, tstr2SCI[typ][0]), due, | |
4917 | hsh['_summary'], f), (tag,), | |
4918 | (uid, typ, | |
4919 | setSummary(hsh, due), time_str, dtl)] | |
4920 | items.append(item) | |
4921 | if not due and not done: # undated | |
4922 | # dts = "none" | |
4923 | dtl = today_datetime | |
4924 | item = [ | |
4925 | ('folder', (f, tstr2SCI[typ][0]), '', | |
4926 | hsh['_summary'], f), | |
4927 | tuple(folders), | |
4928 | (uid, typ, setSummary(hsh, ''), '')] | |
4929 | items.append(item) | |
4930 | ||
4931 | if 'k' in hsh and hsh['itemtype'] != "#": | |
4932 | keywords = [x.strip() for x in hsh['k'].split(':')] | |
4933 | item = [ | |
4934 | ('keyword', (hsh['k'], tstr2SCI[typ][0]), '', | |
4935 | hsh['_summary'], f), tuple(keywords), | |
4936 | (uid, typ, setSummary(hsh, ''), '', dtl)] | |
4937 | items.append(item) | |
4938 | if 't' in hsh and hsh['itemtype'] != "#": | |
4939 | for tag in hsh['t']: | |
4940 | item = [ | |
4941 | ('tag', (tag, tstr2SCI[typ][0]), due, | |
4942 | hsh['_summary'], f), (tag,), | |
4943 | (uid, typ, setSummary(hsh, ''), '', dtl)] | |
4944 | items.append(item) | |
4945 | ||
4946 | else: # not a task type | |
4947 | if 's' in hsh: | |
4948 | if 'rrule' in hsh: | |
4949 | if hsh['itemtype'] in ['^', '*', '~']: | |
4950 | dt = ( | |
4951 | hsh['rrule'].after(today_datetime, inc=True) | |
4952 | or hsh['rrule'].before( | |
4953 | today_datetime, inc=True)) | |
4954 | else: | |
4955 | dt = hsh['rrule'].after(hsh['s'], inc=True) | |
4956 | else: | |
4957 | dt = parse(parse_dtstr(hsh['s'], | |
4958 | hsh['z'])).replace(tzinfo=None) | |
4959 | else: | |
4960 | dt = None | |
4961 | # dts = "none" | |
4962 | sdt = "" | |
4963 | ||
4964 | if dt: | |
4965 | if hsh['itemtype'] == '*': | |
4966 | # sdt = "%s %s" % ( | |
4967 | # fmt_time(dt, True, options=options), | |
4968 | # fmt_date(dt, True)) | |
4969 | sdt = fmt_shortdatetime(dt, options=options) | |
4970 | elif hsh['itemtype'] == '~': | |
4971 | if 'e' in hsh: | |
4972 | sd = fmt_date(dt, True) | |
4973 | sd = leadingzero.sub('', sd) | |
4974 | sdt = "%s: %s" % ( | |
4975 | sd, | |
4976 | fmt_period(hsh['e']) | |
4977 | ) | |
4978 | else: | |
4979 | sdt = "" | |
4980 | else: | |
4981 | # sdt = fmt_date(dt, True) | |
4982 | # sdt = leadingzero.sub('', fmt_date(dt, True)), | |
4983 | sdt = fmt_date(dt, True) | |
4984 | sdt = leadingzero.sub('', sdt) | |
4985 | else: | |
4986 | dt = today_datetime | |
4987 | ||
4988 | if hsh['itemtype'] == '*': | |
4989 | if 'e' in hsh and hsh['e']: | |
4990 | typ = 'ev' | |
4991 | else: | |
4992 | typ = 'rm' | |
4993 | else: | |
4994 | typ = type2Str[hsh['itemtype']] | |
4995 | item = [ | |
4996 | ('folder', (f, tstr2SCI[typ][0]), dt, | |
4997 | hsh['_summary'], f), tuple(folders), | |
4998 | (uid, typ, setSummary(hsh, dt), sdt, dt)] | |
4999 | items.append(item) | |
5000 | if 'k' in hsh and hsh['itemtype'] != "#": | |
5001 | keywords = [x.strip() for x in hsh['k'].split(':')] | |
5002 | item = [ | |
5003 | ('keyword', (hsh['k'], tstr2SCI[typ][0]), dt, | |
5004 | hsh['_summary'], f), tuple(keywords), | |
5005 | (uid, typ, setSummary(hsh, dt), sdt, dt)] | |
5006 | items.append(item) | |
5007 | if 't' in hsh and hsh['itemtype'] != "#": | |
5008 | for tag in hsh['t']: | |
5009 | item = [ | |
5010 | ('tag', (tag, tstr2SCI[typ][0]), dt, | |
5011 | hsh['_summary'], f), (tag,), | |
5012 | (uid, typ, setSummary(hsh, dt), sdt, dt)] | |
5013 | items.append(item) | |
5014 | if hsh['itemtype'] == '#': | |
5015 | # don't include hidden items in any other views | |
5016 | continue | |
5017 | # make in basket and someday entries # | |
5018 | # sort numbers for now view --- we'll add the typ num to | |
5019 | if hsh['itemtype'] == '$': | |
5020 | item = [ | |
5021 | ('inbasket', (0, tstr2SCI['ib'][0]), dt, | |
5022 | hsh['_summary'], f), | |
5023 | (uid, 'ib', setSummary(hsh, dt), sdt, dt)] | |
5024 | items.append(item) | |
5025 | continue | |
5026 | if hsh['itemtype'] == '?': | |
5027 | item = [ | |
5028 | ('someday', 2, (tstr2SCI['so'][0]), dt, | |
5029 | hsh['_summary'], f), | |
5030 | (uid, 'so', setSummary(hsh, dt), sdt, dt)] | |
5031 | items.append(item) | |
5032 | continue | |
5033 | if hsh['itemtype'] == '!': | |
5034 | if not ('k' in hsh and hsh['k']): | |
5035 | hsh['k'] = _("none") | |
5036 | keywords = [x.strip() for x in hsh['k'].split(':')] | |
5037 | item = [ | |
5038 | ('note', (hsh['k'], tstr2SCI[typ][0]), '', | |
5039 | hsh['_summary'], f), tuple(keywords), | |
5040 | (uid, typ, setSummary(hsh, ''), '', dt)] | |
5041 | items.append(item) | |
5042 | # make entry for next view | |
5043 | if 's' not in hsh and hsh['itemtype'] in [u'+', u'-', u'%']: | |
5044 | if 'f' in hsh: | |
5045 | continue | |
5046 | if 'e' in hsh and hsh['e'] is not None: | |
5047 | extstr = fmt_period(hsh['e']) | |
5048 | exttd = hsh['e'] | |
5049 | else: | |
5050 | extstr = '' | |
5051 | exttd = 0 * oneday | |
5052 | if hsh['itemtype'] == '+': | |
5053 | if 'prereqs' in hsh and hsh['prereqs']: | |
5054 | typ = 'cu' | |
5055 | else: | |
5056 | typ = 'cs' | |
5057 | elif hsh['itemtype'] == '%': | |
5058 | typ = 'du' | |
5059 | else: | |
5060 | typ = type2Str[hsh['itemtype']] | |
5061 | ||
5062 | item = [ | |
5063 | ('next', (1, hsh['c'], hsh['_p'], exttd), | |
5064 | tstr2SCI[typ][0], hsh['_p'], hsh['_summary'], f), | |
5065 | (hsh['c'],), (uid, typ, hsh['_summary'], extstr)] | |
5066 | items.append(item) | |
5067 | continue | |
5068 | # make entries for day view and friends | |
5069 | dates = [] | |
5070 | if 'rrule' in hsh: | |
5071 | gotall, dates = get_reps(bef, hsh) | |
5072 | for date in dates: | |
5073 | # add2list("datetimes", (date, f)) | |
5074 | datetimes.append((date, f)) | |
5075 | ||
5076 | elif 's' in hsh and hsh['s'] and 'f' not in hsh: | |
5077 | thisdate = parse( | |
5078 | parse_dtstr( | |
5079 | hsh['s'], hsh['z'])).astimezone( | |
5080 | tzlocal()).replace(tzinfo=None) | |
5081 | dates.append(thisdate) | |
5082 | # add2list("datetimes", (thisdate, f)) | |
5083 | datetimes.append((thisdate, f)) | |
5084 | for dt in dates: | |
5085 | dtl = dt | |
5086 | sd = dtl.date() | |
5087 | st = dtl.time() | |
5088 | if typ == 'oc': | |
5089 | st_fmt = '' | |
5090 | else: | |
5091 | st_fmt = fmt_time(st, options=options) | |
5092 | summary = setSummary(hsh, dtl) | |
5093 | tmpl_hsh = {'i': uid, 'summary': summary, | |
5094 | 'start_date': fmt_date(dtl, True), | |
5095 | 'start_time': fmt_time(dtl, True, options=options)} | |
5096 | if 't' in hsh: | |
5097 | tmpl_hsh['t'] = ', '.join(hsh['t']) | |
5098 | else: | |
5099 | tmpl_hsh['t'] = '' | |
5100 | if 'e' in hsh: | |
5101 | try: | |
5102 | tmpl_hsh['e'] = fmt_period(hsh['e']) | |
5103 | etl = (dtl + hsh['e']) | |
5104 | except: | |
5105 | logger.exception("Could not fmt hsh['e']=%s" % hsh['e']) | |
5106 | else: | |
5107 | tmpl_hsh['e'] = '' | |
5108 | etl = dtl | |
5109 | tmpl_hsh['time_span'] = setItemPeriod( | |
5110 | hsh, dtl, etl, options=options) | |
5111 | tmpl_hsh['busy_span'] = setItemPeriod( | |
5112 | hsh, dtl, etl, True, options=options) | |
5113 | for k in ['c', 'd', 'k', 'l', 'm', 'uid', 'z']: | |
5114 | if k in hsh: | |
5115 | tmpl_hsh[k] = hsh[k] | |
5116 | else: | |
5117 | tmpl_hsh[k] = '' | |
5118 | if '_a' in hsh and hsh['_a']: | |
5119 | for alert in hsh['_a']: | |
5120 | time_deltas, acts, arguments = alert | |
5121 | if not acts: | |
5122 | acts = options['alert_default'] | |
5123 | tmpl_hsh['alert_email'] = tmpl_hsh['alert_process'] = '' | |
5124 | tmpl_hsh["_alert_action"] = acts | |
5125 | tmpl_hsh["_alert_argument"] = arguments | |
5126 | ||
5127 | for td in time_deltas: | |
5128 | adt = dtl - td | |
5129 | if adt.date() == today_date: | |
5130 | this_hsh = deepcopy(tmpl_hsh) | |
5131 | this_hsh['alert_time'] = fmt_time( | |
5132 | adt, True, options=options) | |
5133 | this_hsh['time_left'] = timedelta2Str(td) | |
5134 | this_hsh['when'] = timedelta2Sentence(td) | |
5135 | if adt.date() != dtl.date(): | |
5136 | this_hsh['_event_time'] = fmt_period(td) | |
5137 | else: | |
5138 | this_hsh['_event_time'] = fmt_time( | |
5139 | dtl, True, options=options) | |
5140 | amn = adt.hour * 60 + adt.minute | |
5141 | # we don't want ties in amn else add2list will try to sort on the hash and fail | |
5142 | if amn in alert_minutes: | |
5143 | # add 6 seconds to avoid the tie | |
5144 | alert_minutes[amn] += .1 | |
5145 | else: | |
5146 | alert_minutes[amn] = amn | |
5147 | # add2list(alerts, (amn, this_hsh, f), False) | |
5148 | # add2list("alerts", (alert_minutes[amn], this_hsh['i'], this_hsh, f), False) | |
5149 | alerts.append((alert_minutes[amn], this_hsh['i'], this_hsh, f)) | |
5150 | if (hsh['itemtype'] in ['+', '-', '%'] and dtl < today_datetime): | |
5151 | time_diff = (dtl - today_datetime).days | |
5152 | if time_diff == 0: | |
5153 | time_str = fmt_period(hsh['e']) | |
5154 | pastdue = False | |
5155 | else: | |
5156 | time_str = '%dd' % time_diff | |
5157 | pastdue = True | |
5158 | if hsh['itemtype'] == '%': | |
5159 | if pastdue: | |
5160 | typ = 'pd' | |
5161 | else: | |
5162 | typ = 'ds' | |
5163 | cat = 'Delegated' | |
5164 | sn = (2, tstr2SCI[typ][0]) | |
5165 | elif hsh['itemtype'] == '-': | |
5166 | if pastdue: | |
5167 | typ = 'pt' | |
5168 | else: | |
5169 | typ = 'av' | |
5170 | cat = 'Available' | |
5171 | sn = (1, tstr2SCI[typ][0]) | |
5172 | else: | |
5173 | # group | |
5174 | if 'prereqs' in hsh and hsh['prereqs']: | |
5175 | if pastdue: | |
5176 | typ = 'pu' | |
5177 | else: | |
5178 | typ = 'cu' | |
5179 | cat = 'Waiting' | |
5180 | sn = (2, tstr2SCI[typ][0]) | |
5181 | else: | |
5182 | if pastdue: | |
5183 | typ = 'pc' | |
5184 | else: | |
5185 | typ = 'cs' | |
5186 | cat = 'Available' | |
5187 | sn = (1, tstr2SCI[typ][0]) | |
5188 | if 'f' in hsh and 'rrule' not in hsh: | |
5189 | continue | |
5190 | else: | |
5191 | item = [ | |
5192 | ('now', sn, dtl, hsh['_p'], summary, f), (cat,), | |
5193 | (uid, typ, summary, time_str, dtl)] | |
5194 | items.append(item) | |
5195 | ||
5196 | if 'b' in hsh: | |
5197 | time_diff = (dtl - today_datetime).days | |
5198 | if time_diff > 0 and time_diff <= hsh['b']: | |
5199 | extstr = '%dd' % time_diff | |
5200 | exttd = 0 * oneday | |
5201 | item = [('day', | |
5202 | today_datetime.strftime(sortdatefmt), | |
5203 | tstr2SCI['by'][0], | |
5204 | # tstr2SCI[typ][0], | |
5205 | time_diff, | |
5206 | hsh['_p'], | |
5207 | f), | |
5208 | (fmt_date(today_datetime),), | |
5209 | (uid, 'by', summary, extstr, dtl)] | |
5210 | items.append(item) | |
5211 | ||
5212 | if hsh['itemtype'] == '!': | |
5213 | typ = 'ns' | |
5214 | item = [ | |
5215 | ('day', sd.strftime(sortdatefmt), tstr2SCI[typ][0], | |
5216 | hsh['_p'], '', f), | |
5217 | (fmt_date(dt),), | |
5218 | (uid, typ, summary, '', dtl)] | |
5219 | items.append(item) | |
5220 | continue | |
5221 | if hsh['itemtype'] == '^': | |
5222 | typ = 'oc' | |
5223 | item = [ | |
5224 | ('day', sd.strftime(sortdatefmt), | |
5225 | tstr2SCI[typ][0], hsh['_p'], '', f), | |
5226 | (fmt_date(dt),), | |
5227 | (uid, typ, summary, '', dtl)] | |
5228 | items.append(item) | |
5229 | occasions.append([sd, summary, uid, f]) | |
5230 | continue | |
5231 | if hsh['itemtype'] == '~': | |
5232 | typ = 'ac' | |
5233 | if 'e' in hsh: | |
5234 | sdt = fmt_period(hsh['e']) | |
5235 | else: | |
5236 | sdt = "" | |
5237 | item = [ | |
5238 | ('day', sd.strftime(sortdatefmt), | |
5239 | tstr2SCI[typ][0], hsh['_p'], '', f), | |
5240 | (fmt_date(dt),), | |
5241 | (uid, 'ac', summary, | |
5242 | sdt, dtl)] | |
5243 | items.append(item) | |
5244 | continue | |
5245 | if hsh['itemtype'] == '*': | |
5246 | sm = st.hour * 60 + st.minute | |
5247 | ed = etl.date() | |
5248 | et = etl.time() | |
5249 | em = et.hour * 60 + et.minute | |
5250 | evnt_summary = "%s: %s" % (tmpl_hsh['summary'], tmpl_hsh['busy_span']) | |
5251 | if et != st: | |
5252 | et_fmt = " ~ %s" % fmt_time(et, options=options) | |
5253 | else: | |
5254 | et_fmt = '' | |
5255 | if ed > sd: | |
5256 | # this event overlaps more than one day | |
5257 | # first_min = 24*60 - sm | |
5258 | # last_min = em | |
5259 | # the first day tuple | |
5260 | item = [ | |
5261 | ('day', sd.strftime(sortdatefmt), | |
5262 | tstr2SCI[typ][0], hsh['_p'], | |
5263 | st.strftime(sorttimefmt), f), | |
5264 | (fmt_date(sd),), | |
5265 | (uid, typ, summary, '%s ~ %s' % | |
5266 | (st_fmt, | |
5267 | options['dayend_fmt']), dtl)] | |
5268 | items.append(item) | |
5269 | busytimes.append([sd, sm, day_end_minutes, evnt_summary, uid, f]) | |
5270 | sd += oneday | |
5271 | i = 0 | |
5272 | item_copy = [] | |
5273 | while sd < ed: | |
5274 | item_copy.append([x for x in item]) | |
5275 | item_copy[i][0] = list(item_copy[i][0]) | |
5276 | item_copy[i][1] = list(item_copy[i][1]) | |
5277 | item_copy[i][2] = list(item_copy[i][2]) | |
5278 | item_copy[i][0][1] = sd.strftime(sortdatefmt) | |
5279 | item_copy[i][1][0] = fmt_date(sd) | |
5280 | item_copy[i][2][3] = '%s ~ %s' % ( | |
5281 | options['daybegin_fmt'], | |
5282 | options['dayend_fmt']) | |
5283 | item_copy[i][0] = tuple(item_copy[i][0]) | |
5284 | item_copy[i][1] = tuple(item_copy[i][1]) | |
5285 | item_copy[i][2] = tuple(item_copy[i][2]) | |
5286 | # add2list("items", item_copy[i]) | |
5287 | items.append(item_copy[i]) | |
5288 | busytimes.append([sd, 0, day_end_minutes, evnt_summary, uid, f]) | |
5289 | sd += oneday | |
5290 | i += 1 | |
5291 | # the last day tuple | |
5292 | if em: | |
5293 | item_copy.append([x for x in item]) | |
5294 | item_copy[i][0] = list(item_copy[i][0]) | |
5295 | item_copy[i][1] = list(item_copy[i][1]) | |
5296 | item_copy[i][2] = list(item_copy[i][2]) | |
5297 | item_copy[i][0][1] = sd.strftime(sortdatefmt) | |
5298 | item_copy[i][1][0] = fmt_date(sd) | |
5299 | item_copy[i][2][3] = '%s%s' % ( | |
5300 | options['daybegin_fmt'], et_fmt) | |
5301 | item_copy[i][0] = tuple(item_copy[i][0]) | |
5302 | item_copy[i][1] = tuple(item_copy[i][1]) | |
5303 | item_copy[i][2] = tuple(item_copy[i][2]) | |
5304 | # add2list("items", item_copy[i]) | |
5305 | items.append(item_copy[i]) | |
5306 | busytimes.append([sd, 0, em, evnt_summary, uid, f]) | |
5307 | else: | |
5308 | # single day event or reminder | |
5309 | item = [ | |
5310 | ('day', sd.strftime(sortdatefmt), | |
5311 | tstr2SCI[typ][0], hsh['_p'], | |
5312 | st.strftime(sorttimefmt), f), | |
5313 | (fmt_date(sd),), | |
5314 | (uid, typ, summary, '%s%s' % ( | |
5315 | st_fmt, | |
5316 | et_fmt), dtl)] | |
5317 | items.append(item) | |
5318 | busytimes.append([sd, sm, em, evnt_summary, uid, f]) | |
5319 | continue | |
5320 | # other dated items | |
5321 | if hsh['itemtype'] in ['+', '-', '%']: | |
5322 | if 'e' in hsh: | |
5323 | extstr = fmt_period(hsh['e']) | |
5324 | else: | |
5325 | extstr = '' | |
5326 | if 'f' in hsh and hsh['f'][-1][1] == dtl: | |
5327 | typ = 'fn' | |
5328 | else: | |
5329 | if hsh['itemtype'] == '%': | |
5330 | typ = 'ds' | |
5331 | elif hsh['itemtype'] == '+': | |
5332 | if 'prereqs' in hsh and hsh['prereqs']: | |
5333 | typ = 'cu' | |
5334 | else: | |
5335 | typ = 'cs' | |
5336 | else: | |
5337 | typ = 'av' | |
5338 | item = [ | |
5339 | ('day', sd.strftime(sortdatefmt), tstr2SCI[typ][0], | |
5340 | hsh['_p'], '', f), | |
5341 | (fmt_date(dt),), | |
5342 | (uid, typ, summary, extstr, dtl)] | |
5343 | items.append(item) | |
5344 | continue | |
5345 | if hsh['itemtype'] == '%': | |
5346 | if 'f' in hsh: | |
5347 | typ = 'fn' | |
5348 | else: | |
5349 | typ = 'ds' | |
5350 | item = [ | |
5351 | ('day', sd.strftime(sortdatefmt), tstr2SCI[typ][0], | |
5352 | hsh['_p'], '', f), | |
5353 | (fmt_date(dt),), | |
5354 | (uid, typ, summary, extstr, dtl)] | |
5355 | items.append(item) | |
5356 | continue | |
5357 | if hsh['itemtype'] == '+': | |
5358 | if 'prereqs' in hsh and hsh['prereqs']: | |
5359 | typ = 'cu' | |
5360 | else: | |
5361 | if 'f' in hsh and hsh['f'][-1][1] == dtl: | |
5362 | typ = 'fn' | |
5363 | else: | |
5364 | typ = 'cs' | |
5365 | item = [ | |
5366 | ('day', sd.strftime(sortdatefmt), tstr2SCI[typ][0], | |
5367 | hsh['_p'], '', f), | |
5368 | (fmt_date(dt),), | |
5369 | (uid, typ, summary, extstr, dtl)] | |
5370 | items.append(item) | |
5371 | continue | |
5372 | file2data[f] = [items, alerts, busytimes, datetimes, occasions] | |
5373 | ||
5374 | ||
5375 | # noinspection PyChainedComparisons | |
5376 | def getViewData(bef, file2uuids=None, uuid2hash=None, options=None, file2data=None): | |
5377 | """ | |
5378 | Collect data on all items, apply filters later | |
5379 | """ | |
5380 | tt = TimeIt(loglevel=2, label="getViewData") | |
5381 | if not file2uuids: | |
5382 | file2uuids = {} | |
5383 | if not uuid2hash: | |
5384 | uuid2hash = {} | |
5385 | if not options: | |
5386 | options = {} | |
5387 | file2data = {} | |
5388 | clear_all_data() | |
5389 | logger.debug('calling getDataFromFile for {0} files'.format(len(file2uuids.keys()))) | |
5390 | for f in file2uuids: | |
5391 | getDataFromFile(f, file2data, bef, file2uuids, uuid2hash, options) | |
5392 | logger.debug('calling updateViewFromFile for {0} files'.format(len(file2uuids.keys()))) | |
5393 | for f in file2data: | |
5394 | updateViewFromFile(f, file2data) | |
5395 | numfiles = len(file2uuids.keys()) | |
5396 | numitems = len(uuid2hash.keys()) | |
5397 | logger.info("files: {0}\n file items: {1}\n view items: {2}\n datetimes: {3}\n alerts: {4}\n busytimes: {5}\n occasions: {6}".format(numfiles, numitems, len(list(itemsSL)), len(list(datetimesSL)), len(list(alertsSL)), len(busytimesSL.keys()), len(occasionsSL.keys()))) | |
5398 | tt.stop() | |
5399 | return file2data | |
5400 | ||
5401 | ||
5402 | def updateViewFromFile(f, file2data): | |
5403 | _items, _alerts, _busytimes, _datetimes, _occasions = file2data[f] | |
5404 | # logger.debug('file: {0}'.format(f)) | |
5405 | for item in _items: | |
5406 | # logger.debug('adding item: {0}'.format(item)) | |
5407 | add2list("items", item) | |
5408 | for alert in _alerts: | |
5409 | # logger.debug('adding alert: {0}'.format(alert)) | |
5410 | add2list("alerts", alert) | |
5411 | for dt in _datetimes: | |
5412 | # logger.debug('adding datetime: {0}'.format(dt)) | |
5413 | add2list("datetimes", dt) | |
5414 | for bt in _busytimes: | |
5415 | # logger.debug('adding busytime: {0}'.format(bt)) | |
5416 | sd, sm, em, evnt_summary, uid, rpth = bt | |
5417 | key = sd.isocalendar() | |
5418 | add_busytime(key, sm, em, evnt_summary, uid, rpth) | |
5419 | for oc in _occasions: | |
5420 | # logger.debug('adding occasion: {0}'.format(oc)) | |
5421 | sd, evnt_summary, uid, f = oc | |
5422 | key = sd.isocalendar() | |
5423 | add_occasion(key, evnt_summary, uid, f) | |
5424 | ||
5425 | ||
5426 | def updateViewData(f, bef, file2uuids=None, uuid2hash=None, options=None, file2data=None): | |
5427 | tt = TimeIt(loglevel=2, label="updateViewData") | |
5428 | if not file2uuids: | |
5429 | file2uuids = {} | |
5430 | if not uuid2hash: | |
5431 | uuid2hash = {} | |
5432 | if not options: | |
5433 | options = {} | |
5434 | # clear data for this file | |
5435 | _items = _alerts = _busytimes = _datetimes = _occasions = [] | |
5436 | if f in file2data: | |
5437 | _items, _alerts, _busytimes, _datetimes, _occasions = file2data[f] | |
5438 | if _items: | |
5439 | for item in _items: | |
5440 | # logger.debug('removing item: {0}'.format(item)) | |
5441 | removeFromlist("items", item) | |
5442 | # itemsSL.remove(item) | |
5443 | for alert in _alerts: | |
5444 | # logger.debug('removing alert: {0}'.format(alert)) | |
5445 | removeFromlist("alerts", alert) | |
5446 | # alertsSL.remove(alert) | |
5447 | for dt in _datetimes: | |
5448 | # logger.debug('removing datetime: {0}'.format(datetime)) | |
5449 | removeFromlist("datetimes", dt) | |
5450 | # datetimesSL.remove(datetime) | |
5451 | for bt in _busytimes: | |
5452 | bt = list(bt) | |
5453 | sd = bt.pop(0) | |
5454 | bt = tuple(bt) | |
5455 | key = sd.isocalendar() | |
5456 | # logger.debug('removing busytime: {0}: {1}'.format(key, bt)) | |
5457 | remove_busytime(key, bt) | |
5458 | for oc in _occasions: | |
5459 | oc = list(oc) | |
5460 | sd = oc.pop(0) | |
5461 | oc = tuple(oc) | |
5462 | key = sd.isocalendar() | |
5463 | # logger.debug('removing occasion: {0}: {1}'.format(key, oc)) | |
5464 | remove_occasion(key, oc) | |
5465 | ||
5466 | # remove the old entry for f in file2data | |
5467 | del file2data[f] | |
5468 | ||
5469 | # update file2data | |
5470 | getDataFromFile(f, file2data, bef, file2uuids, uuid2hash, options) | |
5471 | # update itemsSL, ... | |
5472 | updateViewFromFile(f, file2data) | |
5473 | ||
5474 | rows = list(itemsSL) | |
5475 | alerts = list(alertsSL) | |
5476 | datetimes = list(datetimesSL) | |
5477 | busytimes = {} | |
5478 | for key in busytimesSL: | |
5479 | busytimes[key] = list(busytimesSL[key]) | |
5480 | occasions = {} | |
5481 | for key in occasionsSL: | |
5482 | occasions[key] = list(occasionsSL[key]) | |
5483 | ||
5484 | numitems = len(file2uuids[f]) | |
5485 | logger.info("file: {0}\n file items: {1}\n view items: {2}\n datetimes: {3}\n alerts: {4}\n busytimes: {5}\n occasions: {5}".format(f, numitems, len(_items), len(_datetimes), len(_alerts), len(_busytimes), len(_occasions))) | |
5486 | tt.stop() | |
5487 | return rows, alerts, busytimes, datetimes, occasions, file2data | |
5488 | ||
5489 | ||
5490 | def updateCurrentFiles(allrows, file2uuids, uuid2hash, options): | |
5491 | logger.debug("updateCurrent") | |
5492 | # logger.debug(('options: {0}'.format(options))) | |
5493 | res = True | |
5494 | if options['current_textfile']: | |
5495 | if 'current_opts' in options and options['current_opts']: | |
5496 | txt, count2id = getReportData( | |
5497 | options['current_opts'], | |
5498 | file2uuids, | |
5499 | uuid2hash, | |
5500 | options, | |
5501 | colors=0) | |
5502 | else: | |
5503 | tree = getAgenda( | |
5504 | allrows, | |
5505 | colors=options['agenda_colors'], | |
5506 | days=options['agenda_days'], | |
5507 | indent=options['current_indent'], | |
5508 | width1=options['current_width1'], | |
5509 | width2=options['current_width2'], | |
5510 | calendars=options['calendars'], | |
5511 | mode='text' | |
5512 | ) | |
5513 | # logger.debug('text colors: {0}'.format(options['agenda_colors'])) | |
5514 | txt, args0, args1 = tree2Text(tree, colors=options['agenda_colors'], indent=options['current_indent'], width1=options['current_width1'], width2=options['current_width2']) | |
5515 | # logger.debug('text: {0}'.format(txt)) | |
5516 | if txt and not txt[0].strip(): | |
5517 | txt.pop(0) | |
5518 | fo = codecs.open(options['current_textfile'], 'w', file_encoding) | |
5519 | fo.writelines("\n".join(txt)) | |
5520 | fo.close() | |
5521 | if options['current_htmlfile']: | |
5522 | if 'current_opts' in options and options['current_opts']: | |
5523 | html, count2id = getReportData( | |
5524 | options['current_opts'], | |
5525 | file2uuids, | |
5526 | uuid2hash, | |
5527 | options) | |
5528 | else: | |
5529 | tree = getAgenda( | |
5530 | allrows, | |
5531 | colors=options['agenda_colors'], | |
5532 | days=options['agenda_days'], | |
5533 | indent=options['current_indent'], | |
5534 | width1=options['current_width1'], | |
5535 | width2=options['current_width2'], | |
5536 | calendars=options['calendars'], | |
5537 | mode='html') | |
5538 | txt = tree2Html(tree, colors=options['agenda_colors'], indent=options['current_indent'], width1=options['current_width1'], width2=options['current_width2']) | |
5539 | if not txt[0].strip(): | |
5540 | txt.pop(0) | |
5541 | fo = codecs.open(options['current_htmlfile'], 'w', file_encoding) | |
5542 | fo.writelines('<!DOCTYPE html> <html> <head> <meta charset="utf-8">\ | |
5543 | </head><body><pre>%s</pre></body>' % "\n".join(txt)) | |
5544 | fo.close() | |
5545 | if options['current_icsfolder']: | |
5546 | res = export_ical(file2uuids, uuid2hash, options['current_icsfolder'], options['calendars']) | |
5547 | return res | |
5548 | ||
5549 | ||
5550 | def availableDates(s): | |
5551 | """ | |
5552 | start; end; busy | |
5553 | Return dates between start and end that are not in busy where | |
5554 | busy is a comma separated list of dates and date intervals, e.g. | |
5555 | 'jul 3, jul 7 - jul 15, jul 8, jul 6 - jul 10, jul 23 - aug 8'. | |
5556 | """ | |
5557 | start_date, end_date, busy_dates = s.split(';') | |
5558 | ||
5559 | set = dtR.rruleset() | |
5560 | set.rrule(rrule(DAILY, dtstart=parse(start_date), until=parse(end_date))) | |
5561 | parts = busy_dates.split(',') | |
5562 | for part in parts: | |
5563 | interval = part.split('-') | |
5564 | if len(interval) == 1: | |
5565 | set.exdate(parse(interval[0])) | |
5566 | if len(interval) == 2: | |
5567 | set.exrule(rrule(DAILY, dtstart=parse(interval[0]), until=parse(interval[1]))) | |
5568 | ||
5569 | res = "\n ".join(x.strftime("%a %b %d") for x in list(set)) | |
5570 | prompt = "between {0} and {1}\nbut not in {2}:\n\n {3}".format(start_date.strip(), end_date.strip(), busy_dates.strip(), res) | |
5571 | return prompt | |
5572 | ||
5573 | ||
5574 | def tupleSum(list_of_tuples): | |
5575 | # get the common length of the tuples | |
5576 | l = len(list_of_tuples[0]) | |
5577 | res = [] | |
5578 | for i in range(l): | |
5579 | res.append(sum([x[i] for x in list_of_tuples])) | |
5580 | return res | |
5581 | ||
5582 | ||
5583 | def hsh2ical(hsh): | |
5584 | """ | |
5585 | Convert hsh to ical object and return tuple (Success, object) | |
5586 | """ | |
5587 | summary = hsh['_summary'] | |
5588 | if hsh['itemtype'] in ['*', '^']: | |
5589 | element = Event() | |
5590 | elif hsh['itemtype'] in ['-', '%', '+']: | |
5591 | element = Todo() | |
5592 | elif hsh['itemtype'] in ['!', '~']: | |
5593 | element = Journal() | |
5594 | else: | |
5595 | return False, 'Cannot export item type "%s"' % hsh['itemtype'] | |
5596 | ||
5597 | element.add('uid', hsh[u'i']) | |
5598 | if 'z' in hsh: | |
5599 | # pytz is required to get the proper tzid into datetimes | |
5600 | tz = pytz.timezone(hsh['z']) | |
5601 | else: | |
5602 | tz = None | |
5603 | if 's' in hsh: | |
5604 | dt = hsh[u's'] | |
5605 | dz = dt.replace(tzinfo=tz) | |
5606 | tzinfo = dz.tzinfo | |
5607 | dt = dz | |
5608 | dd = dz.date() | |
5609 | else: | |
5610 | dt = None | |
5611 | tzinfo = None | |
5612 | # tzname = None | |
5613 | ||
5614 | if u'_r' in hsh: | |
5615 | # repeating | |
5616 | rlst = hsh[u'_r'] | |
5617 | for r in rlst: | |
5618 | if r['f'] == 'l': | |
5619 | if '+' not in hsh: | |
5620 | logger.warn("An entry for '@=' is required but missing.") | |
5621 | continue | |
5622 | # list only kludge: make it repeat daily for a count of 1 | |
5623 | # using the first element from @+ as the starting datetime | |
5624 | dz = parse( | |
5625 | parse_dtstr( | |
5626 | hsh['+'].pop(0), hsh['z'])).replace(tzinfo=tzinfo) | |
5627 | dt = dz | |
5628 | dd = dz.date() | |
5629 | ||
5630 | r['f'] = 'd' | |
5631 | r['t'] = 1 | |
5632 | ||
5633 | rhsh = {} | |
5634 | for k in ical_rrule_keys: | |
5635 | if k in r: | |
5636 | if k == 'f': | |
5637 | rhsh[ical_hsh[k]] = freq_hsh[r[k]] | |
5638 | elif k == 'w': | |
5639 | if type(r[k]) == list: | |
5640 | rhsh[ical_hsh[k]] = [x.upper() for x in r[k]] | |
5641 | else: | |
5642 | rhsh[ical_hsh[k]] = r[k].upper() | |
5643 | elif k == 'u': | |
5644 | uz = parse(parse_dtstr(r[k], hsh['z'])).replace(tzinfo=tzinfo) | |
5645 | rhsh[ical_hsh[k]] = uz | |
5646 | else: | |
5647 | rhsh[ical_hsh[k]] = r[k] | |
5648 | chsh = CaselessDict(rhsh) | |
5649 | element.add('rrule', chsh) | |
5650 | if '+' in hsh: | |
5651 | for pd in hsh['+']: | |
5652 | element.add('rdate', pd) | |
5653 | if '-' in hsh: | |
5654 | for md in hsh['-']: | |
5655 | element.add('exdate', md) | |
5656 | ||
5657 | element.add('summary', summary) | |
5658 | ||
5659 | if 'q' in hsh: | |
5660 | element.add('priority', hsh['_p']) | |
5661 | if 'l' in hsh: | |
5662 | element.add('location', hsh['l']) | |
5663 | if 't' in hsh: | |
5664 | element.add('categories', hsh['t']) | |
5665 | if 'd' in hsh: | |
5666 | element.add('description', hsh['d']) | |
5667 | if 'm' in hsh: | |
5668 | element.add('comment', hsh['m']) | |
5669 | if 'u' in hsh: | |
5670 | element.add('organizer', hsh['u']) | |
5671 | ||
5672 | if hsh['itemtype'] in ['-', '+', '%']: | |
5673 | done, due, following = getDoneAndTwo(hsh) | |
5674 | if 's' in hsh: | |
5675 | element.add('dtstart', dt) | |
5676 | if done: | |
5677 | finz = done.replace(tzinfo=tzinfo) | |
5678 | fint = vDatetime(finz) | |
5679 | element.add('completed', fint) | |
5680 | if due: | |
5681 | duez = due.replace(tzinfo=tzinfo) | |
5682 | dued = vDate(duez) | |
5683 | element.add('due', dued) | |
5684 | elif hsh['itemtype'] == '^': | |
5685 | element.add('dtstart', dd) | |
5686 | elif dt: | |
5687 | try: | |
5688 | element.add('dtstart', dt) | |
5689 | except: | |
5690 | logger.exception('exception adding dtstart: {0}'.format(dt)) | |
5691 | ||
5692 | if hsh['itemtype'] == '*': | |
5693 | if 'e' in hsh and hsh['e']: | |
5694 | ez = dz + hsh['e'] | |
5695 | else: | |
5696 | ez = dz | |
5697 | try: | |
5698 | element.add('dtend', ez) | |
5699 | except: | |
5700 | logger.exception('exception adding dtend: {0}, {1}'.format(ez, tz)) | |
5701 | elif hsh['itemtype'] == '~': | |
5702 | if 'e' in hsh and hsh['e']: | |
5703 | element.add('comment', timedelta2Str(hsh['e'])) | |
5704 | return True, element | |
5705 | ||
5706 | ||
5707 | def export_ical_item(hsh, vcal_file): | |
5708 | """ | |
5709 | Export a single item in iCalendar format | |
5710 | """ | |
5711 | if not has_icalendar: | |
5712 | logger.error("Could not import icalendar") | |
5713 | return False | |
5714 | ||
5715 | cal = Calendar() | |
5716 | cal.add('prodid', '-//etm_tk %s//dgraham.us//' % version) | |
5717 | cal.add('version', '2.0') | |
5718 | ||
5719 | ok, element = hsh2ical(hsh) | |
5720 | if not ok: | |
5721 | return False | |
5722 | cal.add_component(element) | |
5723 | (name, ext) = os.path.splitext(vcal_file) | |
5724 | pname = "%s.ics" % name | |
5725 | try: | |
5726 | cal_str = cal.to_ical() | |
5727 | except Exception: | |
5728 | logger.exception("could not serialize the calendar") | |
5729 | return False | |
5730 | try: | |
5731 | fo = open(pname, 'wb') | |
5732 | except: | |
5733 | logger.exception("Could not open {0}".format(pname)) | |
5734 | return False | |
5735 | try: | |
5736 | fo.write(cal_str) | |
5737 | except Exception: | |
5738 | logger.exception("Could not write to {0}".format(pname)) | |
5739 | finally: | |
5740 | fo.close() | |
5741 | return True | |
5742 | ||
5743 | ||
5744 | def export_ical_active(file2uuids, uuid2hash, vcal_file, calendars=None): | |
5745 | """ | |
5746 | Export items from active calendars to an ics file with the same name in vcal_folder. | |
5747 | """ | |
5748 | if not has_icalendar: | |
5749 | logger.error('Could not import icalendar') | |
5750 | return False | |
5751 | logger.debug("vcal_file: {0}; calendars: {1}".format(vcal_file, calendars)) | |
5752 | ||
5753 | calendar = Calendar() | |
5754 | calendar.add('prodid', '-//etm_tk {0}//dgraham.us//'.format(version)) | |
5755 | calendar.add('version', '2.0') | |
5756 | ||
5757 | cal_tuples = [] | |
5758 | if calendars: | |
5759 | for cal in calendars: | |
5760 | logger.debug('processing cal: {0}'.format(cal)) | |
5761 | if not cal[1]: | |
5762 | continue | |
5763 | name = cal[0] | |
5764 | regex = re.compile(r'^{0}'.format(cal[2])) | |
5765 | cal_tuples.append((name, regex)) | |
5766 | else: | |
5767 | logger.debug('processing cal: all') | |
5768 | regex = re.compile(r'^.*') | |
5769 | cal_tuples.append(('all', regex)) | |
5770 | ||
5771 | if not cal_tuples: | |
5772 | return | |
5773 | ||
5774 | logger.debug('using cal_tuples: {0}'.format(cal_tuples)) | |
5775 | for rp in file2uuids: | |
5776 | match = False | |
5777 | for name, regex in cal_tuples: | |
5778 | if regex.match(rp): | |
5779 | for uid in file2uuids[rp]: | |
5780 | this_hsh = uuid2hash[uid] | |
5781 | ok, element = hsh2ical(this_hsh) | |
5782 | if ok: | |
5783 | calendar.add_component(element) | |
5784 | break | |
5785 | if not match: | |
5786 | logger.debug('skipping {0} - no match in calendars'.format(rp)) | |
5787 | ||
5788 | try: | |
5789 | cal_str = calendar.to_ical() | |
5790 | except Exception: | |
5791 | logger.exception("Could not serialize the calendar: {0}".format(calendar)) | |
5792 | return False | |
5793 | try: | |
5794 | fo = open(vcal_file, 'wb') | |
5795 | except: | |
5796 | logger.exception("Could not open {0}".format(vcal_file)) | |
5797 | return False | |
5798 | try: | |
5799 | fo.write(cal_str) | |
5800 | except Exception: | |
5801 | logger.exception("Could not write to {0}" .format(vcal_file)) | |
5802 | return False | |
5803 | finally: | |
5804 | fo.close() | |
5805 | return True | |
5806 | ||
5807 | ||
5808 | def export_ical(file2uuids, uuid2hash, vcal_folder, calendars=None): | |
5809 | """ | |
5810 | Export items from each calendar to an ics file with the same name in vcal_folder. | |
5811 | """ | |
5812 | if not has_icalendar: | |
5813 | logger.error('Could not import icalendar') | |
5814 | return False | |
5815 | logger.debug("vcal_folder: {0}; calendars: {1}".format(vcal_folder, calendars)) | |
5816 | ||
5817 | cal_tuples = [] | |
5818 | calfiles = [] | |
5819 | if calendars: | |
5820 | for cal in calendars: | |
5821 | logger.debug('processing cal: {0}'.format(cal)) | |
5822 | name = cal[0] | |
5823 | regex = re.compile(r'^{0}'.format(cal[2])) | |
5824 | calendar = Calendar() | |
5825 | calendar.add('prodid', '-//etm_tk {0}//dgraham.us//'.format(version)) | |
5826 | calendar.add('version', '2.0') | |
5827 | cal_tuples.append((name, regex, calendar)) | |
5828 | else: | |
5829 | logger.debug('processing cal: all') | |
5830 | all = Calendar() | |
5831 | all.add('prodid', '-//etm_tk {0}//dgraham.us//'.format(version)) | |
5832 | all.add('version', '2.0') | |
5833 | regex = re.compile(r'^.*') | |
5834 | cal_tuples.append(('all', regex, all)) | |
5835 | ||
5836 | if not cal_tuples: | |
5837 | return | |
5838 | ||
5839 | logger.debug('using cal_tuples: {0}'.format(cal_tuples)) | |
5840 | for rp in file2uuids: | |
5841 | this_calendar = None | |
5842 | this_file = None | |
5843 | this_lst = [] # for error logging | |
5844 | for name, regex, calendar in cal_tuples: | |
5845 | if regex.match(rp): | |
5846 | this_calendar = calendar | |
5847 | this_file = os.path.join(vcal_folder, "{0}.ics".format(name)) | |
5848 | for uid in file2uuids[rp]: | |
5849 | this_hsh = uuid2hash[uid] | |
5850 | ok, element = hsh2ical(this_hsh) | |
5851 | if ok: | |
5852 | this_lst.append(element) | |
5853 | this_calendar.add_component(element) | |
5854 | calfiles.append([this_calendar, this_file, this_hsh, this_lst]) | |
5855 | break | |
5856 | if not this_calendar: | |
5857 | logger.debug('skipping {0} - no match in calendars'.format(rp)) | |
5858 | ||
5859 | for this_calendar, this_file, this_hsh, this_lst in calfiles: | |
5860 | try: | |
5861 | cal_str = this_calendar.to_ical() | |
5862 | except Exception: | |
5863 | logger.exception("Could not serialize the calendar: {0}; {1}\n {2}\n {3}".format(this_calendar, this_file, this_lst, this_hsh)) | |
5864 | return False | |
5865 | try: | |
5866 | fo = open(this_file, 'wb') | |
5867 | except: | |
5868 | logger.exception("Could not open {0}".format(this_file)) | |
5869 | return False | |
5870 | try: | |
5871 | fo.write(cal_str) | |
5872 | except Exception: | |
5873 | logger.exception("Could not write to {0}" .format(this_file)) | |
5874 | return False | |
5875 | finally: | |
5876 | fo.close() | |
5877 | return True | |
5878 | ||
5879 | ||
5880 | def txt2ical(file2uuids, uuid2hash, datadir, txt_rp, ics_rp): | |
5881 | """ | |
5882 | Export items from txtfile to icsfile. | |
5883 | """ | |
5884 | if not has_icalendar: | |
5885 | logger.error('Could not import icalendar') | |
5886 | return False | |
5887 | ||
5888 | if txt_rp not in file2uuids: | |
5889 | return | |
5890 | ||
5891 | cal = Calendar() | |
5892 | cal.add('prodid', '-//etm_tk {0}//dgraham.us//'.format(version)) | |
5893 | cal.add('version', '2.0') | |
5894 | ||
5895 | for uid in file2uuids[txt_rp]: | |
5896 | hsh = uuid2hash[uid] | |
5897 | ok, element = hsh2ical(hsh) | |
5898 | if ok: | |
5899 | cal.add_component(element) | |
5900 | ||
5901 | try: | |
5902 | cal_str = cal.to_ical() | |
5903 | except Exception: | |
5904 | logger.exception("Could not serialize the calendar") | |
5905 | return False | |
5906 | ics = os.path.join(datadir, ics_rp) | |
5907 | try: | |
5908 | fo = open(ics, 'wb') | |
5909 | except: | |
5910 | logger.exception("Could not open {0}".format(ics)) | |
5911 | return False | |
5912 | try: | |
5913 | fo.write(cal_str) | |
5914 | except Exception: | |
5915 | logger.exception("Could not write to {0}" .format(ics)) | |
5916 | return False | |
5917 | finally: | |
5918 | fo.close() | |
5919 | return True | |
5920 | ||
5921 | ||
5922 | def update_subscription(url, txt): | |
5923 | if python_version2: | |
5924 | import urllib2 as request | |
5925 | else: | |
5926 | from urllib import request | |
5927 | ||
5928 | res = False | |
5929 | u = request.urlopen(url) | |
5930 | vcal = u.read() | |
5931 | if vcal: | |
5932 | res = import_ical(vcal=vcal, txt=txt) | |
5933 | return res | |
5934 | ||
5935 | ||
5936 | def import_ical(ics="", txt="", vcal=""): | |
5937 | logger.debug("ics: {0}, txt: {1}, vcal:{2}".format(ics, txt, vcal)) | |
5938 | if vcal: | |
5939 | cal = Calendar.from_ical(vcal) | |
5940 | else: | |
5941 | g = open(ics, 'rb') | |
5942 | cal = Calendar.from_ical(g.read()) | |
5943 | g.close() | |
5944 | ilst = [] | |
5945 | for comp in cal.walk(): | |
5946 | clst = [] | |
5947 | # dated = False | |
5948 | start = None | |
5949 | t = '' # item type | |
5950 | s = '' # @s | |
5951 | e = '' # @e | |
5952 | f = '' # @f | |
5953 | tzid = comp.get('tzid') | |
5954 | if comp.name == "VEVENT": | |
5955 | t = '*' | |
5956 | start = comp.get('dtstart') | |
5957 | if start: | |
5958 | s = start.to_ical().decode()[:16] | |
5959 | # dated = True | |
5960 | end = comp.get('dtend') | |
5961 | if end: | |
5962 | e = end.to_ical().decode()[:16] | |
5963 | logger.debug('start: {0}, s: {1}, end: {2}, e: {3}'.format(start, s, end, e)) | |
5964 | extent = parse(e) - parse(s) | |
5965 | e = fmt_period(extent) | |
5966 | else: | |
5967 | t = '^' | |
5968 | ||
5969 | elif comp.name == "VTODO": | |
5970 | t = '-' | |
5971 | tmp = comp.get('completed') | |
5972 | if tmp: | |
5973 | f = tmp.to_ical().decode()[:16] | |
5974 | due = comp.get('due') | |
5975 | start = comp.get('dtstart') | |
5976 | if due: | |
5977 | s = due.to_ical().decode() | |
5978 | elif start: | |
5979 | s = start.to_ical().decode() | |
5980 | ||
5981 | elif comp.name == "VJOURNAL": | |
5982 | t = u'!' | |
5983 | tmp = comp.get('dtstart') | |
5984 | if tmp: | |
5985 | s = tmp.to_ical().decode()[:16] | |
5986 | else: | |
5987 | continue | |
5988 | summary = comp.get('summary') | |
5989 | clst = [t, summary] | |
5990 | if start: | |
5991 | if 'TZID' in start.params: | |
5992 | logger.debug("TZID: {0}".format(start.params['TZID'])) | |
5993 | clst.append('@z %s' % start.params['TZID']) | |
5994 | ||
5995 | if s: | |
5996 | clst.append("@s %s" % s) | |
5997 | if e: | |
5998 | clst.append("@e %s" % e) | |
5999 | if f: | |
6000 | clst.append("@f %s" % f) | |
6001 | tzid = comp.get('tzid') | |
6002 | if tzid: | |
6003 | clst.append("@z %s" % tzid.to_ical().decode()) | |
6004 | logger.debug("Using tzid: {0}".format(tzid.to_ical().decode())) | |
6005 | else: | |
6006 | logger.debug("Using tzid: UTC") | |
6007 | clst.append("@z UTC") | |
6008 | ||
6009 | tmp = comp.get('description') | |
6010 | if tmp: | |
6011 | clst.append("@d %s" % tmp.to_ical().decode()) | |
6012 | rule = comp.get('rrule') | |
6013 | if rule: | |
6014 | rlst = [] | |
6015 | keys = rule.sorted_keys() | |
6016 | for key in keys: | |
6017 | if key == 'FREQ': | |
6018 | rlst.append(ical_freq_hsh[rule.get('FREQ')[0].to_ical().decode()]) | |
6019 | elif key in ical_rrule_hsh: | |
6020 | rlst.append("&%s %s" % ( | |
6021 | ical_rrule_hsh[key], | |
6022 | ", ".join(map(str, rule.get(key))))) | |
6023 | clst.append("@r %s" % " ".join(rlst)) | |
6024 | ||
6025 | tags = comp.get('categories') | |
6026 | if tags: | |
6027 | if type(tags) is list: | |
6028 | tags = [x.to_ical().decode() for x in tags] | |
6029 | clst.append("@t %s" % u', '.join(tags)) | |
6030 | else: | |
6031 | clst.append("@t %s" % tags) | |
6032 | tmp = comp.get('organizer') | |
6033 | if tmp: | |
6034 | clst.append("@u %s" % tmp.to_ical().decode()) | |
6035 | ||
6036 | item = u' '.join(clst) | |
6037 | ilst.append(item) | |
6038 | if ilst: | |
6039 | if txt: | |
6040 | if os.path.isfile(txt): | |
6041 | tmpfile = "{0}.tmp".format(os.path.splitext(txt)[0]) | |
6042 | shutil.copy2(txt, tmpfile) | |
6043 | fo = codecs.open(txt, 'w', file_encoding) | |
6044 | fo.write("\n".join(ilst)) | |
6045 | fo.close() | |
6046 | elif vcal: | |
6047 | return "\n".join(ilst) | |
6048 | return True | |
6049 | ||
6050 | ||
6051 | def syncTxt(file2uuids, uuid2hash, datadir, relpath): | |
6052 | root = os.path.splitext(relpath)[0] | |
6053 | ics_rp = "{0}.ics".format(root) | |
6054 | txt_rp = "{0}.txt".format(root) | |
6055 | logger.debug('txt_rp: {0}, ics_rp: {1}'.format(txt_rp, ics_rp)) | |
6056 | sync_ics = os.path.join(datadir, ics_rp) | |
6057 | sync_txt = os.path.join(datadir, txt_rp) | |
6058 | logger.debug('sync_txt: {0}, sync_ics: {1}'.format(sync_txt, sync_ics)) | |
6059 | mode = 0 # do nothing | |
6060 | if os.path.isfile(sync_txt) and not os.path.isfile(sync_ics): | |
6061 | mode = 1 # to ics | |
6062 | elif os.path.isfile(sync_ics) and not os.path.isfile(sync_txt): | |
6063 | mode = 2 # to txt | |
6064 | elif os.path.isfile(sync_ics) and os.path.isfile(sync_txt): | |
6065 | mod_ics = os.path.getmtime(sync_ics) | |
6066 | mod_txt = os.path.getmtime(sync_txt) | |
6067 | if mod_ics < mod_txt: | |
6068 | logger.debug('mode 1, to ics: {0} < {1}'.format(mod_ics, mod_txt)) | |
6069 | mode = 1 # to ics | |
6070 | elif mod_txt < mod_ics: | |
6071 | logger.debug('mode 2, to txt: {0} > {1}'.format(mod_ics, mod_txt)) | |
6072 | mode = 2 # to txt | |
6073 | else: | |
6074 | logger.debug('sync_txt and sync_ics have the same mtime: {0}'.format(mod_txt)) | |
6075 | if not mode: | |
6076 | return | |
6077 | ||
6078 | if mode == 1: # to ics | |
6079 | logger.debug('calling txt2ical: {0}, {1}, {2}'.format(datadir, txt_rp, ics_rp)) | |
6080 | res = txt2ical(file2uuids, uuid2hash, datadir, txt_rp, ics_rp) | |
6081 | if not res: | |
6082 | return | |
6083 | seconds = os.path.getmtime(sync_txt) | |
6084 | ||
6085 | elif mode == 2: # to txt | |
6086 | res = import_ical(ics=sync_ics, txt=sync_txt) | |
6087 | if not res: | |
6088 | return | |
6089 | seconds = os.path.getmtime(sync_ics) | |
6090 | ||
6091 | # update times | |
6092 | logger.debug('updating mtimes using seconds: {0}'.format(seconds)) | |
6093 | os.utime(sync_ics, times=(seconds, seconds)) | |
6094 | os.utime(sync_txt, times=(seconds, seconds)) | |
6095 | ||
6096 | ||
6097 | def ensureMonthly(options, date=None): | |
6098 | """ | |
6099 | """ | |
6100 | retval = None | |
6101 | if ('monthly' in options and | |
6102 | options['monthly']): | |
6103 | monthly = os.path.normpath(os.path.join( | |
6104 | options['datadir'], | |
6105 | options['monthly'])) | |
6106 | if not os.path.isdir(monthly): | |
6107 | os.makedirs(monthly) | |
6108 | sleep(0.5) | |
6109 | if date is None: | |
6110 | date = datetime.now().date() | |
6111 | yr = date.year | |
6112 | mn = date.month | |
6113 | curryear = os.path.normpath(os.path.join(monthly, "%s" % yr)) | |
6114 | if not os.path.isdir(curryear): | |
6115 | os.makedirs(curryear) | |
6116 | sleep(0.5) | |
6117 | currfile = os.path.normpath(os.path.join(curryear, "%02d.txt" % mn)) | |
6118 | if not os.path.isfile(currfile): | |
6119 | fo = codecs.open(currfile, 'w', options['encoding']['file']) | |
6120 | fo.write("") | |
6121 | fo.close() | |
6122 | if os.path.isfile(currfile): | |
6123 | retval = currfile | |
6124 | return retval | |
6125 | ||
6126 | ||
6127 | class ETMCmd(): | |
6128 | """ | |
6129 | Data handling commands | |
6130 | """ | |
6131 | ||
6132 | def __init__(self, options=None, parent=None): | |
6133 | if not options: | |
6134 | options = {} | |
6135 | self.options = options | |
6136 | self.calendars = deepcopy(options['calendars']) | |
6137 | ||
6138 | self.cal_regex = None | |
6139 | self.messages = [] | |
6140 | self.cmdDict = { | |
6141 | '?': self.do_help, | |
6142 | 'a': self.do_a, | |
6143 | 'd': self.do_d, | |
6144 | 'n': self.do_n, | |
6145 | 'k': self.do_k, | |
6146 | 'm': self.do_m, | |
6147 | 'N': self.do_N, | |
6148 | 'p': self.do_p, | |
6149 | 'c': self.do_c, | |
6150 | 't': self.do_t, | |
6151 | 'v': self.do_v, | |
6152 | } | |
6153 | ||
6154 | self.helpDict = { | |
6155 | 'help': self.help_help, | |
6156 | 'a': self.help_a, | |
6157 | 'd': self.help_d, | |
6158 | 'n': self.help_n, | |
6159 | 'k': self.help_k, | |
6160 | 'm': self.help_m, | |
6161 | 'N': self.help_N, | |
6162 | 'p': self.help_p, | |
6163 | 'c': self.help_c, | |
6164 | 't': self.help_t, | |
6165 | 'v': self.help_v, | |
6166 | } | |
6167 | self.do_update = False | |
6168 | self.ruler = '-' | |
6169 | # self.rows = [] | |
6170 | self.file2uuids = {} | |
6171 | self.file2lastmodified = {} | |
6172 | self.uuid2hash = {} | |
6173 | self.loop = False | |
6174 | self.number = True | |
6175 | self.count2id = {} | |
6176 | self.uuid2labels = {} | |
6177 | self.last_rep = "" | |
6178 | self.item_hsh = {} | |
6179 | self.output = 'text' | |
6180 | self.tkversion = '' | |
6181 | self.rows = None | |
6182 | self.busytimes = None | |
6183 | self.busydays = None | |
6184 | self.alerts = None | |
6185 | self.occasions = None | |
6186 | self.file2data = None | |
6187 | self.prevnext = None | |
6188 | self.line_length = self.options['agenda_indent'] + self.options['agenda_width1'] + self.options['agenda_width2'] | |
6189 | self.currfile = '' # ensureMonthly(options) | |
6190 | if 'edit_cmd' in self.options and self.options['edit_cmd']: | |
6191 | self.editcmd = self.options['edit_cmd'] | |
6192 | else: | |
6193 | self.editcmd = '' | |
6194 | self.tmpfile = os.path.normpath(os.path.join(self.options['etmdir'], '.temp.txt')) | |
6195 | ||
6196 | def do_command(self, s): | |
6197 | logger.debug('processing command: {0}'.format(s)) | |
6198 | args = s.split(' ') | |
6199 | cmd = args.pop(0) | |
6200 | if args: | |
6201 | arg_str = " ".join(args) | |
6202 | else: | |
6203 | arg_str = '' | |
6204 | if cmd not in self.cmdDict: | |
6205 | return _('"{0}" is an unrecognized command.').format(cmd) | |
6206 | logger.debug('do_command: {0}, {1}'.format(cmd, arg_str)) | |
6207 | res = self.cmdDict[cmd](arg_str) | |
6208 | return res | |
6209 | ||
6210 | def do_help(self, cmd): | |
6211 | if cmd: | |
6212 | return self.helpDict[cmd]() | |
6213 | else: | |
6214 | return self.help_help() | |
6215 | ||
6216 | def mk_rep(self, arg_str): | |
6217 | logger.debug("arg_str: {0}".format(arg_str)) | |
6218 | # we need to return the output string rather than print it | |
6219 | self.last_rep = arg_str | |
6220 | cmd = arg_str[0] | |
6221 | ret = [] | |
6222 | views = { | |
6223 | # everything but agenda and week | |
6224 | 'd': 'day', | |
6225 | 'p': 'folder', | |
6226 | 't': 'tag', | |
6227 | 'n': 'note', | |
6228 | 'k': 'keyword' | |
6229 | } | |
6230 | try: | |
6231 | if cmd == 'a': | |
6232 | if len(arg_str) > 2: | |
6233 | f = arg_str[1:].strip() | |
6234 | else: | |
6235 | f = None | |
6236 | logger.debug('calling getAgenda') | |
6237 | return (getAgenda( | |
6238 | self.rows, | |
6239 | colors=self.options['agenda_colors'], | |
6240 | days=self.options['agenda_days'], | |
6241 | indent=self.options['agenda_indent'], | |
6242 | width1=self.options['agenda_width1'], | |
6243 | width2=self.options['agenda_width2'], | |
6244 | calendars=self.calendars, | |
6245 | mode=self.output, | |
6246 | fltr=f)) | |
6247 | elif cmd in views: | |
6248 | view = views[cmd] | |
6249 | if len(arg_str) > 2: | |
6250 | f = arg_str[1:].strip() | |
6251 | else: | |
6252 | f = None | |
6253 | if not self.rows: | |
6254 | return "no output" | |
6255 | rows = deepcopy(self.rows) | |
6256 | return (makeTree(rows, view=view, calendars=self.calendars, fltr=f, hide_finished=self.options['hide_finished'])) | |
6257 | else: | |
6258 | res = getReportData( | |
6259 | arg_str, | |
6260 | self.file2uuids, | |
6261 | self.uuid2hash, | |
6262 | self.options) | |
6263 | return res | |
6264 | ||
6265 | except: | |
6266 | logger.exception("could not process '{0}'".format(arg_str)) | |
6267 | s = str(_('Could not process "{0}".')).format(arg_str) | |
6268 | # p = str(_('Enter ? r or ? t for help.')) | |
6269 | ret.append(s) | |
6270 | return '\n'.join(ret) | |
6271 | ||
6272 | def loadData(self, e=None): | |
6273 | self.count2id = {} | |
6274 | now = datetime.now() | |
6275 | year, wn, dn = now.isocalendar() | |
6276 | weeks_after = self.options['weeks_after'] | |
6277 | if dn > 1: | |
6278 | days = dn - 1 | |
6279 | else: | |
6280 | days = 0 | |
6281 | week_beg = now - days * oneday | |
6282 | bef = (week_beg + (7 * (weeks_after + 1)) * oneday) | |
6283 | self.options['bef'] = bef | |
6284 | self.file2data = {} | |
6285 | logger.debug('calling get_data') | |
6286 | uuid2hash, uuid2labels, file2uuids, self.file2lastmodified, bad_datafiles, messages = get_data(options=self.options) | |
6287 | self.file2uuids = file2uuids | |
6288 | self.uuid2hash = uuid2hash | |
6289 | self.uuid2labels = uuid2labels | |
6290 | logger.debug('calling getViewData') | |
6291 | self.file2data = getViewData(bef, file2uuids, uuid2hash, self.options) | |
6292 | self.rows = tuple(itemsSL) | |
6293 | self.alerts = list(alertsSL) | |
6294 | self.datetimes = list(datetimesSL) | |
6295 | self.busytimes = {} | |
6296 | for key in busytimesSL: | |
6297 | self.busytimes[key] = list(busytimesSL[key]) | |
6298 | self.occasions = {} | |
6299 | for key in occasionsSL: | |
6300 | self.occasions[key] = list(occasionsSL[key]) | |
6301 | ||
6302 | self.do_update = True | |
6303 | self.currfile = ensureMonthly(self.options, now) | |
6304 | if self.last_rep: | |
6305 | logger.debug('calling mk_rep with {0}'.format(self.last_rep)) | |
6306 | return self.mk_rep(self.last_rep) | |
6307 | ||
6308 | def updateDataFromFile(self, fp, rp): | |
6309 | """ | |
6310 | Called from safe_save. Calls process_one_file to produce hashes | |
6311 | for the items in the file | |
6312 | """ | |
6313 | logger.debug('starting updateDataFromFile: {0}; {1}'.format(fp, rp)) | |
6314 | self.count2id = {} | |
6315 | now = datetime.now() | |
6316 | year, wn, dn = now.isocalendar() | |
6317 | weeks_after = self.options['weeks_after'] | |
6318 | if dn > 1: | |
6319 | days = dn - 1 | |
6320 | else: | |
6321 | days = 0 | |
6322 | week_beg = now - days * oneday | |
6323 | bef = (week_beg + (7 * (weeks_after + 1)) * oneday) | |
6324 | self.options['bef'] = bef | |
6325 | if rp in self.file2uuids: | |
6326 | ids = self.file2uuids[rp] | |
6327 | else: | |
6328 | ids = [] | |
6329 | logger.debug('rp: {0}; ids: {1}'.format(rp, ids)) | |
6330 | # remove the old | |
6331 | logger.debug('removing the relevant entries in uuid2hash') | |
6332 | for id in ids: | |
6333 | if id in self.uuid2hash: | |
6334 | del self.uuid2hash[id] | |
6335 | if id in self.uuid2labels: | |
6336 | logger.debug('removing uuid2label[{0}] = {1}'.format(id, self.uuid2labels[id])) | |
6337 | del self.uuid2labels[id] | |
6338 | logger.debug('removing the relevant entry in file2uuids') | |
6339 | self.file2uuids[rp] = [] | |
6340 | msg, hashes, u2l = process_one_file(fp, rp, self.options) | |
6341 | logger.debug('update labels: {0}'.format(u2l)) | |
6342 | self.uuid2labels.update(u2l) | |
6343 | loh = [x for x in hashes if x] | |
6344 | for hsh in loh: | |
6345 | if hsh['itemtype'] == '=': | |
6346 | continue | |
6347 | logger.debug('adding: {0}, {1}'.format(hsh['i'], hsh['_summary'])) | |
6348 | id = hsh['i'] | |
6349 | self.uuid2hash[id] = hsh | |
6350 | self.file2uuids.setdefault(rp, []).append(id) | |
6351 | mtime = os.path.getmtime(fp) | |
6352 | self.file2lastmodified[(fp, rp)] = mtime | |
6353 | (self.rows, self.alerts, self.busytimes, self.datetimes, self.occasions, self.file2data) = updateViewData(rp, bef, self.file2uuids, self.uuid2hash, self.options, self.file2data) | |
6354 | logger.debug('ended updateDataFromFile') | |
6355 | ||
6356 | def edit_tmp(self): | |
6357 | if not self.editcmd: | |
6358 | term_print("""\ | |
6359 | Either ITEM must be provided or edit_cmd must be specified in etmtk.cfg. | |
6360 | """) | |
6361 | return [], {} | |
6362 | hsh = {'file': self.tmpfile, 'line': 1} | |
6363 | cmd = expand_template(self.editcmd, hsh) | |
6364 | msg = True | |
6365 | while msg: | |
6366 | subprocess.call(cmd, shell=True) | |
6367 | # check the item | |
6368 | fo = codecs.open(self.tmpfile, 'r', file_encoding) | |
6369 | lines = [unicode(u'%s') % x.rstrip() for x in fo.readlines()] | |
6370 | fo.close() | |
6371 | if len(lines) >= 1: | |
6372 | while len(lines) >= 1 and not lines[-1]: | |
6373 | lines.pop(-1) | |
6374 | if not lines: | |
6375 | term_print(_('canceled')) | |
6376 | return False | |
6377 | item = "\n".join(lines) | |
6378 | new_hsh, msg = str2hsh(item, options=self.options) | |
6379 | if msg: | |
6380 | term_print('Error messages:') | |
6381 | term_print("\n".join(msg)) | |
6382 | rep = raw_input('Correct item? [Yn] ') | |
6383 | if rep.lower() == 'n': | |
6384 | term_print(_('canceled')) | |
6385 | return [], {} | |
6386 | item = unicode(u"{0}".format(hsh2str(new_hsh, self.options)[0])) | |
6387 | lines = item.split('\n') | |
6388 | return lines, new_hsh | |
6389 | ||
6390 | def commit(self, file, mode=""): | |
6391 | if self.options['vcs_system']: | |
6392 | mesg = u"{0}".format(mode) | |
6393 | if python_version == 2 and type(mesg) == unicode: | |
6394 | # hack to avoid unicode in .format() for python 2 | |
6395 | cmd = self.options['vcs']['commit'].format( | |
6396 | repo=self.options['vcs']['repo'], | |
6397 | work=self.options['vcs']['work'], | |
6398 | mesg="XXX") | |
6399 | cmd = cmd.replace("XXX", mesg) | |
6400 | else: | |
6401 | cmd = self.options['vcs']['commit'].format( | |
6402 | repo=self.options['vcs']['repo'], | |
6403 | work=self.options['vcs']['work'], | |
6404 | mesg=mesg) | |
6405 | subprocess.call(cmd, shell=True) | |
6406 | logger.debug("executed vcs commit command:\n {0}".format(cmd)) | |
6407 | return True | |
6408 | ||
6409 | def safe_save(self, file, s, mode="", cli=False): | |
6410 | """ | |
6411 | Try writing the s to tmpfile and then, if it succeeds, | |
6412 | copy tmpfile to file. | |
6413 | """ | |
6414 | if not mode: | |
6415 | mode = "Edited file" | |
6416 | logger.debug('starting safe_save: {0}, {1}, cli: {2}'.format(file, mode, cli)) | |
6417 | try: | |
6418 | fo = codecs.open(self.tmpfile, 'w', file_encoding) | |
6419 | # add a trailing newline to make diff happy | |
6420 | fo.write("{0}\n".format(s.rstrip())) | |
6421 | fo.close() | |
6422 | except: | |
6423 | return 'error writing to file - aborted' | |
6424 | shutil.copy2(self.tmpfile, file) | |
6425 | logger.debug("modified file: '{0}'".format(file)) | |
6426 | pathname, ext = os.path.splitext(file) | |
6427 | if not cli and ext == ".txt": | |
6428 | # this is a data file | |
6429 | fp = file | |
6430 | rp = relpath(fp, self.options['datadir']) | |
6431 | # this will update self.uuid2hash, ... | |
6432 | self.updateDataFromFile(fp, rp) | |
6433 | return self.commit(file, mode) | |
6434 | ||
6435 | def get_itemhash(self, arg_str): | |
6436 | try: | |
6437 | count = int(arg_str) | |
6438 | except: | |
6439 | return _('an integer argument is required') | |
6440 | if count not in self.count2id: | |
6441 | return _('Item number {0} not found').format(count) | |
6442 | uid, dtstr = self.count2id[count].split('::') | |
6443 | hsh = self.uuid2hash[uid] | |
6444 | if dtstr: | |
6445 | hsh['_dt'] = parse(parse_dtstr(dtstr, hsh['z'])) | |
6446 | return hsh | |
6447 | ||
6448 | def do_a(self, arg_str): | |
6449 | return self.mk_rep('a {0}'.format(arg_str)) | |
6450 | ||
6451 | def help_a(self): | |
6452 | return ("""\ | |
6453 | Usage: | |
6454 | ||
6455 | etm a | |
6456 | ||
6457 | Generate an agenda including dated items for the next {0} days (agenda_days from etmtk.cfg) together with any now and next items.\ | |
6458 | """.format(self.options['agenda_days'])) | |
6459 | ||
6460 | def cmd_do_delete(self, choice): | |
6461 | if not choice: | |
6462 | return False | |
6463 | try: | |
6464 | choice = int(choice) | |
6465 | except: | |
6466 | return False | |
6467 | ||
6468 | if choice in [1, 2, 4]: | |
6469 | hsh = self.item_hsh | |
6470 | dt = parse( | |
6471 | hsh['_dt']).replace( | |
6472 | tzinfo=tzlocal()).astimezone( | |
6473 | gettz(hsh['z'])) | |
6474 | dtn = dt.replace(tzinfo=None) | |
6475 | hsh_rev = deepcopy(hsh) | |
6476 | ||
6477 | if choice == 1: | |
6478 | # delete this instance only by removing it from @+ | |
6479 | # or adding it to @- | |
6480 | if '+' in hsh_rev and dtn in hsh_rev['+']: | |
6481 | hsh_rev['+'].remove(dtn) | |
6482 | if not hsh_rev['+'] and hsh_rev['r'] == 'l': | |
6483 | del hsh_rev['r'] | |
6484 | del hsh_rev['_r'] | |
6485 | else: | |
6486 | hsh_rev.setdefault('-', []).append(dt) | |
6487 | # newstr = hsh2str(hsh_rev, self.options) | |
6488 | self.replace_item(hsh_rev) | |
6489 | ||
6490 | elif choice == 2: | |
6491 | # delete this and all subsequent instances by adding | |
6492 | # this instance - one minute to &u for each @r | |
6493 | ||
6494 | tmp = [] | |
6495 | for h in hsh_rev['_r']: | |
6496 | if 'f' in h and h['f'] != u'l': | |
6497 | h['u'] = dtn - oneminute | |
6498 | tmp.append(h) | |
6499 | hsh_rev['_r'] = tmp | |
6500 | if u'+' in hsh: | |
6501 | tmp_rev = [] | |
6502 | for d in hsh_rev['+']: | |
6503 | if d < dtn: | |
6504 | tmp_rev.append(d) | |
6505 | hsh_rev['+'] = tmp_rev | |
6506 | if u'-' in hsh: | |
6507 | tmp_rev = [] | |
6508 | for d in hsh_rev['-']: | |
6509 | if d < dtn: | |
6510 | tmp_rev.append(d) | |
6511 | hsh_rev['-'] = tmp_rev | |
6512 | hsh_rev['s'] = dtn | |
6513 | # rev_str = hsh2str(hsh_rev, self.options) | |
6514 | self.replace_item(hsh_rev) | |
6515 | ||
6516 | elif choice == 4: | |
6517 | # delete all previous instances | |
6518 | if u'+' in hsh: | |
6519 | logger.debug('starting @+: {0}'.format(hsh['+'])) | |
6520 | tmp_rev = [] | |
6521 | for d in hsh_rev['+']: | |
6522 | if d >= dtn: | |
6523 | tmp_rev.append(d) | |
6524 | hsh_rev['+'] = tmp_rev | |
6525 | logger.debug('ending @+: {0}'.format(hsh['+'])) | |
6526 | if u'-' in hsh: | |
6527 | logger.debug('starting @-: {0}'.format(hsh['-'])) | |
6528 | tmp_rev = [] | |
6529 | for d in hsh_rev['-']: | |
6530 | if d >= dtn: | |
6531 | tmp_rev.append(d) | |
6532 | hsh_rev['-'] = tmp_rev | |
6533 | logger.debug('ending @-: {0}'.format(hsh['-'])) | |
6534 | hsh_rev['s'] = dtn | |
6535 | self.replace_item(hsh_rev) | |
6536 | else: | |
6537 | self.delete_item() | |
6538 | ||
6539 | def cmd_do_reschedule(self, new_dtn): | |
6540 | # new_dtn = new_dt.astimezone(gettz(self.item_hsh['z'])).replace(tzinfo=None) | |
6541 | hsh_rev = deepcopy(self.item_hsh) | |
6542 | if self.old_dt: | |
6543 | # old_dtn = self.old_dt.astimezone(gettz(self.item_hsh['z'])).replace(tzinfo=None) | |
6544 | old_dtn = self.old_dt | |
6545 | if 'r' in hsh_rev: | |
6546 | if '+' in hsh_rev and old_dtn in hsh_rev['+']: | |
6547 | hsh_rev['+'].remove(old_dtn) | |
6548 | if not hsh_rev['+'] and hsh_rev['r'] == 'l': | |
6549 | del hsh_rev['r'] | |
6550 | del hsh_rev['_r'] | |
6551 | else: | |
6552 | hsh_rev.setdefault('-', []).append(old_dtn) | |
6553 | hsh_rev.setdefault('+', []).append(new_dtn) | |
6554 | # check starting time | |
6555 | if new_dtn < hsh_rev['s']: | |
6556 | d = (hsh_rev['s'] - new_dtn).days | |
6557 | hsh_rev['s'] = hsh_rev['s'] - (d + 1) * oneday | |
6558 | else: # dated but not repeating | |
6559 | hsh_rev['s'] = new_dtn | |
6560 | else: # either undated or not repeating | |
6561 | hsh_rev['s'] = new_dtn | |
6562 | logger.debug(('replacement: {0}'.format(hsh_rev))) | |
6563 | self.replace_item(hsh_rev) | |
6564 | ||
6565 | def cmd_do_schedulenew(self, new_dtn): | |
6566 | # new_dtn = new_dt.astimezone(gettz(self.item_hsh['z'])).replace(tzinfo=None) | |
6567 | hsh_rev = deepcopy(self.item_hsh) | |
6568 | if self.old_dt: | |
6569 | # old_dtn = self.old_dt.astimezone(gettz(self.item_hsh['z'])).replace(tzinfo=None) | |
6570 | if 'r' in hsh_rev: | |
6571 | if '+' in hsh_rev and new_dtn in hsh_rev['+']: | |
6572 | return | |
6573 | if '-' in hsh_rev and new_dtn in hsh_rev['-']: | |
6574 | hsh_rev['-'].remove(new_dtn) | |
6575 | else: | |
6576 | hsh_rev.setdefault('+', []).append(new_dtn) | |
6577 | # check starting time | |
6578 | if new_dtn < hsh_rev['s']: | |
6579 | d = (hsh_rev['s'] - new_dtn).days | |
6580 | hsh_rev['s'] = hsh_rev['s'] - (d + 1) * oneday | |
6581 | else: # dated but not repeating | |
6582 | if hsh_rev['s'] == new_dtn: | |
6583 | return | |
6584 | hsh_rev['r'] = 'l' | |
6585 | hsh_rev.setdefault('+', []).append(new_dtn) | |
6586 | else: # either undated or not repeating | |
6587 | hsh_rev['s'] = new_dtn | |
6588 | logger.debug(('replacement: {0}'.format(hsh_rev))) | |
6589 | self.replace_item(hsh_rev) | |
6590 | ||
6591 | def delete_item(self): | |
6592 | f, begline, endline = self.item_hsh['fileinfo'] | |
6593 | fp = os.path.normpath(os.path.join(self.options['datadir'], f)) | |
6594 | fo = codecs.open(fp, 'r', file_encoding) | |
6595 | lines = fo.readlines() | |
6596 | fo.close() | |
6597 | self.replace_lines(fp, lines, begline, endline, []) | |
6598 | return True | |
6599 | ||
6600 | def replace_item(self, new_hsh): | |
6601 | new_item, msg = hsh2str(new_hsh, self.options) | |
6602 | logger.debug(new_item) | |
6603 | newlines = new_item.split('\n') | |
6604 | f, begline, endline = new_hsh['fileinfo'] | |
6605 | fp = os.path.normpath(os.path.join(self.options['datadir'], f)) | |
6606 | fo = codecs.open(fp, 'r', file_encoding) | |
6607 | lines = fo.readlines() | |
6608 | fo.close() | |
6609 | self.replace_lines(fp, lines, begline, endline, newlines) | |
6610 | # self.loadData() | |
6611 | return True | |
6612 | ||
6613 | def append_item(self, new_hsh, file, cli=False): | |
6614 | """ | |
6615 | """ | |
6616 | # new_item, msg = hsh2str(new_hsh, self.options, include_uid=True) | |
6617 | new_item, msg = hsh2str(new_hsh, self.options) | |
6618 | old_items = getFileItems(file, self.options['datadir'], False) | |
6619 | items = [u'%s' % x[0].rstrip() for x in old_items if x[0].strip()] | |
6620 | items.append(new_item) | |
6621 | itemstr = "\n".join(items) | |
6622 | mode = _("added item") | |
6623 | logger.debug('saving {0} to {1}, mode: {2}'.format(itemstr, file, mode)) | |
6624 | self.safe_save(file, itemstr, mode=mode, cli=cli) | |
6625 | # self.loadData() | |
6626 | return "break" | |
6627 | ||
6628 | def cmd_do_finish(self, dt, options={}): | |
6629 | """ | |
6630 | Called by do_f to process the finish datetime and add it to the file. | |
6631 | """ | |
6632 | hsh = self.item_hsh | |
6633 | done, due, following = getDoneAndTwo(hsh) | |
6634 | if 'z' not in hsh: | |
6635 | hsh['z'] = options['local_timezone'] | |
6636 | if due: | |
6637 | # undated tasks won't have a due date | |
6638 | ddn = due.replace( | |
6639 | tzinfo=tzlocal()).astimezone( | |
6640 | gettz(hsh['z'])).replace(tzinfo=None) | |
6641 | else: | |
6642 | ddn = '' | |
6643 | if hsh['itemtype'] == u'+': | |
6644 | m = group_regex.match(hsh['_summary']) | |
6645 | if m: | |
6646 | group, num, tot, job = m.groups() | |
6647 | hsh['_j'][int(num) - 1]['f'] = [ | |
6648 | (dt.replace(tzinfo=None), ddn)] | |
6649 | finished = True | |
6650 | # check to see if all jobs are finished | |
6651 | for job in hsh['_j']: | |
6652 | if 'f' not in job: | |
6653 | finished = False | |
6654 | break | |
6655 | if finished: | |
6656 | # move the finish dates from the jobs to the history | |
6657 | for j in range(len(hsh['_j'])): | |
6658 | job = hsh['_j'][j] | |
6659 | job.setdefault('h', []).append(job['f'][0]) | |
6660 | del job['f'] | |
6661 | hsh['_j'][j] = job | |
6662 | ||
6663 | # and add the last finish date (this one) to the group | |
6664 | completion = (dt.replace(tzinfo=None), ddn) | |
6665 | hsh['f'] = [completion] | |
6666 | else: | |
6667 | dtz = dt.replace(tzinfo=tzlocal()).astimezone(gettz(hsh['z'])).replace(tzinfo=None) | |
6668 | if not ddn: | |
6669 | ddn = dtz | |
6670 | hsh.setdefault('f', []).append((dtz, ddn)) | |
6671 | logger.debug('finish hsh: {0}'.format(hsh)) | |
6672 | self.replace_item(hsh) | |
6673 | ||
6674 | def do_k(self, arg_str): | |
6675 | # self.prevnext = getPrevNext(self.dates) | |
6676 | return self.mk_rep('k {0}'.format(arg_str)) | |
6677 | ||
6678 | @staticmethod | |
6679 | def help_k(): | |
6680 | return ("""\ | |
6681 | Usage: | |
6682 | ||
6683 | etm k [FILTER] | |
6684 | ||
6685 | Show items grouped and sorted by keyword optionally limited to those containing a case insenstive match for the regex FILTER.\ | |
6686 | """) | |
6687 | ||
6688 | def do_m(self, arg_str): | |
6689 | lines = self.options['reports'] | |
6690 | try: | |
6691 | n = int(arg_str) | |
6692 | if n < 1 or n > len(lines): | |
6693 | return _('report {0} does not exist'.format(n)) | |
6694 | except: | |
6695 | return self.help_m() | |
6696 | rep_spec = "{0}".format(lines[n - 1].strip().split('#')[0]) | |
6697 | logger.debug(('rep_spec: {0}'.format(rep_spec))) | |
6698 | tree = getReportData( | |
6699 | rep_spec, | |
6700 | self.file2uuids, | |
6701 | self.uuid2hash, | |
6702 | self.options) | |
6703 | return(tree) | |
6704 | ||
6705 | def help_m(self): | |
6706 | res = [] | |
6707 | lines = self.options['reports'] | |
6708 | if lines: | |
6709 | res.append(_("""\ | |
6710 | Usage: | |
6711 | ||
6712 | etm m N | |
6713 | ||
6714 | where N is the number of a report specification:\n """)) | |
6715 | for i in range(len(lines)): | |
6716 | res.append("{0:>2}. {1}".format(i + 1, lines[i].strip())) | |
6717 | return "\n".join(res) | |
6718 | # return(res) | |
6719 | ||
6720 | def do_n(self, arg_str): | |
6721 | return self.mk_rep('n {0}'.format(arg_str)) | |
6722 | ||
6723 | @staticmethod | |
6724 | def help_n(): | |
6725 | return ("""\ | |
6726 | Usage: | |
6727 | ||
6728 | etm N [FILTER] | |
6729 | ||
6730 | Show notes grouped and sorted by keyword optionally limited to those containing a case insenstive match for the regex FILTER.\ | |
6731 | """) | |
6732 | ||
6733 | def do_N(self, arg_str='', itemstr=""): | |
6734 | logger.debug('arg_str: {0}'.format(arg_str)) | |
6735 | if arg_str: | |
6736 | new_item = s2or3(arg_str) | |
6737 | new_hsh, msg = str2hsh(new_item, options=self.options) | |
6738 | logger.debug('new_hsh: {0}'.format(new_hsh)) | |
6739 | if msg: | |
6740 | return "\n".join(msg) | |
6741 | if 's' not in new_hsh: | |
6742 | new_hsh['s'] = None | |
6743 | res = self.append_item(new_hsh, self.currfile, cli=True) | |
6744 | if res: | |
6745 | return _("item saved") | |
6746 | ||
6747 | @staticmethod | |
6748 | def help_N(): | |
6749 | return _("""\ | |
6750 | Usage: | |
6751 | ||
6752 | etm n ITEM | |
6753 | ||
6754 | Create a new item from ITEM. E.g., | |
6755 | ||
6756 | etm n '* meeting @s +0 4p @e 1h30m' | |
6757 | ||
6758 | The item will be appended to the monthly file for the current month.\ | |
6759 | """) | |
6760 | ||
6761 | @staticmethod | |
6762 | def do_q(line): | |
6763 | sys.exit() | |
6764 | ||
6765 | @staticmethod | |
6766 | def help_q(): | |
6767 | return _('quit\n') | |
6768 | ||
6769 | def do_c(self, arg): | |
6770 | logger.debug('custom spec: {0}, {1}'.format(arg, type(arg))) | |
6771 | """report (non actions) specification""" | |
6772 | if not arg: | |
6773 | return self.help_c() | |
6774 | res = getReportData( | |
6775 | arg, | |
6776 | self.file2uuids, | |
6777 | self.uuid2hash, | |
6778 | self.options) | |
6779 | return res | |
6780 | ||
6781 | @staticmethod | |
6782 | def help_c(): | |
6783 | return _("""\ | |
6784 | Usage: | |
6785 | ||
6786 | etm c <type> <groupby> [options] | |
6787 | ||
6788 | Generate a custom view where type is either 'a' (action) or 'c' (composite). | |
6789 | Groupby can include *semicolon* separated date specifications and | |
6790 | elements from: | |
6791 | c context | |
6792 | f file path | |
6793 | k keyword | |
6794 | t tag | |
6795 | u user | |
6796 | ||
6797 | A *date specification* is either | |
6798 | w: week number | |
6799 | or a combination of one or more of the following: | |
6800 | yy: 2-digit year | |
6801 | yyyy: 4-digit year | |
6802 | MM: month: 01 - 12 | |
6803 | MMM: locale specific abbreviated month name: Jan - Dec | |
6804 | MMMM: locale specific month name: January - December | |
6805 | dd: month day: 01 - 31 | |
6806 | ddd: locale specific abbreviated week day: Mon - Sun | |
6807 | dddd: locale specific week day: Monday - Sunday | |
6808 | ||
6809 | Options include: | |
6810 | -b begin date | |
6811 | -c context regex | |
6812 | -d depth (CLI a reports only) | |
6813 | -e end date | |
6814 | -f file regex | |
6815 | -k keyword regex | |
6816 | -l location regex | |
6817 | -o omit (r reports only) | |
6818 | -s summary regex | |
6819 | -S search regex | |
6820 | -t tags regex | |
6821 | -u user regex | |
6822 | -w column 1 width | |
6823 | -W column 2 width | |
6824 | ||
6825 | Example: | |
6826 | ||
6827 | etm c 'c ddd, MMM dd yyyy -b 1 -e +1/1' | |
6828 | """) | |
6829 | ||
6830 | def do_d(self, arg_str): | |
6831 | if self.calendars: | |
6832 | cal_pattern = r'^%s' % '|'.join( | |
6833 | [x[2] for x in self.calendars if x[1]]) | |
6834 | self.cal_regex = re.compile(cal_pattern) | |
6835 | logger.debug("cal_pattern: {0}".format(cal_pattern)) | |
6836 | self.prevnext = getPrevNext(self.datetimes, self.cal_regex) | |
6837 | return self.mk_rep('d {0}'.format(arg_str)) | |
6838 | ||
6839 | @staticmethod | |
6840 | def help_d(): | |
6841 | return ("""\ | |
6842 | Usage: | |
6843 | ||
6844 | etm d [FILTER] | |
6845 | ||
6846 | Show the day view with dated items grouped and sorted by date and type, optionally limited to those containing a case insensitive match for the regex FILTER.\ | |
6847 | """) | |
6848 | ||
6849 | def do_p(self, arg_str): | |
6850 | return self.mk_rep('p {0}'.format(arg_str)) | |
6851 | ||
6852 | @staticmethod | |
6853 | def help_p(): | |
6854 | return ("""\ | |
6855 | Usage: | |
6856 | ||
6857 | etm p [FILTER] | |
6858 | ||
6859 | Show items grouped and sorted by file path, optionally limited to those containing a case insensitive match for the regex FILTER.\ | |
6860 | """) | |
6861 | ||
6862 | def do_t(self, arg_str): | |
6863 | return self.mk_rep('t {0}'.format(arg_str)) | |
6864 | ||
6865 | @staticmethod | |
6866 | def help_t(): | |
6867 | return ("""\ | |
6868 | Usage: | |
6869 | ||
6870 | etm t [FILTER] | |
6871 | ||
6872 | Show items grouped and sorted by tag, optionally limited to those containing a case insensitive match for the regex FILTER.\ | |
6873 | """) | |
6874 | ||
6875 | def do_v(self, arg_str): | |
6876 | d = { | |
6877 | 'copyright': '2009-%s' % datetime.today().strftime("%Y"), | |
6878 | 'home': 'www.duke.edu/~dgraham/etmtk', | |
6879 | 'dev': 'daniel.graham@duke.edu', | |
6880 | 'group': "groups.google.com/group/eventandtaskmanager", | |
6881 | 'gpl': 'www.gnu.org/licenses/gpl.html', | |
6882 | 'etmversion': fullversion, | |
6883 | 'platform': platform.system(), | |
6884 | 'python': platform.python_version(), | |
6885 | 'dateutil': dateutil_version, | |
6886 | 'pyyaml': yaml.__version__, | |
6887 | 'tkversion': self.tkversion, | |
6888 | 'github': 'https://github.com/dagraham/etm-tk', | |
6889 | } | |
6890 | if not d['tkversion']: # command line | |
6891 | d['tkversion'] = 'NA' | |
6892 | return _("""\ | |
6893 | Event and Task Manager | |
6894 | etmtk {0[etmversion]} | |
6895 | ||
6896 | This application provides a format for using plain text files to store events, tasks and other items and a Tk based GUI for creating and modifying items as well as viewing them. | |
6897 | ||
6898 | System Information: | |
6899 | Python: {0[python]} | |
6900 | Dateutil: {0[dateutil]} | |
6901 | PyYaml: {0[pyyaml]} | |
6902 | Tk/Tcl: {0[tkversion]} | |
6903 | Platform: {0[platform]} | |
6904 | ||
6905 | ETM Information: | |
6906 | Homepage: | |
6907 | {0[home]} | |
6908 | Discussion: | |
6909 | {0[group]} | |
6910 | GitHub: | |
6911 | {0[github]} | |
6912 | Developer: | |
6913 | {0[dev]} | |
6914 | GPL License: | |
6915 | {0[gpl]} | |
6916 | ||
6917 | Copyright {0[copyright]} {0[dev]}. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.\ | |
6918 | """.format(d)) | |
6919 | ||
6920 | @staticmethod | |
6921 | def help_v(): | |
6922 | return _("""\ | |
6923 | Display information about etm and the operating system.""") | |
6924 | ||
6925 | @staticmethod | |
6926 | def help_help(): | |
6927 | return (USAGE) | |
6928 | ||
6929 | def replace_lines(self, fp, oldlines, begline, endline, newlines): | |
6930 | lines = oldlines | |
6931 | del lines[begline - 1:endline] | |
6932 | newlines.reverse() | |
6933 | for x in newlines: | |
6934 | lines.insert(begline - 1, x) | |
6935 | itemstr = "\n".join([unicode(u'%s') % x.rstrip() for x in lines | |
6936 | if x.strip()]) | |
6937 | if newlines: | |
6938 | mode = _("replaced item") | |
6939 | else: | |
6940 | mode = _("removed item") | |
6941 | self.safe_save(fp, itemstr, mode=mode) | |
6942 | return "break" | |
6943 | ||
6944 | ||
6945 | def main(etmdir='', argv=[]): | |
6946 | logger.debug("data.main etmdir: {0}, argv: {1}".format(etmdir, argv)) | |
6947 | use_locale = () | |
6948 | (user_options, options, use_locale) = get_options(etmdir) | |
6949 | ARGS = ['a', 'k', 'm', 'n', 'N', 'p', 'c', 'd', 't', 'v'] | |
6950 | if len(argv) > 1: | |
6951 | c = ETMCmd(options) | |
6952 | c.loop = False | |
6953 | c.number = False | |
6954 | args = [] | |
6955 | if len(argv) == 2 and argv[1] == "?": | |
6956 | term_print(USAGE) | |
6957 | elif len(argv) == 2 and argv[1] == 'v': | |
6958 | term_print(c.do_v("")) | |
6959 | elif len(argv) == 3 and '?' in argv: | |
6960 | if argv[1] == '?': | |
6961 | args = ['?', argv[2]] | |
6962 | else: | |
6963 | args = ['?', argv[1]] | |
6964 | if args[1] not in ARGS: | |
6965 | term_print(USAGE) | |
6966 | else: | |
6967 | argstr = ' '.join(args) | |
6968 | res = c.do_command(argstr) | |
6969 | term_print(res) | |
6970 | elif argv[1] in ARGS: | |
6971 | for x in argv[1:]: | |
6972 | x = s2or3(x) | |
6973 | args.append(x) | |
6974 | argstr = ' '.join(args) | |
6975 | opts = {} | |
6976 | if len(args) > 1: | |
6977 | try: | |
6978 | tmp = str2opts(" ".join(args[1:]), options) | |
6979 | except: | |
6980 | logger.exception('Could not process" {0}'.format(args[1:])) | |
6981 | return | |
6982 | if len(tmp) == 3: | |
6983 | opts = str2opts(" ".join(args[1:]), options)[0] | |
6984 | tt = TimeIt(loglevel=2, label="cmd '{0}'".format(argstr)) | |
6985 | c.loadData() | |
6986 | res = c.do_command(argstr) | |
6987 | if opts and 'width1' in opts: | |
6988 | width1 = opts['width1'] | |
6989 | elif options and 'report_width1' in options: | |
6990 | width1 = options['report_width1'] | |
6991 | else: | |
6992 | width1 = 43 | |
6993 | if opts and 'width2' in opts: | |
6994 | width2 = opts['width2'] | |
6995 | elif options and 'report_width2' in options: | |
6996 | width2 = options['report_width2'] | |
6997 | else: | |
6998 | width2 = 20 | |
6999 | ||
7000 | if options and 'report_indent' in options: | |
7001 | indent = options['report_indent'] | |
7002 | else: | |
7003 | indent = 4 | |
7004 | ||
7005 | if type(res) is dict: | |
7006 | lines = tree2Text(res, indent=indent, width1=width1, width2=width2)[0] | |
7007 | if lines and not lines[0]: | |
7008 | lines.pop(0) | |
7009 | res = "\n".join(lines) | |
7010 | tt.stop() | |
7011 | ||
7012 | term_print(res) | |
7013 | else: | |
7014 | logger.warn("argv: {0}".format(argv)) | |
7015 | ||
7016 | if __name__ == "__main__": | |
7017 | etmdir = '' | |
7018 | if len(sys.argv) > 1: | |
7019 | if sys.argv[1] not in ['a', 'c']: | |
7020 | etmdir = sys.argv.pop(1) | |
7021 | main(etmdir, sys.argv) |
0 | #!/usr/bin/env python3 | |
1 | # -*- coding: utf-8 -*- | |
2 | from __future__ import (absolute_import, division, print_function, | |
3 | unicode_literals) | |
4 | ||
5 | import logging | |
6 | import logging.config | |
7 | import uuid | |
8 | import os | |
9 | import os.path | |
10 | ||
11 | logger = logging.getLogger() | |
12 | ||
13 | import platform | |
14 | ||
15 | if platform.python_version() >= '3': | |
16 | import tkinter | |
17 | from tkinter import Entry, END, Label, Toplevel, Button, Frame, LEFT, Text, StringVar, IntVar, BooleanVar, ACTIVE, Radiobutton, Checkbutton, W, X, TclError, Listbox, BROWSE, Scrollbar | |
18 | from tkinter import font as tkFont | |
19 | utf8 = lambda x: x | |
20 | else: | |
21 | import Tkinter as tkinter | |
22 | from Tkinter import Entry, END, Label, Toplevel, Button, Frame, LEFT, Text, StringVar, IntVar, BooleanVar, ACTIVE, Radiobutton, Checkbutton, W, X, TclError, Listbox, BROWSE, Scrollbar | |
23 | import tkFont | |
24 | ||
25 | def utf8(s): | |
26 | return s | |
27 | ||
28 | from datetime import datetime, timedelta | |
29 | ||
30 | from etmTk.data import fmt_period, parse_dt, get_current_time, relpath, ensureMonthly, parse_period | |
31 | ||
32 | import gettext | |
33 | ||
34 | _ = gettext.gettext | |
35 | ||
36 | ||
37 | def sanitize_id(id): | |
38 | return id.strip().replace(" ", "") | |
39 | ||
40 | (_ADD, _DELETE, _INSERT) = range(3) | |
41 | (_ROOT, _DEPTH, _WIDTH) = range(3) | |
42 | ||
43 | ||
44 | ONEMINUTE = timedelta(minutes=1) | |
45 | ONEHOUR = timedelta(hours=1) | |
46 | ||
47 | ONEDAY = timedelta(days=1) | |
48 | ONEWEEK = timedelta(weeks=1) | |
49 | ||
50 | STOPPED = _('stopped') | |
51 | PAUSED = _('paused') | |
52 | RUNNING = _('running') | |
53 | ||
54 | FOUND = "found" | |
55 | CLOSE = _("Close") | |
56 | ||
57 | BGLCOLOR = "#f2f2f2" | |
58 | BGCOLOR = "#ebebeb" | |
59 | ||
60 | ||
61 | class OriginalCommand: | |
62 | ||
63 | def __init__(self, redir, operation): | |
64 | self.redir = redir | |
65 | self.operation = operation | |
66 | self.tk = redir.tk | |
67 | self.orig = redir.orig | |
68 | self.tk_call = self.tk.call | |
69 | self.orig_and_operation = (self.orig, self.operation) | |
70 | ||
71 | def __repr__(self): | |
72 | return "OriginalCommand(%r, %r)" % (self.redir, self.operation) | |
73 | ||
74 | def __call__(self, *args): | |
75 | return self.tk_call(self.orig_and_operation + args) | |
76 | ||
77 | ||
78 | ######################################################## | |
79 | # WidgetRedirector and OriginalCommand are from idlelib | |
80 | ######################################################## | |
81 | ||
82 | class WidgetRedirector: | |
83 | ||
84 | """Support for redirecting arbitrary widget subcommands. | |
85 | ||
86 | Some Tk operations don't normally pass through Tkinter. For example, if a | |
87 | character is inserted into a Text widget by pressing a key, a default Tk | |
88 | binding to the widget's 'insert' operation is activated, and the Tk library | |
89 | processes the insert without calling back into Tkinter. | |
90 | ||
91 | Although a binding to <Key> could be made via Tkinter, what we really want | |
92 | to do is to hook the Tk 'insert' operation itself. | |
93 | ||
94 | When a widget is instantiated, a Tcl command is created whose name is the | |
95 | same as the pathname widget._w. This command is used to invoke the various | |
96 | widget operations, e.g. insert (for a Text widget). We are going to hook | |
97 | this command and provide a facility ('register') to intercept the widget | |
98 | operation. | |
99 | ||
100 | In IDLE, the function being registered provides access to the top of a | |
101 | Percolator chain. At the bottom of the chain is a call to the original | |
102 | Tk widget operation. | |
103 | ||
104 | """ | |
105 | def __init__(self, widget): | |
106 | self._operations = {} | |
107 | self.widget = widget # widget instance | |
108 | self.tk = tk = widget.tk # widget's root | |
109 | w = widget._w # widget's (full) Tk pathname | |
110 | self.orig = w + "_orig" | |
111 | # Rename the Tcl command within Tcl: | |
112 | tk.call("rename", w, self.orig) | |
113 | # Create a new Tcl command whose name is the widget's pathname, and | |
114 | # whose action is to dispatch on the operation passed to the widget: | |
115 | tk.createcommand(w, self.dispatch) | |
116 | ||
117 | def __repr__(self): | |
118 | return "WidgetRedirector(%s<%s>)" % (self.widget.__class__.__name__, | |
119 | self.widget._w) | |
120 | ||
121 | def close(self): | |
122 | for operation in list(self._operations): | |
123 | self.unregister(operation) | |
124 | widget = self.widget | |
125 | del self.widget | |
126 | orig = self.orig | |
127 | del self.orig | |
128 | tk = widget.tk | |
129 | w = widget._w | |
130 | tk.deletecommand(w) | |
131 | # restore the original widget Tcl command: | |
132 | tk.call("rename", orig, w) | |
133 | ||
134 | def register(self, operation, function): | |
135 | self._operations[operation] = function | |
136 | setattr(self.widget, operation, function) | |
137 | return OriginalCommand(self, operation) | |
138 | ||
139 | def unregister(self, operation): | |
140 | if operation in self._operations: | |
141 | function = self._operations[operation] | |
142 | del self._operations[operation] | |
143 | if hasattr(self.widget, operation): | |
144 | delattr(self.widget, operation) | |
145 | return function | |
146 | else: | |
147 | return None | |
148 | ||
149 | def dispatch(self, operation, *args): | |
150 | '''Callback from Tcl which runs when the widget is referenced. | |
151 | ||
152 | If an operation has been registered in self._operations, apply the | |
153 | associated function to the args passed into Tcl. Otherwise, pass the | |
154 | operation through to Tk via the original Tcl function. | |
155 | ||
156 | Note that if a registered function is called, the operation is not | |
157 | passed through to Tk. Apply the function returned by self.register() | |
158 | to *args to accomplish that. For an example, see ColorDelegator.py. | |
159 | ||
160 | ''' | |
161 | m = self._operations.get(operation) | |
162 | try: | |
163 | if m: | |
164 | return m(*args) | |
165 | else: | |
166 | return self.tk.call((self.orig, operation) + args) | |
167 | except TclError: | |
168 | return "" | |
169 | ||
170 | ||
171 | class Node: | |
172 | ||
173 | def __init__(self, name, identifier=None, expanded=True): | |
174 | self.__identifier = (str(uuid.uuid1()) if identifier is None else sanitize_id(str(identifier))) | |
175 | self.name = name | |
176 | self.expanded = expanded | |
177 | self.__bpointer = None | |
178 | self.__fpointer = [] | |
179 | ||
180 | @property | |
181 | def identifier(self): | |
182 | return self.__identifier | |
183 | ||
184 | @property | |
185 | def fpointer(self): | |
186 | return self.__fpointer | |
187 | ||
188 | def update_fpointer(self, identifier, mode=_ADD): | |
189 | if mode is _ADD: | |
190 | self.__fpointer.append(sanitize_id(identifier)) | |
191 | elif mode is _DELETE: | |
192 | self.__fpointer.remove(sanitize_id(identifier)) | |
193 | elif mode is _INSERT: | |
194 | self.__fpointer = [sanitize_id(identifier)] | |
195 | ||
196 | ||
197 | class MenuTree: | |
198 | ||
199 | def __init__(self): | |
200 | self.nodes = [] | |
201 | self.lst = [] | |
202 | ||
203 | def get_index(self, position): | |
204 | for index, node in enumerate(self.nodes): | |
205 | if node.identifier == position: | |
206 | break | |
207 | return index | |
208 | ||
209 | def create_node(self, name, identifier=None, parent=None): | |
210 | # logger.debug("name: {0}, identifier: {1}; parent: {2}".format(name, identifier, parent)) | |
211 | ||
212 | node = Node(name, identifier) | |
213 | self.nodes.append(node) | |
214 | self.__update_fpointer(parent, node.identifier, _ADD) | |
215 | node.bpointer = parent | |
216 | return node | |
217 | ||
218 | def showMenu(self, position, level=_ROOT): | |
219 | queue = self[position].fpointer | |
220 | if level == _ROOT: | |
221 | self.lst = [] | |
222 | else: | |
223 | name, key = self[position].name.split("::") | |
224 | name = "{0}{1}".format(" " * (level - 1), name.strip()) | |
225 | s = "{0:<48} {1:^12}".format(name, key.strip()) | |
226 | self.lst.append(s) | |
227 | logger.debug("position: {0}, level: {1}, name: {2}, key: {3}".format(position, level, name, key)) | |
228 | if self[position].expanded: | |
229 | level += 1 | |
230 | for element in queue: | |
231 | self.showMenu(element, level) # recursive call | |
232 | return "\n".join(self.lst) | |
233 | ||
234 | def __update_fpointer(self, position, identifier, mode): | |
235 | if position is None: | |
236 | return | |
237 | else: | |
238 | self[position].update_fpointer(identifier, mode) | |
239 | ||
240 | def __getitem__(self, key): | |
241 | return self.nodes[self.get_index(key)] | |
242 | ||
243 | ||
244 | class Timer(): | |
245 | def __init__(self, parent=None, options={}): | |
246 | """ | |
247 | Methods providing the action timer | |
248 | """ | |
249 | self.timer_clear() | |
250 | self.parent = parent | |
251 | self.options = options | |
252 | self.idle_active = False | |
253 | self.idle_delta = 0 * ONEMINUTE | |
254 | ||
255 | def timer_clear(self): | |
256 | self.timer_delta = 0 * ONEMINUTE | |
257 | self.timer_active = False | |
258 | self.timer_status = STOPPED | |
259 | self.stop_status = STOPPED | |
260 | self.timer_last = None | |
261 | self.timer_hsh = None | |
262 | self.timer_summary = None | |
263 | ||
264 | def idle_start(self): | |
265 | if self.idle_active: | |
266 | return | |
267 | self.idle_starttime = datetime.now() | |
268 | self.idle_active = True | |
269 | self.parent.timerStatus.set(self.get_time()) | |
270 | logger.debug('idle start: {0}'.format(self.idle_starttime)) | |
271 | ||
272 | def idle_stop(self): | |
273 | if not self.idle_active: | |
274 | return | |
275 | if self.timer_status != STOPPED: | |
276 | self.timer_stop() | |
277 | if self.idle_delta: | |
278 | self.idle_resolve() | |
279 | logger.debug('idle stop: {0}'.format(self.idle_starttime)) | |
280 | self.idle_active = False | |
281 | ||
282 | def idle_resolve(self): | |
283 | """ | |
284 | Called when action timer is started or restarted | |
285 | """ | |
286 | if not self.idle_active or self.idle_delta < ONEMINUTE: | |
287 | return | |
288 | self.idle_delta += datetime.now() - self.idle_starttime | |
289 | logger.debug('resolve, idle time: {0}'.format(self.idle_delta)) | |
290 | opts = {'idle_delta': self.idle_delta, 'keywords': self.options['keywords'], 'currfile': ensureMonthly(self.options), 'tz': self.options['local_timezone']} | |
291 | self.idle_delta = ResolveIdleTime(self.parent, title="assign idle time", opts=opts).idle_delta | |
292 | ||
293 | def idle_resume(self): | |
294 | if not self.idle_active: | |
295 | return | |
296 | self.idle_starttime = datetime.now() | |
297 | logger.debug('resume, idle time: {0}'.format(self.idle_delta)) | |
298 | ||
299 | def timer_start(self, hsh=None, toggle=True): | |
300 | if not hsh: | |
301 | hsh = {} | |
302 | self.timer_starttime = datetime.now() | |
303 | self.timer_hsh = hsh | |
304 | text = hsh['_summary'] | |
305 | # self.timer_hsh['s'] = self.starttime | |
306 | if 'e' in hsh: | |
307 | self.timer_delta = hsh['e'] | |
308 | if len(text) > 16: | |
309 | self.timer_summary = "{0}~".format(text[:15]) | |
310 | else: | |
311 | self.timer_summary = text | |
312 | if toggle: | |
313 | self.timer_toggle(self.timer_hsh) | |
314 | else: | |
315 | self.timer_status = self.stop_status | |
316 | ||
317 | def timer_stop(self, create=True): | |
318 | if self.timer_status == STOPPED: | |
319 | return () | |
320 | self.idle_resume() | |
321 | self.stop_status = self.timer_status | |
322 | if self.timer_status == RUNNING: | |
323 | self.timer_delta += datetime.now() - self.timer_last | |
324 | self.timer_status = PAUSED | |
325 | ||
326 | self.timer_delta = max(self.timer_delta, ONEMINUTE) | |
327 | self.timer_hsh['e'] = self.timer_delta | |
328 | self.timer_hsh['s'] = self.timer_starttime | |
329 | self.timer_hsh['itemtype'] = '~' | |
330 | ||
331 | def timer_toggle(self, hsh=None): | |
332 | if not hsh: | |
333 | hsh = {} | |
334 | if self.timer_status == STOPPED: | |
335 | self.get_time() | |
336 | self.timer_last = datetime.now() | |
337 | self.timer_status = RUNNING | |
338 | elif self.timer_status == RUNNING: | |
339 | self.idle_resume() | |
340 | self.timer_delta += datetime.now() - self.timer_last | |
341 | self.timer_status = PAUSED | |
342 | elif self.timer_status == PAUSED: | |
343 | self.timer_status = RUNNING | |
344 | self.timer_last = datetime.now() | |
345 | if self.parent: | |
346 | self.parent.update_idletasks() | |
347 | ||
348 | def get_time(self): | |
349 | # if self.timer_status == STOPPED: | |
350 | if self.idle_active: | |
351 | if self.timer_status in [STOPPED, PAUSED]: | |
352 | self.idle_delta += datetime.now() - self.idle_starttime | |
353 | self.idle_starttime = datetime.now() | |
354 | idle = "[{0}]".format(fmt_period(self.idle_delta)) | |
355 | logger.debug("idle: {0}, {1}".format(self.idle_starttime, self.idle_delta)) | |
356 | else: | |
357 | idle = "" | |
358 | if self.timer_status == STOPPED: | |
359 | ret = idle | |
360 | self.timer_minutes = 0 | |
361 | self.elapsed_time = 0 * ONEMINUTE | |
362 | else: | |
363 | if self.timer_status == PAUSED: | |
364 | elapsed_time = self.timer_delta | |
365 | elif self.timer_status == RUNNING: | |
366 | elapsed_time = (self.timer_delta + datetime.now() - self.timer_last) | |
367 | else: | |
368 | elapsed_time = self.timer_delta | |
369 | plus = " ({0})".format(_("paused")) | |
370 | self.timer_minutes = elapsed_time.seconds // 60 | |
371 | if self.timer_status == RUNNING: | |
372 | plus = " ({0})".format(_("running")) | |
373 | # ret = "{0} {1}{2}".format(self.timer_summary, self.timer_time, s) | |
374 | ret = "{1} {2}{3} {0}".format(idle, self.timer_summary, fmt_period(elapsed_time), plus) | |
375 | logger.debug("timer: {0}, {1}".format(self.timer_last, elapsed_time)) | |
376 | return ret | |
377 | ||
378 | ||
379 | class ReadOnlyText(Text): | |
380 | # noinspection PyShadowingNames | |
381 | def __init__(self, *args, **kwargs): | |
382 | Text.__init__(self, *args, **kwargs) | |
383 | self.redirector = WidgetRedirector(self) | |
384 | self.insert = self.redirector.register("insert", lambda *args, **kw: "break") | |
385 | self.delete = self.redirector.register("delete", lambda *args, **kw: "break") | |
386 | self.configure(highlightthickness=0, insertwidth=0, takefocus=0, wrap="word") | |
387 | ||
388 | ||
389 | class MessageWindow(): | |
390 | # noinspection PyShadowingNames | |
391 | def __init__(self, parent, title, prompt, opts={}): | |
392 | self.win = Toplevel(parent) | |
393 | self.win.protocol("WM_DELETE_WINDOW", self.cancel) | |
394 | self.parent = parent | |
395 | self.options = opts | |
396 | self.win.title(title) | |
397 | tkfixedfont = tkFont.nametofont("TkFixedFont") | |
398 | if 'fontsize_fixed' in self.options and self.options['fontsize_fixed']: | |
399 | tkfixedfont.configure(size=self.options['fontsize_fixed']) | |
400 | ||
401 | self.content = ReadOnlyText(self.win, wrap="word", padx=3, bd=2, height=10, relief="sunken", font=tkfixedfont, width=46, takefocus=False) | |
402 | self.content.pack(fill=tkinter.BOTH, expand=1, padx=10, pady=10) | |
403 | self.content.insert("1.0", prompt) | |
404 | b = Button(self.win, text=_('OK'), width=10, command=self.cancel, default='active', pady=2) | |
405 | b.pack() | |
406 | self.win.bind('<Return>', (lambda e, b=b: b.invoke())) | |
407 | self.win.bind('<Escape>', (lambda e, b=b: b.invoke())) | |
408 | self.win.focus_set() | |
409 | self.win.grab_set() | |
410 | self.win.transient(parent) | |
411 | self.win.wait_window(self.win) | |
412 | return | |
413 | ||
414 | def cancel(self, event=None): | |
415 | # put focus back to the parent window | |
416 | self.parent.focus_set() | |
417 | self.win.destroy() | |
418 | ||
419 | ||
420 | class FileChoice(object): | |
421 | def __init__(self, master=None, title=None, prefix=None, list=[], start='', ext="txt", new=False): | |
422 | self.master = master | |
423 | self.value = None | |
424 | self.prefix = prefix | |
425 | self.list = list | |
426 | if prefix and start: | |
427 | self.start = relpath(start, prefix) | |
428 | else: | |
429 | self.start = start | |
430 | self.ext = ext | |
431 | self.new = new | |
432 | ||
433 | self.modalPane = Toplevel(self.master, highlightbackground=BGCOLOR, background=BGCOLOR) | |
434 | if master: | |
435 | logger.debug('winfo: {0}, {1}; {2}, {3}'.format(master.winfo_rootx(), type(master.winfo_rootx()), master.winfo_rooty(), type(master.winfo_rooty()))) | |
436 | self.modalPane.geometry("+%d+%d" % (master.winfo_rootx() + 50, master.winfo_rooty() + 50)) | |
437 | ||
438 | self.modalPane.transient(self.master) | |
439 | self.modalPane.grab_set() | |
440 | ||
441 | self.modalPane.bind("<Return>", self._choose) | |
442 | self.modalPane.bind("<Escape>", self._cancel) | |
443 | ||
444 | if title: | |
445 | self.modalPane.title(title) | |
446 | ||
447 | if new: | |
448 | nameFrame = Frame(self.modalPane, highlightbackground=BGCOLOR, background=BGCOLOR) | |
449 | nameFrame.pack(side="top", padx=18, pady=2, fill="x") | |
450 | ||
451 | nameLabel = Label(nameFrame, text=_("file:"), bd=1, relief="flat", anchor="w", padx=0, pady=0, highlightbackground=BGCOLOR, background=BGCOLOR) | |
452 | nameLabel.pack(side="left") | |
453 | ||
454 | self.fileName = StringVar(self.modalPane) | |
455 | self.fileName.set("untitled.{0}".format(ext)) | |
456 | self.fileName.trace_variable("w", self.onSelect) | |
457 | self.fname = Entry(nameFrame, textvariable=self.fileName, bd=1, highlightbackground=BGCOLOR) | |
458 | self.fname.pack(side="left", fill="x", expand=1, padx=0, pady=0) | |
459 | self.fname.icursor(END) | |
460 | self.fname.bind("<Up>", self.cursorUp) | |
461 | self.fname.bind("<Down>", self.cursorDown) | |
462 | ||
463 | filterFrame = Frame(self.modalPane, highlightbackground=BGCOLOR, background=BGCOLOR) | |
464 | filterFrame.pack(side="top", padx=18, pady=4, fill="x") | |
465 | ||
466 | filterLabel = Label(filterFrame, text=_("filter:"), bd=1, relief="flat", anchor="w", padx=0, pady=0, highlightbackground=BGCOLOR, background=BGCOLOR) | |
467 | filterLabel.pack(side="left") | |
468 | ||
469 | self.filterValue = StringVar(self.modalPane) | |
470 | self.filterValue.set("") | |
471 | self.filterValue.trace_variable("w", self.setMatching) | |
472 | self.fltr = Entry(filterFrame, textvariable=self.filterValue, bd=1, highlightbackground=BGCOLOR) | |
473 | self.fltr.pack(side="left", fill="x", expand=1, padx=0, pady=0) | |
474 | self.fltr.icursor(END) | |
475 | ||
476 | prefixFrame = Frame(self.modalPane, highlightbackground=BGCOLOR, background=BGCOLOR) | |
477 | prefixFrame.pack(side="top", padx=8, pady=2, fill="x") | |
478 | ||
479 | self.prefixLabel = Label(prefixFrame, text=_("{0}:").format(prefix), bd=1, highlightbackground=BGCOLOR, background=BGCOLOR) | |
480 | self.prefixLabel.pack(side="left", expand=0, padx=0, pady=0) | |
481 | ||
482 | buttonFrame = Frame(self.modalPane, highlightbackground=BGCOLOR, background=BGCOLOR) | |
483 | buttonFrame.pack(side="bottom", padx=10, pady=2) | |
484 | ||
485 | chooseButton = Button(buttonFrame, text="Choose", command=self._choose, highlightbackground=BGCOLOR, background=BGCOLOR, pady=2) | |
486 | chooseButton.pack(side="right", padx=10) | |
487 | ||
488 | cancelButton = Button(buttonFrame, text="Cancel", command=self._cancel, highlightbackground=BGCOLOR, background=BGCOLOR, pady=2) | |
489 | cancelButton.pack(side="left") | |
490 | ||
491 | selectionFrame = Frame(self.modalPane, highlightbackground=BGCOLOR, background=BGCOLOR) | |
492 | selectionFrame.pack(side="bottom", padx=8, pady=2, fill="x") | |
493 | ||
494 | self.selectionValue = StringVar(self.modalPane) | |
495 | self.selectionValue.set("") | |
496 | self.selection = Label(selectionFrame, textvariable=self.selectionValue, bd=1, highlightbackground=BGCOLOR, background=BGCOLOR) | |
497 | self.selection.pack(side="left", fill="x", expand=1, padx=0, pady=0) | |
498 | ||
499 | listFrame = Frame(self.modalPane, highlightbackground=BGCOLOR, background=BGCOLOR, width=40) | |
500 | listFrame.pack(side="top", fill="both", expand=1, padx=5, pady=2) | |
501 | ||
502 | scrollBar = Scrollbar(listFrame, width=8) | |
503 | scrollBar.pack(side="right", fill="y") | |
504 | self.listBox = Listbox(listFrame, selectmode=BROWSE, width=36) | |
505 | self.listBox.pack(side="left", fill="both", expand=1, ipadx=4, padx=2, pady=0) | |
506 | self.listBox.bind('<<ListboxSelect>>', self.onSelect) | |
507 | self.listBox.bind("<Double-1>", self._choose) | |
508 | self.modalPane.bind("<Return>", self._choose) | |
509 | self.modalPane.bind("<Escape>", self._cancel) | |
510 | # self.modalPane.bind("<Up>", self.cursorUp) | |
511 | # self.modalPane.bind("<Down>", self.cursorDown) | |
512 | self.fltr.bind("<Up>", self.cursorUp) | |
513 | self.fltr.bind("<Down>", self.cursorDown) | |
514 | scrollBar.config(command=self.listBox.yview) | |
515 | self.listBox.config(yscrollcommand=scrollBar.set) | |
516 | self.setMatching() | |
517 | ||
518 | def ignore(self, e=None): | |
519 | return "break" | |
520 | ||
521 | def onSelect(self, *args): | |
522 | # Note here that Tkinter passes an event object to onselect() | |
523 | ||
524 | if self.listBox.curselection(): | |
525 | firstIndex = self.listBox.curselection()[0] | |
526 | value = self.matches[int(firstIndex)] | |
527 | r = value[1] | |
528 | p = os.path.join(self.prefix, r) | |
529 | if self.new: | |
530 | if os.path.isfile(p): | |
531 | p = os.path.split(p)[0] | |
532 | r = os.path.split(r)[0] | |
533 | f = self.fileName.get() | |
534 | r = os.path.join(r, f) | |
535 | p = os.path.join(p, f) | |
536 | ||
537 | self.selectionValue.set(r) | |
538 | self.value = p | |
539 | return "break" | |
540 | ||
541 | def cursorUp(self, event=None): | |
542 | cursel = int(self.listBox.curselection()[0]) | |
543 | newsel = max(0, cursel - 1) | |
544 | self.listBox.select_clear(cursel) | |
545 | self.listBox.select_set(newsel) | |
546 | self.listBox.see(newsel) | |
547 | self.onSelect() | |
548 | return "break" | |
549 | ||
550 | def cursorDown(self, event=None): | |
551 | cursel = int(self.listBox.curselection()[0]) | |
552 | newsel = min(len(self.list) - 1, cursel + 1) | |
553 | self.listBox.select_clear(cursel) | |
554 | self.listBox.select_set(newsel) | |
555 | self.listBox.see(newsel) | |
556 | self.onSelect() | |
557 | return "break" | |
558 | ||
559 | def setMatching(self, *args): | |
560 | # disabled = "#BADEC3" | |
561 | # disabled = "#91CC9E" | |
562 | disabled = "#62B374" | |
563 | match = self.filterValue.get() | |
564 | if match: | |
565 | self.matches = matches = [x for x in self.list if x and match.lower() in x[1].lower()] | |
566 | else: | |
567 | self.matches = matches = self.list | |
568 | self.listBox.delete(0, END) | |
569 | index = 0 | |
570 | init_index = 0 | |
571 | for item in matches: | |
572 | if type(item) is tuple: | |
573 | # only show the label | |
574 | # (label, value, disabled)FF | |
575 | self.listBox.insert(END, item[0]) | |
576 | if self.new: | |
577 | if not item[-1]: | |
578 | self.listBox.itemconfig(index, fg=disabled) | |
579 | else: | |
580 | self.listBox.itemconfig(index, fg="blue") | |
581 | if self.start and item[1] == self.start: | |
582 | init_index = index | |
583 | else: | |
584 | if item[-1]: | |
585 | self.listBox.itemconfig(index, fg=disabled) | |
586 | else: | |
587 | self.listBox.itemconfig(index, fg="blue") | |
588 | if self.start and item[1] == self.start: | |
589 | init_index = index | |
590 | # elif files: | |
591 | else: | |
592 | self.listBox.insert(END, item) | |
593 | index += 1 | |
594 | self.listBox.select_set(init_index) | |
595 | self.listBox.see(init_index) | |
596 | self.fltr.focus_set() | |
597 | self.onSelect() | |
598 | ||
599 | def _choose(self, event=None): | |
600 | try: | |
601 | if self.listBox.curselection(): | |
602 | firstIndex = self.listBox.curselection()[0] | |
603 | if self.new: | |
604 | if not self.value or os.path.isfile(self.value): | |
605 | return | |
606 | else: | |
607 | tup = self.matches[int(firstIndex)] | |
608 | if tup[-1]: | |
609 | return | |
610 | self.value = os.path.join(self.prefix, tup[1]) | |
611 | else: | |
612 | return | |
613 | except IndexError: | |
614 | self.value = None | |
615 | self.modalPane.destroy() | |
616 | ||
617 | def _cancel(self, event=None): | |
618 | self.value = None | |
619 | self.modalPane.destroy() | |
620 | ||
621 | def returnValue(self): | |
622 | self.master.wait_window(self.modalPane) | |
623 | return self.value | |
624 | ||
625 | ||
626 | class Dialog(Toplevel): | |
627 | ||
628 | def __init__(self, parent, title=None, prompt=None, opts=None, default=None, modal=True, xoffset=50, yoffset=50, event=None, process=None, font=None): | |
629 | ||
630 | Toplevel.__init__(self, parent, highlightbackground=BGCOLOR, background=BGCOLOR) | |
631 | self.protocol("WM_DELETE_WINDOW", self.quit) | |
632 | if modal: | |
633 | logger.debug('modal') | |
634 | self.transient(parent) | |
635 | else: | |
636 | logger.debug('non modal') | |
637 | ||
638 | if title: | |
639 | self.title(title) | |
640 | ||
641 | self.parent = parent | |
642 | ||
643 | self.event = event | |
644 | logger.debug("parent: {0}".format(self.parent)) | |
645 | self.prompt = prompt | |
646 | self.options = opts | |
647 | self.font = font | |
648 | self.default = default | |
649 | self.value = "" | |
650 | self.process = process | |
651 | ||
652 | self.error_message = None | |
653 | ||
654 | # self.buttonbox() | |
655 | ||
656 | body = Frame(self, highlightbackground=BGCOLOR, background=BGCOLOR) | |
657 | # self.initial_focus = self.body(body) | |
658 | self.body(body).focus_set() | |
659 | ||
660 | self.buttonbox() | |
661 | # don't expand body or it will fill below the actual content | |
662 | body.pack(side="top", fill=tkinter.BOTH, padx=0, pady=0, expand=1) | |
663 | self.protocol("WM_DELETE_WINDOW", self.quit) | |
664 | if parent: | |
665 | self.geometry("+%d+%d" % (parent.winfo_rootx() + xoffset, parent.winfo_rooty() + yoffset)) | |
666 | if modal: | |
667 | self.grab_set() | |
668 | self.wait_window(self) | |
669 | ||
670 | def body(self, master): | |
671 | # create dialog body. return widget that should have | |
672 | # initial focus. this method should be overridden | |
673 | pass | |
674 | ||
675 | def buttonbox(self): | |
676 | # add standard button box. override if you don't want the | |
677 | # standard buttons | |
678 | box = Frame(self, background=BGCOLOR, highlightbackground=BGCOLOR) | |
679 | w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE, highlightbackground=BGCOLOR, pady=2) | |
680 | w.pack(side="right", padx=5, pady=2) | |
681 | w = Button(box, text="Cancel", width=10, command=self.cancel, highlightbackground=BGCOLOR, pady=2) | |
682 | w.pack(side="right", padx=5, pady=2) | |
683 | self.bind("<Return>", self.ok) | |
684 | self.bind("<Escape>", self.cancel) | |
685 | ||
686 | box.pack(side='bottom') | |
687 | ||
688 | # standard button semantics | |
689 | ||
690 | def ok(self, event=None): | |
691 | res = self.validate() | |
692 | logger.debug('validate: {0}, value: "{1}"'.format(res, self.value)) | |
693 | if not res: | |
694 | if self.error_message: | |
695 | self.messageWindow('error', self.error_message) | |
696 | ||
697 | # self.initial_focus.focus_set() # put focus back | |
698 | return "break" | |
699 | ||
700 | self.withdraw() | |
701 | self.update_idletasks() | |
702 | ||
703 | self.apply() | |
704 | self.quit() | |
705 | ||
706 | def cancel(self, event=None): | |
707 | # return the focus to the tree view in the main window | |
708 | self.value = None | |
709 | logger.debug('value: "{0}"'.format(self.value)) | |
710 | self.quit() | |
711 | ||
712 | def quit(self, event=None): | |
713 | if self.parent: | |
714 | logger.debug("returning focus to parent: {0}".format(self.parent)) | |
715 | self.parent.focus() | |
716 | # self.parent.tree.focus_set() | |
717 | if self.parent.weekly or self.parent.monthly: | |
718 | self.parent.canvas.focus_set() | |
719 | else: | |
720 | self.parent.tree.focus_set() | |
721 | else: | |
722 | logger.debug("returning focus, no parent") | |
723 | self.destroy() | |
724 | ||
725 | # command hooks | |
726 | def validate(self): | |
727 | return 1 # override | |
728 | ||
729 | def apply(self): | |
730 | pass # override | |
731 | ||
732 | def messageWindow(self, title, prompt): | |
733 | MessageWindow(self.parent, title, prompt) | |
734 | ||
735 | ||
736 | class ResolveIdleTime(Dialog): | |
737 | ||
738 | def body(self, master): | |
739 | """ | |
740 | !!! copy completions filter/listbox setup | |
741 | ||
742 | Assign time period [ time period entry ] | |
743 | to [ keyword combo box ] | |
744 | need: | |
745 | options['idle_delta'] | |
746 | options['keywords'] | |
747 | ||
748 | file to append idle time assigned actions | |
749 | options['idle_file'] | |
750 | file to append new keywords | |
751 | options['keywords_file'] | |
752 | """ | |
753 | self.idle_delta = self.options['idle_delta'] | |
754 | self.completions = self.options['keywords'] | |
755 | self.currfile = self.options['currfile'] | |
756 | self.tz = self.options['tz'] | |
757 | period_frame = Frame(master, background=BGCOLOR) | |
758 | period_frame.pack(side="top", fill="x", padx=4, pady=2) | |
759 | ||
760 | period_label = Label(period_frame, text=_("Assign"), bg=BGCOLOR) | |
761 | period_label.pack(side="left") | |
762 | self.time_period = StringVar(self) | |
763 | self.period_entry = Entry(period_frame, textvariable=self.time_period, highlightbackground=BGCOLOR) | |
764 | ||
765 | self.idletime = StringVar(self) | |
766 | self.idletime.set(fmt_period(self.idle_delta)) | |
767 | self.idle_label = Label(period_frame, textvariable=self.idletime, bg=BGCOLOR, takefocus=0) | |
768 | self.idle_label.pack(side="right", padx=2) | |
769 | ||
770 | of_label = Label(period_frame, text=_("of"), bg=BGCOLOR, takefocus=0) | |
771 | of_label.pack(side="right") | |
772 | self.period_entry.pack(side="left", fill="x", expand=1, padx=4) | |
773 | self.keyword_frame = keyword_frame = Frame(master, background=BGCOLOR) | |
774 | keyword_frame.pack(side="top", fill="both", padx=4, expand=1) | |
775 | self.outcome = StringVar(self) | |
776 | self.outcome.set("") | |
777 | self.outcome_label = Label(keyword_frame, textvariable=self.outcome, bg=BGCOLOR, takefocus=0) | |
778 | self.outcome_label.pack(side="bottom") | |
779 | self.filterValue = StringVar(self) | |
780 | self.filterValue.set("") | |
781 | self.filterValue.trace_variable("w", self.setCompletions) | |
782 | self.fltr = Entry(self.keyword_frame, textvariable=self.filterValue, highlightbackground=BGCOLOR) | |
783 | self.fltr.pack(fill="x") | |
784 | self.fltr.icursor(END) | |
785 | self.listbox = listbox = Listbox(self.keyword_frame, exportselection=False, width=self.parent.options['completions_width']) | |
786 | listbox.pack(fill="both", expand=True, padx=2, pady=2) | |
787 | self.keyword_frame.bind("<Double-1>", self.apply) | |
788 | self.keyword_frame.bind("<Return>", self.apply) | |
789 | self.listbox.bind("<Up>", self.cursorUp) | |
790 | self.listbox.bind("<Down>", self.cursorDown) | |
791 | self.fltr.bind("<Up>", self.cursorUp) | |
792 | self.fltr.bind("<Down>", self.cursorDown) | |
793 | self.setCompletions() | |
794 | return self.period_entry | |
795 | ||
796 | def setCompletions(self, *args): | |
797 | match = self.filterValue.get() | |
798 | self.matches = matches = [x for x in self.completions if x and x.lower().startswith(match.lower())] | |
799 | self.listbox.delete(0, END) | |
800 | for item in matches: | |
801 | self.listbox.insert(END, item) | |
802 | self.listbox.select_set(0) | |
803 | self.listbox.see(0) | |
804 | ||
805 | def cursorUp(self, event=None): | |
806 | cursel = int(self.listbox.curselection()[0]) | |
807 | newsel = max(0, cursel - 1) | |
808 | self.listbox.select_clear(cursel) | |
809 | self.listbox.select_set(newsel) | |
810 | self.listbox.see(newsel) | |
811 | return "break" | |
812 | ||
813 | def cursorDown(self, event=None): | |
814 | cursel = int(self.listbox.curselection()[0]) | |
815 | newsel = min(len(self.matches) - 1, cursel + 1) | |
816 | self.listbox.select_clear(cursel) | |
817 | self.listbox.select_set(newsel) | |
818 | self.listbox.see(newsel) | |
819 | return "break" | |
820 | ||
821 | def apply(self): | |
822 | """ | |
823 | Make sure values are ok, write action and update idle time | |
824 | """ | |
825 | period_str = self.period_entry.get() | |
826 | keyword_str = self.matches[int(self.listbox.curselection()[0])] | |
827 | if not (period_str and keyword_str): | |
828 | return | |
829 | try: | |
830 | period = parse_period(period_str) | |
831 | except: | |
832 | self.outcome.set(_("Could not parse period: {0}").format(period_str)) | |
833 | return | |
834 | hsh = {'itemtype': '~', '_summary': 'idle time', 's': get_current_time(), 'e': period, 'k': keyword_str, 'z': self.tz} | |
835 | self.parent.loop.append_item(hsh, self.currfile) | |
836 | self.outcome.set(_("assigned {0} to {1}").format(fmt_period(period), keyword_str)) | |
837 | self.time_period.set("") | |
838 | self.idle_delta -= period | |
839 | self.idletime.set(fmt_period(self.idle_delta)) | |
840 | if self.idle_delta < ONEMINUTE: | |
841 | self.cancel() | |
842 | ||
843 | def ok(self, event=None): | |
844 | self.apply() | |
845 | ||
846 | ||
847 | class TextVariableWindow(Dialog): | |
848 | def body(self, master): | |
849 | if 'textvariable' not in self.options: | |
850 | return | |
851 | self.entry = Entry(master, textvariable=self.options['textvariable']) | |
852 | self.entry.pack(side="bottom", padx=5, pady=5) | |
853 | Label(master, text=self.prompt, justify='left', highlightbackground=BGLCOLOR, background=BGLCOLOR).pack(side="top", fill=tkinter.BOTH, expand=1, padx=10, pady=5) | |
854 | self.entry.focus_set() | |
855 | self.entry.bind('<Escape>', self.entry.delete(0, END)) | |
856 | return self.entry | |
857 | ||
858 | def buttonbox(self): | |
859 | # add standard button box. override if you don't want the | |
860 | # standard buttons | |
861 | box = Frame(self, highlightbackground=BGCOLOR, background=BGCOLOR) | |
862 | ||
863 | w = Button(box, text=CLOSE, width=10, command=self.ok, | |
864 | default=ACTIVE, highlightbackground=BGCOLOR, pady=2) | |
865 | w.pack(side=LEFT, padx=5, pady=5) | |
866 | self.bind("<Return>", self.ok) | |
867 | self.bind("<Escape>", self.ok) | |
868 | box.pack(side='bottom') | |
869 | ||
870 | def quit(self, event=None): | |
871 | if self.parent: | |
872 | logger.debug("returning focus to parent: {0}".format(self.parent)) | |
873 | self.parent.focus() | |
874 | self.parent.focus_set() | |
875 | else: | |
876 | logger.debug("returning focus, no parent") | |
877 | self.entry.delete(0, END) | |
878 | self.options['textvariable'].set("") | |
879 | self.destroy() | |
880 | ||
881 | ||
882 | class DialogWindow(Dialog): | |
883 | # master will be a frame in Dialog | |
884 | def body(self, master): | |
885 | self.entry = Entry(master) | |
886 | self.entry.pack(side="bottom", padx=5, pady=2, fill=X) | |
887 | tkfixedfont = self.font | |
888 | lines = self.prompt.split('\n') | |
889 | height = min(20, len(lines) + 1) | |
890 | lengths = [len(line) for line in lines] | |
891 | width = min(70, max(lengths) + 2) | |
892 | self.text = ReadOnlyText( | |
893 | master, wrap="word", padx=2, pady=2, bd=2, relief="sunken", | |
894 | font=tkfixedfont, | |
895 | height=height, | |
896 | width=width, | |
897 | bg=BGLCOLOR, | |
898 | takefocus=False) | |
899 | self.text.insert("1.1", self.prompt) | |
900 | self.text.pack(side="top", fill=tkinter.BOTH, expand=1, padx=6, pady=2) | |
901 | if self.default is not None: | |
902 | self.entry.insert(0, self.default) | |
903 | self.entry.select_range(0, END) | |
904 | return self.entry | |
905 | ||
906 | ||
907 | class TextDialog(Dialog): | |
908 | ||
909 | def body(self, master): | |
910 | tkfixedfont = self.font | |
911 | lines = self.prompt.split('\n') | |
912 | height = min(25, len(lines) + 1) | |
913 | lengths = [len(line) for line in lines] | |
914 | width = min(70, max(lengths) + 2) | |
915 | self.text = ReadOnlyText( | |
916 | master, wrap="word", padx=2, pady=2, bd=2, relief="sunken", | |
917 | # font=tkFont.Font(family="Lucida Sans Typewriter"), | |
918 | font=tkfixedfont, | |
919 | height=height, | |
920 | width=width, | |
921 | bg=BGLCOLOR, | |
922 | highlightbackground=BGLCOLOR, | |
923 | takefocus=False) | |
924 | self.text.insert("1.1", self.prompt) | |
925 | self.text.pack(side='left', fill=tkinter.BOTH, expand=1, padx=5, | |
926 | pady=2) | |
927 | # ysb = Scrollbar(master, orient='vertical', command=self.text | |
928 | # .yview, width=8) | |
929 | # ysb.pack(side='right', fill=tkinter.Y, expand=0, padx=0, pady=0) | |
930 | # self.text.configure(yscroll=ysb.set) | |
931 | return self.text | |
932 | ||
933 | def buttonbox(self): | |
934 | # add standard button box. override if you don't want the | |
935 | # standard buttons | |
936 | ||
937 | box = Frame(self, highlightbackground=BGCOLOR, background=BGCOLOR) | |
938 | ||
939 | w = Button(box, text="OK", width=6, command=self.cancel, | |
940 | default=ACTIVE, highlightbackground=BGCOLOR, pady=2) | |
941 | w.pack(side=LEFT, padx=5, pady=0) | |
942 | ||
943 | self.bind("<Return>", self.ok) | |
944 | self.bind("<Escape>", self.ok) | |
945 | ||
946 | box.pack(side='bottom') | |
947 | ||
948 | ||
949 | class OptionsDialog(): | |
950 | def __init__(self, parent, master=None, title="", prompt="", opts=None, radio=True, yesno=True, list=False): | |
951 | if not opts: | |
952 | opts = [] | |
953 | self.win = Toplevel(parent) | |
954 | self.win.protocol("WM_DELETE_WINDOW", self.quit) | |
955 | if parent: | |
956 | self.win.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50)) | |
957 | self.parent = parent | |
958 | self.master = master | |
959 | self.options = opts | |
960 | self.radio = radio | |
961 | self.win.title(title) | |
962 | if list: | |
963 | self.win.configure(bg=BGCOLOR) | |
964 | tkfixedfont = tkFont.nametofont("TkFixedFont") | |
965 | if 'fontsize_fixed' in self.parent.options and self.parent.options['fontsize_fixed']: | |
966 | tkfixedfont.configure(size=self.parent.options['fontsize_fixed']) | |
967 | self.content = ReadOnlyText(self.win, wrap="word", padx=3, bd=2, height=10, relief="sunken", font=tkfixedfont, bg=BGLCOLOR, highlightbackground=BGLCOLOR, width=46, takefocus=False) | |
968 | self.content.pack(fill=tkinter.BOTH, expand=1, padx=10, pady=5) | |
969 | self.content.insert("1.0", prompt) | |
970 | else: | |
971 | Label(self.win, text=prompt, justify='left').pack(fill=tkinter.BOTH, expand=1, padx=10, pady=5) | |
972 | self.sv = StringVar(parent) | |
973 | self.sv = IntVar(parent) | |
974 | self.sv.set(1) | |
975 | if self.options: | |
976 | if radio: | |
977 | self.value = opts[0] | |
978 | for i in range(min(9, len(self.options))): | |
979 | txt = self.options[i] | |
980 | val = i + 1 | |
981 | # bind keyboard numbers 1-9 (at most) to options selection, i.e., press 1 to select option 1, 2 to select 2, etc. | |
982 | self.win.bind(str(val), (lambda e, x=val: self.sv.set(x))) | |
983 | Radiobutton(self.win, text="{0}: {1}".format(val, txt), padx=20, indicatoron=True, variable=self.sv, command=self.getValue, value=val).pack(padx=10, anchor=W) | |
984 | else: | |
985 | self.check_values = {} | |
986 | # show 0, check 1, return 2 | |
987 | for i in range(min(9, len(self.options))): | |
988 | txt = self.options[i][0] | |
989 | self.check_values[i] = BooleanVar(self.parent) | |
990 | self.check_values[i].set(self.options[i][1]) | |
991 | Checkbutton(self.win, text=self.options[i][0], padx=20, variable=self.check_values[i]).pack(padx=10, anchor=W) | |
992 | box = Frame(self.win) | |
993 | if list: | |
994 | box.configure(bg=BGCOLOR) | |
995 | if yesno: | |
996 | YES = _("Yes") | |
997 | NO = _("No") | |
998 | else: | |
999 | YES = _("Ok") | |
1000 | NO = _("Cancel") | |
1001 | c = Button(box, text=NO, width=10, command=self.cancel, pady=2) | |
1002 | c.pack(side=LEFT, padx=5, pady=5) | |
1003 | o = Button(box, text=YES, width=10, default='active', command=self.ok, pady=2) | |
1004 | o.pack(side=LEFT, padx=5, pady=5) | |
1005 | if list: | |
1006 | for b in [c, o]: | |
1007 | b.configure(bg=BGCOLOR, highlightbackground=BGCOLOR) | |
1008 | box.pack() | |
1009 | self.win.bind('<Return>', (lambda e, o=o: o.invoke())) | |
1010 | self.win.bind('<Control-w>', self.Ok) | |
1011 | self.win.bind('<Escape>', (lambda e, c=c: c.invoke())) | |
1012 | # self.choice.focus_set() | |
1013 | logger.debug('parent: {0}'.format(parent)) | |
1014 | self.win.focus_set() | |
1015 | self.win.transient(parent) | |
1016 | self.win.wait_window(self.win) | |
1017 | ||
1018 | def getValue(self, e=None): | |
1019 | v = self.sv.get() | |
1020 | logger.debug("sv: {0}".format(v)) | |
1021 | if self.options: | |
1022 | if self.radio: | |
1023 | if v - 1 in range(len(self.options)): | |
1024 | o = self.options[v - 1] | |
1025 | logger.debug( | |
1026 | 'OptionsDialog returning {0}: {1}'.format(v, o)) | |
1027 | return v, o | |
1028 | # return o, v | |
1029 | else: | |
1030 | logger.debug( | |
1031 | 'OptionsDialog returning {0}: {1}'.format(v, None)) | |
1032 | return 0, None | |
1033 | else: # checkbutton | |
1034 | values = [] | |
1035 | for i in range(len(self.options)): | |
1036 | bool = self.check_values[i].get() > 0 | |
1037 | values.append([self.options[i][0], bool, self.options[i][2]]) | |
1038 | return values | |
1039 | else: # askokcancel type dialog | |
1040 | logger.debug( | |
1041 | 'OptionsDialog returning {0}: {1}'.format(v, None)) | |
1042 | return v, v | |
1043 | ||
1044 | def ok(self, event=None): | |
1045 | # self.parent.update_idletasks() | |
1046 | self.quit() | |
1047 | ||
1048 | def Ok(self, event=None): | |
1049 | # self.parent.update_idletasks() | |
1050 | self.sv.set(2) | |
1051 | self.quit() | |
1052 | ||
1053 | def cancel(self, event=None): | |
1054 | self.sv.set(0) | |
1055 | self.quit() | |
1056 | ||
1057 | def quit(self, event=None): | |
1058 | # put focus back to the parent window | |
1059 | if self.master: | |
1060 | self.master.focus_set() | |
1061 | elif self.parent: | |
1062 | self.parent.focus_set() | |
1063 | self.win.destroy() | |
1064 | ||
1065 | ||
1066 | class GetInteger(DialogWindow): | |
1067 | def validate(self): | |
1068 | minvalue = maxvalue = None | |
1069 | if len(self.options) > 0: | |
1070 | minvalue = self.options[0] | |
1071 | if len(self.options) > 1: | |
1072 | maxvalue = self.options[1] | |
1073 | res = self.entry.get() | |
1074 | try: | |
1075 | val = int(res) | |
1076 | ok = (minvalue is None or val >= minvalue) and ( | |
1077 | maxvalue is None or val <= maxvalue) | |
1078 | except: | |
1079 | val = None | |
1080 | ok = False | |
1081 | ||
1082 | if ok: | |
1083 | self.value = val | |
1084 | return True | |
1085 | else: | |
1086 | self.value = None | |
1087 | msg = [_('an integer')] | |
1088 | conj = "" | |
1089 | if minvalue is not None: | |
1090 | msg.append(_("no less than {0}".format(minvalue))) | |
1091 | conj = _("and ") | |
1092 | if maxvalue is not None: | |
1093 | msg.append(_("{0}no greater than {0}").format(conj, maxvalue)) | |
1094 | msg.append(_("is required")) | |
1095 | self.error_message = "\n".join(msg) | |
1096 | return False | |
1097 | ||
1098 | ||
1099 | class GetDateTime(DialogWindow): | |
1100 | def validate(self): | |
1101 | res = self.entry.get() | |
1102 | logger.debug('res: {0}'.format(res)) | |
1103 | ok = False | |
1104 | if not res.strip(): | |
1105 | # return the current time if ok is pressed with no entry | |
1106 | val = get_current_time() | |
1107 | ok = True | |
1108 | else: | |
1109 | try: | |
1110 | # val = parse(parse_datetime(res)) | |
1111 | val = parse_dt(res) | |
1112 | ok = True | |
1113 | except: | |
1114 | val = None | |
1115 | if ok: | |
1116 | self.value = val | |
1117 | return True | |
1118 | else: | |
1119 | self.value = False | |
1120 | self.error_message = _('could not parse "{0}"').format(res) | |
1121 | return False | |
1122 | ||
1123 | ||
1124 | class GetString(DialogWindow): | |
1125 | def validate(self): | |
1126 | nullok = False | |
1127 | if 'nullok' in self.options and self.options['nullok']: | |
1128 | nullok = True | |
1129 | # an entry is required | |
1130 | val = self.entry.get() | |
1131 | if val.strip(): | |
1132 | self.value = val | |
1133 | return True | |
1134 | elif nullok: # null and null is ok | |
1135 | if self.value is None: | |
1136 | return False | |
1137 | else: | |
1138 | self.value = val | |
1139 | return True | |
1140 | else: | |
1141 | self.error_message = _('an entry is required') | |
1142 | return False | |
1143 | ||
1144 | def ok(self, event=None): | |
1145 | res = self.validate() | |
1146 | logger.debug('validate: {0}, value: "{1}"'.format(res, self.value)) | |
1147 | if not res: | |
1148 | if self.error_message: | |
1149 | self.messageWindow('error', self.error_message) | |
1150 | return "break" | |
1151 | if self.process: | |
1152 | res = self.process(self.value) | |
1153 | self.text.delete("1.0", END) | |
1154 | self.text.insert("1.0", res) | |
1155 | else: | |
1156 | self.withdraw() | |
1157 | self.update_idletasks() | |
1158 | self.apply() | |
1159 | self.quit() |
0 | #!/usr/bin/env python3 | |
1 | # -*- coding: utf-8 -*- | |
2 | from __future__ import (absolute_import, division, print_function, | |
3 | unicode_literals) | |
4 | ||
5 | import os | |
6 | import platform | |
7 | import codecs | |
8 | from datetime import datetime | |
9 | ||
10 | # from copy import deepcopy | |
11 | ||
12 | if platform.python_version() >= '3': | |
13 | import tkinter | |
14 | from tkinter import Entry, INSERT, END, Toplevel, Frame, LEFT, RIGHT, Text, StringVar, X, BOTH, Button, FLAT, Listbox | |
15 | from tkinter import ttk | |
16 | # from ttk import Button, Style | |
17 | from tkinter import font as tkFont | |
18 | unicode = str | |
19 | else: | |
20 | import Tkinter as tkinter | |
21 | from Tkinter import Entry, INSERT, END, Toplevel, Frame, LEFT, RIGHT, Text, StringVar, X, BOTH, Button, FLAT, Listbox | |
22 | import ttk | |
23 | import tkFont | |
24 | ||
25 | import string | |
26 | ID_CHARS = string.ascii_letters + string.digits + "_@/" | |
27 | ||
28 | import gettext | |
29 | _ = gettext.gettext | |
30 | ||
31 | import logging | |
32 | import logging.config | |
33 | logger = logging.getLogger() | |
34 | ||
35 | ||
36 | SOMEREPS = _('selected repetitions') | |
37 | ALLREPS = _('all repetitions') | |
38 | MESSAGES = _('Error messages') | |
39 | VALID = _("Valid entry") | |
40 | FOUND = "found" # for found text marking | |
41 | ||
42 | MAKE = _("Make") | |
43 | PRINT = _("Print") | |
44 | EXPORTTEXT = _("Export report in text format ...") | |
45 | EXPORTCSV = _("Export report in CSV format ...") | |
46 | SAVESPECS = _("Save changes to report specifications") | |
47 | CLOSE = _("Close") | |
48 | ||
49 | # VALID = _("Valid {0}").format(u"\u2714") | |
50 | SAVEANDEXIT = _("Save changes and exit?") | |
51 | UNCHANGEDEXIT = _("Item is unchanged. Exit?") | |
52 | CREATENEW = _("creating a new item") | |
53 | EDITEXISTING = _("editing an existing item") | |
54 | ||
55 | type2Text = { | |
56 | '$': _("In Basket item"), | |
57 | '^': _("Occasion"), | |
58 | '*': _("Event"), | |
59 | '~': _("Action"), | |
60 | '!': _("Note"), # undated only appear in folders | |
61 | '-': _("Task"), # for next view | |
62 | '+': _("Task Group"), # for next view | |
63 | '%': _("Delegated Task"), | |
64 | '?': _("Someday Maybe item"), | |
65 | '#': _("Hidden item") | |
66 | } | |
67 | ||
68 | from etmTk.data import hsh2str, str2hsh, get_reps, rrulefmt, ensureMonthly, commandShortcut, completion_regex, getFileTuples, fmt_shortdatetime, fmt_date, FINISH, uniqueId, import_ical | |
69 | ||
70 | from etmTk.dialog import BGCOLOR, OptionsDialog, ReadOnlyText, FileChoice | |
71 | ||
72 | ||
73 | class SimpleEditor(Toplevel): | |
74 | ||
75 | def __init__(self, parent=None, master=None, file=None, line=None, newhsh=None, rephsh=None, options=None, title=None, start=None, modified=False): | |
76 | """ | |
77 | If file is given, open file for editing. | |
78 | Otherwise, we are creating a new item and/or replacing an item | |
79 | mode: | |
80 | 1: new: edit newhsh, replace none | |
81 | 2: replace: edit and replace rephsh | |
82 | 3: new and replace: edit newhsh, replace rephsh | |
83 | ||
84 | :param parent: | |
85 | :param file: path to file to be edited | |
86 | """ | |
87 | # self.frame = frame = Frame(parent) | |
88 | if master is None: | |
89 | master = parent | |
90 | self.master = master | |
91 | Toplevel.__init__(self, master) | |
92 | self.minsize(400, 300) | |
93 | self.geometry('500x200') | |
94 | self.transient(parent) | |
95 | self.configure(background=BGCOLOR, highlightbackground=BGCOLOR) | |
96 | self.parent = parent | |
97 | self.loop = parent.loop | |
98 | self.messages = self.loop.messages | |
99 | self.messages = [] | |
100 | self.mode = None | |
101 | self.changed = False | |
102 | ||
103 | self.scrollbar = None | |
104 | self.listbox = None | |
105 | self.autocompletewindow = None | |
106 | self.line = None | |
107 | self.match = None | |
108 | ||
109 | self.file = file | |
110 | self.initfile = None | |
111 | self.fileinfo = None | |
112 | self.repinfo = None | |
113 | self.title = title | |
114 | self.edithsh = {} | |
115 | self.newhsh = newhsh | |
116 | self.rephsh = rephsh | |
117 | self.value = '' | |
118 | self.options = options | |
119 | self.tkfixedfont = tkFont.nametofont("TkFixedFont") | |
120 | self.tkfixedfont.configure(size=self.options['fontsize_fixed']) | |
121 | # self.text_value.trace_variable("w", self.setSaveStatus) | |
122 | frame = Frame(self, bd=0, relief=FLAT) | |
123 | frame.pack(side="bottom", fill=X, padx=4, pady=0) | |
124 | frame.configure(background=BGCOLOR, highlightbackground=BGCOLOR) | |
125 | ||
126 | # quit with a warning prompt if modified | |
127 | Button(frame, text=_("Cancel"), highlightbackground=BGCOLOR, pady=2, command=self.quit).pack(side=LEFT, padx=4) | |
128 | self.bind("<Escape>", self.quit) | |
129 | ||
130 | l, c = commandShortcut('q') | |
131 | self.bind(c, self.quit) | |
132 | self.bind("<Escape>", self.cancel) | |
133 | ||
134 | # finish will evaluate the item entry and, if repeating, show reps | |
135 | finish = Button(frame, text=FINISH, highlightbackground=BGCOLOR, command=self.onFinish, pady=2) | |
136 | # self.bind("<Control-w>", self.onCheck) | |
137 | self.bind("<Control-w>", self.onFinish) | |
138 | ||
139 | finish.pack(side=RIGHT, padx=4) | |
140 | ||
141 | # find | |
142 | Button(frame, text='x', command=self.clearFind, highlightbackground=BGCOLOR, padx=8, pady=2).pack(side=LEFT, padx=0) | |
143 | self.find_text = StringVar(frame) | |
144 | self.e = Entry(frame, textvariable=self.find_text, width=10, highlightbackground=BGCOLOR) | |
145 | self.e.pack(side=LEFT, padx=0, expand=1, fill=X) | |
146 | self.e.bind("<Return>", self.onFind) | |
147 | Button(frame, text='>', command=self.onFind, highlightbackground=BGCOLOR, padx=8, pady=2).pack(side=LEFT, padx=0) | |
148 | ||
149 | text = Text(self, wrap="word", bd=2, relief="sunken", padx=3, pady=2, font=self.tkfixedfont, undo=True, width=70) | |
150 | text.configure(highlightthickness=0) | |
151 | text.tag_configure(FOUND, background="lightskyblue") | |
152 | ||
153 | text.pack(side="bottom", padx=4, pady=3, expand=1, fill=BOTH) | |
154 | self.text = text | |
155 | ||
156 | self.completions = self.loop.options['completions'] | |
157 | ||
158 | if start is not None: | |
159 | # we have the starting text but will need a new uid | |
160 | text = start | |
161 | if self.rephsh is None: | |
162 | self.edithsh = {} | |
163 | self.mode = 1 | |
164 | self.title = CREATENEW | |
165 | else: | |
166 | self.edithsh = self.rephsh | |
167 | self.mode = 2 | |
168 | self.title = EDITEXISTING | |
169 | ||
170 | elif file is not None: | |
171 | # we're editing a file - if it's a data file we will add uid's | |
172 | # as necessary when saving | |
173 | self.mode = 'file' | |
174 | if not os.path.isfile(file): | |
175 | logger.warn('could not open: {0}'.format(file)) | |
176 | text = "" | |
177 | else: | |
178 | with codecs.open(file, 'r', self.options['encoding']['file']) as f: | |
179 | text = f.read() | |
180 | else: | |
181 | # we are creating a new item and/or replacing an item | |
182 | # mode: | |
183 | # 1: new | |
184 | # 2: replace | |
185 | # 3: new and replace | |
186 | initfile = ensureMonthly(options=self.options, date=datetime.now()) | |
187 | # set the mode | |
188 | if newhsh is None and rephsh is None: | |
189 | # we are creating a new item from scratch and will need | |
190 | # a new uid | |
191 | self.mode = 1 | |
192 | self.title = CREATENEW | |
193 | self.edithsh = {} | |
194 | self.edithsh['i'] = uniqueId() | |
195 | text = '' | |
196 | elif rephsh is None: # newhsh is not None | |
197 | # we are creating a new item as a copy and will need | |
198 | # a new uid | |
199 | self.mode = 1 | |
200 | self.title = CREATENEW | |
201 | self.edithsh = self.newhsh | |
202 | self.edithsh['i'] = uniqueId() | |
203 | if ('fileinfo' in newhsh and newhsh['fileinfo']): | |
204 | initfile = newhsh['fileinfo'][0] | |
205 | text, msg = hsh2str(self.edithsh, self.options) | |
206 | elif newhsh is None: | |
207 | # we are editing and replacing rephsh - no file prompt | |
208 | # using existing uid | |
209 | self.title = EDITEXISTING | |
210 | self.mode = 2 | |
211 | # self.repinfo = rephsh['fileinfo'] | |
212 | self.edithsh = self.rephsh | |
213 | text, msg = hsh2str(self.edithsh, self.options) | |
214 | else: # neither is None | |
215 | # we are changing some instances of a repeating item | |
216 | # we will be writing but not editing rephsh using its fileinfo | |
217 | # and its existing uid | |
218 | # we will be editing and saving newhsh using self.initfile | |
219 | # we will need a new uid for newhsh | |
220 | self.mode = 3 | |
221 | self.title = CREATENEW | |
222 | self.edithsh = self.newhsh | |
223 | self.edithsh['i'] = uniqueId() | |
224 | if 'fileinfo' in newhsh and newhsh['fileinfo'][0]: | |
225 | initfile = self.newhsh['fileinfo'][0] | |
226 | text, msg = hsh2str(self.edithsh, self.options) | |
227 | self.initfile = initfile | |
228 | logger.debug('mode: {0}; initfile: {1}; edit: {2}'.format(self.mode, self.initfile, self.edithsh)) | |
229 | if self.title is not None: | |
230 | self.wm_title(self.title) | |
231 | self.settext(text) | |
232 | ||
233 | # clear the undo buffer | |
234 | if not modified: | |
235 | self.text.edit_reset() | |
236 | self.setmodified(False) | |
237 | self.text.bind('<<Modified>>', self.updateSaveStatus) | |
238 | ||
239 | self.text.focus_set() | |
240 | self.protocol("WM_DELETE_WINDOW", self.quit) | |
241 | if parent: | |
242 | self.geometry("+%d+%d" % (parent.winfo_rootx() + 50, | |
243 | parent.winfo_rooty() + 50)) | |
244 | self.configure(background=BGCOLOR) | |
245 | l, c = commandShortcut('f') | |
246 | self.bind(c, lambda e: self.e.focus_set()) | |
247 | l, c = commandShortcut('g') | |
248 | self.bind(c, lambda e: self.onFind()) | |
249 | if start: | |
250 | self.text.tag_add("sel", "1.1", "1.{0}".format(len(start))) | |
251 | self.text.mark_set(INSERT, END) | |
252 | elif line: | |
253 | self.text.mark_set(INSERT, "{0}.0".format(line)) | |
254 | else: | |
255 | self.text.mark_set(INSERT, END) | |
256 | self.text.see(INSERT) | |
257 | # l, c = commandShortcut('/') | |
258 | logger.debug("/: {0}, {1}".format(l, c)) | |
259 | self.text.bind("<Control-space>", self.showCompletions) | |
260 | self.grab_set() | |
261 | self.wait_window(self) | |
262 | ||
263 | def settext(self, text=''): | |
264 | self.text.delete('1.0', END) | |
265 | self.text.insert(INSERT, text) | |
266 | self.text.mark_set(INSERT, '1.0') | |
267 | self.text.focus() | |
268 | logger.debug("modified: {0}".format(self.checkmodified())) | |
269 | ||
270 | def gettext(self): | |
271 | return self.text.get('1.0', END + '-1c') | |
272 | ||
273 | def setCompletions(self, *args): | |
274 | match = self.filterValue.get() | |
275 | self.matches = matches = [x for x in self.completions if x and x.lower().startswith(match.lower())] | |
276 | self.listbox.delete(0, END) | |
277 | for item in matches: | |
278 | self.listbox.insert(END, item) | |
279 | self.listbox.select_set(0) | |
280 | self.listbox.see(0) | |
281 | self.fltr.focus_set() | |
282 | ||
283 | def showCompletions(self, e=None): | |
284 | if not self.completions: | |
285 | return "break" | |
286 | if self.autocompletewindow: | |
287 | return "break" | |
288 | line = self.text.get("insert linestart", INSERT) | |
289 | m = completion_regex.search(line) | |
290 | if not m: | |
291 | logger.debug("no match in {0}".format(line)) | |
292 | return "break" | |
293 | ||
294 | # set self.match here since it determines the characters to be replaced | |
295 | self.match = match = m.groups()[0] | |
296 | logger.debug("found match '{0}' in line '{1}'".format(match, line)) | |
297 | ||
298 | self.autocompletewindow = acw = Toplevel(master=self.text) | |
299 | acw.geometry("+%d+%d" % (self.text.winfo_rootx() + 50, self.text.winfo_rooty() + 50)) | |
300 | ||
301 | self.autocompletewindow.wm_attributes("-topmost", 1) | |
302 | ||
303 | self.filterValue = StringVar(self) | |
304 | self.filterValue.set(match) | |
305 | self.filterValue.trace_variable("w", self.setCompletions) | |
306 | self.fltr = Entry(acw, textvariable=self.filterValue) | |
307 | self.fltr.pack(side="top", fill="x") | |
308 | self.fltr.icursor(END) | |
309 | ||
310 | self.listbox = listbox = Listbox(acw, exportselection=False, width=self.loop.options['completions_width']) | |
311 | listbox.pack(side="bottom", fill=BOTH, expand=True) | |
312 | ||
313 | self.autocompletewindow.bind("<Double-1>", self.completionSelected) | |
314 | self.autocompletewindow.bind("<Return>", self.completionSelected) | |
315 | self.autocompletewindow.bind("<Escape>", self.hideCompletions) | |
316 | self.autocompletewindow.bind("<Up>", self.cursorUp) | |
317 | self.autocompletewindow.bind("<Down>", self.cursorDown) | |
318 | self.fltr.bind("<Up>", self.cursorUp) | |
319 | self.fltr.bind("<Down>", self.cursorDown) | |
320 | self.setCompletions() | |
321 | ||
322 | def is_active(self): | |
323 | return self.autocompletewindow is not None | |
324 | ||
325 | def hideCompletions(self, e=None): | |
326 | if not self.is_active(): | |
327 | return | |
328 | # destroy widgets | |
329 | self.listbox.destroy() | |
330 | self.listbox = None | |
331 | self.autocompletewindow.destroy() | |
332 | self.autocompletewindow = None | |
333 | ||
334 | def completionSelected(self, event): | |
335 | # Put the selected completion in the text, and close the list | |
336 | modified = False | |
337 | if self.matches: | |
338 | cursel = self.matches[int(self.listbox.curselection()[0])] | |
339 | else: | |
340 | cursel = self.filterValue.get() | |
341 | modified = True | |
342 | ||
343 | start = "insert-{0}c".format(len(self.match)) | |
344 | end = "insert-1c wordend" | |
345 | logger.debug("cursel: {0}; match: {1}; start: {2}; insert: {3}".format( | |
346 | cursel, self.match, start, INSERT)) | |
347 | self.text.delete(start, end) | |
348 | self.text.insert(INSERT, cursel) | |
349 | self.hideCompletions() | |
350 | if modified: | |
351 | file = FileChoice(self, "append completion to file", prefix=self.loop.options['etmdir'], list=self.loop.options['completion_files']).returnValue() | |
352 | if (file and os.path.isfile(file)): | |
353 | with codecs.open(file, 'r', self.loop.options['encoding']['file']) as fo: | |
354 | lines = fo.readlines() | |
355 | lines.append(cursel) | |
356 | lines.sort() | |
357 | content = "\n".join([x.strip() for x in lines if x.strip()]) | |
358 | with codecs.open(file, 'w', self.loop.options['encoding']['file']) as fo: | |
359 | fo.write(content) | |
360 | self.completions.append(cursel) | |
361 | self.completions.sort() | |
362 | ||
363 | def cursorUp(self, event=None): | |
364 | cursel = int(self.listbox.curselection()[0]) | |
365 | # newsel = max(0, cursel=1) | |
366 | newsel = max(0, cursel - 1) | |
367 | self.listbox.select_clear(cursel) | |
368 | self.listbox.select_set(newsel) | |
369 | self.listbox.see(newsel) | |
370 | return "break" | |
371 | ||
372 | def cursorDown(self, event=None): | |
373 | cursel = int(self.listbox.curselection()[0]) | |
374 | newsel = min(len(self.matches) - 1, cursel + 1) | |
375 | self.listbox.select_clear(cursel) | |
376 | self.listbox.select_set(newsel) | |
377 | self.listbox.see(newsel) | |
378 | return "break" | |
379 | ||
380 | def setmodified(self, bool): | |
381 | if bool is not None: | |
382 | self.text.edit_modified(bool) | |
383 | ||
384 | def checkmodified(self): | |
385 | return self.text.edit_modified() | |
386 | ||
387 | def updateSaveStatus(self, event=None): | |
388 | # Called by <<Modified>> | |
389 | if self.checkmodified(): | |
390 | self.wm_title("{0} (modified)".format(self.title)) | |
391 | else: | |
392 | self.wm_title("{0}".format(self.title)) | |
393 | ||
394 | def onFinish(self, e=None): | |
395 | if self.mode == 'file': | |
396 | self.onSave() | |
397 | else: | |
398 | self.onCheck() | |
399 | ||
400 | def onSave(self, e=None, v=0): | |
401 | if not self.checkmodified(): | |
402 | self.quit() | |
403 | elif self.file is not None: | |
404 | # we are editing a file | |
405 | alltext = self.gettext() | |
406 | self.loop.safe_save(self.file, alltext) | |
407 | self.setmodified(False) | |
408 | self.changed = True | |
409 | self.quit() | |
410 | else: | |
411 | # we are editing an item | |
412 | if self.mode in [1, 3]: # new | |
413 | dir = self.options['datadir'] | |
414 | if 's' in self.edithsh and self.edithsh['s']: | |
415 | dt = self.edithsh['s'] | |
416 | file = ensureMonthly(self.options, dt.date()) | |
417 | else: | |
418 | dt = None | |
419 | file = ensureMonthly(self.options) | |
420 | dir, initfile = os.path.split(file) | |
421 | # we need a filename for the new item | |
422 | # make datadir the root | |
423 | prefix, tuples = getFileTuples(self.options['datadir'], include=r'*.txt') | |
424 | if v == 2: | |
425 | filename = file | |
426 | else: | |
427 | ret = FileChoice(self, "etm data files", prefix=prefix, list=tuples, start=file).returnValue() | |
428 | if not ret: | |
429 | return False | |
430 | filename = os.path.join(prefix, ret) | |
431 | if not os.path.isfile(filename): | |
432 | return False | |
433 | filename = os.path.normpath(filename) | |
434 | logger.debug('saving to: {0}'.format(filename)) | |
435 | self.text.focus_set() | |
436 | logger.debug('edithsh: {0}'.format(self.edithsh)) | |
437 | if self.mode == 1: | |
438 | if self.loop.append_item(self.edithsh, filename): | |
439 | logger.debug('append mode: {0}'.format(self.mode)) | |
440 | elif self.mode == 2: | |
441 | if self.loop.replace_item(self.edithsh): | |
442 | logger.debug('replace mode: {0}'.format(self.mode)) | |
443 | else: # self.mode == 3 | |
444 | if self.loop.append_item(self.edithsh, filename): | |
445 | logger.debug('append mode: {0}'.format(self.mode)) | |
446 | if self.loop.replace_item(self.rephsh): | |
447 | logger.debug('replace mode: {0}'.format(self.mode)) | |
448 | ||
449 | # update the return value so that when it is not null then modified | |
450 | # is false and when modified is true then it is null | |
451 | self.setmodified(False) | |
452 | self.changed = True | |
453 | self.quit() | |
454 | return "break" | |
455 | ||
456 | def onCheck(self, event=None, showreps=True, showres=True): | |
457 | # only called when editing an item and finish is pressed | |
458 | self.loop.messages = [] | |
459 | text = self.gettext() | |
460 | msg = [] | |
461 | reps = [] | |
462 | if text.startswith("BEGIN:VCALENDAR"): | |
463 | text = import_ical(vcal=text) | |
464 | logger.debug("text: {0} '{01}'".format(type(text), text)) | |
465 | if self.edithsh and 'i' in self.edithsh: | |
466 | uid = self.edithsh['i'] | |
467 | else: | |
468 | uid = None | |
469 | hsh, msg = str2hsh(text, options=self.options, uid=uid) | |
470 | ||
471 | if not msg: | |
472 | # we have a good hsh | |
473 | pre = post = "" | |
474 | if 'r' in hsh: | |
475 | pre = _("Repeating ") | |
476 | elif 's' in hsh: | |
477 | dt = hsh['s'] | |
478 | if not dt.hour and not dt.minute: | |
479 | dtfmt = fmt_date(dt, short=True) | |
480 | else: | |
481 | dtfmt = fmt_shortdatetime(hsh['s'], self.options) | |
482 | post = _(" scheduled for {0}").format(dtfmt) | |
483 | else: # unscheduled | |
484 | pre = _("Unscheduled ") | |
485 | ||
486 | prompt = "{0}{1}{2}".format(pre, type2Text[hsh['itemtype']], post) | |
487 | ||
488 | if self.edithsh and 'fileinfo' in self.edithsh: | |
489 | fileinfo = self.edithsh['fileinfo'] | |
490 | self.edithsh = hsh | |
491 | self.edithsh['fileinfo'] = fileinfo | |
492 | else: | |
493 | # we have a new item without fileinfo | |
494 | self.edithsh = hsh | |
495 | # update missing fields | |
496 | logger.debug('calling hsh2str with {0}'.format(hsh)) | |
497 | str, msg = hsh2str(hsh, options=self.options) | |
498 | ||
499 | self.loop.messages.extend(msg) | |
500 | if self.loop.messages: | |
501 | messages = "{0}".format("\n".join(self.loop.messages)) | |
502 | logger.debug("messages: {0}".format(messages)) | |
503 | self.messageWindow(MESSAGES, messages, opts=self.options) | |
504 | return False | |
505 | ||
506 | logger.debug("back from hsh2str with: {0}".format(str)) | |
507 | if 'r' in hsh: | |
508 | showing_all, reps = get_reps(self.loop.options['bef'], hsh) | |
509 | if reps: | |
510 | if showreps: | |
511 | try: | |
512 | repsfmt = [unicode(x.strftime(rrulefmt)) for x in reps] | |
513 | except: | |
514 | repsfmt = [unicode(x.strftime("%X %x")) for x in reps] | |
515 | logger.debug("{0}: {1}".format(showing_all, repsfmt)) | |
516 | if showing_all: | |
517 | reps = ALLREPS | |
518 | else: | |
519 | reps = SOMEREPS | |
520 | prompt = "{0}, {1}:\n {2}".format(prompt, reps, "\n ".join(repsfmt)) | |
521 | # self.messageWindow(VALID, repetitions, opts=self.options) | |
522 | else: | |
523 | repetitions = "No repetitions were generated." | |
524 | self.loop.messages.append(repetitions) | |
525 | if self.loop.messages: | |
526 | messages = "{0}".format("\n".join(self.loop.messages)) | |
527 | logger.debug("messages: {0}".format(messages)) | |
528 | self.messageWindow(MESSAGES, messages, opts=self.options) | |
529 | return False | |
530 | ||
531 | if self.checkmodified(): | |
532 | prompt += "\n\n{0}".format(SAVEANDEXIT) | |
533 | else: | |
534 | prompt += "\n\n{0}".format(UNCHANGEDEXIT) | |
535 | ||
536 | if str != text: | |
537 | self.settext(str) | |
538 | ans, value = OptionsDialog(parent=self, title=self.title, prompt=prompt, yesno=False, list=True).getValue() | |
539 | if ans: | |
540 | self.onSave(v=value) | |
541 | return | |
542 | ||
543 | def clearFind(self, *args): | |
544 | self.text.tag_remove(FOUND, "0.0", END) | |
545 | self.find_text.set("") | |
546 | ||
547 | def onFind(self, *args): | |
548 | target = self.find_text.get() | |
549 | logger.debug('target: {0}'.format(target)) | |
550 | if target: | |
551 | where = self.text.search(target, INSERT, nocase=1) | |
552 | if where: | |
553 | pastit = where + ('+%dc' % len(target)) | |
554 | # self.text.tag_remove(SEL, '1.0', END) | |
555 | self.text.tag_add(FOUND, where, pastit) | |
556 | self.text.mark_set(INSERT, pastit) | |
557 | self.text.see(INSERT) | |
558 | self.text.focus() | |
559 | ||
560 | def cancel(self, e=None): | |
561 | t = self.find_text.get() | |
562 | if t.strip(): | |
563 | self.clearFind() | |
564 | return "break" | |
565 | if self.autocompletewindow: | |
566 | self.hideCompletions() | |
567 | return "break" | |
568 | if self.text.tag_ranges("sel"): | |
569 | self.text.tag_remove('sel', "1.0", END) | |
570 | return | |
571 | # if self.checkmodified(): | |
572 | # return "break" | |
573 | logger.debug(('calling quit')) | |
574 | self.quit() | |
575 | ||
576 | def quit(self, e=None): | |
577 | if self.checkmodified(): | |
578 | ans = self.confirm(parent=self, title=_('Quit'), prompt=_("There are unsaved changes.\nDo you really want to quit?")) | |
579 | else: | |
580 | ans = True | |
581 | if ans: | |
582 | if self.master: | |
583 | logger.debug('setting focus') | |
584 | self.master.focus() | |
585 | self.master.focus_set() | |
586 | logger.debug('focus set') | |
587 | self.destroy() | |
588 | logger.debug('done') | |
589 | ||
590 | def messageWindow(self, title, prompt, opts=None, height=8, width=52): | |
591 | win = Toplevel(self) | |
592 | win.title(title) | |
593 | win.geometry("+%d+%d" % (self.text.winfo_rootx() + 50, self.text.winfo_rooty() + 50)) | |
594 | f = Frame(win) | |
595 | # pack the button first so that it doesn't disappear with resizing | |
596 | b = Button(win, text=_('OK'), width=10, command=win.destroy, default='active', pady=2) | |
597 | b.pack(side='bottom', fill=tkinter.NONE, expand=0, pady=0) | |
598 | win.bind('<Return>', (lambda e, b=b: b.invoke())) | |
599 | win.bind('<Escape>', (lambda e, b=b: b.invoke())) | |
600 | tkfixedfont = tkFont.nametofont("TkFixedFont") | |
601 | if 'fontsize_fixed' in self.loop.options and self.loop.options['fontsize_fixed']: | |
602 | tkfixedfont.configure(size=self.loop.options['fontsize_fixed']) | |
603 | ||
604 | t = ReadOnlyText( | |
605 | f, wrap="word", padx=2, pady=2, bd=2, relief="sunken", | |
606 | font=tkfixedfont, | |
607 | height=height, | |
608 | width=width, | |
609 | takefocus=False) | |
610 | t.insert("0.0", prompt) | |
611 | t.pack(side='left', fill=tkinter.BOTH, expand=1, padx=0, pady=0) | |
612 | if height > 1: | |
613 | ysb = ttk.Scrollbar(f, orient='vertical', command=t.yview) | |
614 | ysb.pack(side='right', fill=tkinter.Y, expand=0, padx=0, pady=0) | |
615 | t.configure(state="disabled", yscroll=ysb.set) | |
616 | t.configure(yscroll=ysb.set) | |
617 | f.pack(padx=2, pady=2, fill=tkinter.BOTH, expand=1) | |
618 | ||
619 | win.focus_set() | |
620 | win.grab_set() | |
621 | win.transient(self) | |
622 | win.wait_window(win) | |
623 | ||
624 | def confirm(self, parent=None, title="", prompt="", instance="xyz"): | |
625 | ok, value = OptionsDialog(parent=parent, title=_("confirm").format(instance), prompt=prompt).getValue() | |
626 | return ok | |
627 | ||
628 | ||
629 | if __name__ == '__main__': | |
630 | print('edit.py should only be imported. Run etm or view.py instead.') |
0 | .\" Text automatically generated by txt2man | |
1 | .TH etm 1 "15 September 2014" "version 3.0.40" "Unix user's manual" | |
2 | .SH NAME | |
3 | \fBetm \fP- manage events and tasks using simple text files | |
4 | .SH SYNOPSIS | |
5 | .nf | |
6 | .fam C | |
7 | \fBetm\fP [\fIlogging\fP \fIlevel\fP] [\fIpath\fP] [?] [\fIacmsv\fP] | |
8 | .fam T | |
9 | .fi | |
10 | .fam T | |
11 | .fi | |
12 | .SH DESCRIPTION | |
13 | With no arguments, \fBetm\fP will use settings from the configuration file | |
14 | ~/.etm/etmtk.cfg, set \fIlogging\fP \fIlevel\fP 3 (warn) and open the GUI. | |
15 | .PP | |
16 | if the first argument is an integer not less than 1 (debug) and not | |
17 | greater than 5 (critical), then \fBetm\fP will use that \fIlogging\fP \fIlevel\fP | |
18 | and remove the argument. | |
19 | .PP | |
20 | If the first (remaining) argument is the \fIpath\fP to a directory which | |
21 | contains a file named etm.cfg, then \fBetm\fP will use that configuration file | |
22 | and remove the argument. | |
23 | .PP | |
24 | If the first (remaining) argument is one of the commands listed below, | |
25 | then \fBetm\fP will execute the remaining arguments without opening the GUI. | |
26 | .PP | |
27 | .nf | |
28 | .fam C | |
29 | a ARG display the agenda view using ARG, if given, as a filter. | |
30 | k ARG display the keywords view using ARG, if given, as a filter. | |
31 | n ARGS Create a new item using the remaining arguments | |
32 | as the item specification. | |
33 | m INT display a report using the remaining argument, which must be a | |
34 | positive integer, to display a report using the corresponding | |
35 | entry from the file given by report_specifications in etmtk.cfg. | |
36 | Use ? m to display the numbered list of entries from this file. | |
37 | p ARG display the path view using ARG, if given, as a filter. | |
38 | r ARGS display a report using the remaining arguments as the | |
39 | report specification. | |
40 | s ARG display the schedule view using ARG, if given, as a filter. | |
41 | t ARG display the tags view using ARG, if given, as a filter. | |
42 | v display information about etm and the operating system. | |
43 | ? ARGS display (this) command line help information if ARGS = '' or, | |
44 | if ARGS = X where X is one of the above commands, then display | |
45 | details about command X. 'X ?' is equivalent to '? X'.\ | |
46 | ||
47 | .fam T | |
48 | .fi | |
49 | .SH EXAMPLES | |
50 | .SS COMMAND LINE | |
51 | Group items by year, month and day together | |
52 | .PP | |
53 | .nf | |
54 | .fam C | |
55 | etm r c ddd, MMM d yyyy | |
56 | ||
57 | .fam T | |
58 | .fi | |
59 | Output: | |
60 | .PP | |
61 | .nf | |
62 | .fam C | |
63 | Fri, Apr 1 2011 | |
64 | items for April 1 | |
65 | Sat, Apr 2 2011 | |
66 | items for April 2 | |
67 | \.\.\. | |
68 | .fam T | |
69 | .fi | |
70 | .SS DATA FILES | |
71 | Data items begin with a data type character and continue on one or more lines either until the end of the file is reached or another line is found that begins with a type character. Data type characters and the associated data types: | |
72 | .TP | |
73 | .B | |
74 | \%~ | |
75 | Action: a record of time and/or money spent. | |
76 | .TP | |
77 | .B | |
78 | \%* | |
79 | Event: happens on a particular date and time. | |
80 | .TP | |
81 | .B | |
82 | \%^ | |
83 | Occasion: happens on a particular date, e.g., a holiday, | |
84 | anniversary or birthday. | |
85 | .TP | |
86 | .B | |
87 | \%! | |
88 | Note: a record of some useful information. | |
89 | .TP | |
90 | .B | |
91 | \%\- | |
92 | Task: something that needs to be done. | |
93 | .TP | |
94 | .B | |
95 | \%% | |
96 | Delegated task: a task assigned to someone else. | |
97 | .TP | |
98 | .B | |
99 | \%+ | |
100 | Task group: a group of related tasks, some of which may be | |
101 | prerequisites for others. | |
102 | .TP | |
103 | .B | |
104 | \%$ | |
105 | Inbasket: quick entry to be edited later when time permits. | |
106 | .TP | |
107 | .B | |
108 | \%? | |
109 | Someday maybe: remember but don't show in the common views. | |
110 | .TP | |
111 | .B | |
112 | \%# | |
113 | Hidden: remember but hide from all \fBetm\fP views except \fIpath\fP view. | |
114 | .TP | |
115 | .B | |
116 | \%= | |
117 | Defaults: set default options for subsequent entries in the | |
118 | same data file. | |
119 | .PP | |
120 | The beginning data type character for each item is followed by the item summary and then, perhaps, by one or more '@key value' option pairs. Examples: | |
121 | .IP \(bu 3 | |
122 | A sales meeting (an event) a week from today from 9:00am until 10:00am with a 5 minute early warning alert: | |
123 | .PP | |
124 | .nf | |
125 | .fam C | |
126 | \%* sales meeting @s +7 9a @e 1h @a 5 | |
127 | ||
128 | .fam T | |
129 | .fi | |
130 | .IP \(bu 3 | |
131 | Prepare a report (a task) for the meeting beginning 3 days early: | |
132 | .PP | |
133 | .nf | |
134 | .fam C | |
135 | \%\- prepare report @s +7 @b 3 | |
136 | ||
137 | .fam T | |
138 | .fi | |
139 | .IP \(bu 3 | |
140 | A 35 minute period (an action) spent working on the report yesterday: | |
141 | .PP | |
142 | .nf | |
143 | .fam C | |
144 | \%~ report preparation @s \-1 @e 35 | |
145 | ||
146 | .fam T | |
147 | .fi | |
148 | .IP \(bu 3 | |
149 | Get a haircut (a task) on the 24th of the current month and then [r]epeatedly at (d)aily [i]ntervals of 14 days and, [o]n completion, (r)estart from the completion date: | |
150 | .PP | |
151 | .nf | |
152 | .fam C | |
153 | \%\- get haircut @s 24 @r d &i 14 @o r | |
154 | ||
155 | .fam T | |
156 | .fi | |
157 | .IP \(bu 3 | |
158 | Do the jobs in the following task group in 'q' order to finish the dog house project: | |
159 | .PP | |
160 | .nf | |
161 | .fam C | |
162 | \%+ dog house | |
163 | @j pickup lumber and paint &q 1 | |
164 | @j cut pieces &q 2 | |
165 | @j assemble &q 3 | |
166 | @j paint &q 4 | |
167 | ||
168 | .fam T | |
169 | .fi | |
170 | .IP \(bu 3 | |
171 | Payday (an occassion) on the last week day of each month. The '&s' part of the entry extracts the last date which is both a weekday and falls within the last three days of the month.): | |
172 | .PP | |
173 | .nf | |
174 | .fam C | |
175 | \%^ payday @s 1/1 @r m &w (MO, TU, WE, TH, FR) &m (\-1, \-2, \-3) &s \-1 | |
176 | ||
177 | .fam T | |
178 | .fi | |
179 | .IP \(bu 3 | |
180 | Take a prescribed medication daily (a reminder) for the next three days at 10am, 2pm, 6pm and 10pm and trigger the default alert zero minutes before each event: | |
181 | .PP | |
182 | .nf | |
183 | .fam C | |
184 | \%* take Rx @s +0 @r d &h 10, 14, 18, 22 &u +4 @a 0 | |
185 | ||
186 | .fam T | |
187 | .fi | |
188 | .IP \(bu 3 | |
189 | Presidential election day (an occassion) every four years on the first Tuesday after a Monday in November: | |
190 | .PP | |
191 | .nf | |
192 | .fam C | |
193 | \%^ Presidential Election Day @s 2012-11-06 | |
194 | @r y &i 4 &M 11 &m range(2,9) &w TU | |
195 | ||
196 | .fam T | |
197 | .fi | |
198 | .IP \(bu 3 | |
199 | Join the \fBetm\fP discussion group (a task). Because of the @g (goto) link, pressing Ctrl-G when the details of this item are displayed in the gui would open the link using the system default application: | |
200 | .PP | |
201 | .nf | |
202 | .fam C | |
203 | \%\- join the etm discussion group | |
204 | @g http://groups.google.com/group/eventandtaskmanager/topics | |
205 | .fam T | |
206 | .fi | |
207 | .SH SEE ALSO | |
208 | Extensive documentation can be found in the folder: | |
209 | .PP | |
210 | .nf | |
211 | .fam C | |
212 | http://people.duke.edu/~dgraham/etmtk/help/ | |
213 | .fam T | |
214 | .fi | |
215 | .SH BUGS | |
216 | Please report bugs to the \fBetm\fP discussion group: | |
217 | .PP | |
218 | .nf | |
219 | .fam C | |
220 | http://groups.google.com/forum/#!forum/eventandtaskmanager | |
221 | .fam T | |
222 | .fi | |
223 | .SH AUTHOR | |
224 | Daniel A Graham <daniel.graham@duke.edu> | |
225 | .SH COPYRIGHT | |
226 | Copyright (c) 2009-2014 [Daniel Graham]. All rights reserved. |
0 | <?xml version="1.0" encoding="UTF-8"?> | |
1 | <!-- Copyright 2014 Daniel Graham <daniel.graham@duke.edu> --> | |
2 | <application> | |
3 | <id type="desktop">etm.desktop</id> | |
4 | <metadata_license>CC0-1.0</metadata_license> | |
5 | <project_license>GPL-2.0+ and GFDL-1.3</project_license> | |
6 | <name>Event and task manager</name> | |
7 | <summary>Manage events and tasks using simple text files | |
8 | </summary> | |
9 | <description> | |
10 | <p>Examples:</p> | |
11 | <ul> | |
12 | <li>A sales meeting (an event) [s]tarting seven days from today at 9:00am and [e]xtending for one hour with a default [a]lert 5 minutes before the start: | |
13 | <pre>* sales meeting @s +7 9a @e 1h @a 5</pre> | |
14 | </li> | |
15 | <li>Get a haircut (a task) on the 24th of the current month and then [r]epeatedly at (d)aily [i]ntervals of (14) days and, [o]n completion, (r)estart from the completion date: | |
16 | <pre>- get haircut @s 24 @r d &i 14 @o r</pre> | |
17 | </li> | |
18 | <li>Presidential election day (an occasion) every four years on the first Tuesday after a Monday in November: | |
19 | <pre>^ Presidential Election Day @s 2012-11-06 | |
20 | @r y &i 4 &M 11 &m 2, 3, 4, 5, 6, 7, 8 &w TU</pre> | |
21 | </li> | |
22 | </ul> | |
23 | </description> | |
24 | <screenshots> | |
25 | <screenshot type="default" width="800" height="600">http://people.duke.edu/~dgraham/etmtk/images/agenda.gif</screenshot> | |
26 | <screenshot type="default" width="800" height="600">http://people.duke.edu/~dgraham/etmtk/images/day.gif</screenshot> | |
27 | <screenshot type="default" width="800" height="600">http://people.duke.edu/~dgraham/etmtk/images/week.gif</screenshot> | |
28 | <screenshot type="default" width="800" height="600">http://people.duke.edu/~dgraham/etmtk/images/monthly.gif</screenshot> | |
29 | </screenshots> | |
30 | <url type="homepage">http://people.duke.edu/~dgraham/etmtk/</url> | |
31 | <updatecontact>daniel.graham_at_duke.edu</updatecontact> | |
32 | </application> |
0 | [Desktop Entry] | |
1 | Type=Application | |
2 | Name=Event and task manager | |
3 | Comment=Manage events and tasks using simple text files | |
4 | Exec=etm | |
5 | Icon=etmtk | |
6 | Categories=Office;Calendar;Clock; | |
7 | Keywords=Calendar;Task;Event;Alarm;Reminder;Todo;Time;Note; |
0 | /* XPM */ | |
1 | static char *etmlogo____x___x__[] = { | |
2 | /* columns rows colors chars-per-pixel */ | |
3 | "128 128 66 1", | |
4 | " c #134157", | |
5 | ". c #13445A", | |
6 | "X c #154B64", | |
7 | "o c #164E68", | |
8 | "O c #17516B", | |
9 | "+ c #18536E", | |
10 | "@ c #195672", | |
11 | "# c #195875", | |
12 | "$ c #1A5C7B", | |
13 | "% c #1B607F", | |
14 | "& c #1C6282", | |
15 | "* c #1D6689", | |
16 | "= c #1F6B8E", | |
17 | "- c #1F6D91", | |
18 | "; c #206E93", | |
19 | ": c #207196", | |
20 | "> c #22759C", | |
21 | ", c #237BA3", | |
22 | "< c #247FA9", | |
23 | "1 c #2582AD", | |
24 | "2 c #2880A9", | |
25 | "3 c #2685B1", | |
26 | "4 c #2B86B0", | |
27 | "5 c #2789B6", | |
28 | "6 c #2A89B7", | |
29 | "7 c #288CBB", | |
30 | "8 c #2D90BE", | |
31 | "9 c #2B93C4", | |
32 | "0 c #2B97C9", | |
33 | "q c #2C99CC", | |
34 | "w c #3D9AC5", | |
35 | "e c #339BCB", | |
36 | "r c #2D9DD2", | |
37 | "t c #329FD1", | |
38 | "y c #2EA1D6", | |
39 | "u c #2FA3D9", | |
40 | "i c #35A1D5", | |
41 | "p c #3AA2D2", | |
42 | "a c #30A7DE", | |
43 | "s c #30A8DF", | |
44 | "d c #3DA9DC", | |
45 | "f c #31A9E1", | |
46 | "g c #3BADE2", | |
47 | "h c #40A6D6", | |
48 | "j c #49AEDD", | |
49 | "k c #4BB0DE", | |
50 | "l c #41AFE3", | |
51 | "z c #45B1E3", | |
52 | "x c #4CB4E4", | |
53 | "c c #51B6E5", | |
54 | "v c #56B8E6", | |
55 | "b c #5BBBE7", | |
56 | "n c #62BDE7", | |
57 | "m c #64BEE8", | |
58 | "M c #67C0E9", | |
59 | "N c #6BC1E9", | |
60 | "B c #73C4EA", | |
61 | "V c #79C7EB", | |
62 | "C c #7DC9EC", | |
63 | "Z c #83CBEC", | |
64 | "A c #8BCEEE", | |
65 | "S c #8ED0EE", | |
66 | "D c #92D2EE", | |
67 | "F c #98D4EF", | |
68 | "G c #96D3F0", | |
69 | "H c None", | |
70 | /* pixels */ | |
71 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
72 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
73 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
74 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
75 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
76 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
77 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
78 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
79 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
80 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
81 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
82 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
83 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
84 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
85 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
86 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
87 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
88 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
89 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
90 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
91 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
92 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
93 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
94 | "HHAZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZnHHHSZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZCbHHHAZZZZZZZZZZZVcHHHHHHHHHHDAZZZZZZZZZZZZZZZZZZZZZZZZmHH", | |
95 | "HBNvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvp1HHNbvvvvvvvvvvvvvvvvvvvvvvvvvvvvvceHHBNvvvvvvvvvvvxeHHHHHHHHHHVnvvvvvvvvvvvvvvvvvvvvvvvvp3H", | |
96 | "Hbxfffffffffffffffffffffffffffffffa7-HHcffffffffffffffffffffffffffffffa3HHbxfffffffffffa5HHHHHHHHHHbgfffffffffffffffffffffffs7-H", | |
97 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxfffffffffffa7HHHHHHHHHHbgfffffffffffffffffffffffa7-H", | |
98 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxfffffffffffa9,HHHHHHHHHvgfffffffffffffffffffffffa7-H", | |
99 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxffffffffffff0,HHHHHHHHMcffffffffffffffffffffffffa7-H", | |
100 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxffffffffffffr<HHHHHHHHmxffffffffffffffffffffffffa7-H", | |
101 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxffffffffffffy1HHHHHHHHmxffffffffffffffffffffffffa7-H", | |
102 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxffffffffffffu3HHHHHHHHmzffffffffffffffffffffffffa7-H", | |
103 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxffffffffffffa7HHHHHHHHnlffffffffffffffffffffffffa7-H", | |
104 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxffffffffffffa7>HHHHHHHbgffffffffffffffffffffffffa7-H", | |
105 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxffffffffffffs9,HHHHHHHbgffffffffffffffffffffffffa7-H", | |
106 | "Hbxfffffffffffffffffffffffffffffffa6=HHxffffffffffffffffffffffffffffffu3HHbxfffffffffffffq,HHHHHHHvfffffffffffffffffffffffffa7-H", | |
107 | "Hbxffffffffffffaaaaaaaaaaaaaaaaaaau5=HHxsaaaaaaaaaaaffffffffffffffffffa3HHbxfffffffffffffr<HHHHHHMcfffffffffffffffffffffffffa7-H", | |
108 | "Hbxfffffffffaq5<,,,,,,,,,,,,,,,,,<,*#HHe1,,,,,,,,<<5qaffffaurrrrrrrrrrq,HHbxfffffffffffffy1HHHHHHmxfffffffffffffffffffffffffa7-H", | |
109 | "Hbxffffffffs0=oXXXXXXXXXXXXXXXXXXXX.HHH-oXXXXXXXXXo$,rffffr<*&&&&&&&&&&+HHnxfffffffffffffu3HHHHHHmxfffffffffffffffffffffffffa7-H", | |
110 | "Hbxffffffffa1OHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHifffs7&HHHHHHHHHHHHHHNxfffffffffffffa6HHHHHHmzfffffffffffffffffffffffffa7-H", | |
111 | "Hbxffffffffu3HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHvlffa6*HHHHHHHHHHHHHHVzfffffffffffffa7>HHHHHnlfffffffffffffffffffffffffa7-H", | |
112 | "Hbxffffffffaq9HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHBzffa7-HHSZZZZZZZZZACbgfffffffffffffs9,HHHHHbgfffffffffffffffffffffffffa7-H", | |
113 | "HbxfffffffffauHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHAmgffa7-HHNbvvvvvvvvvzgfffffffffffffff0,HHHHHbgfffffffffffffffffffffffffa7-H", | |
114 | "HbxffffffffffsuHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHVzfffa7-HHcgffffffffffffffffffffffffffr<HHHHHvffffffffffffffffffffffffffa7-H", | |
115 | "HbxfffffffffffauHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHZvffffa7-HHcgffffffffffffffffffffffffffy1HHHHMcffffffffffffffffffffffffffa7-H", | |
116 | "HbxffffffffffffaHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHAmgffffa7-HHcgffffffffffffffffffffffffffu3HHHHmxffffffffffffffffffffffffffa7-H", | |
117 | "HbxffffffffffffsuHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHBzfffffa7-HHcgffffffffffffffffffffffffffu6HHHHmzffffffffffffffffffffffffffa7-H", | |
118 | "HbxfffffffffffffauHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHZcffffffa7-HHcgffffffffffffffffffffffffffa7>HHHnzffffffffffffffffffffffffffa7-H", | |
119 | "HbxffffffffffffffaHHHHHAZVVVVVVVVVVVVVVVVBcHHHHHAmgffffffa7-HHcgffffffffffffffffffffffffffs9,HHHngfffffffffaafffffffffffffffa7-H", | |
120 | "HbxffffffffffffffsuHHHHcxzzzzzzzzzzzzzzzzd2HHHHHBzfffffffa7-HHcgfffffffffffffffaaffffffffff0,HHHbgffffffffayusffffffffffffffa7-H", | |
121 | "HbxfffffffffffffffauHHHrufffffffffffffffu3+HHHHCcffffffffa7-HHcgffffffffffffffauusfffffffffr<HHHbgffffffffuqufffffffffffffffa7-H", | |
122 | "HbxffffffffffffffffaHHHHqqq0qqqqqqruffffq&HHHHAngffffffffa7-HHcgffffffffffffffuqysfffffffffr1HHHvfffffffffr9agffffffffffffffa7-H", | |
123 | "HbxfffffffffffffffffaHHHH:&&&&&&&&>qfffu,XHHHHBlfffffffffa7-HHcgffffffffffffffu0yffffffffffu3HHMcffffffffs07ggffffffffffffffa7-H", | |
124 | "HbxfffffffffffffffffsaHHHHHHHHHHHHHigfs9$HHHHCxffffffffffa7-HHcgffffffffffffffy9ygfffffffffu6HHmxffffffffa95ggffffffffffffffa7-H", | |
125 | "HbxffffffffffffffffffgbHHHHHHHHHHHHxlfy:.HHHAbgffffffffffa7-HHcgffffffffffffffr7tgfffffffffa7,Hmzffffffffa71zlffffffffffffffa7-H", | |
126 | "Hbxffffffffffffffffffgcnmmmmmmmmk0Hbza5@HHHANgfffffffffffa7-HHcgffffffffffffffr5Hggffffffffs9<Hnzffffffffu3Hxzffffffffffffffa7-H", | |
127 | "Hbxfffffffffffffffffffffffffffff9>Hvzq*HHHHCxffffffffffffa7-HHcgffffffffffffffr1Hzgfffffffffq5Hbgffffffffy<Hczffffffffffffffa7-H", | |
128 | "Hbxffffffffffffffffffffffffffffa7:Hvl1oHHHZbfffffffffffffa7-HHcgffffffffffffffr1Hzgfffffffffr9Hbgffffffffq>Hczffffffffffffffa7-H", | |
129 | "Hbxffffffffffffffffffffffffffffs7:Hvp%HHHANgfffffffffffffa7-HHcgffffffffffffffr<Hzgfffffffffyrccgffffffff0:Hvzffffffffffffffa7-H", | |
130 | "Hbxffffffffffffffffffffffffffffs7:Hc4XHHHVxffffffffffffffa7-HHcgffffffffffffffr,Hxlfffffffffasxxfffffffff9=Hvzffffffffffffffa7-H", | |
131 | "Hbxffffffffffffffffffffffffffffs7:Hd;HHHZbfffffffffffffffa7-HHcgffffffffffffffr,HHzffffffffffflgffffffffa7*Hvzffffffffffffffa7-H", | |
132 | "Hbxffffffffffffffffffffffffffffs7:H7&HHANgfffffffffffffffa7-HHcgffffffffffffffr,HHzfffffffffffffffffffffa3&Hbzffffffffffffffa7-H", | |
133 | "Hbxffffffffffffffffffffffffffffs7:H<HHHNzffffffffffffffffa7-HHcgffffffffffffffr,HHzgffffffffffffffffffffu<HHbzffffffffffffffa7-H", | |
134 | "Hbxffffffffffffffffffffffffffffs7;HHHHHdsffffffffffffffffa7-HHcgffffffffffffffr,HHxgffffffffffffffffffffy,HHbzffffffffffffffa7-H", | |
135 | "Hbxffffffffffffffffffffffffffffs7;HHHHH0rafffffffffffffffa7-HHcgffffffffffffffr,HHxgffffffffffffffffffffr>HHbzffffffffffffffa7-H", | |
136 | "Hbxffffffffffffffffffffffffffffs7;HHHHHH0yfffffffffffffffa7-HHcgffffffffffffffr,HHxgffffffffffffffffffffq-HHbzffffffffffffffa7-H", | |
137 | "Hbxfffffffffffffffffffffffffffff7;HHHHHHHquffffffffffffffa7-HHcgffffffffffffffr,HHxzffffffffffffffffffff9=HHbzffffffffffffffa7-H", | |
138 | "Hbxffffffffffffffffffffffssssssa7-HHHHHHH9rafffffffffffffa7-HHcgffffffffffffffr,HHHzffffffffffffffffffff7*HHbzffffffffffffffa7-H", | |
139 | "Hbxffffffffffffffffffsr711111111;$HHHHHHHH0yfffffffffffffa7-HHcgffffffffffffffr,HHHzfffffffffffffffffffa5&HHbzffffffffffffffa7-H", | |
140 | "Hbxffffffffffffffffffr,@XoooooooXHHHHHHHHHHquffffffffffffa7-HHcgffffffffffffffr,HHHzgffffffffffffffffffu1HHHbzffffffffffffffa7-H", | |
141 | "Hbxfffffffffffffffffa1OHHHHHHHHHHHHHHHHHHHHHrafffffffffffa7-HHcgffffffffffffffr,HHHxgffffffffffffffffffu,HHHbzffffffffffffffa7-H", | |
142 | "Hbxffffffffffffffffs9$HHHHHHHHHHCCjHHHHHHHHH0yfffffffffffa7-HHcgffffffffffffffr,HHHxgffffffffffffffffffr>HHHbzffffffffffffffa7-H", | |
143 | "Hbxffffffffffffffffr-.HHHHHHHHHHBv9HHHHHHHHHHquffffffffffa7-HHcgffffffffffffffr,HHHxlffffffffffffffffffq;HHHbzffffffffffffffa7-H", | |
144 | "Hbxfffffffffffffffu1oHHHHHHHHHHHMz7HHHHHHHHHHHrafffffffffa7-HHcgffffffffffffffr,HHHxzffffffffffffffffff0=HHHbzffffffffffffffa7-H", | |
145 | "Hbxffffffffffffffa9#HHHHHHHHHHHHxi>HHHHHHHHHHH0ysffffffffa7-HHcgffffffffffffffr,HHHHzffffffffffffffffff7*HHHbzffffffffffffffa7-H", | |
146 | "Hbxffffffffffffffr= HHHHHHHHHHHHxt>HHHHHHHHHHHH0uffffffffa7-HHcgffffffffffffffr,HHHHzfffffffffffffffffa5&HHHbzffffffffffffffa7-H", | |
147 | "Hbxfffffffffffffu<XHHHHHHHHHHHDABxlnHHHHHHHHHHHHqafffffffa7-HHcgffffffffffffffr,HHHHzgffffffffffffffffa1&HHHbzffffffffffffffa7-H", | |
148 | "Hbxffffffffffffa7#HHHHHHHHHFAVmbxzzvnmmHHHHHHHHH9rsffffffa7-HHcgffffffffffffffr,HHHHxgffffffffffffffffu,HHHHbzffffffffffffffa7-H", | |
149 | "Hbxffffffffffffq* HHHHHHHFZNcgffffffgzcbbHHHHHHHH0uffffffa7-HHcgffffffffffffffr,HHHHxgffffffffffffffffy>HHHHbzffffffffffffffa7-H", | |
150 | "Hbxfffffffffffu,XHHHHHHHABxgffffffffffflxcHHHHHHHHqafffffa7-HHcgffffffffffffffr,HHHHxlffffffffffffffffr:HHHHbzffffffffffffffa7-H", | |
151 | "Hbxffffffffffa7@HHHHHHFZbgffffffffffffffglxHHHHHHH9raffffa7-HHcgffffffffffffffr,HHHHxzffffffffffffffff0-HHHHbzffffffffffffffa7-H", | |
152 | "Hbxffffffffffq*HHHHHHGVxffffffffffffffffffgzzHHHHHH0uffffa7-HHcgffffffffffffffr,HHHHHzffffffffffffffff9*HHHHbzffffffffffffffa7-H", | |
153 | "Hbxfffffffffu,XHHHHHCmhqqqqqqqqqqqqqqqqqqqqtitHHHHHHqafffa7-HHcgffffffffffffffr,HHHHHzfffffffffffffffs7*HHHHbzffffffffffffffa7-H", | |
154 | "Hbxffffffffa7#HHHHHjw>&&&&&&&&&&&&&&&&&&&&&&*:HHHHHH0ufffa7-HHcgffffffffffffffr,HHHHHxgffffffffffffffa3&HHHHbzffffffffffffffa7-H", | |
155 | "Hbxffffffffu<+HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHdgffa7-HHcgffffffffffffffr,HHHHHxgffffffffffffffu<HHHHHbzffffffffffffffa7-H", | |
156 | "Hbxffffffffu5HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHbzffa7-HHcgffffffffffffffr,HHHHHxgffffffffffffffy,HHHHHbzffffffffffffffa7-H", | |
157 | "HbxffffffffayiHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHBzffa7-HHcgffffffffffffffr,HHHHHxlffffffffffffffr>HHHHHbzffffffffffffffa7-H", | |
158 | "HbxffffffffffznVZZZZZZZZZZZZZZZZZZZmHHHSZZZZZZZZZZZAVvgffa7-HHcgffffffffffffffr,HHHHHxzffffffffffffffq;HHHHHbzffffffffffffffa7-H", | |
159 | "Hbxffffffffffgzcvvvvvvbbbbbbbbbbbbbh6HHNbvvvvvvvvvvvzffffa7-HHcgffffffffffffffr,HHHHHHzffffffffffffff9=HHHHHbzffffffffffffffa7-H", | |
160 | "Hbxfffffffffffffffffffffffffffffffs7-HHcfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHzffffffffffffff7*HHHHHbzffffffffffffffa7-H", | |
161 | "Hbxfffffffffffffffffffffffffffffffa6=HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHxgffffffffffffa5&HHHHHbzffffffffffffffa7-H", | |
162 | "Hbxfffffffffffffffffffffffffffffffa6-HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHxgffffffffffffa1$HHHHHbzffffffffffffffa7-H", | |
163 | "Hbxfffffffffffffffffffffffffffffffa6-HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHxgffffffffffffu,HHHHHHbzffffffffffffffa7-H", | |
164 | "Hbxfffffffffffffffffffffffffffffffa6-HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHxlffffffffffffr>HHHHHHbzffffffffffffffa7-H", | |
165 | "Hbxfffffffffffffffffffffffffffffffa6-HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHHzffffffffffffq:HHHHHHbzffffffffffffffa7-H", | |
166 | "Hbxfffffffffffffffffffffffffffffffa6-HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHHzffffffffffff0=HHHHHHbzffffffffffffffa7-H", | |
167 | "Hbxfffffffffffffffffffffffffffffffa6-HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHHzgfffffffffff9*HHHHHHbzffffffffffffffa7-H", | |
168 | "Hbxfffffffffffffffffffffffffffffffa6-HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHHxgffffffffffa6&HHHHHHbzffffffffffffffa7-H", | |
169 | "Hbxfffffffffffffffffffffffffffffffa6-HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHHxgffffffffffa1&HHHHHHbzffffffffffffffa7-H", | |
170 | "Hbxfffffffffffffffffffffffffffffffa6-HHxfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHHxgffffffffffu<HHHHHHHbzffffffffffffffa7-H", | |
171 | "Hbxfffffffffffffffffffffffffffffffa7-HHcfffffffffffffffffa7-HHcgffffffffffffffr,HHHHHHHxzffffffffffy>HHHHHHHbzffffffffffffffs7-H", | |
172 | "Hvzauuuuuuuuuuuuuuuuuuuuuuuuuuuuuuy3*HHxiyyyyyyyyyyyyyyyyy3*HHxauuuuuuuuuuuuuu0>HHHHHHHHgauuuuuuuuu0;HHHHHHHvguuuuuuuuuuuuuuy5=H", | |
173 | "Hi6:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-$OHH6;===============-=$OHH8>;;;;;;;;;;;;;;*@HHHHHHHH6:;;;;;;;;;*OHHHHHHHe2;;;;;;;;;;;;;;-$OH", | |
174 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
175 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
176 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
177 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
178 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
179 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
180 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
181 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
182 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
183 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
184 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
185 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
186 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
187 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
188 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
189 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
190 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
191 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
192 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
193 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
194 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
195 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
196 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
197 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", | |
198 | "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH" | |
199 | }; |
Binary diff not shown
Binary diff not shown
Binary diff not shown
0 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
1 | <html xmlns="http://www.w3.org/1999/xhtml"> | |
2 | <head> | |
3 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
4 | <meta http-equiv="Content-Style-Type" content="text/css" /> | |
5 | <meta name="generator" content="pandoc" /> | |
6 | <title>ETM Users Manual</title> | |
7 | <style type="text/css">code{white-space: pre;}</style> | |
8 | </head> | |
9 | <body> | |
10 | <style> | |
11 | body { | |
12 | margin: auto; | |
13 | padding-right: 1em; | |
14 | padding-left: 1em; | |
15 | max-width: 44em; | |
16 | border-left: 1px solid black; | |
17 | border-right: 1px solid black; | |
18 | color: black; | |
19 | font-family: Verdana, sans-serif; | |
20 | font-size: 100%; | |
21 | line-height: 140%; | |
22 | color: #333; | |
23 | } | |
24 | pre, tt{ | |
25 | font-family: monospace; | |
26 | padding: 2px 4px; | |
27 | } | |
28 | code{ | |
29 | white-space: pre-wrap; | |
30 | font-size: 110%; | |
31 | padding: 1px 1px; | |
32 | } | |
33 | h1 a, h2 a, h3 a, h4 a, h5 a, li a { | |
34 | text-decoration: none; | |
35 | color: #7a5ada; | |
36 | } | |
37 | header, h1, h2, h3, h4, h5 { font-family: verdana; | |
38 | font-weight: bold; | |
39 | /*border-bottom: 1px dotted black;*/ | |
40 | color: #7a5ada; } | |
41 | header { | |
42 | font-size: 130%; | |
43 | } | |
44 | ||
45 | h1 { | |
46 | font-size: 130%; | |
47 | } | |
48 | ||
49 | h2 { | |
50 | font-size: 110%; | |
51 | border-bottom: 1px dotted black; | |
52 | } | |
53 | ||
54 | h3 { | |
55 | font-size: 100%; | |
56 | border-bottom: 1px dotted black; | |
57 | } | |
58 | ||
59 | h4 { | |
60 | font-size: 90%; | |
61 | font-style: italic; | |
62 | border-bottom: 1px dotted black; | |
63 | } | |
64 | ||
65 | h5 { | |
66 | font-size: 85%; | |
67 | font-style: italic; | |
68 | border-bottom: 1px dotted black; | |
69 | } | |
70 | ||
71 | h1.title { | |
72 | font-size: 200%; | |
73 | font-weight: bold; | |
74 | padding-top: 0.2em; | |
75 | padding-bottom: 0.2em; | |
76 | text-align: left; | |
77 | border: none; | |
78 | } | |
79 | ||
80 | dt code { | |
81 | font-weight: bold; | |
82 | } | |
83 | dd p { | |
84 | margin-top: 0; | |
85 | } | |
86 | ||
87 | #footer { | |
88 | padding-top: 1em; | |
89 | font-size: 70%; | |
90 | color: gray; | |
91 | text-align: center; | |
92 | } | |
93 | </style> | |
94 | <div id="header"> | |
95 | <h1 class="title">ETM Users Manual</h1> | |
96 | </div> | |
97 | <div id="TOC"> | |
98 | <ul> | |
99 | <li><a href="#overview">Overview</a><ul> | |
100 | <li><a href="#sample-entries">Sample entries</a></li> | |
101 | <li><a href="#starting-etm">Starting etm</a></li> | |
102 | <li><a href="#views">Views</a></li> | |
103 | <li><a href="#creating-new-items">Creating New Items</a></li> | |
104 | <li><a href="#editing-existing-items">Editing Existing Items</a></li> | |
105 | <li><a href="#sharing-with-other-calendar-applications">Sharing with other calendar applications</a></li> | |
106 | <li><a href="#tools">Tools</a></li> | |
107 | <li><a href="#data-organization-and-calendars">Data Organization and Calendars</a></li> | |
108 | </ul></li> | |
109 | <li><a href="#item-types">Item types</a><ul> | |
110 | <li><a href="#action">~ Action</a></li> | |
111 | <li><a href="#event">* Event</a></li> | |
112 | <li><a href="#occasion">^ Occasion</a></li> | |
113 | <li><a href="#note">! Note</a></li> | |
114 | <li><a href="#task">- Task</a></li> | |
115 | <li><a href="#delegated-task">% Delegated task</a></li> | |
116 | <li><a href="#task-group">+ Task group</a></li> | |
117 | <li><a href="#in-basket">$ In basket</a></li> | |
118 | <li><a href="#someday-maybe">? Someday maybe</a></li> | |
119 | <li><a href="#hidden"># Hidden</a></li> | |
120 | <li><a href="#defaults">= Defaults</a></li> | |
121 | </ul></li> | |
122 | <li><a href="#keys">@Keys</a><ul> | |
123 | <li><a href="#a-alert"><span class="citation">@a</span> alert</a></li> | |
124 | <li><a href="#b-beginby"><span class="citation">@b</span> beginby</a></li> | |
125 | <li><a href="#c-context"><span class="citation">@c</span> context</a></li> | |
126 | <li><a href="#d-description"><span class="citation">@d</span> description</a></li> | |
127 | <li><a href="#e-extent"><span class="citation">@e</span> extent</a></li> | |
128 | <li><a href="#f-done-due"><span class="citation">@f</span> done[; due]</a></li> | |
129 | <li><a href="#g-goto"><span class="citation">@g</span> goto</a></li> | |
130 | <li><a href="#h-history"><span class="citation">@h</span> history</a></li> | |
131 | <li><a href="#j-job"><span class="citation">@j</span> job</a></li> | |
132 | <li><a href="#k-keyword"><span class="citation">@k</span> keyword</a></li> | |
133 | <li><a href="#l-location"><span class="citation">@l</span> location</a></li> | |
134 | <li><a href="#m-memo"><span class="citation">@m</span> memo</a></li> | |
135 | <li><a href="#o-overdue"><span class="citation">@o</span> overdue</a></li> | |
136 | <li><a href="#p-priority"><span class="citation">@p</span> priority</a></li> | |
137 | <li><a href="#r-repetition-rule"><span class="citation">@r</span> repetition rule</a></li> | |
138 | <li><a href="#s-starting-datetime"><span class="citation">@s</span> starting datetime</a></li> | |
139 | <li><a href="#t-tags"><span class="citation">@t</span> tags</a></li> | |
140 | <li><a href="#u-user"><span class="citation">@u</span> user</a></li> | |
141 | <li><a href="#v-action_rates-key"><span class="citation">@v</span> action_rates key</a></li> | |
142 | <li><a href="#w-action_markups-key"><span class="citation">@w</span> action_markups key</a></li> | |
143 | <li><a href="#x-expense"><span class="citation">@x</span> expense</a></li> | |
144 | <li><a href="#z-time-zone"><span class="citation">@z</span> time zone</a></li> | |
145 | <li><a href="#include">@+ include</a></li> | |
146 | <li><a href="#exclude">@- exclude</a></li> | |
147 | </ul></li> | |
148 | <li><a href="#dates">Dates</a><ul> | |
149 | <li><a href="#fuzzy-dates">Fuzzy dates</a></li> | |
150 | <li><a href="#time-periods">Time periods</a></li> | |
151 | <li><a href="#time-zones">Time zones</a></li> | |
152 | <li><a href="#anniversary-substitutions">Anniversary substitutions</a></li> | |
153 | <li><a href="#easter">Easter</a></li> | |
154 | </ul></li> | |
155 | <li><a href="#preferences">Preferences</a><ul> | |
156 | <li><a href="#template-expansions">Template expansions</a></li> | |
157 | <li><a href="#options">Options</a></li> | |
158 | </ul></li> | |
159 | <li><a href="#reports">Reports</a><ul> | |
160 | <li><a href="#report-type-characters">Report type characters</a></li> | |
161 | <li><a href="#groupby-setting">Groupby setting</a></li> | |
162 | <li><a href="#options-1">Options</a></li> | |
163 | </ul></li> | |
164 | <li><a href="#shortcuts">Shortcuts</a><ul> | |
165 | <li><a href="#menubar">Menubar</a></li> | |
166 | <li><a href="#main">Main</a></li> | |
167 | <li><a href="#edit">Edit</a></li> | |
168 | </ul></li> | |
169 | </ul> | |
170 | </div> | |
171 | <h1 id="overview"><a href="#overview">Overview</a></h1> | |
172 | <p>In contrast to most calendar/todo applications, creating items (events, tasks, and so forth) in etm does not require filling out fields in a form. Instead, items are created as free-form text entries using a simple, intuitive format and stored in plain text files.</p> | |
173 | <p>Dates in the examples below are entered using <em>fuzzy parsing</em> - e.g., <code>+7</code> for seven days from today, <code>fri</code> for the first Friday on or after today, <code>+1/1</code> for the first day of next month, <code>sun - 6d</code> for Monday of the current week. See <a href="DATES#dates">Dates</a> for details.</p> | |
174 | <h2 id="sample-entries"><a href="#sample-entries">Sample entries</a></h2> | |
175 | <ul> | |
176 | <li><p>A sales meeting (an event) [s]tarting seven days from today at 9:00am with an [e]xtent of one hour and a default [a]lert 5 minutes before the start:</p> | |
177 | <pre><code>* sales meeting @s +7 9a @e 1h @a 5</code></pre></li> | |
178 | <li><p>The sales meeting with another [a]lert 2 days before the meeting to (e)mail a reminder to a list of recipients:</p> | |
179 | <pre><code>* sales meeting @s +7 9a @e 1h @a 5 | |
180 | @a 2d: e; who@when.com, what@where.org</code></pre></li> | |
181 | <li><p>Prepare a report (a task) for the sales meeting [b]eginning 3 days early:</p> | |
182 | <pre><code>- prepare report @s +7 @b 3</code></pre></li> | |
183 | <li><p>A period [e]xtending 35 minutes (an action) spent working on the report yesterday:</p> | |
184 | <pre><code>~ report preparation @s -1 @e 35</code></pre></li> | |
185 | <li><p>Get a haircut (a task) on the 24th of the current month and then [r]epeatedly at (d)aily [i]ntervals of (14) days and, [o]n completion, (r)estart from the completion date:</p> | |
186 | <pre><code>- get haircut @s 24 @r d &i 14 @o r</code></pre></li> | |
187 | <li><p>Payday (an occasion) on the last week day of each month. The <code>&s -1</code> part of the entry extracts the last date which is both a weekday and falls within the last three days of the month):</p> | |
188 | <pre><code>^ payday @s 1/1 @r m &w MO, TU, WE, TH, FR | |
189 | &m -1, -2, -3 &s -1</code></pre></li> | |
190 | <li><p>Take a prescribed medication daily (a reminder) [s]tarting today and [r]epeating (d)aily at [h]ours 10am, 2pm, 6pm and 10pm [u]ntil (12am on) the fourth day from today. Trigger the default [a]lert zero minutes before each reminder:</p> | |
191 | <pre><code>* take Rx @s +0 @r d &h 10, 14, 18, 22 &u +4 @a 0</code></pre></li> | |
192 | <li><p>Move the water sprinkler (a reminder) every thirty mi[n]utes on Sunday afternoons using the default alert zero minutes before each reminder:</p> | |
193 | <pre><code>* Move sprinkler @s 1 @r w &w SU &h 14, 15, 16, 17 &n 0, 30 @a 0</code></pre> | |
194 | <p>To limit the sprinkler movement reminders to the [M]onths of April through September each year change the <span class="citation">@r</span> entry to this:</p> | |
195 | <pre><code>@r w &w SU &h 14, 15, 16, 17 &n 0, 30 &M 4, 5, 6, 7, 8, 9</code></pre> | |
196 | <p>or this:</p> | |
197 | <pre><code>@r n &i 30 &w SU &h 14, 15, 16, 17 &M 4, 5, 6, 7, 8, 9</code></pre></li> | |
198 | <li><p>Presidential election day (an occasion) every four years on the first Tuesday after a Monday in November:</p> | |
199 | <pre><code>^ Presidential Election Day @s 2012-11-06 | |
200 | @r y &i 4 &M 11 &m 2, 3, 4, 5, 6, 7, 8 &w TU</code></pre></li> | |
201 | <li><p>Join the etm discussion group (a task) [s]tarting 14 days from today. Because of the <span class="citation">@g</span> (goto) link, pressing <em>G</em> when this item is selected in the gui would open the link using the system default application which, in this case, would be your default browser:</p> | |
202 | <pre><code>- join the etm discussion group @s +14 | |
203 | @g groups.google.com/group/eventandtaskmanager/topics</code></pre></li> | |
204 | </ul> | |
205 | <h2 id="starting-etm"><a href="#starting-etm">Starting etm</a></h2> | |
206 | <p>To start the etm GUI open a terminal window and enter <code>etm</code> at the prompt:</p> | |
207 | <pre><code>$ etm</code></pre> | |
208 | <p>If you have not done a system installation of etm you will need first to cd to the directory where you unpacked etm.</p> | |
209 | <p>You can add a command to use the CLI instead of the GUI. For example, to get the complete command line usage information printed to the terminal window just add a question mark:</p> | |
210 | <pre><code>$ etm ? | |
211 | Usage: | |
212 | ||
213 | etm [logging level] [path] [?] [acmsv] | |
214 | ||
215 | With no arguments, etm will set logging level 3 (warn), use settings from | |
216 | the configuration file ~/.etm/etmtk.cfg, and open the GUI. | |
217 | ||
218 | If the first argument is an integer not less than 1 (debug) and not greater | |
219 | than 5 (critical), then set that logging level and remove the argument. | |
220 | ||
221 | If the first (remaining) argument is the path to a directory that contains | |
222 | a file named etmtk.cfg, then use that configuration file and remove the | |
223 | argument. | |
224 | ||
225 | If the first (remaining) argument is one of the commands listed below, then | |
226 | execute the remaining arguments without opening the GUI. | |
227 | ||
228 | a ARG display the agenda view using ARG, if given, as a filter. | |
229 | c ARGS display a custom view using the remaining arguments as the | |
230 | specification. (Enclose ARGS in single quotes to prevent shell | |
231 | expansion.) | |
232 | d ARG display the day view using ARG, if given, as a filter. | |
233 | k ARG display the keywords view using ARG, if given, as a filter. | |
234 | m INT display a report using the remaining argument, which must be a | |
235 | positive integer, to display a report using the corresponding | |
236 | entry from the file given by report_specifications in etmtk.cfg. | |
237 | Use ? m to display the numbered list of entries from this file. | |
238 | n ARG display the notes view using ARG, if given, as a filter. | |
239 | N ARGS Create a new item using the remaining arguments as the item | |
240 | specification. (Enclose ARGS in single quotes to prevent shell | |
241 | expansion.) | |
242 | p ARG display the path view using ARG, if given, as a filter. | |
243 | t ARG display the tags view using ARG, if given, as a filter. | |
244 | v display information about etm and the operating system. | |
245 | ? ARG display (this) command line help information if ARGS = '' or, | |
246 | if ARGS = X where X is one of the above commands, then display | |
247 | details about command X. 'X ?' is equivalent to '? X'.</code></pre> | |
248 | <p>For example, you can print your agenda to the terminal window by adding the letter "a":</p> | |
249 | <pre><code>$ etm a | |
250 | Sun Apr 06, 2014 | |
251 | > set up luncheon meeting with Joe Smith 4d | |
252 | Mon Apr 07, 2014 | |
253 | * test command line event 3pm ~ 4pm | |
254 | * Aerobics 5pm ~ 6pm | |
255 | - follow up with Mary Jones | |
256 | Wed Apr 09, 2014 | |
257 | * Aerobics 5pm ~ 6pm | |
258 | Thu Apr 10, 2014 | |
259 | * Frank Burns conference call 1pm Pacif.. 4pm ~ 5:30pm | |
260 | * Book club 7pm ~ 9pm | |
261 | - sales meeting | |
262 | - set up luncheon meeting with Joe Smith 15m | |
263 | Now | |
264 | Available | |
265 | - Hair cut -1d | |
266 | Next | |
267 | errands | |
268 | - milk and eggs | |
269 | phone | |
270 | - reservation for Saturday dinner | |
271 | Someday | |
272 | ? lose weight and exercise more</code></pre> | |
273 | <p>You can filter the output by adding a (case-insensitive) argument:</p> | |
274 | <pre><code>$ etm a smith | |
275 | Sun Apr 06, 2014 | |
276 | > set up luncheon meeting with Joe Smith 4d | |
277 | Thu Apr 10, 2014 | |
278 | - set up luncheon meeting with Joe Smith 15m</code></pre> | |
279 | <p>or <code>etm d mar .*2014</code> to show your items for March, 2014.</p> | |
280 | <p>You can add a question mark to a command to get details about the commmand, e.g.:</p> | |
281 | <pre><code>Usage: | |
282 | ||
283 | etm c <type> <groupby> [options] | |
284 | ||
285 | Generate a custom view where type is either 'a' (action) or 'c' (composite). | |
286 | Groupby can include *semicolon* separated date specifications and | |
287 | elements from: | |
288 | c context | |
289 | f file path | |
290 | k keyword | |
291 | t tag | |
292 | u user | |
293 | ||
294 | A *date specification* is either | |
295 | w: week number | |
296 | or a combination of one or more of the following: | |
297 | yy: 2-digit year | |
298 | yyyy: 4-digit year | |
299 | MM: month: 01 - 12 | |
300 | MMM: locale specific abbreviated month name: Jan - Dec | |
301 | MMMM: locale specific month name: January - December | |
302 | dd: month day: 01 - 31 | |
303 | ddd: locale specific abbreviated week day: Mon - Sun | |
304 | dddd: locale specific week day: Monday - Sunday | |
305 | ||
306 | Options include: | |
307 | -b begin date | |
308 | -c context regex | |
309 | -d depth (CLI a reports only) | |
310 | -e end date | |
311 | -f file regex | |
312 | -k keyword regex | |
313 | -l location regex | |
314 | -o omit (r reports only) | |
315 | -s summary regex | |
316 | -S search regex | |
317 | -t tags regex | |
318 | -u user regex | |
319 | -w column 1 width | |
320 | -W column 2 width | |
321 | ||
322 | Example: | |
323 | ||
324 | etm c 'c ddd, MMM dd yyyy -b 1 -e +1/1'</code></pre> | |
325 | <p>Note: The CLI offers the same views and reporting, with the exception of week and month view, as the GUI. It is also possible to create new items in the CLI with the <code>n</code> command. Other modifications such as copying, deleting, finishing and so forth, can only be done in the GUI or, perhaps, in your favorite text editor. An advantage to using the GUI is that it provides auto-completion and validation.</p> | |
326 | <p>Tip: If you have a terminal open, you can create a new item or put something to finish later in your inbox quickly and easily with the "N" command. For example,</p> | |
327 | <pre><code> etm N '123 456-7890'</code></pre> | |
328 | <p>would create an entry in your inbox with this phone number. (With no type character an "$" would be supplied automatically to make the item an inbox entry and no further validation would be done.)</p> | |
329 | <h2 id="views"><a href="#views">Views</a></h2> | |
330 | <p>All views display only items consistent with the current choices of active calendars.</p> | |
331 | <p>If a (case-insensitive) filter is entered then the display in all views other than week, month and custom view will be limited to items that match somewhere in either the branch or the leaf. Relevant branches will automatically be expanded to show matches.</p> | |
332 | <p>In day, week and month views, pressing the space bar will move the display to the current date. In all other views, pressing the space bar will move the display to the first item in the outline.</p> | |
333 | <p>In day, week and month views, pressing <em>J</em> will first prompt for a fuzzy-parsed date and then "jump" to the specified date.</p> | |
334 | <p>If you scroll or jump to a date in day, week or month view and then switch to another one of these views, the same day(s) will be displayed.</p> | |
335 | <p>In all views, pressing <em>Return</em> with an item selected or double clicking an item or a busy period in week view will open a context menu with options to copy, delete, edit and so forth.</p> | |
336 | <p>In all views, clicking in the details panel with an item selected will open the item for editing if it is not repeating and otherwise prompt for the instance(s) to be changed.</p> | |
337 | <p>In all views other than week and month view, pressing <em>O</em> will open a dialog to choose the outline depth.</p> | |
338 | <p>In all views other than week and month view, pressing <em>L</em> will toggle the display of a column displaying item <em>labels</em> where, for example, an item with <span class="citation">@a</span>, <span class="citation">@d</span> and <span class="citation">@u</span> fields would have the label "adu".</p> | |
339 | <p>In all views other than week and month view, pressing <em>S</em> will show a text verion of the current display suitable for copy and paste. The text version will respect the current outline depth in the view.</p> | |
340 | <p>In custom view it is possible to export the current report in either text or CSV (comma separated values) format to a file of your choosing.</p> | |
341 | <p>Note. In custom view you need to move the focus from the report specification entry field in order for the shortcuts <em>O</em>, <em>L</em> and <em>S</em> to work.</p> | |
342 | <p>In all views:</p> | |
343 | <ul> | |
344 | <li><p>if an item is selected:</p> | |
345 | <ul> | |
346 | <li><p>pressing <em>Shift-H</em> will show a history of changes for the file containing the selected item, first prompting for the number of changes.</p></li> | |
347 | <li><p>pressing <em>Shift-X</em> will export the selected item in iCal format.</p></li> | |
348 | </ul></li> | |
349 | <li><p>if an item is not selected:</p> | |
350 | <ul> | |
351 | <li><p>pressing <em>Shift-H</em> will show a history of changes for all files, first prompting for the number of changes.</p></li> | |
352 | <li><p>pressing <em>Shift-X</em> will export all items in active calendars in iCal format.</p></li> | |
353 | </ul></li> | |
354 | </ul> | |
355 | <h3 id="agenda-view"><a href="#agenda-view">Agenda View</a></h3> | |
356 | <p>What you need to know now beginning with your schedule for the next few days and followed by items in these groups:</p> | |
357 | <ul> | |
358 | <li><p><strong>In basket</strong>: In basket items and items with missing types or other errors.</p></li> | |
359 | <li><p><strong>Now</strong>: All <em>scheduled</em> (dated) tasks whose due dates have passed including delegated tasks and waiting tasks (tasks with unfinished prerequisites) grouped by available, delegated and waiting and, within each group, by the due date.</p></li> | |
360 | <li><p><strong>Next</strong>: All <em>unscheduled</em> (undated) tasks grouped by context (home, office, phone, computer, errands and so forth) and sorted by priority and extent. These tasks correspond to GTD's <em>next actions</em>. These are tasks which don't really have a deadline and can be completed whenever a convenient opportunity arises. Check this view, for example, before you leave to run errands for opportunities to clear other errands.</p></li> | |
361 | <li><p><strong>Someday</strong>: Someday (maybe) items for periodic review.</p></li> | |
362 | </ul> | |
363 | <p>Note: Finished tasks, actions and notes are not displayed in this view.</p> | |
364 | <h3 id="day-view"><a href="#day-view">Day View</a></h3> | |
365 | <p>All dated items appear in this view, grouped by date and sorted by starting time and item type. This includes:</p> | |
366 | <ul> | |
367 | <li><p>All non-repeating, dated items.</p></li> | |
368 | <li><p>All repetitions of repeating items with a finite number of repetitions. This includes 'list-only' repeating items and items with <code>&u</code> (until) or <code>&t</code> (total number of repetitions) entries.</p></li> | |
369 | <li><p>For repeating items with an infinite number of repetitions, those repetitions that occur within the first <code>weeks_after</code> weeks after the current week are displayed along with the first repetition after this interval. This assures that at least one repetition will be displayed for infrequently repeating items such as voting for president.</p></li> | |
370 | </ul> | |
371 | <p>Tip: Want to see your next appointment with Dr. Jones? Switch to day view and enter "jones" in the filter.</p> | |
372 | <h3 id="week-view"><a href="#week-view">Week View</a></h3> | |
373 | <p>Events and occasions displayed graphically by week with one column for each day. Left and right cursor keys change, respectively, to the previous and next week. Up and down cursor keys select, respectively, the previous and next items within the given week. Items can also be selected by moving the mouse over the item. The details for the selected item are displayed at the bottom of the screen. Pressing return with an item selected or double-clicking an item opens a context menu. Control-clicking an unscheduled time opens a dialog to create an event for that date and time.</p> | |
374 | <p>Days with events that fall outside the 7am - 11pm range will have a red line at the top (earlier than 7am) or at the bottom (later than 11pm).</p> | |
375 | <p>Tip. You can display a list of busy times or, after providing the needed period in minutes, a list of free times that would accomodate the requirement within the selected week. Both options are in the <em>View</em> menu.</p> | |
376 | <h3 id="month-view"><a href="#month-view">Month View</a></h3> | |
377 | <p>Events and occasions displayed graphically by month. Left and right cursor keys change, respectively, to the previous and next month. Up and down cursor keys select, respectively, the previous and next days within the given month. Days can also be selected by moving the mouse over the item. A list of occasions and events for the selected day is displayed at the bottom of the screen. Double clicking a date or pressing <em>Return</em> with a date selected opens a dialog to create an item for that date.</p> | |
378 | <p>The current date and days with occasions are highlighted.</p> | |
379 | <p>Days with scheduled events have an <em>active times</em> border that wraps clockwise around the date box with 7am - 11am to the right along the top, 11am - 3pm down the right side, 3pm - 7pm to the left along the bottom and 7pm - 11pm upward along the left side. Days with events scheduled that fall outside the 7am - 11pm range have a red box in the top, left-hand corner of the date box.</p> | |
380 | <p>Tip. You can display a list of busy times or, after providing the needed period in minutes, a list of free times that would accomodate the requirement within the selected month. Both options are in the <em>View</em> menu.</p> | |
381 | <h3 id="tag-view"><a href="#tag-view">Tag View</a></h3> | |
382 | <p>All items with tag entries grouped by tag and sorted by type and <em>relevant datetime</em>. Note that items with multiple tags will be listed under each tag.</p> | |
383 | <p>Tip: Use the filter to limit the display to items with a particular tag.</p> | |
384 | <h3 id="keyword-view"><a href="#keyword-view">Keyword View</a></h3> | |
385 | <p>All items grouped by keyword and sorted by type and <em>relevant datetime</em>.</p> | |
386 | <h3 id="path-view"><a href="#path-view">Path View</a></h3> | |
387 | <p>All items grouped by file path and sorted by type and <em>relevant datetime</em>. Use this view to review the status of your projects.</p> | |
388 | <p>The <em>relevant datetime</em> is the past due date for any past due task, the starting datetime for any non-repeating item and the datetime of the next instance for any repeating item.</p> | |
389 | <p>Note: Items that you have "commented out" by beginning the item with a <code>#</code> will only be visible in this view.</p> | |
390 | <h3 id="note-view"><a href="#note-view">Note View</a></h3> | |
391 | <p>All notes grouped and sorted by keyword and summary.</p> | |
392 | <h3 id="custom-view"><a href="#custom-view">Custom View</a></h3> | |
393 | <p>Design your own view. See <a href="#reports">Reports</a> for details.</p> | |
394 | <h2 id="creating-new-items"><a href="#creating-new-items">Creating New Items</a></h2> | |
395 | <p>Items of any type can be created by pressing <em>N</em> in the GUI and then providing the details for the item in the resulting dialog.</p> | |
396 | <p>An event can also be created by double-clicking in a free period in the Week View - the date and time corresponding to the mouse position will be entered as the starting datetime when the dialog opens.</p> | |
397 | <p>An action can also be created by pressing <em>T</em> to start a timer for the action. You will be prompted for a summary (title) and, optionally, an <code>@e</code> entry to specify a starting time for the timer. If an item is selected when you press <em>T</em> then you will have the additional option of creating the action as a copy of the selected item.</p> | |
398 | <p>The timer starts automatically when you close the dialog. Once the timer is running, pressing <em>T</em> toggles the timer between running and paused. Pressing <em>Shift-T</em> when a timer is active (either running or paused) stops the timer and begins a dialog to provide the details of the action - the elapsed time will already be entered.</p> | |
399 | <p>While a timer is active, the title, elapsed time and status - running or paused - is displayed in the status bar.</p> | |
400 | <p>When editing an item, clicking on <em>Finish</em> or pressing <em>Shift-Return</em> will validate your entry. If there are errors, they will be displayed and you can return to the editor to correct them. If there are no errors, this will be indicated in a dialog, e.g.,</p> | |
401 | <pre><code>Task scheduled for Tue Jun 03 | |
402 | ||
403 | Save changes and exit?</code></pre> | |
404 | <p>Tip. When creating or editing a repeating item, pressing <em>Finish</em> will also display a list of instances that will be generated.</p> | |
405 | <p>Click on <em>Ok</em> or press <em>Return</em> or <em>Shift-Return</em> to save the item and close the editor. Click on <em>Cancel</em> or press <em>Escape</em> to return to the editor.</p> | |
406 | <p>If this is a new item and there are no errors, clicking on <em>Ok</em> or pressing <em>Return</em> will open a dialog to select the file to store the item with the current monthly file already selected. Pressing <em>Shift-Return</em> will bypass the file selection dialog and save to the current monthly file.</p> | |
407 | <p>Idle timing is also supported. An illustrative work flow would be to activate the idle timer first thing each morning by pressing <em>I</em>. The current value of this timer will then be displayed in brackets in the lower, left-hand corner of the GUI.</p> | |
408 | <p>If you later start an action timer, a dialog will open showing the current idle time and offering the opportunity to assign some or all of this period to a keyword that you select from a list. You can continue assigning time in this fashion until idle time is zero or you can press <em>Cancel</em> or <em>Escape</em> at any point to cancel the dialog and open the action timer dialog. While the action timer is running, the idle timer will be paused and vice-versa.</p> | |
409 | <p>If an action timer is canceled (stopped and not recorded), then the time for the action timer will be added to the current idle time.</p> | |
410 | <p>You can assign idle time without starting an action timer by pressing <em>I</em> or, at the end of the day, by pressing <em>Shift-I</em> to stop the idle timer.</p> | |
411 | <p>Note that the dialog to assign idle time will only open when starting or restarting an action timer if the current idle time is at least the number of minutes specified by <code>idle_minimum</code> in your etmtk.cfg file.</p> | |
412 | <h2 id="editing-existing-items"><a href="#editing-existing-items">Editing Existing Items</a></h2> | |
413 | <p>Double-clicking an item or pressing <em>Return</em> when an item is selected will open a context menu of possible actions:</p> | |
414 | <ul> | |
415 | <li>Copy</li> | |
416 | <li>Delete</li> | |
417 | <li>Edit</li> | |
418 | <li>Edit file</li> | |
419 | <li>Finish (unfinished tasks only)</li> | |
420 | <li>Reschedule</li> | |
421 | <li>Schedule new</li> | |
422 | <li>Open link (items with <code>@g</code> entries only)</li> | |
423 | <li>Show user details (items with <code>@u</code> entries only)</li> | |
424 | </ul> | |
425 | <p>When either <em>Copy</em> or <em>Edit</em> is chosen for a repeating item, you can further choose:</p> | |
426 | <ol style="list-style-type: decimal"> | |
427 | <li>this instance</li> | |
428 | <li>this and all subsequent instances</li> | |
429 | <li>all instances</li> | |
430 | </ol> | |
431 | <p>When <em>Delete</em> is chosen for a repeating item, a further choice is available:</p> | |
432 | <ol start="4" style="list-style-type: decimal"> | |
433 | <li>all previous instances</li> | |
434 | </ol> | |
435 | <p>Tip: Use <em>Reschedule</em> to enter a date for an undated item or to change the scheduled date for the item or the selected instance of a repeating item. All you have to do is enter the new (fuzzy parsed) datetime.</p> | |
436 | <h2 id="sharing-with-other-calendar-applications"><a href="#sharing-with-other-calendar-applications">Sharing with other calendar applications</a></h2> | |
437 | <p>Both export and import are supported for files in iCalendar format in ways that depend upon settings in <code>etmtk.cfg</code>.</p> | |
438 | <p>If an absolute path is entered for <code>current_icsfolder</code>, for example, then <code>.ics</code> files corresponding to the entries in <code>calendars</code> will be created in this folder and updated as necessary. If there are no entries in calendars, then a single file, <code>all.ics</code>, will be created in this folder and updated as necessary.</p> | |
439 | <p>If an item is selected, then pressing Shift-X in the gui will export the selected item in iCalendar format to <code>icsitem_file</code>. If an item is not selected, pressing Shift-X will export the active calendars in iCalendar format to <code>icscal_file</code>.</p> | |
440 | <p>If <code>icssync_folder</code> is given, then files in this folder with the extension <code>.txt</code> and <code>.ics</code> will automatically kept concurrent using export to iCalendar and import from iCalendar. I.e., if the <code>.txt</code> file is more recent than than the <code>.ics</code> then the <code>.txt</code> file will be exported to the <code>.ics</code> file. On the other hand, if the <code>.ics</code> file is more recent then it will be imported to the <code>.txt</code> file. In either case, the contents of the file to be updated will be overwritten with the new content and the last acess/modified times for both will be set to the current time.</p> | |
441 | <p>If <code>ics_subscriptions</code> is given, it should be a list of [URL, FILE] tuples. The URL is a calendar subscription, e.g., for a Google Calendar subscription the URL, FILE tuple would be something like:</p> | |
442 | <pre><code> ['https://www.google.com/calendar/ical/.../basic.ics', 'personal/google.txt'] | |
443 | </code></pre> | |
444 | <p>With this entry, pressing Shift-M in the gui would import the calendar from the URL, convert it from ics to etm format and then write the result to <code>personal/google.txt</code> in the etm data directory. Note that this data file should be regarded as read-only since any changes made to it will be lost with the next subscription update.</p> | |
445 | <p>Finally, when creating a new item in the etm editor, you can paste an iCalendar entry such as the following VEVENT:</p> | |
446 | <pre><code>BEGIN:VCALENDAR | |
447 | VERSION:2.0 | |
448 | PRODID:-//ForeTees//NONSGML v1.0//EN | |
449 | CALSCALE:GREGORIAN | |
450 | METHOD:PUBLISH | |
451 | BEGIN:VEVENT | |
452 | UID:1403607754438-11547@127.0.0.1-33 | |
453 | DTSTAMP:20140624T070234 | |
454 | DTSTART:20140630T080000 | |
455 | SUMMARY:8:00 AM Tennis Reservation | |
456 | LOCATION:Governors Club | |
457 | DESCRIPTION: Player 1: ... | |
458 | ||
459 | URL:http://www1.foretees.com/governorsclub | |
460 | END:VEVENT | |
461 | END:VCALENDAR</code></pre> | |
462 | <p>When you press <em>Finish</em>, the entry will be converted to etm format</p> | |
463 | <pre><code>^ 8:00 AM Tennis Reservation @s 2014-06-30 8am | |
464 | @d Player 1: ... | |
465 | @z US/Eastern</code></pre> | |
466 | <p>and you can choose the file to hold it.</p> | |
467 | <p>The following etm and iCalendar item types are supported:</p> | |
468 | <ul> | |
469 | <li><p>export from etm:</p> | |
470 | <ul> | |
471 | <li>occasion to VEVENT without end time</li> | |
472 | <li>event (with or without extent) to VEVENT</li> | |
473 | <li>action to VJOURNAL</li> | |
474 | <li>note to VJOURNAL</li> | |
475 | <li>task to VTODO</li> | |
476 | <li>delegated task to VTODO</li> | |
477 | <li>task group to VTODO (one for each job)</li> | |
478 | </ul></li> | |
479 | <li><p>import from iCalendar</p> | |
480 | <ul> | |
481 | <li>VEVENT without end time to occasion</li> | |
482 | <li>VEVENT with end time to event</li> | |
483 | <li>VJOURNAL to note</li> | |
484 | <li>VTODO to task</li> | |
485 | </ul></li> | |
486 | </ul> | |
487 | <h2 id="tools"><a href="#tools">Tools</a></h2> | |
488 | <h3 id="date-and-time-calculator"><a href="#date-and-time-calculator">Date and time calculator</a></h3> | |
489 | <p>Enter an expression of the form <code>x [+-] y</code> where <code>x</code> is a date and <code>y</code> is either a date or a time period if <code>-</code> is used and a time period if <code>+</code> is used. Both <code>x</code> and <code>y</code> can be followed by timezones, e.g.,</p> | |
490 | <pre><code> 4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai: | |
491 | ||
492 | 14h25m</code></pre> | |
493 | <p>or</p> | |
494 | <pre><code> 4/20 4:50p Asia/Shanghai + 14h25m US/Central: | |
495 | ||
496 | 2014-04-20 18:15-0500</code></pre> | |
497 | <p>The local timezone is assumed when none is given.</p> | |
498 | <h3 id="available-dates-calculator"><a href="#available-dates-calculator">Available dates calculator</a></h3> | |
499 | <p>Enter an expression of the form</p> | |
500 | <pre><code>start; end; busy</code></pre> | |
501 | <p>where start and end are dates and busy is comma separated list of busy dates or busy intervals. E.g., entering</p> | |
502 | <pre><code>6/1; 6/30; 6/2, 6/14-6/22, 6/5-6/9, 6/11-6/15, 6/17-6/29</code></pre> | |
503 | <p>would give:</p> | |
504 | <pre><code>Sun Jun 01 | |
505 | Tue Jun 03 | |
506 | Wed Jun 04 | |
507 | Tue Jun 10 | |
508 | Mon Jun 30</code></pre> | |
509 | <h3 id="yearly-calendar"><a href="#yearly-calendar">Yearly calendar</a></h3> | |
510 | <p>Gives a display such as</p> | |
511 | <pre><code> January 2014 February 2014 March 2014 | |
512 | Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su | |
513 | 1 2 3 4 5 1 2 1 2 | |
514 | 6 7 8 9 10 11 12 3 4 5 6 7 8 9 3 4 5 6 7 8 9 | |
515 | 13 14 15 16 17 18 19 10 11 12 13 14 15 16 10 11 12 13 14 15 16 | |
516 | 20 21 22 23 24 25 26 17 18 19 20 21 22 23 17 18 19 20 21 22 23 | |
517 | 27 28 29 30 31 24 25 26 27 28 24 25 26 27 28 29 30 | |
518 | 31 | |
519 | ||
520 | April 2014 May 2014 June 2014 | |
521 | Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su | |
522 | 1 2 3 4 5 6 1 2 3 4 1 | |
523 | 7 8 9 10 11 12 13 5 6 7 8 9 10 11 2 3 4 5 6 7 8 | |
524 | 14 15 16 17 18 19 20 12 13 14 15 16 17 18 9 10 11 12 13 14 15 | |
525 | 21 22 23 24 25 26 27 19 20 21 22 23 24 25 16 17 18 19 20 21 22 | |
526 | 28 29 30 26 27 28 29 30 31 23 24 25 26 27 28 29 | |
527 | 30 | |
528 | ||
529 | July 2014 August 2014 September 2014 | |
530 | Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su | |
531 | 1 2 3 4 5 6 1 2 3 1 2 3 4 5 6 7 | |
532 | 7 8 9 10 11 12 13 4 5 6 7 8 9 10 8 9 10 11 12 13 14 | |
533 | 14 15 16 17 18 19 20 11 12 13 14 15 16 17 15 16 17 18 19 20 21 | |
534 | 21 22 23 24 25 26 27 18 19 20 21 22 23 24 22 23 24 25 26 27 28 | |
535 | 28 29 30 31 25 26 27 28 29 30 31 29 30 | |
536 | ||
537 | October 2014 November 2014 December 2014 | |
538 | Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su | |
539 | 1 2 3 4 5 1 2 1 2 3 4 5 6 7 | |
540 | 6 7 8 9 10 11 12 3 4 5 6 7 8 9 8 9 10 11 12 13 14 | |
541 | 13 14 15 16 17 18 19 10 11 12 13 14 15 16 15 16 17 18 19 20 21 | |
542 | 20 21 22 23 24 25 26 17 18 19 20 21 22 23 22 23 24 25 26 27 28 | |
543 | 27 28 29 30 31 24 25 26 27 28 29 30 29 30 31</code></pre> | |
544 | <p>Left and right cursor keys move backward and forward a year at a time, respectively, and pressing the spacebar returns to the current year.</p> | |
545 | <h3 id="history-of-changes"><a href="#history-of-changes">History of changes</a></h3> | |
546 | <p>This requires that either <em>git</em> or <em>mercurial</em> is installed. If an item is selected show a history of changes to the file that contains the item. Otherwise show a history of changes for all etm data files. In either case, choose an integer number of the most recent changes to show or choose 0 to show all changes.</p> | |
547 | <h2 id="data-organization-and-calendars"><a href="#data-organization-and-calendars">Data Organization and Calendars</a></h2> | |
548 | <p><em>etm</em> offers two hierarchical ways of organizing your data: by keyword and file path. There are no hard and fast rules about how to use these hierarchies but the goal is a system that makes complementary uses of folder and keyword and fits your needs. As with any filing system, planning and consistency are paramount.</p> | |
549 | <p>For example, one pattern of use for a business would be to use folders for people and keywords for client-project-category.</p> | |
550 | <p>Similarly, a family could use folders to separate personal and shared items for family members, for example:</p> | |
551 | <pre><code>root etm data directory | |
552 | personal | |
553 | dag | |
554 | erp | |
555 | shared | |
556 | holidays | |
557 | birthdays | |
558 | events</code></pre> | |
559 | <p>Here</p> | |
560 | <pre><code>~dag/.etm/etm.cfg | |
561 | ~erp/.etm/etm.cfg</code></pre> | |
562 | <p>would both contain <code>datadir</code> entries specifying the common root data directory. Additionally, if these configuration files contained, respectively, the entries</p> | |
563 | <pre><code>~dag/.etm/etm.cfg | |
564 | calendars | |
565 | - [dag, true, personal/dag] | |
566 | - [erp, false, personal/erp] | |
567 | - [shared, true, shared]</code></pre> | |
568 | <p>and</p> | |
569 | <pre><code>~erp/.etm/etm.cfg | |
570 | calendars | |
571 | - [erp, true, personal/erp] | |
572 | - [dag, false, personal/dag] | |
573 | - [shared, true, shared]</code></pre> | |
574 | <p>then, by default, both dag and erp would see the entries from their personal files as well as the shared entries and each could optionally view the entries from the other's personal files as well. See the <a href="#preferences">Preferences</a> for details on the <code>calendars</code> entry.</p> | |
575 | <p>Note for Windows users. The path separator needs to be "escaped" in the calendar paths, e.g., you should enter</p> | |
576 | <pre><code> - [dag, true, personal\\dag]</code></pre> | |
577 | <p>instead of</p> | |
578 | <pre><code> - [dag, true, personal\dag]</code></pre> | |
579 | <h1 id="item-types"><a href="#item-types">Item types</a></h1> | |
580 | <p>There are several types of items in etm. Each item begins with a type character such as an asterisk (event) and continues on one or more lines either until the end of the file is reached or another line is found that begins with a type character. The type character for each item is followed by the item summary and then, perhaps, by one or more <code>@key value</code> pairs - see <a href="#keys">@-Keys</a> for details. The order in which such pairs are entered does not matter.</p> | |
581 | <h2 id="action"><a href="#action">~ Action</a></h2> | |
582 | <p>A record of the expenditure of time (<code>@e</code>) and/or money (<code>@x</code>). Actions are not reminders, they are instead records of how time and/or money was actually spent. Action lines begin with a tilde, <code>~</code>.</p> | |
583 | <pre><code> ~ picked up lumber and paint @s mon 3p @e 1h15m @x 127.32</code></pre> | |
584 | <p>Entries such as <code>@s mon 3p</code>, <code>@e 1h15m</code> and <code>@x 127.32</code> are discussed below under <em>Item details</em>. Action entries form the basis for time and expense billing using action reports - see <a href="#reports">Reports</a> for details.</p> | |
585 | <p>Tip: You can use either path or keyword or a combination of the two to organize your actions.</p> | |
586 | <h2 id="event"><a href="#event">* Event</a></h2> | |
587 | <p>Something that will happen on particular day(s) and time(s). Event lines begin with an asterick, <code>*</code>.</p> | |
588 | <pre><code> * dinner with Karen and Al @s sat 7p @e 3h</code></pre> | |
589 | <p>Events have a starting datetime, <code>@s</code> and an extent, <code>@e</code>. The ending datetime is given implicitly as the sum of the starting datetime and the extent. Events that span more than one day are possible, e.g.,</p> | |
590 | <pre><code> * Sales conference @s 9a wed @e 2d8h</code></pre> | |
591 | <p>begins at 9am on Wednesday and ends at 5pm on Friday.</p> | |
592 | <p>An event without an <code>@e</code> entry or with <code>@e 0</code> is regarded as a <em>reminder</em> and, since there is no extent, will not be displayed in <em>busy times</em>.</p> | |
593 | <h2 id="occasion"><a href="#occasion">^ Occasion</a></h2> | |
594 | <p>Holidays, anniversaries, birthdays and such. Similar to an event with a date but no starting time and no extent. Occasions begin with a caret sign, <code>^</code>.</p> | |
595 | <pre><code> ^ The !1776! Independence Day @s 2010-07-04 @r y &M 7 &m 4</code></pre> | |
596 | <p>On July 4, 2013, this would appear as <code>The 237th Independence Day</code>. Here !1776!` is an example of an <em>anniversary substitution</em> - see <a href="#dates">Dates</a> for details.</p> | |
597 | <h2 id="note"><a href="#note">! Note</a></h2> | |
598 | <p>A record of some useful information. Note lines begin with an exclamation point, <code>!</code>.</p> | |
599 | <pre><code>! xyz software @k software:passwords @d user: dnlg, pw: abc123def</code></pre> | |
600 | <p>Tip: Since both the GUI and CLI note views group and sort by keyword, it is a good idea to use keywords to organize your notes.</p> | |
601 | <h2 id="task"><a href="#task">- Task</a></h2> | |
602 | <p>Something that needs to be done. It may or may not have a due date. Task lines begin with a minus sign, <code>-</code>.</p> | |
603 | <pre><code>- pay bills @s Oct 25</code></pre> | |
604 | <p>A task with an <code>@s</code> entry becomes due on that date and past due when that date has passed. If the task also has an <code>@b</code> begin-by entry, then advance warnings of the task will begin appearing the specified number of days before the task is due. An <code>@e</code> entry in a task is interpreted as an estimate of the time required to finish the task.</p> | |
605 | <h2 id="delegated-task"><a href="#delegated-task">% Delegated task</a></h2> | |
606 | <p>A task that is assigned to someone else, usually the person designated in an <code>@u</code> entry. Delegated tasks begin with a percent sign, <code>%</code>.</p> | |
607 | <pre><code> % make reservations for trip @u joe @s fri</code></pre> | |
608 | <h2 id="task-group"><a href="#task-group">+ Task group</a></h2> | |
609 | <p>A collection of related tasks, some of which may be prerequisite for others. Task groups begin with a plus sign, <code>+</code>.</p> | |
610 | <pre><code> + dog house | |
611 | @j pickup lumber and paint &q 1 | |
612 | @j cut pieces &q 2 | |
613 | @j assemble &q 3 | |
614 | @j paint &q 4</code></pre> | |
615 | <p>Note that a task group is a single item and is treated as such. E.g., if any job is selected for editing then the entire group is displayed.</p> | |
616 | <p>Individual jobs are given by the <code>@j</code> entries. The <em>queue</em> entries, <code>&q</code>, set the order --- tasks with smaller &q values are prerequisites for subsequent tasks with larger &q values. In the example above, neither "pickup lumber" nor "pickup paint" have any prerequisites. "Pickup lumber", however, is a prerequisite for "cut pieces" which, in turn, is a prerequisite for "assemble". Both "assemble" and "pickup paint" are prerequisites for "paint".</p> | |
617 | <h2 id="in-basket"><a href="#in-basket">$ In basket</a></h2> | |
618 | <p>A quick, don't worry about the details item to be edited later when you have the time. In basket entries begin with a dollar sign, <code>$</code>.</p> | |
619 | <pre><code> $ joe 919 123-4567</code></pre> | |
620 | <p>If you create an item using <em>etm</em> and forget to provide a type character, an <code>$</code> will automatically be inserted.</p> | |
621 | <h2 id="someday-maybe"><a href="#someday-maybe">? Someday maybe</a></h2> | |
622 | <p>Something are you don't want to forget about altogether but don't want to appear on your next or scheduled lists. Someday maybe items begin with a question mark, <code>?</code>.</p> | |
623 | <pre><code> ? lose weight and exercise more</code></pre> | |
624 | <h2 id="hidden"><a href="#hidden"># Hidden</a></h2> | |
625 | <p>Hidden items begin with a hash mark, <code>#</code>. Such items are ignored by etm save for appearing in the folder view. Stick a hash mark in front of any item that you don't want to delete but don't want to see in your other views.</p> | |
626 | <h2 id="defaults"><a href="#defaults">= Defaults</a></h2> | |
627 | <p>Default entries begin with an equal sign, <code>=</code>. These entries consist of <code>@key value</code> pairs which then become the defaults for subsequent entries in the same file until another <code>=</code> entry is reached.</p> | |
628 | <p>Suppose, for example, that a particular file contains items relating to "project_a" for "client_1". Then entering</p> | |
629 | <pre><code>= @k client_1:project_a</code></pre> | |
630 | <p>on the first line of the file and</p> | |
631 | <pre><code>=</code></pre> | |
632 | <p>on the twentieth line of the file would set the default keyword for entries between the first and twentieth line in the file.</p> | |
633 | <h1 id="keys"><a href="#keys">@Keys</a></h1> | |
634 | <h2 id="a-alert"><a href="#a-alert"><span class="citation">@a</span> alert</a></h2> | |
635 | <p>The specification of the alert(s) to use with the item. One or more alerts can be specified in an item. E.g.,</p> | |
636 | <pre><code>@a 10m, 5m | |
637 | @a 1h: s</code></pre> | |
638 | <p>would trigger the alert(s) specified by <code>default_alert</code> in your <code>etm.cfg</code> at 10 and 5 minutes before the starting time and a (s)ound alert one hour before the starting time.</p> | |
639 | <p>The alert</p> | |
640 | <pre><code>@a 2d: e; who@what.com, where2@when.org; filepath1, filepath2</code></pre> | |
641 | <p>would send an email to the two listed recipients exactly 2 days (48 hours) before the starting time of the item with the item summary as the subject, with file1 and file2 as attachments and with the body of the message composed using <code>email_template</code> from your <code>etm.cfg</code>.</p> | |
642 | <p>Similarly, the alert</p> | |
643 | <pre><code>@a 10m: t; 9191234567@vtext.com, 9197654321@txt.att.net</code></pre> | |
644 | <p>would send a text message 10 minutes before the starting time of the item to the two mobile phones listed (using 10 digit area code and carrier mms extension) together with the settings for <code>sms</code> in <code>etm.cfg</code>. If no numbers are given, the number and mms extension specified in <code>sms.phone</code> will be used. Here are the mms extensions for the major US carriers:</p> | |
645 | <pre><code>Alltel @message.alltel.com | |
646 | AT&T @txt.att.net | |
647 | Nextel @messaging.nextel.com | |
648 | Sprint @messaging.sprintpcs.com | |
649 | SunCom @tms.suncom.com | |
650 | T-mobile @tmomail.net | |
651 | VoiceStream @voicestream.net | |
652 | Verizon @vtext.com</code></pre> | |
653 | <p>Finally,</p> | |
654 | <pre><code>@a 0: p; program_path</code></pre> | |
655 | <p>would execute <code>program_path</code> at the starting time of the item.</p> | |
656 | <p>The format for each of these:</p> | |
657 | <pre><code>@a <trigger times> [: action [; arguments]]</code></pre> | |
658 | <p>In addition to the default action used when the optional <code>: action</code> is not given, there are the following possible values for <code>action</code>:</p> | |
659 | <pre><code>d Execute alert_displaycmd in etm.cfg. | |
660 | ||
661 | e; recipients[;attachments] Send an email to recipients (a comma separated list of email addresses) optionally attaching attachments (a comma separated list of file paths). The item summary is used as the subject of the email and the expanded value of email_template from etm.cfg as the body. | |
662 | ||
663 | m Display an internal etm message box using alert_template. | |
664 | ||
665 | p; process Execute the command given by process. | |
666 | ||
667 | s Execute alert_soundcmd in etm.cfg. | |
668 | ||
669 | t [; phonenumbers] Send text messages to phonenumbers (a comma separated list of 10 digit phone numbers with the sms extension of the carrier appended) with the expanded value of sms.message as the text message. | |
670 | ||
671 | v Execute alert_voicecmd in etm.cfg.</code></pre> | |
672 | <p>Note: either <code>e</code> or <code>p</code> can be combined with other actions in a single alert but not with one another.</p> | |
673 | <h2 id="b-beginby"><a href="#b-beginby"><span class="citation">@b</span> beginby</a></h2> | |
674 | <p>An integer number of days before the starting date time at which to begin displaying <em>begin by</em> notices. When notices are displayed they will be sorted by the item's starting datetime and then by the item's priority, if any.</p> | |
675 | <h2 id="c-context"><a href="#c-context"><span class="citation">@c</span> context</a></h2> | |
676 | <p>Intended primarily for tasks to indicate the context in which the task can be completed. Common contexts include home, office, phone, computer and errands. The "next view" supports this usage by showing undated tasks, grouped by context. If you're about to run errands, for example, you can open the "next view", look under "errands" and be sure that you will have no "wish I had remembered" regrets.</p> | |
677 | <h2 id="d-description"><a href="#d-description"><span class="citation">@d</span> description</a></h2> | |
678 | <p>An elaboration of the details of the item to complement the summary.</p> | |
679 | <h2 id="e-extent"><a href="#e-extent"><span class="citation">@e</span> extent</a></h2> | |
680 | <p>A time period string such as <code>1d2h</code> (1 day 2 hours). For an action, this would be the elapsed time. For a task, this could be an estimate of the time required for completion. For an event, this would be the duration. The ending time of the event would be this much later than the starting datetime.</p> | |
681 | <p>Tip. Need to determine the appropriate value for <code>@e</code> for a flight when you have the departure and arrival datetimes but the timezones are different? The date calculator (shortcut F5) will accept timezone information so that, e.g., entering the arrival time minus the departure time</p> | |
682 | <pre><code>4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai</code></pre> | |
683 | <p>into the calculator would give</p> | |
684 | <pre><code>14h25m</code></pre> | |
685 | <p>as the flight time.</p> | |
686 | <h2 id="f-done-due"><a href="#f-done-due"><span class="citation">@f</span> done[; due]</a></h2> | |
687 | <p>Datetimes; tasks, delegated tasks and task groups only. When a task is completed an <code>@f done</code> entry is added to the task. When the task has a due date, <code>; due</code> is appended to the entry. Similarly, when a job from a task group is completed in etm, an <code>&f done</code> or <code>&f done; due</code> entry is appended to the job and it is removed from the list of prerequisites for the other jobs. In both cases <code>done</code> is the completion datetime and <code>due</code>, if added, is the datetime that the task or job was due. The completed task or job is shown as finished on the completion date. When the last job in a task group is finished an <code>@f done</code> or <code>@f done; due</code> entry is added to the task group itself reflecting the datetime that the last job was done and, if the task group is repeating, the <code>&f</code> entries are removed from the individual jobs.</p> | |
688 | <p>Another step is taken for repeating task groups. When the first job in a task group is completed, the <code>@s</code> entry is updated using the setting for <code>@o</code> (above) to show the next datetime the task group is due and the <code>@f</code> entry is removed from the task group. This means when some, but not all of the jobs for the current repetition have been completed, only these job completions will be displayed. Otherwise, when none of the jobs for the current repetition have been completed, then only that last completion of the task group itself will be displayed.</p> | |
689 | <p>Consider, for example, the following repeating task group which repeats monthly on the last weekday on or before the 25th.</p> | |
690 | <pre><code>+ pay bills @s 11/23 @f 10/24;10/25 | |
691 | @r m &w MO,TU,WE,TH,FR &m 23,24,25 &s -1 | |
692 | @j organize bills &q 1 | |
693 | @j pay on-line bills &q 3 | |
694 | @j get stamps, envelopes, checkbook &q 1 | |
695 | @j write checks &q 2 | |
696 | @j mail checks &q 3</code></pre> | |
697 | <p>Here "organize bills" and "get stamps, envelopes, checkbook" have no prerequisites. "Organize bills", however, is a prerequisite for "pay on-line bills" and both "organize bills" and "get stamps, envelops, checkbook" are prerequisites for "write checks" which, in turn, is a prerequisite for "mail checks".</p> | |
698 | <p>The repetition that was due on 10/25 was completed on 10/24. The next repetition was due on 11/23 and, since none of the jobs for this repetition have been completed, the completion of the group on 10/24 and the list of jobs due on 11/23 will be displayed initially. The following sequence of screen shots show the effect of completing the jobs for the 11/23 repetition one by one on 11/27.</p> | |
699 | <h2 id="g-goto"><a href="#g-goto"><span class="citation">@g</span> goto</a></h2> | |
700 | <p>The path to a file or a URL to be opened using the system default application when the user presses <em>G</em> in the GUI. E.g., here's a task to join the etm discussion group with the URL of the group as the link. In this case, pressing <em>G</em> would open the URL in your default browser.</p> | |
701 | <pre><code>- join the etm discussion group @s +1/1 | |
702 | @g http://groups.google.com/group/eventandtaskmanager/topics</code></pre> | |
703 | <p>Tip. Have a pdf file with the agenda for a meeting? Stick an <span class="citation">@g</span> entry with the path to the file in the event you create for the meeting. Then whenever the meeting is selected, <em>G</em> will bring up the agenda.</p> | |
704 | <h2 id="h-history"><a href="#h-history"><span class="citation">@h</span> history</a></h2> | |
705 | <p>Used internally with task groups to track completion done;due pairs.</p> | |
706 | <h2 id="j-job"><a href="#j-job"><span class="citation">@j</span> job</a></h2> | |
707 | <p>Component tasks or jobs within a task group are given by <code>@j job</code> entries. <code>@key value</code> entries prior to the first <code>@j</code> become the defaults for the jobs that follow. <code>&key value</code> entries given in jobs use <code>&</code> rather than <code>@</code> and apply only to the specific job.</p> | |
708 | <p>Many key-value pairs can be given either in the group task using <code>@</code> or in the component jobs using <code>&</code>:</p> | |
709 | <pre><code>@c or &c context | |
710 | @d or &d description | |
711 | @e or &e extent | |
712 | @f or &f done[; due] datetime | |
713 | @k or &k keyword | |
714 | @l or &l location | |
715 | @u or &u user</code></pre> | |
716 | <p>The key-value pair <code>&h</code> is used internally to track job done;due completions in task groups.</p> | |
717 | <p>The key-value pair <code>&q</code> (queue position) can <em>only</em> be given in component jobs where it is required. Key-values other than <code>&q</code> and those listed above, can <em>only</em> be given in the initial group task entry and their values are inherited by the component jobs.</p> | |
718 | <h2 id="k-keyword"><a href="#k-keyword"><span class="citation">@k</span> keyword</a></h2> | |
719 | <p>A heirarchical classifier for the item. Intended for actions to support time billing where a common format would be <code>client:job:category</code>. <em>etm</em> treats such a keyword as a heirarchy so that an action report grouped by month and then keyword might appear as follows</p> | |
720 | <pre><code> 27.5h) Client 1 (3) | |
721 | 4.9h) Project A (1) | |
722 | 15h) Project B (1) | |
723 | 7.6h) Project C (1) | |
724 | 24.2h) Client 2 (3) | |
725 | 3.1h) Project D (1) | |
726 | 21.1h) Project E (2) | |
727 | 5.1h) Category a (1) | |
728 | 16h) Category b (1) | |
729 | 4.2h) Client 3 (1) | |
730 | 8.7h) Client 4 (2) | |
731 | 2.1h) Project F (1) | |
732 | 6.6h) Project G (1)</code></pre> | |
733 | <p>An arbitrary number of heirarchical levels in keywords is supported.</p> | |
734 | <h2 id="l-location"><a href="#l-location"><span class="citation">@l</span> location</a></h2> | |
735 | <p>The location at which, for example, an event will take place.</p> | |
736 | <h2 id="m-memo"><a href="#m-memo"><span class="citation">@m</span> memo</a></h2> | |
737 | <p>Further information about the item not included in the summary or the description. Since the summary is used as the subject of an email alert and the descripton is commonly included in the body of an email alert, this field could be used for information not to be included in the email.</p> | |
738 | <h2 id="o-overdue"><a href="#o-overdue"><span class="citation">@o</span> overdue</a></h2> | |
739 | <p>Repeating tasks only. One of the following choices: k) keep, r) restart, or s) skip. Details below.</p> | |
740 | <h2 id="p-priority"><a href="#p-priority"><span class="citation">@p</span> priority</a></h2> | |
741 | <p>Either 0 (no priority) or an intger between 1 (highest priority) and 9 (lowest priority). Primarily used with undated tasks.</p> | |
742 | <h2 id="r-repetition-rule"><a href="#r-repetition-rule"><span class="citation">@r</span> repetition rule</a></h2> | |
743 | <p>The specification of how an item is to repeat. Repeating items <strong>must</strong> have an <code>@s</code> entry as well as one or more <code>@r</code> entries. Generated datetimes are those satisfying any of the <code>@r</code> entries and falling <strong>on or after</strong> the datetime given in <code>@s</code>. Note that the datetime given in <code>@s</code> will only be included if it matches one of the datetimes generated by the <code>@r</code> entry.</p> | |
744 | <p>A repetition rule begins with</p> | |
745 | <pre><code>@r frequency</code></pre> | |
746 | <p>where <code>frequency</code> is one of the following characters:</p> | |
747 | <pre><code>y yearly | |
748 | m monthly | |
749 | w weekly | |
750 | d daily | |
751 | h hourly | |
752 | n minutely | |
753 | l list (a list of datetimes will be provided using @+)</code></pre> | |
754 | <p>The <code>@r frequency</code> entry can, optionally, be followed by one or more <code>&key value</code> pairs:</p> | |
755 | <pre><code>&i: interval (positive integer, default = 1) E.g, with frequency w, interval 3 would repeat every three weeks. | |
756 | &t: total (positive integer) Include no more than this total number of repetitions. | |
757 | &s: bysetpos (integer). When multiple dates satisfy the rule, take the date from this position in the list, e.g, &s 1 would choose the first element and &s -1 the last. See the payday example below for an illustration of bysetpos. | |
758 | &u: until (datetime) Only include repetitions falling **before** (not including) this datetime. | |
759 | &M: bymonth (1, 2, ..., 12) | |
760 | &m: bymonthday (1, 2, ..., 31) Use, e.g., -1 for the last day of the month. | |
761 | &W: byweekno (1, 2, ..., 53) | |
762 | &w: byweekday (*English* weekday abbreviation SU ... SA). Use, e.g., 3WE for the 3rd Wednesday or -1FR, for the last Friday in the month. | |
763 | &h: byhour (0 ... 23) | |
764 | &n: byminute (0 ... 59) | |
765 | &E: byeaster (integer number of days before, < 0, or after, > 0, Easter)</code></pre> | |
766 | <p>Repetition examples:</p> | |
767 | <ul> | |
768 | <li><p>1st and 3rd Wednesdays of each month.</p> | |
769 | <pre><code>^ 1st and 3rd Wednesdays | |
770 | @r m &w 1WE, 3WE</code></pre></li> | |
771 | <li><p>Payday (an occasion) on the last week day of each month. (The <code>&s -1</code> entry extracts the last date which is both a weekday and falls within the last three days of the month.)</p> | |
772 | <pre><code>^ payday @s 2010-07-01 | |
773 | @r m &w MO, TU, WE, TH, FR &m -1, -2, -3 &s -1</code></pre></li> | |
774 | <li><p>Take a prescribed medication daily (an event) from the 23rd through the 27th of the current month at 10am, 2pm, 6pm and 10pm and trigger an alert zero minutes before each event.</p> | |
775 | <pre><code>* take Rx @d 10a 23 @r d &u 11p 27 &h 10, 14 18, 22 @a 0</code></pre></li> | |
776 | <li><p>Vote for president (an occasion) every four years on the first Tuesday after a Monday in November. (The <code>&m range(2,9)</code> requires the month day to fall within 2 ... 8 and thus, combined with <code>&w TU</code> to be the first Tuesday following a Monday.)</p> | |
777 | <pre><code>^ Vote for president @s 2012-11-06 | |
778 | @r y &i 4 &M 11 &m range(2,9) &w TU</code></pre></li> | |
779 | <li><p>Ash Wednesday (an occasion) that occurs 46 days before Easter each year.</p> | |
780 | <p>^ Ash Wednesday 2010-01-01 <span class="citation">@r</span> y &E -46</p></li> | |
781 | <li><p>Easter Sunday (an occasion).</p> | |
782 | <p>^ Easter Sunday 2010-01-01 <span class="citation">@r</span> y &E 0</p></li> | |
783 | </ul> | |
784 | <p>Optionally, <code>@+</code> and <code>@-</code> entries can be given.</p> | |
785 | <ul> | |
786 | <li><code>@+</code>: include (comma separated list to datetimes to be <em>added</em> to those generated by the <code>@r</code> entries)</li> | |
787 | <li><code>@-</code>: exclude (comma separated list to datetimes to be <em>removed</em> from those generated by the <code>@r</code> entries)</li> | |
788 | </ul> | |
789 | <p>A repeating <em>task</em> may optionally also include an <code>@o <k|s|r></code> entry (default = k):</p> | |
790 | <ul> | |
791 | <li><p><code>@o k</code>: Keep the current due date if it becomes overdue and use the next due date from the recurrence rule if it is finished early. This would be appropriate, for example, for the task 'file tax return'. The return due April 15, 2009 must still be filed even if it is overdue and the 2010 return won't be due until April 15, 2010 even if the 2009 return is finished early.</p></li> | |
792 | <li><p><code>@o s</code>: Skip overdue due dates and set the due date for the next repetition to the first due date from the recurrence rule on or after the current date. This would be appropriate, for example, for the task 'put out the trash' since there is no point in putting it out on Tuesday if it's picked up on Mondays. You might just as well wait until the next Monday to put it out. There's also no point in being reminded until the next Monday.</p></li> | |
793 | <li><p><code>@o r</code>: Restart the repetitions based on the last completion date. Suppose you want to mow the grass once every ten days and that when you mowed yesterday, you were already nine days past due. Then you want the next due date to be ten days from yesterday and not today. Similarly, if you were one day early when you mowed yesterday, then you would want the next due date to be ten days from yesterday and not ten days from today.</p></li> | |
794 | </ul> | |
795 | <h2 id="s-starting-datetime"><a href="#s-starting-datetime"><span class="citation">@s</span> starting datetime</a></h2> | |
796 | <p>When an action is started, an event begins or a task is due.</p> | |
797 | <h2 id="t-tags"><a href="#t-tags"><span class="citation">@t</span> tags</a></h2> | |
798 | <p>A tag or list of tags for the item.</p> | |
799 | <h2 id="u-user"><a href="#u-user"><span class="citation">@u</span> user</a></h2> | |
800 | <p>Intended to specify the person to whom a delegated task is assigned. Could also be used in actions to indicate the person performing the action.</p> | |
801 | <h2 id="v-action_rates-key"><a href="#v-action_rates-key"><span class="citation">@v</span> action_rates key</a></h2> | |
802 | <p>Actions only. A key from <code>action_rates</code> in your etm.cft to apply to the value of <code>@e</code>. Used in actions to apply a billing rate to time spent in an action. E.g., with</p> | |
803 | <pre><code> minutes: 6 | |
804 | action_rates: | |
805 | br1: 45.0 | |
806 | br2: 60.0</code></pre> | |
807 | <p>then entries of <code>@v br1</code> and <code>@e 2h25m</code> in an action would entail a value of <code>45.0 * 2.5 = 112.50</code>.</p> | |
808 | <h2 id="w-action_markups-key"><a href="#w-action_markups-key"><span class="citation">@w</span> action_markups key</a></h2> | |
809 | <p>A key from <code>action_markups</code> in your <code>etm.cfg</code> to apply to the value of <code>@x</code>. Used in actions to apply a markup rate to expense in an action. E.g., with</p> | |
810 | <pre><code> weights: | |
811 | mr1: 1.5 | |
812 | mr2: 10.0</code></pre> | |
813 | <p>then entries of <code>@w mr1</code> and <code>@x 27.50</code> in an action would entail a value of <code>27.50 * 1.5 = 41.25</code>.</p> | |
814 | <h2 id="x-expense"><a href="#x-expense"><span class="citation">@x</span> expense</a></h2> | |
815 | <p>Actions only. A currency amount such as <code>27.50</code>. Used in conjunction with <span class="citation">@w</span> above to bill for action expenditures.</p> | |
816 | <h2 id="z-time-zone"><a href="#z-time-zone"><span class="citation">@z</span> time zone</a></h2> | |
817 | <p>The time zone of the item, e.g., US/Eastern. The starting and other datetimes in the item will be interpreted as belonging to this time zone.</p> | |
818 | <p>Tip. You live in the US/Eastern time zone but a flight that departs Sydney on April 20 at 9pm bound for New York with a flight duration of 14 hours and 30 minutes. The hard way is to convert this to US/Eastern time and enter the flight using that time zone. The easy way is to use Australia/Sydney and skip the conversion:</p> | |
819 | <pre><code>* Sydney to New York @s 2014-04-23 9pm @e 14h30m @z Australia/Sydney</code></pre> | |
820 | <p>This flight will be displayed while you're in the Australia/Sydney time zone as extending from 9pm on April 23 until 11:30am on April 24, but in the US/Eastern time zone it will be displayed as extending from 7am until 9:30pm on April 23.</p> | |
821 | <h2 id="include"><a href="#include">@+ include</a></h2> | |
822 | <p>A datetime or list of datetimes to be added to the repetitions generated by the <code>@r rrule</code> entry. If only a date is provided, 12:00am is assumed.</p> | |
823 | <h2 id="exclude"><a href="#exclude">@- exclude</a></h2> | |
824 | <p>A datetime or list of datetimes to be removed from the repetitions generated by the <code>@r rrule</code> entry. If only a date is provided, 12:00am is assumed.</p> | |
825 | <p>Note that to exclude a datetime from the recurrence rule, the @- datetime <em>must exactly match both the date and time</em> generated by the recurrence rule.</p> | |
826 | <h1 id="dates"><a href="#dates">Dates</a></h1> | |
827 | <h2 id="fuzzy-dates"><a href="#fuzzy-dates">Fuzzy dates</a></h2> | |
828 | <p>When either a <em>datetime</em> or an <em>time period</em> is to be entered, special formats are used in <em>etm</em>. Examples include entering a starting datetime for an item using <code>@s</code>, jumping to a date using Ctrl-J and calculating a date using F5.</p> | |
829 | <p>Suppose, for example, that it is currently 8:30am on Friday, February 15, 2013. Then, <em>fuzzy dates</em> would expand into the values illustrated below.</p> | |
830 | <pre><code> mon 2p or mon 14h 2:00pm Monday, February 19 | |
831 | fri 12:00am Friday, February 15 | |
832 | 9a -1/1 or 9h -1/1 9:00am Tuesday, January 1 | |
833 | +2/15 12:00am Monday, April 15 2013 | |
834 | 8p +7 or 20h +7 8:00pm Friday, February 22 | |
835 | -14 12:00am Friday, February 1 | |
836 | now 8:30am Friday, February 15</code></pre> | |
837 | <p>Note that 12am is the default time when a time is not explicity entered. E.g., <code>+2/15</code> in the examples above gives 12:00am on April 15.</p> | |
838 | <p>To avoid ambiguity, always append either 'a', 'p' or 'h' when entering an hourly time, e.g., use <code>1p</code> or <code>13h</code>.</p> | |
839 | <h2 id="time-periods"><a href="#time-periods">Time periods</a></h2> | |
840 | <p>Time periods are entered using the format <code>DdHhMm</code> where D, H and M are integers and d, h and m refer to days, hours and minutes respectively. For example:</p> | |
841 | <pre><code> 2h30m 2 hours, 30 minutes | |
842 | 7d 7 days | |
843 | 45m 45 minutes</code></pre> | |
844 | <p>As an example, if it is currently 8:50am on Friday February 15, 2013, then entering <code>now + 2d4h30m</code> into the date calculator would give <code>2013-02-17 1:20pm</code>.</p> | |
845 | <h2 id="time-zones"><a href="#time-zones">Time zones</a></h2> | |
846 | <p>Dates and times are always stored in <em>etm</em> data files as times in the time zone given by the entry for <code>@z</code>. On the other hand, dates and times are always displayed in <em>etm</em> using the local time zone of the system.</p> | |
847 | <p>For example, if it is currently 8:50am EST on Friday February 15, 2013, and an item is saved on a system in the <code>US/Eastern</code> time zone containing the entry</p> | |
848 | <pre><code>@s now @z Australia/Sydney</code></pre> | |
849 | <p>then the data file would contain</p> | |
850 | <pre><code>@s 2013-02-16 12:50am @z Australia/Sydney</code></pre> | |
851 | <p>but this item would be displayed as starting at <code>8:50am 2013-02-15</code> on the system in the <code>US/Eastern</code> time zone.</p> | |
852 | <p>Tip. Need to determine the flight time when the departing timezone is different that the arriving timezone? The date calculator (shortcut F5) will accept timezone information so that, e.g., entering the arrival time minus the departure time</p> | |
853 | <pre><code>4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai</code></pre> | |
854 | <p>into the calculator would give</p> | |
855 | <pre><code>14h25m</code></pre> | |
856 | <p>as the flight time.</p> | |
857 | <h2 id="anniversary-substitutions"><a href="#anniversary-substitutions">Anniversary substitutions</a></h2> | |
858 | <p>An anniversary substitution is an expression of the form <code>!YYYY!</code> that appears in an item summary. Consider, for example, the occassion</p> | |
859 | <pre><code>^ !2010! anniversary @s 2011-02-20 @r y</code></pre> | |
860 | <p>This would appear on Feb 20 of 2011, 2012, 2013 and 2014, respectively, as <em>1st anniversary</em>, <em>2nd anniversary</em>, <em>3rd anniversary</em> and <em>4th anniversary</em>. The suffixes, <em>st</em>, <em>nd</em> and so forth, depend upon the translation file for the locale.</p> | |
861 | <h2 id="easter"><a href="#easter">Easter</a></h2> | |
862 | <p>An expression of the form <code>easter(yyyy)</code> can be used as a date specification in <code>@s</code> entries and in the datetime calculator. E.g.</p> | |
863 | <pre><code>@s easter(2014) 4p</code></pre> | |
864 | <p>would expand to <code>2014-04-20 4pm</code>. Similarly, in the date calculator</p> | |
865 | <pre><code>easter(2014) - 48d</code></pre> | |
866 | <p>(Rose Monday) would return <code>2014-03-03</code>. In repeating items <code>easter(yyyy)</code> is replaced by <code>&E</code>, e.g.,</p> | |
867 | <pre><code>^ Easter Sunday @s 2010-01-01 @r y &E 0 | |
868 | ^ Ash Wednesday @s 2010-01-01 @r y &E -46 | |
869 | ^ Rose Monday @s 2010-01-01 @r y &E -48</code></pre> | |
870 | <h1 id="preferences"><a href="#preferences">Preferences</a></h1> | |
871 | <p>Configuration options are stored in a file named <code>etmtk.cfg</code> which, by default, belongs to the folder <code>.etm</code> in your home directory. When this file is edited in <em>etm</em> (Shift Ctrl-P), your changes become effective as soon as they are saved --- you do not need to restart <em>etm</em>. These options are listed below with illustrative entries and brief descriptions.</p> | |
872 | <h2 id="template-expansions"><a href="#template-expansions">Template expansions</a></h2> | |
873 | <p>The following template expansions can be used in <code>alert_displaycmd</code>, <code>alert_template</code>, <code>alert_voicecmd</code>, <code>email_template</code>, <code>sms_message</code> and <code>sms_subject</code> below.</p> | |
874 | <h3 id="summary"><a href="#summary"><code>!summary!</code></a></h3> | |
875 | <p>the item's summary (this will be used as the subject of email and message alerts)</p> | |
876 | <h3 id="start_date"><a href="#start_date"><code>!start_date!</code></a></h3> | |
877 | <p>the starting date of the event</p> | |
878 | <h3 id="start_time"><a href="#start_time"><code>!start_time!</code></a></h3> | |
879 | <p>the starting time of the event</p> | |
880 | <h3 id="time_span"><a href="#time_span"><code>!time_span!</code></a></h3> | |
881 | <p>the time span of the event (see below)</p> | |
882 | <h3 id="alert_time"><a href="#alert_time"><code>!alert_time!</code></a></h3> | |
883 | <p>the time the alert is triggered</p> | |
884 | <h3 id="time_left"><a href="#time_left"><code>!time_left!</code></a></h3> | |
885 | <p>the time remaining until the event starts</p> | |
886 | <h3 id="when"><a href="#when"><code>!when!</code></a></h3> | |
887 | <p>the time remaining until the event starts as a sentence (see below)</p> | |
888 | <h3 id="d"><a href="#d"><code>!d!</code></a></h3> | |
889 | <p>the item's <code>@d</code> (description)</p> | |
890 | <h3 id="l"><a href="#l"><code>!l!</code></a></h3> | |
891 | <p>the item's <code>@l</code> (location)</p> | |
892 | <p>The value of <code>!time_span!</code> depends on the starting and ending datetimes. Here are some examples:</p> | |
893 | <ul> | |
894 | <li><p>if the start and end <em>datetimes</em> are the same (zero extent): <code>10am Wed, Aug 4</code></p></li> | |
895 | <li><p>else if the times are different but the <em>dates</em> are the same: <code>10am - 2pm Wed, Aug 4</code></p></li> | |
896 | <li><p>else if the dates are different: <code>10am Wed, Aug 4 - 9am Thu, Aug 5</code></p></li> | |
897 | <li><p>additionally, the year is appended if a date falls outside the current year:</p> | |
898 | <pre><code>10am - 2pm Thu, Jan 3 2013 | |
899 | 10am Mon, Dec 31 - 2pm Thu, Jan 3 2013</code></pre></li> | |
900 | </ul> | |
901 | <p>Here are values of <code>!time_left!</code> and <code>!when!</code> for some illustrative periods:</p> | |
902 | <ul> | |
903 | <li><p><code>2d3h15m</code></p> | |
904 | <pre><code>time_left : '2 days 3 hours 15 minutes' | |
905 | when : '2 days 3 hours 15 minutes from now'</code></pre></li> | |
906 | <li><p><code>20m</code></p> | |
907 | <pre><code>time_left : '20 minutes' | |
908 | when : '20 minutes from now'</code></pre></li> | |
909 | <li><p><code>0m</code></p> | |
910 | <pre><code>time_left : '' | |
911 | when : 'now'</code></pre></li> | |
912 | </ul> | |
913 | <p>Note that 'now', 'from now', 'days', 'day', 'hours' and so forth are determined by the translation file in use.</p> | |
914 | <h2 id="options"><a href="#options">Options</a></h2> | |
915 | <h3 id="action_interval"><a href="#action_interval">action_interval</a></h3> | |
916 | <pre><code>action_interval: 1</code></pre> | |
917 | <p>Every <code>action_interval</code> minutes, execute <code>action_timercmd</code> when the timer is running and <code>action_pausecmd</code> when the timer is paused. Choose zero to disable executing these commands.</p> | |
918 | <h3 id="action_markups"><a href="#action_markups">action_markups</a></h3> | |
919 | <pre><code>action_markups: | |
920 | default: 1.0 | |
921 | mu1: 1.5 | |
922 | mu2: 2.0</code></pre> | |
923 | <p>Possible markup rates to use for <code>@x</code> expenses in actions. An arbitrary number of rates can be entered using whatever labels you like. These labels can then be used in actions in the <code>@w</code> field so that, e.g.,</p> | |
924 | <pre><code>... @x 25.80 @w mu1 ...</code></pre> | |
925 | <p>in an action would give this expansion in an action template:</p> | |
926 | <pre><code>!expense! = 25.80 | |
927 | !charge! = 38.70</code></pre> | |
928 | <h3 id="action_minutes"><a href="#action_minutes">action_minutes</a></h3> | |
929 | <pre><code>action_minutes: 6</code></pre> | |
930 | <p>Round action times up to the nearest <code>action_minutes</code> in action reports. Possible choices are 1, 6, 12, 15, 30 and 60. With 1, no rounding is done and times are reported as integer minutes. Otherwise, the prescribed rounding is done and times are reported as floating point hours.</p> | |
931 | <h3 id="action_rates"><a href="#action_rates">action_rates</a></h3> | |
932 | <pre><code>action_rates: | |
933 | default: 30.0 | |
934 | br1: 45.0 | |
935 | br2: 60.0</code></pre> | |
936 | <p>Possible billing rates to use for <code>@e</code> times in actions. An arbitrary number of rates can be entered using whatever labels you like. These labels can then be used in the <code>@v</code> field in actions so that, e.g., with <code>action_minutes: 6</code> then:</p> | |
937 | <pre><code>... @e 75m @v br1 ...</code></pre> | |
938 | <p>in an action would give these expansions in an action template:</p> | |
939 | <pre><code>!hours! = 1.3 | |
940 | !value! = 58.50</code></pre> | |
941 | <p>If the label <code>default</code> is used, the corresponding rate will be used when <code>@v</code> is not specified in an action.</p> | |
942 | <p>Note that etm accumulates group totals from the <code>time</code> and <code>value</code> of individual actions. Thus</p> | |
943 | <pre><code>... @e 75m @v br1 ... | |
944 | ... @e 60m @v br2 ...</code></pre> | |
945 | <p>would aggregate to</p> | |
946 | <pre><code>!hours! = 2.3 (= 1.3 + 1) | |
947 | !value! = 118.50 (= 1.3 * 45.0 + 1 * 60.0)</code></pre> | |
948 | <h3 id="action_template"><a href="#action_template">action_template</a></h3> | |
949 | <pre><code>action_template: '!hours!h) !label! (!count!)'</code></pre> | |
950 | <p>Used for action reports. With the above settings for <code>action_minutes</code> and <code>action_template</code>, a report might appear as follows:</p> | |
951 | <pre><code>27.5h) Client 1 (3) | |
952 | 4.9h) Project A (1) | |
953 | 15h) Project B (1) | |
954 | 7.6h) Project C (1) | |
955 | 24.2h) Client 2 (3) | |
956 | 3.1h) Project D (1) | |
957 | 21.1h) Project E (2) | |
958 | 5.1h) Category a (1) | |
959 | 16h) Category b (1) | |
960 | 4.2h) Client 3 (1) | |
961 | 8.7h) Client 4 (2) | |
962 | 2.1h) Project F (1) | |
963 | 6.6h) Project G (1)</code></pre> | |
964 | <p>Available template expansions for <code>action_template</code> include:</p> | |
965 | <ul> | |
966 | <li><p><code>!label!</code>: the item or group label.</p></li> | |
967 | <li><p><code>!count!</code>: the number of children represented in the reported item or group.</p></li> | |
968 | <li><p><code>!minutes!:</code> the total time from <code>@e</code> entries in minutes rounded up using the setting for <code>action_minutes</code>.</p></li> | |
969 | <li><p><code>!hours!</code>: if action_minutes = 1, the time in hours and minutes. Otherwise, the time in floating point hours.</p></li> | |
970 | <li><p><code>!value!</code>: the billing value of the rounded total time. Requires an action entry such as <code>@v br1</code> and a setting for <code>action_rates</code>.</p></li> | |
971 | <li><p><code>!expense!</code>: the total expense from <code>@x</code> entries.</p></li> | |
972 | <li><p><code>!charge!</code>: the billing value of the total expense. Requires an action entry such as <code>@w mu1</code> and a setting for <code>action_markups</code>.</p></li> | |
973 | <li><p><code>!total!</code>: the sum of <code>!value!</code> and <code>!charge!</code>.</p></li> | |
974 | </ul> | |
975 | <p>Note: when aggregating amounts in action reports, billing and markup rates are applied first to times and expenses for individual actions and the resulting amounts are then aggregated. Similarly, when times are rounded up, the rounding is done for individual actions and the results are then aggregated.</p> | |
976 | <h3 id="action_timer"><a href="#action_timer">action_timer</a></h3> | |
977 | <pre><code>action_timer: | |
978 | paused: 'play ~/.etm/sounds/timer_paused.wav' | |
979 | running: 'play ~/.etm/sounds/timer_running.wav' | |
980 | idle: 'play ~/.etm/sounds/timer_idle.wav'</code></pre> | |
981 | <p>The command <code>running</code> is executed every <code>action_interval</code> minutes whenever the action timer is running and <code>paused</code> every minute when the action timer is paused. The command <code>idle</code> is executed every <code>action_interval</code> minutes when the idle timer is running and the action timer is neither running nor paused.</p> | |
982 | <h3 id="agenda"><a href="#agenda">agenda</a></h3> | |
983 | <pre><code>agenda_days: 4, | |
984 | agenda_colors: 2, | |
985 | agenda_indent: 2, | |
986 | agenda_width1: 43, | |
987 | agenda_width2: 17,</code></pre> | |
988 | <p>Sets the number of days with scheduled items to display in agenda view and other parameters affecting the display in the CLI.</p> | |
989 | <h3 id="alert_default"><a href="#alert_default">alert_default</a></h3> | |
990 | <pre><code>alert_default: [m]</code></pre> | |
991 | <p>The alert or list of alerts to be used when an alert is specified for an item but the type is not given. Possible values for the list include: - d: display (requires <code>alert_displaycmd</code>) - m: message (using <code>alert_template</code>) - s: sound (requires <code>alert_soundcmd</code>) - v: voice (requires <code>alert_voicecmd</code>)</p> | |
992 | <h3 id="alert_displaycmd"><a href="#alert_displaycmd">alert_displaycmd</a></h3> | |
993 | <pre><code>alert_displaycmd: growlnotify -t !summary! -m '!time_span!'</code></pre> | |
994 | <p>The command to be executed when <code>d</code> is included in an alert. Possible template expansions are discussed at the beginning of this tab.</p> | |
995 | <h3 id="alert_soundcmd"><a href="#alert_soundcmd">alert_soundcmd</a></h3> | |
996 | <pre><code>alert_soundcmd: 'play ~/.etm/sounds/etm_alert.wav'</code></pre> | |
997 | <p>The command to execute when <code>s</code> is included in an alert. Possible template expansions are discussed at the beginning of this tab.</p> | |
998 | <h3 id="alert_template"><a href="#alert_template">alert_template</a></h3> | |
999 | <pre><code>alert_template: '!time_span!\n!l!\n\n!d!'</code></pre> | |
1000 | <p>The template to use for the body of <code>m</code> (message) alerts. See the discussion of template expansions at the beginning of this tab for other possible expansion items.</p> | |
1001 | <h3 id="alert_voicecmd"><a href="#alert_voicecmd">alert_voicecmd</a></h3> | |
1002 | <pre><code>alert_voicecmd: say -v 'Alex' '!summary! begins !when!.'</code></pre> | |
1003 | <p>The command to be executed when <code>v</code> is included in an alert. Possible expansions are are discussed at the beginning of this tab.</p> | |
1004 | <h3 id="alert_wakecmd"><a href="#alert_wakecmd">alert_wakecmd</a></h3> | |
1005 | <pre><code>alert_wakecmd: ~/bin/SleepDisplay -w</code></pre> | |
1006 | <p>If given, this command will be issued to "wake up the display" before executing <code>alert_displaycmd</code>.</p> | |
1007 | <h3 id="ampm"><a href="#ampm">ampm</a></h3> | |
1008 | <pre><code>ampm: true</code></pre> | |
1009 | <p>Use ampm times if true and twenty-four hour times if false. E.g., 2:30pm (true) or 14:30 (false).</p> | |
1010 | <h3 id="completions_width"><a href="#completions_width">completions_width</a></h3> | |
1011 | <pre><code>completions_width: 36</code></pre> | |
1012 | <p>The width in characters of the auto completions popup window.</p> | |
1013 | <h3 id="calendars"><a href="#calendars">calendars</a></h3> | |
1014 | <pre><code>calendars: | |
1015 | - [dag, true, personal/dag] | |
1016 | - [erp, false, personal/erp] | |
1017 | - [shared, true, shared]</code></pre> | |
1018 | <p>These are (label, default, path relative to <code>datadir</code>) tuples to be interpreted as separate calendars. Those for which default is <code>true</code> will be displayed as default calendars. E.g., with the <code>datadir</code> below, <code>dag</code> would be a default calendar and would correspond to the absolute path <code>/Users/dag/.etm/data/personal/dag</code>. With this setting, the calendar selection dialog would appear as follows:</p> | |
1019 | <p>When non-default calendars are selected, busy times in the "week view" will appear in one color for events from default calendars and in another color for events from non-default calendars.</p> | |
1020 | <p><strong>Only data files that belong to one of the calendar directories or their subdirectories will be accessible within etm.</strong></p> | |
1021 | <h3 id="cfg_files"><a href="#cfg_files">cfg_files</a></h3> | |
1022 | <pre><code>cfg_files: | |
1023 | - completions: [] | |
1024 | - reports: [] | |
1025 | - users: []</code></pre> | |
1026 | <p>Each of the three list brackets can contain one or more comma separated <em>absolute</em> file paths. Additionally, paths corresponding to active calendars in the <code>datadir</code> directory are searched for files named <code>completions.cfg</code>, <code>reports.cfg</code> and <code>users.cfg</code> and these are processed in addition to the ones from <code>cfg_files</code>.</p> | |
1027 | <p>Note. Windows users should place each absolute path in quotes and escape backslashes, i.e., use <code>\\</code> anywhere <code>\</code> appears in a path.</p> | |
1028 | <ul> | |
1029 | <li><p>Completions</p> | |
1030 | <p>Each line in a completions file provides a possible completion when using the editor. E.g. with these completions</p> | |
1031 | <pre><code>@c computer | |
1032 | @c home | |
1033 | @c errands | |
1034 | @c office | |
1035 | @c phone | |
1036 | @z US/Eastern | |
1037 | @z US/Central | |
1038 | @z US/Mountain | |
1039 | @z US/Pacific | |
1040 | dnlgrhm@gmail.com</code></pre> | |
1041 | <p>entering, for example, "<span class="citation">@c</span>" in the editor and pressing Ctrl-Space, would popup a list of possible completions. Choosing the one you want and pressing <em>Return</em> would insert it and close the popup.</p> | |
1042 | <p>Up and down arrow keys change the selection and either <em>Tab</em> or <em>Return</em> inserts the selection.</p></li> | |
1043 | <li><p>Reports</p> | |
1044 | <p>Each line in a reports file provides a possible reports specification. These are available when using the CLI <code>m</code> command and in the GUI custom view. See <a href="#reports">Reports</a> for details.</p></li> | |
1045 | <li><p>Users</p> | |
1046 | <p>User files contain user (contact) information in a free form, text database. Each entry begins with a unique key for the person and is followed by detail lines each of which begins with a minus sign and contains some detail about the person that you want to record. Any detail line containing a colon should be quoted, e.g.,</p> | |
1047 | <pre><code>jbrown: | |
1048 | - Brown, Joe | |
1049 | - jbr@whatever.com | |
1050 | - 'home: 123 456-7890' | |
1051 | - 'birthday: 1978-12-14' | |
1052 | dcharles: | |
1053 | - Charles, Debbie | |
1054 | - dch@sometime.com | |
1055 | - 'cell: 456 789-0123' | |
1056 | - 'spouse: Rebecca'</code></pre> | |
1057 | <p>Keys from this file are added to auto-completions so that if you type, say, <code>@u jb</code> and press <em>Ctrl-Space</em>, then <code>@u jbrown</code> would be offered for completion.</p> | |
1058 | <p>If an item with the entry <code>@u jbrown</code> is selected in the GUI, you can press "u" to see a popup with the details:</p> | |
1059 | <pre><code>Brown, Joe | |
1060 | jbr@whatever.com | |
1061 | home: 123 456-7890 | |
1062 | birthday: 1978-12-14</code></pre></li> | |
1063 | </ul> | |
1064 | <h3 id="current-files"><a href="#current-files">current files</a></h3> | |
1065 | <pre><code>current_htmlfile: '' | |
1066 | current_textfile: '' | |
1067 | current_icsfolder: '' | |
1068 | current_indent: 3 | |
1069 | current_opts: '' | |
1070 | current_width1: 40 | |
1071 | current_width2: 17</code></pre> | |
1072 | <p>If absolute file paths are entered for <code>current_textfile</code> and/or <code>current_htmlfile</code>, then these files will be created and automatically updated by etm as as plain text or html files, respectively. If <code>current_opts</code> is given then the file will contain a report using these options; otherwise the file will contain an agenda. Indent and widths are taken from these setting with other settings, including color, from <em>report</em> or <em>agenda</em>, respectively.</p> | |
1073 | <p>If an absolute path is entered for <code>current_icsfolder</code>, then ics files corresponding to the entries in <code>calendars</code> will be created in this folder and updated as necessary. If there are no entries in calendars, then a single file, <code>all.ics</code>, will be created in this folder and updated as necessary.</p> | |
1074 | <p>Hint: fans of geektool can use the shell command <code>cat <current_textfile></code> to have the current agenda displayed on their desktops.</p> | |
1075 | <h3 id="datadir"><a href="#datadir">datadir</a></h3> | |
1076 | <pre><code>datadir: ~/.etm/data</code></pre> | |
1077 | <p>All etm data files are in this directory.</p> | |
1078 | <h3 id="dayfirst"><a href="#dayfirst">dayfirst</a></h3> | |
1079 | <pre><code>dayfirst: false</code></pre> | |
1080 | <p>If dayfirst is False, the MM-DD-YYYY format will have precedence over DD-MM-YYYY in an ambiguous date. See also <code>yearfirst</code>.</p> | |
1081 | <h3 id="details_rows"><a href="#details_rows">details_rows</a></h3> | |
1082 | <pre><code>details_rows: 4</code></pre> | |
1083 | <p>The number of rows to display in the bottom, details panel of the main window.</p> | |
1084 | <h3 id="edit_cmd"><a href="#edit_cmd">edit_cmd</a></h3> | |
1085 | <pre><code>edit_cmd: ~/bin/vim !file! +!line!</code></pre> | |
1086 | <p>This command is used in the command line version of etm to create and edit items. When the command is expanded, <code>!file!</code> will be replaced with the complete path of the file to be edited and <code>!line!</code> with the starting line number in the file. If the editor will open a new window, be sure to include the command to wait for the file to be closed before returning, e.g., with vim:</p> | |
1087 | <pre><code>edit_cmd: ~/bin/gvim -f !file! +!line!</code></pre> | |
1088 | <p>or with sublime text:</p> | |
1089 | <pre><code>edit_cmd: ~/bin/subl -n -w !file!:!line!</code></pre> | |
1090 | <h3 id="email_template"><a href="#email_template">email_template</a></h3> | |
1091 | <pre><code>email_template: 'Time: !time_span! | |
1092 | Locaton: !l! | |
1093 | ||
1094 | ||
1095 | !d!'</code></pre> | |
1096 | <p>Note that two newlines are required to get one empty line when the template is expanded. This template might expand as follows:</p> | |
1097 | <pre><code> Time: 1pm - 2:30pm Wed, Aug 4 | |
1098 | Location: Conference Room | |
1099 | ||
1100 | <contents of @d></code></pre> | |
1101 | <p>See the discussion of template expansions at the beginning of this tab for other possible expansion items.</p> | |
1102 | <h3 id="etmdir"><a href="#etmdir">etmdir</a></h3> | |
1103 | <pre><code>etmdir: ~/.etm</code></pre> | |
1104 | <p>Absolute path to the directory for etm.cfg and other etm configuration files.</p> | |
1105 | <h3 id="encoding"><a href="#encoding">encoding</a></h3> | |
1106 | <pre><code>encoding: {file: utf-8, gui: utf-8, term: utf-8}</code></pre> | |
1107 | <p>The encodings to be used for file IO, the GUI and terminal IO.</p> | |
1108 | <h3 id="filechange_alert"><a href="#filechange_alert">filechange_alert</a></h3> | |
1109 | <pre><code>filechange_alert: 'play ~/.etm/sounds/etm_alert.wav'</code></pre> | |
1110 | <p>The command to be executed when etm detects an external change in any of its data files. Leave this command empty to disable the notification.</p> | |
1111 | <h3 id="fontsize_fixed"><a href="#fontsize_fixed">fontsize_fixed</a></h3> | |
1112 | <pre><code>fontsize_fixed: 0</code></pre> | |
1113 | <p>Use this font size in the details panel, editor and reports. Use 0 to keep the system default.</p> | |
1114 | <h3 id="fontsize_tree"><a href="#fontsize_tree">fontsize_tree</a></h3> | |
1115 | <pre><code>fontsize_tree: 0</code></pre> | |
1116 | <p>Use this font size in the gui treeviews. Use 0 to keep the system default.</p> | |
1117 | <p>Tip: Leave the font sizes set to 0 and run etm with logging level 2 to see the system default sizes.</p> | |
1118 | <h3 id="freetimes"><a href="#freetimes">freetimes</a></h3> | |
1119 | <pre><code>freetimes: | |
1120 | opening: 480 # 8*60 minutes after midnight = 8am | |
1121 | closing: 1020 # 17*60 minutes after midnight = 5pm | |
1122 | minimum: 30 # 30 minutes | |
1123 | buffer: 15 # 15 minutes</code></pre> | |
1124 | <p>Only display free periods between <em>opening</em> and <em>closing</em> that last at least <em>minimum</em> minutes and preserve at least <em>buffer</em> minutes between events. Note that each of these settings must be an <em>interger</em> number of minutes.</p> | |
1125 | <p>E.g., with the above settings and these busy periods:</p> | |
1126 | <pre><code>Busy periods in Week 16: Apr 14 - 20, 2014 | |
1127 | ------------------------------------------ | |
1128 | Mon 14: 10:30am-11:00am; 12:00pm-1:00pm; 5:00pm-6:00pm | |
1129 | Tue 15: 9:00am-10:00am | |
1130 | Wed 16: 8:30am-9:30am; 2:00pm-3:00pm; 5:00pm-6:00pm | |
1131 | Thu 17: 11:00am-12:00pm; 6:00pm-7:00pm; 7:00pm-9:00pm | |
1132 | Fri 18: 3:00pm-4:00pm; 5:00pm-6:00pm | |
1133 | Sat 19: 9:00am-10:30am; 7:30pm-10:00pm</code></pre> | |
1134 | <p>This would be the corresponding list of free periods:</p> | |
1135 | <pre><code>Free periods in Week 16: Apr 14 - 20, 2014 | |
1136 | ------------------------------------------ | |
1137 | Mon 14: 8:00am-10:15am; 11:15am-11:45am; 1:15pm-4:45pm | |
1138 | Tue 15: 8:00am-8:45am; 10:15am-5:00pm | |
1139 | Wed 16: 9:45am-1:45pm; 3:15pm-4:45pm | |
1140 | Thu 17: 8:00am-10:45am; 12:15pm-5:00pm | |
1141 | Fri 18: 8:00am-2:45pm; 4:15pm-4:45pm | |
1142 | Sat 19: 8:00am-8:45am; 10:45am-5:00pm | |
1143 | Sun 20: 8:00am-5:00pm | |
1144 | ---------------------------------------- | |
1145 | Only periods of at least 30 minutes are displayed.</code></pre> | |
1146 | <p>When displaying free times in week view you will be prompted for the shortest period to display using the setting for <em>minimum</em> as the default.</p> | |
1147 | <p>Tip: Need to tell someone when you're free in a given week? Jump to that week in week view, press <em>Ctrl-F</em>, set the minimum period and then copy and paste the resulting list into an email.</p> | |
1148 | <h3 id="icalendar-settings"><a href="#icalendar-settings">iCalendar settings</a></h3> | |
1149 | <h4 id="icscal_file"><a href="#icscal_file">icscal_file</a></h4> | |
1150 | <p>If an item is not selected, pressing Shift-X in the gui will export the active calendars in iCalendar format to this file.</p> | |
1151 | <pre><code>icscal_file: ~/.etm/etmcal.ics</code></pre> | |
1152 | <h4 id="icsitem_file"><a href="#icsitem_file">icsitem_file</a></h4> | |
1153 | <p>If an item is selected, pressing Shift-X in the gui will export the selected item in iCalendar format to this file.</p> | |
1154 | <pre><code>icsitem_file: ~/.etm/etmitem.ics</code></pre> | |
1155 | <h4 id="icssync_folder"><a href="#icssync_folder">icssync_folder</a></h4> | |
1156 | <pre><code>icssync_folder: ''</code></pre> | |
1157 | <p>A relative path from <code>etmdata</code> to a folder. If given, files in this folder with the extension <code>.txt</code> and <code>.ics</code> will automatically kept concurrent using export to iCalendar and import from iCalendar. I.e., if the <code>.txt</code> file is more recent than than the <code>.ics</code> then the <code>.txt</code> file will be exported to the <code>.ics</code> file. On the other hand, if the <code>.ics</code> file is more recent then it will be imported to the <code>.txt</code> file. In either case, the contents of the file to be updated will be overwritten with the new content and the last acess/modified times for both will be set to the current time.</p> | |
1158 | <p>Note that the calendar application you use to modify the <code>.ics</code> file will impose restrictions on the subsequent content of the <code>.txt</code> file. E.g., if the <code>.txt</code> file has a note entry, then this note will be exported by etm as a VJOURNAL entry to the <code>.ics</code> file. But VJOURNAL entries are not be recognized by many (most) calendar apps. When importing this file to such an application, the note will be omitted and thus will be missing from the <code>.ics</code> file after the next export from the application. The note will then be missing from the <code>.txt</code> file as well after the next automatic update. Restricting the content to events should be safe with with any calendar application.</p> | |
1159 | <p>Additionally, if an absolute path is entered for <code>current_icsfolder</code>, then ics files corresponding to the entries in <code>calendars</code> will be created in this folder and updated as necessary. If there are no entries in calendars, then a single file, <code>all.ics</code>, will be created in this folder and updated as necessary.</p> | |
1160 | <h4 id="ics_subscriptions"><a href="#ics_subscriptions">ics_subscriptions</a></h4> | |
1161 | <pre><code>ics_subscriptions: []</code></pre> | |
1162 | <p>A list of (URL, path) tuples for automatic updates. The URL is a calendar subscription, e.g., for a Google Calendar subscription the entry might be something like:</p> | |
1163 | <pre><code>ics_subscriptions: | |
1164 | - ['https://www.google.com/calendar/ical/.../basic.ics', 'personal/dag/google.txt'] | |
1165 | </code></pre> | |
1166 | <p>With this entry, pressing Shift-M in the gui would import the calendar from the URL, convert it from ics to etm format and then write the result to <code>personal/google.txt</code> in the etm data directory. Note that this data file should be regarded as read-only since any changes made to it will be lost with the next subscription update.</p> | |
1167 | <h3 id="idle_minutes"><a href="#idle_minutes">idle_minutes</a></h3> | |
1168 | <pre><code>idle_minutes: 10</code></pre> | |
1169 | <p>When the idle timer is running and an action timer is started or restarted, only open the dialog to assign idle time if the current idle time is at least this many minutes.</p> | |
1170 | <h3 id="local_timezone"><a href="#local_timezone">local_timezone</a></h3> | |
1171 | <pre><code>local_timezone: US/Eastern</code></pre> | |
1172 | <p>This timezone will be used as the default when a value for <code>@z</code> is not given in an item.</p> | |
1173 | <h3 id="monthly"><a href="#monthly">monthly</a></h3> | |
1174 | <pre><code>monthly: monthly</code></pre> | |
1175 | <p>Relative path from <code>datadir</code>. With the settings above and for <code>datadir</code> the suggested location for saving new items in, say, October 2012, would be the file:</p> | |
1176 | <pre><code>~/.etm/data/monthly/2012/10.txt</code></pre> | |
1177 | <p>The directories <code>monthly</code> and <code>2012</code> and the file <code>10.txt</code> would, if necessary, be created. The user could either accept this default or choose a different file.</p> | |
1178 | <h3 id="outline_depth"><a href="#outline_depth">outline_depth</a></h3> | |
1179 | <pre><code>outline_depth: 2</code></pre> | |
1180 | <p>The default outline depth to use when opening keyword, note, path or tag view. Once any view is opened, use Ctrl-O to change the depth for that view.</p> | |
1181 | <h3 id="prefix"><a href="#prefix">prefix</a></h3> | |
1182 | <pre><code>prefix: "\n " | |
1183 | prefix_uses: "rj+-tldm"</code></pre> | |
1184 | <p>Apply <code>prefix</code> (whitespace only) to the <span class="citation">@keys</span> in <code>prefix_uses</code> when displaying and saving items. The default would cause the selected elements to begin on a newline and indented by two spaces. E.g.,</p> | |
1185 | <pre><code>+ summary @s 2014-05-09 12am @z US/Eastern | |
1186 | @m memo | |
1187 | @j job 1 &f 20140510T1411;20140509T0000 &q 1 | |
1188 | @j job 2 &f 20140510T1412;20140509T0000 &q 2 | |
1189 | @j job 3 &q 3 | |
1190 | @d description</code></pre> | |
1191 | <h3 id="report"><a href="#report">report</a></h3> | |
1192 | <pre><code>report_begin: '1' | |
1193 | report_end: '+1/1' | |
1194 | report_colors: 2 | |
1195 | report_width1: 61 | |
1196 | report_width2: 19</code></pre> | |
1197 | <p>Report begin and end are fuzzy parsed dates specifying the default period for reports that group by dates. Each line in the file specified by <code>report_specifications</code> provides a possible specification for a report. E.g.</p> | |
1198 | <pre><code>a MMM yyyy; k[0]; k[1:] -b -1/1 -e 1 | |
1199 | a k, MMM yyyy -b -1/1 -e 1 | |
1200 | c ddd MMM d yyyy | |
1201 | c f</code></pre> | |
1202 | <p>In custom view these appear in the report specifications pop-up list. A specification from the list can be selected and, perhaps, modified or an entirely new specification can be entered. See <a href="#reports">Reports</a> for details. See also the <a href="#agenda">agenda</a> settings above.</p> | |
1203 | <h3 id="retain_ids"><a href="#retain_ids">retain_ids</a></h3> | |
1204 | <pre><code>retain_ids: false</code></pre> | |
1205 | <p>If true, the unique ids that etm associates with items will be written to the data files and retained between sessions. If false, new ids will be generated for each session.</p> | |
1206 | <p>Retaining ids enables etm to avoid duplicates when importing and exporting iCalendar files.</p> | |
1207 | <h3 id="show_finished"><a href="#show_finished">show_finished</a></h3> | |
1208 | <pre><code>show_finished: 1</code></pre> | |
1209 | <p>Show this many of the most recent completions of repeated tasks or, if 0, show all completions.</p> | |
1210 | <h3 id="smtp"><a href="#smtp">smtp</a></h3> | |
1211 | <pre><code>smtp_from: dnlgrhm@gmail.com | |
1212 | smtp_id: dnlgrhm | |
1213 | smtp_pw: ********** | |
1214 | smtp_server: smtp.gmail.com</code></pre> | |
1215 | <p>Required settings for the smtp server to be used for email alerts.</p> | |
1216 | <h3 id="sms"><a href="#sms">sms</a></h3> | |
1217 | <pre><code>sms_message: '!summary!' | |
1218 | sms_subject: '!time_span!' | |
1219 | sms_from: dnlgrhm@gmail.com | |
1220 | sms_pw: ********** | |
1221 | sms_phone: 0123456789@vtext.com | |
1222 | sms_server: smtp.gmail.com:587</code></pre> | |
1223 | <p>Required settings for text messaging in alerts. Enter the 10-digit area code and number and mms extension for the mobile phone to receive the text message when no numbers are specified in the alert. The illustrated phone number is for Verizon. Here are the mms extensions for the major carriers:</p> | |
1224 | <pre><code>Alltel @message.alltel.com | |
1225 | AT&T @txt.att.net | |
1226 | Nextel @messaging.nextel.com | |
1227 | Sprint @messaging.sprintpcs.com | |
1228 | SunCom @tms.suncom.com | |
1229 | T-mobile @tmomail.net | |
1230 | VoiceStream @voicestream.net | |
1231 | Verizon @vtext.com</code></pre> | |
1232 | <h3 id="sundayfirst"><a href="#sundayfirst">sundayfirst</a></h3> | |
1233 | <pre><code>sundayfirst: false</code></pre> | |
1234 | <p>The setting affects only the twelve month calendar display.</p> | |
1235 | <h3 id="update_minutes"><a href="#update_minutes">update_minutes</a></h3> | |
1236 | <pre><code>update_minutes: 15</code></pre> | |
1237 | <p>Update <code>current_html</code>, <code>current_text</code> and the files in <code>icssync_folder</code> when the number of minutes past the hour modulo <code>update_minutes</code> is equal to zero. I.e. with the default, the update would occur on the hour and at 15, 30 and 45 minutes past the hour. Acceptable settings are integers between 1 and 59. Note that with a setting greater than or equal to 30, the update will occur only twice each hour.</p> | |
1238 | <h3 id="vcs_settings"><a href="#vcs_settings">vcs_settings</a></h3> | |
1239 | <pre><code>vcs_settings: | |
1240 | command: '' | |
1241 | commit: '' | |
1242 | dir: '' | |
1243 | file: '' | |
1244 | history: '' | |
1245 | init: '' | |
1246 | limit: ''</code></pre> | |
1247 | <p>These settings are ignored unless the setting for <code>vcs_system</code> below is either <code>git</code> or <code>mercurial</code>.</p> | |
1248 | <p>Default values will be provided for these settings based on the choice of <code>vcs_system</code> below. Any of the settings that you define here will overrule the defaults.</p> | |
1249 | <p>Here, for example, are the default values of these settings for git under OS X:</p> | |
1250 | <pre><code>vcs_settings: | |
1251 | command: '/usr/bin/git --git-dir {repo} --work-dir {work}' | |
1252 | commit: '/usr/bin/git --git-dir {repo} --work-dir {work} add */\*.txt | |
1253 | && /usr/bin/git --git-dir {repo} --work-dir {work} commit -a -m "{mesg}"' | |
1254 | dir: '.git' | |
1255 | file: '' | |
1256 | history: '/usr/bin/git -git-dir {repo} --work-dir {work} log | |
1257 | --pretty=format:"- %ar: %an%n%w(70,0,4)%s" -U1 {numchanges} | |
1258 | {file}' | |
1259 | init: '/usr/bin/git init {work}; /usr/bin/git -git-dir {repo} | |
1260 | --work-dir {work} add */\*.txt; /usr/bin/git-git-dir {repo} | |
1261 | --work-dir {work} commit -a -m "{mesg}"' | |
1262 | limit: '-n'</code></pre> | |
1263 | <p>In these settings, <code>{mesg}</code> will be replaced with an internally generated commit message, <code>{numchanges}</code> with an expression that depends upon <code>limit</code> that determines how many changes to show and, when a file is selected, <code>{file}</code> with the corresponding path. If <code>~/.etm/data</code> is your etm datadir, the <code>{repo}</code> would be replaced with <code>~/.etm/data/.git</code> and {work} with <code>~/.etm/data</code>.</p> | |
1264 | <p>Leave these settings empty to use the defaults.</p> | |
1265 | <h3 id="vcs_system"><a href="#vcs_system">vcs_system</a></h3> | |
1266 | <pre><code>vcs_system: ''</code></pre> | |
1267 | <p>This setting must be either <code>''</code> or <code>git</code> or <code>mercurial</code>.</p> | |
1268 | <p>If you specify either git or mercurial here (and have it installed on your system), then etm will automatically commit any changes you make to any of your data files. The history of these changes is available in the GUI with the show changes command (<em>Ctrl-H</em>) and you can, of course, use any git or mercurial commands in your terminal to, for example, restore a previous version of a file.</p> | |
1269 | <h3 id="weeks_after"><a href="#weeks_after">weeks_after</a></h3> | |
1270 | <pre><code>weeks_after: 52</code></pre> | |
1271 | <p>In the day view, all non-repeating, dated items are shown. Additionally all repetitions of repeating items with a finite number of repetitions are shown. This includes 'list-only' repeating items and items with <code>&u</code> (until) or <code>&t</code> (total number of repetitions) entries. For repeating items with an infinite number of repetitions, those repetitions that occur within the first <code>weeks_after</code> weeks after the current week are displayed along with the first repetition after this interval. This assures that for infrequently repeating items such as voting for president, at least one repetition will be displayed.</p> | |
1272 | <h3 id="yearfirst"><a href="#yearfirst">yearfirst</a></h3> | |
1273 | <pre><code>yearfirst: true</code></pre> | |
1274 | <p>If yearfirst is true, the YY-MM-DD format will have precedence over MM-DD-YY in an ambiguous date. See also <code>dayfirst</code>.</p> | |
1275 | <h1 id="reports"><a href="#reports">Reports</a></h1> | |
1276 | <p>To create a report open the custom view in the GUI. If you have entries in your report specifications file, <code>~./etm/reports.cfg</code> by default, you can choose one of them in the selection box at the bottom of the window.</p> | |
1277 | <p>You can also add report specifications to the list by selecting any item from the list and then replacing the content with anything you like. Press <em>Return</em> to <em>add</em> your specification temporarily to the list. <em>Note that the original entry will not be affected.</em> When you leave the custom view you will have an opportunity to save the additions you have made. If you choose a file, your additions will be inserted into the list and it will be opened for editing.</p> | |
1278 | <p>When you have selected a report specification, press <em>Return</em> to generate the report and display it.</p> | |
1279 | <p>A <em>report specification</em> is created by entering a report <em>type character</em>, either "a" or "c", followed by a <em>groupby setting</em> and, perhaps, by one or more <em>report options</em>:</p> | |
1280 | <pre><code><a|c> <groupby setting> [options]</code></pre> | |
1281 | <p>Together, the type character, groupby setting and options determine which items will appear in the report and how they will be organized and displayed.</p> | |
1282 | <h2 id="report-type-characters"><a href="#report-type-characters">Report type characters</a></h2> | |
1283 | <ul> | |
1284 | <li><p><strong>a</strong>: action report.</p> | |
1285 | <p>A report of expenditures of time and money recorded in <em>actions</em> with output formatted using <code>action_template</code> computations and expansions. See <a href="#preferences">Preferences</a> for further details about the role of <code>action_template</code> in formatting action report output.</p></li> | |
1286 | <li><p><strong>c</strong>: composite report.</p> | |
1287 | <p>Any item types, including actions, but without <code>action_template</code> computations and expansions. Note that only unfinished tasks and unfinished instances of repeating tasks will be displayed.</p></li> | |
1288 | </ul> | |
1289 | <h2 id="groupby-setting"><a href="#groupby-setting">Groupby setting</a></h2> | |
1290 | <p>A semicolon separated list that determines how items will be grouped and sorted. Possible elements include <em>date specifications</em> and elements from</p> | |
1291 | <ul> | |
1292 | <li><p>c: context</p></li> | |
1293 | <li><p>f: file path</p></li> | |
1294 | <li><p>k: keyword</p></li> | |
1295 | <li><p>t: tag</p></li> | |
1296 | <li><p>u: user</p></li> | |
1297 | </ul> | |
1298 | <p>A <em>date specification</em> is either</p> | |
1299 | <ul> | |
1300 | <li>w: week number</li> | |
1301 | </ul> | |
1302 | <p>or a combination of one or more of the following:</p> | |
1303 | <ul> | |
1304 | <li><p>yy: 2-digit year</p></li> | |
1305 | <li><p>yyyy: 4-digit year</p></li> | |
1306 | <li><p>MM: month: 01 - 12</p></li> | |
1307 | <li><p>MMM: locale specific abbreviated month name: Jan - Dec</p></li> | |
1308 | <li><p>MMMM: locale specific month name: January - December</p></li> | |
1309 | <li><p>dd: month day: 01 - 31</p></li> | |
1310 | <li><p>ddd: locale specific abbreviated week day: Mon - Sun</p></li> | |
1311 | <li><p>dddd: locale specific week day: Monday - Sunday</p></li> | |
1312 | </ul> | |
1313 | <p>For example, the report specification <code>c ddd, MMM dd yyyy</code> would group by year, month and day together to give output such as</p> | |
1314 | <pre><code>Fri, Apr 1 2011 | |
1315 | items for April 1 | |
1316 | Sat, Apr 2 2011 | |
1317 | items for April 2 | |
1318 | ...</code></pre> | |
1319 | <p>On the other hand, the report specificaton <code>a w; u; k[0]; k[1:]</code> would group by week number, user and keywords to give output such as</p> | |
1320 | <pre><code>13.1) 2014 Week 14: Mar 31 - Apr 6 | |
1321 | 6.3) agent 1 | |
1322 | 1.3) client 1 | |
1323 | 1.3) project 2 | |
1324 | 1.3) Activity (12) | |
1325 | 5) client 2 | |
1326 | 4.5) project 1 | |
1327 | 4.5) Activity (21) | |
1328 | 0.5) project 2 | |
1329 | 0.5) Activity (22) | |
1330 | 6.8) agent 2 | |
1331 | 2.2) client 1 | |
1332 | 2.2) project 2 | |
1333 | 2.2) Activity (13) | |
1334 | 4.6) client 2 | |
1335 | 3.9) project 1 | |
1336 | 3.9) Activity (23) | |
1337 | 0.7) project 2 | |
1338 | 0.7) Activity (23)</code></pre> | |
1339 | <p>With the heirarchial elements, file path and keyword, it is possible to use parts of the element as well as the whole. Consider, for example, the file path <code>A/B/C</code> with the components <code>[A, B, C]</code>. Then for this file path:</p> | |
1340 | <pre><code>f[0] = A | |
1341 | f[:2] = A/B | |
1342 | f[2:] = C | |
1343 | f = A/B/C</code></pre> | |
1344 | <p>Suppose that keywords have the format <code>client:project</code>. Then grouping by year and month, then client and finally project to give output such as</p> | |
1345 | <pre><code>report: a MMM yyyy; u; k[0]; k[1] -b 1 -e +1/1 | |
1346 | ||
1347 | 13.1) Feb 2014 | |
1348 | 6.3) agent 1 | |
1349 | 1.3) client 1 | |
1350 | 1.3) project 2 | |
1351 | 1.3) Activity 12 | |
1352 | 5) client 2 | |
1353 | 4.5) project 1 | |
1354 | 4.5) Activity 21 | |
1355 | 0.5) project 2 | |
1356 | 0.5) Activity 22 | |
1357 | 6.8) agent 2 | |
1358 | 2.2) client 1 | |
1359 | 2.2) project 2 | |
1360 | 2.2) Activity 13 | |
1361 | 4.6) client 2 | |
1362 | 3.9) project 1 | |
1363 | 3.9) Activity 23 | |
1364 | 0.7) project 2 | |
1365 | 0.7) Activity 23</code></pre> | |
1366 | <p>Items that are missing an element specified in <code>groupby</code> will be omitted from the output. E.g., undated tasks and notes will be omitted when a date specification is included, items without keywords will be omitted when <code>k</code> is included and so forth.</p> | |
1367 | <p>When a date specification is not included in the groupby setting, undated notes and tasks will be potentially included, but only those instances of dated items that correspond to the <em>relevant datetime</em> of the item of the item will be included, where the <em>relevant datetime</em> is the past due date for any past due tasks, the starting datetime for any non-repeating item and the datetime of the next instance for any repeating item.</p> | |
1368 | <p>Within groups, items are automatically sorted by date, type and time.</p> | |
1369 | <h2 id="options-1"><a href="#options-1">Options</a></h2> | |
1370 | <p>Report options are listed below. Report types <code>c</code> supports all options except <code>-d</code>. Report type <code>a</code> supports all options except <code>-o</code> and <code>-h</code>.</p> | |
1371 | <h3 id="b-begin_date"><a href="#b-begin_date">-b BEGIN_DATE</a></h3> | |
1372 | <p>Fuzzy parsed date. Limit the display of dated items to those with datetimes falling <em>on or after</em> this datetime. Relative day and month expressions can also be used so that, for example, <code>-b -14</code> would begin 14 days before the current date and <code>-b -1/1</code> would begin on the first day of the previous month. It is also possible to add (or subtract) a time period from the fuzzy date, e.g., <code>-b mon + 7d</code> would begin with the second Monday falling on or after today. Default: None.</p> | |
1373 | <h3 id="c-context-1"><a href="#c-context-1">-c CONTEXT</a></h3> | |
1374 | <p>Regular expression. Limit the display to items with contexts matching CONTEXT (ignoring case). Prepend an exclamation mark, i.e., use !CONTEXT rather than CONTEXT, to limit the display to items which do NOT have contexts matching CONTEXT.</p> | |
1375 | <h3 id="d-depth"><a href="#d-depth">-d DEPTH</a></h3> | |
1376 | <p>CLI only. In the GUI use <em>View/Set outline depth</em>. The default, <code>-d 0</code>, includes all outline levels. Use <code>-d 1</code> to include only level 1, <code>-d 2</code> to include levels 1 and 2 and so forth. This setting applies to the CLI only. In the GUI use the command <em>set outline depth</em>.</p> | |
1377 | <p>For example, modifying the report above by adding <code>-d 3</code> would give the following:</p> | |
1378 | <pre><code>report: a MMM yyyy; u; k[0]; k[1] -b 1 -e +1/1 -d 3 | |
1379 | ||
1380 | 13.1) Feb 2014 | |
1381 | 6.3) agent 1 | |
1382 | 1.3) client 1 | |
1383 | 5) client 2 | |
1384 | 6.8) agent 2 | |
1385 | 2.2) client 1 | |
1386 | 4.6) client 2</code></pre> | |
1387 | <h3 id="e-end_date"><a href="#e-end_date">-e END_DATE</a></h3> | |
1388 | <p>Fuzzy parsed date. Limit the display of dated items to those with datetimes falling <em>before</em> this datetime. As with BEGIN_DATE relative month expressions can be used so that, for example, <code>-b -1/1 -e 1</code> would include all items from the previous month. As with <code>-b</code>, period strings can be appended, e.g., <code>-b mon -e mon + 7d</code> would include items from the week that begins with the first Monday falling on or after today. Default: None.</p> | |
1389 | <h3 id="f-file"><a href="#f-file">-f FILE</a></h3> | |
1390 | <p>Regular expression. Limit the display to items from files whose paths match FILE (ignoring case). Prepend an exclamation mark, i.e., use !FILE rather than FILE, to limit the display to items from files whose path does NOT match FILE.</p> | |
1391 | <h3 id="k-keyword-1"><a href="#k-keyword-1">-k KEYWORD</a></h3> | |
1392 | <p>Regular expression. Limit the display to items with contexts matching KEYWORD (ignoring case). Prepend an exclamation mark, i.e., use !KEYWORD rather than KEYWORD, to limit the display to items which do NOT have keywords matching KEYWORD.</p> | |
1393 | <h3 id="l-location-1"><a href="#l-location-1">-l LOCATION</a></h3> | |
1394 | <p>Regular expression. Limit the display to items with location matching LOCATION (ignoring case). Prepend an exclamation mark, i.e., use !LOCATION rather than LOCATION, to limit the display to items which do NOT have a location that matches LOCATION.</p> | |
1395 | <h3 id="o-omit"><a href="#o-omit">-o OMIT</a></h3> | |
1396 | <p>String. Composite reports only. Show/hide a)ctions, d)elegated tasks, e)vents, g)roup tasks, n)otes, o)ccasions and/or other t)asks. For example, <code>-o on</code> would show everything except occasions and notes and <code>-o !on</code> would show only occasions and notes.</p> | |
1397 | <h3 id="s-summary"><a href="#s-summary">-s SUMMARY</a></h3> | |
1398 | <p>Regular expression. Limit the display to items containing SUMMARY (ignoring case) in the item summary. Prepend an exclamation mark, i.e., use !SUMMARY rather than SUMMARY, to limit the display to items which do NOT contain SUMMARY in the summary.</p> | |
1399 | <h3 id="s-search"><a href="#s-search">-S SEARCH</a></h3> | |
1400 | <p>Regular expression. Composite reports only. Limit the display to items containing SEARCH (ignoring case) anywhere in the item or its file path. Prepend an exclamation mark, i.e., use !SEARCH rather than SEARCH, to limit the display to items which do NOT contain SEARCH in the item or its file path.</p> | |
1401 | <h3 id="t-tags-1"><a href="#t-tags-1">-t TAGS</a></h3> | |
1402 | <p>Comma separated list of case insensitive regular expressions. E.g., use</p> | |
1403 | <pre><code>-t tag1, !tag2</code></pre> | |
1404 | <p>or</p> | |
1405 | <pre><code>-t tag1, -t !tag2</code></pre> | |
1406 | <p>to display items with one or more tags that match 'tag1' but none that match 'tag2'.</p> | |
1407 | <h3 id="u-user-1"><a href="#u-user-1">-u USER</a></h3> | |
1408 | <p>Regular expression. Limit the display to items with user matching USER (ignoring case). Prepend an exclamation mark, i.e., use !USER rather than USER, to limit the display to items which do NOT have a user that matches USER.</p> | |
1409 | <h1 id="shortcuts"><a href="#shortcuts">Shortcuts</a></h1> | |
1410 | <h2 id="menubar"><a href="#menubar">Menubar</a></h2> | |
1411 | <pre><code>File | |
1412 | New | |
1413 | Item N | |
1414 | File Shift-N | |
1415 | Begin/Pause Action Timer T | |
1416 | Finish Action Timer Shift-T | |
1417 | Start/Resolve Idle Timer I | |
1418 | Stop Idle Timer Shift-I | |
1419 | Open | |
1420 | Data file ... Shift-F | |
1421 | Configuration file ... Shift-C | |
1422 | preferences Shift-P | |
1423 | scratchpad Shift-S | |
1424 | ---- | |
1425 | Quit Ctrl-Q | |
1426 | View | |
1427 | Home Space | |
1428 | Show remaining alerts for today A | |
1429 | Jump to date J | |
1430 | ---- | |
1431 | Next sibling Control-Down | |
1432 | Previous sibling Control-Up | |
1433 | Set outline filter Ctrl-F | |
1434 | Clear outline filter Shift-Ctrl-F | |
1435 | Toggle displaying labels column L | |
1436 | Set outline depth O | |
1437 | Show outline as text S | |
1438 | Print outline P | |
1439 | Toggle displaying finished X | |
1440 | ---- | |
1441 | Previous week/month Left | |
1442 | Next week/month Right | |
1443 | Previous item/day in week/month Up | |
1444 | Next item/day in week/month Down | |
1445 | List busy times in week/month B | |
1446 | List free times in week/month F | |
1447 | Item | |
1448 | Copy C | |
1449 | Delete BackSpace | |
1450 | Edit E | |
1451 | Edit file Shift-E | |
1452 | Finish F | |
1453 | Move M | |
1454 | Reschedule R | |
1455 | Schedule new Shift-R | |
1456 | Open link G | |
1457 | Show user details U | |
1458 | Tools | |
1459 | Date and time calculator Shift-D | |
1460 | Available dates calculator Shift-A | |
1461 | Yearly calendar Shift-Y | |
1462 | History of changes Shift-H | |
1463 | Export to iCal Shift-X | |
1464 | Update calendar subscriptions Shift-M | |
1465 | Reload data from files Shift-L | |
1466 | Custom | |
1467 | Create and display selected report Return | |
1468 | Export report in text format ... Ctrl-T | |
1469 | Export report in csv format ... Ctrl-X | |
1470 | Save changes to report specifications Ctrl-W | |
1471 | Expand report list Down | |
1472 | Help | |
1473 | Search | |
1474 | Shortcuts ? | |
1475 | User manual F1 | |
1476 | About F2 | |
1477 | Check for update F3 </code></pre> | |
1478 | <h2 id="main"><a href="#main">Main</a></h2> | |
1479 | <pre><code>Views | |
1480 | Agenda Ctrl-A | |
1481 | ---- | |
1482 | Day Ctrl-D | |
1483 | Week Ctrl-W | |
1484 | Month Ctrl-M | |
1485 | ---- | |
1486 | Tag Ctrl-T | |
1487 | Keyword Ctrl-K | |
1488 | Path Ctrl-P | |
1489 | ---- | |
1490 | Note Ctrl-N | |
1491 | Custom Ctrl-C </code></pre> | |
1492 | <h2 id="edit"><a href="#edit">Edit</a></h2> | |
1493 | <pre><code>Show completions Ctrl-Space | |
1494 | Cancel Escape | |
1495 | Finish ... Ctrl-W </code></pre> | |
1496 | </body> | |
1497 | </html>⏎ |
0 | version = "3.0.40" |
0 | version = "3.0.40 [2014-09-13 10:31:26 -0400]" |
0 | #!/usr/bin/env python3 | |
1 | # -*- coding: utf-8 -*- | |
2 | from __future__ import (absolute_import, division, print_function, | |
3 | unicode_literals) | |
4 | ||
5 | import os | |
6 | # import sys | |
7 | import re | |
8 | import uuid | |
9 | from copy import deepcopy | |
10 | import subprocess | |
11 | from dateutil.tz import tzlocal | |
12 | import codecs | |
13 | ||
14 | import logging | |
15 | import logging.config | |
16 | ||
17 | logger = logging.getLogger() | |
18 | ||
19 | import platform | |
20 | ||
21 | if platform.python_version() >= '3': | |
22 | import tkinter | |
23 | from tkinter import Tk, Entry, INSERT, END, Label, Toplevel, Button, Frame, LEFT, PanedWindow, OptionMenu, StringVar, IntVar, Menu, X, Canvas, CURRENT, Scrollbar | |
24 | from tkinter import ttk | |
25 | from tkinter import font as tkFont | |
26 | utf8 = lambda x: x | |
27 | unicode = str | |
28 | ||
29 | else: | |
30 | import Tkinter as tkinter | |
31 | from Tkinter import Tk, Entry, INSERT, END, Label, Toplevel, Button, Frame, LEFT, PanedWindow, OptionMenu, StringVar, IntVar, Menu, X, Canvas, CURRENT, Scrollbar | |
32 | import ttk | |
33 | import tkFont | |
34 | ||
35 | def utf8(s): | |
36 | return s | |
37 | ||
38 | tkversion = tkinter.Tcl().eval('info patchlevel') | |
39 | ||
40 | import etmTk.data as data | |
41 | ||
42 | from dateutil.parser import parse | |
43 | ||
44 | from calendar import Calendar | |
45 | ||
46 | from decimal import Decimal | |
47 | ||
48 | from etmTk.data import ( | |
49 | init_localization, fmt_weekday, fmt_dt, str2hsh, tstr2SCI, leadingzero, relpath, s2or3, send_mail, send_text, get_changes, checkForNewerVersion, datetime2minutes, calyear, expand_template, id2Type, get_current_time, windoz, mac, setup_logging, gettz, commandShortcut, rrulefmt, tree2Text, date_calculator, AFTER, export_ical_item, export_ical_active, fmt_time, TimeIt, getReportData, getFileTuples, getAllFiles, updateCurrentFiles, FINISH, availableDates, syncTxt, update_subscription) | |
50 | ||
51 | from etmTk.dialog import MenuTree, Timer, ReadOnlyText, MessageWindow, TextDialog, OptionsDialog, GetInteger, GetDateTime, GetString, FileChoice, STOPPED, PAUSED, RUNNING, BGCOLOR, ONEDAY, ONEMINUTE | |
52 | ||
53 | from etmTk.edit import SimpleEditor | |
54 | ||
55 | import gettext | |
56 | ||
57 | _ = gettext.gettext | |
58 | ||
59 | ||
60 | from datetime import datetime, time | |
61 | ||
62 | ETM = "etm" | |
63 | ||
64 | # STOPPED = _('stopped') | |
65 | # PAUSED = _('paused') | |
66 | # RUNNING = _('running') | |
67 | ||
68 | FILTER = _("filter") | |
69 | FILTERCOLOR = "gray" | |
70 | ||
71 | # Views # | |
72 | AGENDA = _('Agenda') | |
73 | ||
74 | DAY = _('Day') | |
75 | WEEK = _("Week") | |
76 | MONTH = _("Month") | |
77 | ||
78 | PATH = _('Path') | |
79 | KEYWORD = _('Keyword') | |
80 | TAG = _('Tag') | |
81 | ||
82 | NOTE = _('Note') | |
83 | CUSTOM = _("Custom") | |
84 | ||
85 | CALENDARS = _("Calendars") | |
86 | ||
87 | COPY = _("Copy") | |
88 | EDIT = _("Edit") | |
89 | DELETE = _("Delete") | |
90 | ||
91 | FILE = _("File") | |
92 | NEW = _("New") | |
93 | OPEN = _("Open") | |
94 | VIEW = _("View") | |
95 | ITEM = _("Item") | |
96 | TOOLS = _("Tools") | |
97 | HELP = _("Help") | |
98 | ||
99 | MAKE = _("Make report") | |
100 | PRINT = _("Print") | |
101 | EXPORTTEXT = _("Export report in text format ...") | |
102 | EXPORTCSV = _("Export report in CSV format ...") | |
103 | SAVESPECS = _("Save changes to report specifications") | |
104 | ||
105 | CLOSE = _("Close") | |
106 | ||
107 | SEP = "----" | |
108 | ||
109 | ACTIVEFILL = "#FAFCAC" | |
110 | ACTIVEOUTLINE = "gray40" | |
111 | ||
112 | DEFAULTFILL = "#D4DCFC" # blue | |
113 | OTHERFILL = "#C7EDC8" # green | |
114 | ||
115 | BUSYOUTLINE = "" | |
116 | ||
117 | CONFLICTFILL = "#C1C4C9" | |
118 | CURRENTFILL = "#FCF2F0" | |
119 | # CURRENTLINE = "#D053E0" | |
120 | CURRENTLINE = "#3C3FDE" | |
121 | OUTSIDELINE = "#E0535C" | |
122 | LASTLTR = re.compile(r'([a-z])$') | |
123 | LINECOLOR = "gray80" | |
124 | ||
125 | OCCASIONFILL = "gray96" | |
126 | ||
127 | this_dir, this_filename = os.path.split(__file__) | |
128 | USERMANUAL = os.path.normpath(os.path.join(this_dir, "help", "UserManual.html")) | |
129 | ||
130 | ||
131 | class App(Tk): | |
132 | def __init__(self, path=None): | |
133 | Tk.__init__(self) | |
134 | ||
135 | self.minsize(460, 460) | |
136 | self.uuidSelected = None | |
137 | self.timerItem = None | |
138 | self.loop = loop | |
139 | self.actionTimer = Timer(self, options=loop.options) | |
140 | self.monthly_calendar = Calendar() | |
141 | self.activeAlerts = [] | |
142 | self.configure(background=BGCOLOR) | |
143 | self.option_add('*tearOff', False) | |
144 | self.menu_lst = [] | |
145 | self.menutree = MenuTree() | |
146 | self.chosen_day = None | |
147 | self.active_date = None | |
148 | self.busy_info = None | |
149 | self.weekly = False | |
150 | self.monthly = False | |
151 | self.today_col = None | |
152 | self.specsModified = False | |
153 | self.active_tree = {} | |
154 | root = "_" | |
155 | ||
156 | ef = "%a %b %d" | |
157 | if 'ampm' in loop.options and loop.options['ampm']: | |
158 | self.efmt = "%I:%M%p {0}".format(ef) | |
159 | else: | |
160 | self.efmt = "%H:%M {0}".format(ef) | |
161 | ||
162 | self.default_calendars = deepcopy(loop.options['calendars']) | |
163 | ||
164 | # create the root node for the menu tree | |
165 | self.menutree.create_node(root, root) | |
166 | ||
167 | # leaf: (parent, (option, [accelerator]) | |
168 | ||
169 | self.outline_depths = {} | |
170 | for view in DAY, TAG, KEYWORD, NOTE, PATH: | |
171 | # set all to the default | |
172 | logger.debug('Setting depth for {0} to {1}'.format(view, loop.options['outline_depth'])) | |
173 | self.outline_depths[view] = loop.options['outline_depth'] | |
174 | # set CUSTOM to 0 | |
175 | self.outline_depths[AGENDA] = 0 | |
176 | self.outline_depths[CUSTOM] = 0 | |
177 | ||
178 | self.panedwindow = panedwindow = PanedWindow(self, orient="vertical", sashwidth=8, sashrelief='flat') | |
179 | self.toppane = toppane = Frame(panedwindow, bd=0, highlightthickness=0, background=BGCOLOR) | |
180 | self.tree = ttk.Treeview(toppane, show='tree', columns=["#1", "#2"], selectmode='browse') | |
181 | self.canvas = Canvas(self.toppane, background="white", bd=2, relief="sunken") | |
182 | ||
183 | self.canvas.bind("<Control-Button-1>", self.on_select_item) | |
184 | self.canvas.bind("<Double-1>", self.on_select_item) | |
185 | self.canvas.bind("<Configure>", self.configureCanvas) | |
186 | ||
187 | self.canvas.bind("<Return>", lambda e: self.on_activate_item(event=e)) | |
188 | self.canvas.bind('<Left>', (lambda e: self.priorWeekMonth(event=e))) | |
189 | self.canvas.bind('<Right>', (lambda e: self.nextWeekMonth(event=e))) | |
190 | self.canvas.bind('<Up>', (lambda e: self.selectId(event=e, d=-1))) | |
191 | self.canvas.bind('<Down>', (lambda e: self.selectId(event=e, d=1))) | |
192 | ||
193 | self.canvas.bind("b", lambda event: self.after(AFTER, self.showBusyPeriods)) | |
194 | self.canvas.bind("f", lambda event: self.after(AFTER, self.showFreePeriods)) | |
195 | ||
196 | # main menu | |
197 | self.menubar = menubar = Menu(self) | |
198 | menu = _("Menubar") | |
199 | self.add2menu(root, (menu,)) | |
200 | ||
201 | # File menu | |
202 | filemenu = Menu(menubar, tearoff=0) | |
203 | path = FILE | |
204 | self.add2menu(menu, (path, )) | |
205 | ||
206 | self.newmenu = newmenu = Menu(filemenu, tearoff=0) | |
207 | self.add2menu(path, (NEW, )) | |
208 | path = NEW | |
209 | ||
210 | label = _("Item") | |
211 | l = "N" | |
212 | c = "n" | |
213 | newmenu.add_command(label=label, command=self.newItem) | |
214 | # self.bindTop("n", self.newItem) | |
215 | self.bindTop(c, lambda e: self.after(AFTER, self.newItem(e))) | |
216 | self.canvas.bind(c, lambda e: self.after(AFTER, self.newItem(e))) | |
217 | ||
218 | newmenu.entryconfig(0, accelerator=l) | |
219 | self.add2menu(path, (label, l)) | |
220 | ||
221 | l = "Shift-N" | |
222 | c = "N" | |
223 | label = _("File") | |
224 | newmenu.add_command(label=label, command=self.newFile) | |
225 | self.bindTop(c, self.newFile) | |
226 | newmenu.entryconfig(1, accelerator=l) | |
227 | self.add2menu(path, (label, l)) | |
228 | ||
229 | label = _("Begin/Pause Action Timer") | |
230 | l = "T" | |
231 | c = 't' | |
232 | newmenu.add_command(label=label, command=self.startActionTimer) | |
233 | self.bindTop(c, self.startActionTimer) | |
234 | newmenu.entryconfig(2, accelerator=l) | |
235 | self.add2menu(path, (label, l)) | |
236 | ||
237 | label = _("Finish Action Timer") | |
238 | l = "Shift-T" | |
239 | c = "T" | |
240 | newmenu.add_command(label=label, command=self.finishActionTimer) | |
241 | self.bind(c, self.finishActionTimer) | |
242 | filemenu.add_cascade(label=NEW, menu=newmenu) | |
243 | newmenu.entryconfig(3, state="disabled") | |
244 | self.add2menu(path, (label, l)) | |
245 | ||
246 | label = _("Start/Resolve Idle Timer") | |
247 | l = "I" | |
248 | c = 'i' | |
249 | newmenu.add_command(label=label, command=self.startIdleTimer) | |
250 | self.bindTop(c, self.startIdleTimer) | |
251 | newmenu.entryconfig(4, accelerator=l) | |
252 | self.add2menu(path, (label, l)) | |
253 | ||
254 | label = _("Stop Idle Timer") | |
255 | l = "Shift-I" | |
256 | c = "I" | |
257 | newmenu.add_command(label=label, command=self.stopIdleTimer) | |
258 | self.bind(c, self.stopIdleTimer) | |
259 | newmenu.entryconfig(5, accelerator=l) | |
260 | self.add2menu(path, (label, l)) | |
261 | newmenu.entryconfig(5, state="disabled") | |
262 | ||
263 | path = FILE | |
264 | ||
265 | # Open | |
266 | openmenu = Menu(filemenu, tearoff=0) | |
267 | self.add2menu(path, (OPEN, )) | |
268 | path = OPEN | |
269 | ||
270 | l = "Shift-F" | |
271 | c = "F" | |
272 | label = _("Data file ...") | |
273 | openmenu.add_command(label=label, command=self.editData) | |
274 | self.bindTop(c, self.editData) | |
275 | openmenu.entryconfig(0, accelerator=l) | |
276 | self.add2menu(path, (label, l)) | |
277 | ||
278 | l = "Shift-C" | |
279 | c = "C" | |
280 | label = _("Configuration file ...") | |
281 | openmenu.add_command(label=label, command=self.editCfgFiles) | |
282 | self.bindTop(c, self.editCfgFiles) | |
283 | openmenu.entryconfig(1, accelerator=l) | |
284 | self.add2menu(path, (label, l)) | |
285 | ||
286 | l = "Shift-P" | |
287 | c = "P" | |
288 | label = "preferences" | |
289 | openmenu.add_command(label=label, command=self.editConfig) | |
290 | self.bindTop(c, self.editConfig) | |
291 | ||
292 | openmenu.entryconfig(2, accelerator=l) | |
293 | self.add2menu(path, (label, l)) | |
294 | ||
295 | l = "Shift-S" | |
296 | c = "S" | |
297 | file = loop.options['scratchpad'] | |
298 | label = relpath(file, loop.options['etmdir']) | |
299 | openmenu.add_command(label=label, command=self.editScratch) | |
300 | self.bindTop(c, self.editScratch) | |
301 | openmenu.entryconfig(3, accelerator=l) | |
302 | self.add2menu(path, (label, l)) | |
303 | ||
304 | filemenu.add_cascade(label=OPEN, menu=openmenu) | |
305 | ||
306 | path = FILE | |
307 | ||
308 | filemenu.add_separator() | |
309 | self.add2menu(path, (SEP, )) | |
310 | ||
311 | # quit | |
312 | l, c = commandShortcut('q') | |
313 | label = _("Quit") | |
314 | filemenu.add_command(label=label, underline=0, | |
315 | command=self.quit) | |
316 | self.bind(c, self.quit) # w | |
317 | self.add2menu(path, (label, l)) | |
318 | ||
319 | menubar.add_cascade(label=path, underline=0, menu=filemenu) | |
320 | ||
321 | # View menu | |
322 | self.viewmenu = viewmenu = Menu(menubar, tearoff=0) | |
323 | path = VIEW | |
324 | self.add2menu(menu, (path, )) | |
325 | ||
326 | # go home | |
327 | l = "Space" | |
328 | label = _("Home") | |
329 | viewmenu.add_command(label=label, command=self.goHome) | |
330 | ||
331 | viewmenu.entryconfig(0, accelerator=l) | |
332 | self.add2menu(path, (label, l)) | |
333 | self.bindTop('<space>', self.goHome) | |
334 | ||
335 | # show alerts | |
336 | l = "A" | |
337 | c = "a" | |
338 | label = _("Show remaining alerts for today") | |
339 | viewmenu.add_command(label=label, underline=1, command=self.showAlerts) | |
340 | self.bindTop(c, self.showAlerts) | |
341 | ||
342 | viewmenu.entryconfig(1, accelerator=l) | |
343 | self.add2menu(path, (label, l)) | |
344 | ||
345 | # go to date | |
346 | l = "J" | |
347 | c = "j" | |
348 | label = _("Jump to date") | |
349 | viewmenu.add_command(label=label, command=self.goToDate) | |
350 | self.bindTop(c, self.goToDate) | |
351 | ||
352 | viewmenu.entryconfig(2, accelerator=l) | |
353 | self.add2menu(path, (label, l)) | |
354 | ||
355 | viewmenu.add_separator() # 3 | |
356 | self.add2menu(path, (SEP, )) | |
357 | ||
358 | l = "Control-Down" | |
359 | label = _("Next sibling") | |
360 | viewmenu.add_command(label=label, underline=1, command=self.nextItem) | |
361 | ||
362 | viewmenu.entryconfig(4, accelerator=l) | |
363 | self.add2menu(path, (label, l)) | |
364 | ||
365 | l = "Control-Up" | |
366 | label = _("Previous sibling") | |
367 | viewmenu.add_command(label=label, underline=1, command=self.prevItem) | |
368 | ||
369 | viewmenu.entryconfig(5, accelerator=l) | |
370 | self.add2menu(path, (label, l)) | |
371 | ||
372 | # apply filter | |
373 | l, c = commandShortcut('f') | |
374 | label = _("Set outline filter") | |
375 | viewmenu.add_command(label=label, underline=1, command=self.setFilter) | |
376 | self.bind(c, self.setFilter) | |
377 | ||
378 | viewmenu.entryconfig(6, accelerator=l) | |
379 | self.add2menu(path, (label, l)) | |
380 | ||
381 | # clear filter | |
382 | l = "Shift-Ctrl-F" | |
383 | label = _("Clear outline filter") | |
384 | viewmenu.add_command(label=label, underline=1, command=self.clearFilter) | |
385 | ||
386 | viewmenu.entryconfig(7, accelerator=l) | |
387 | self.add2menu(path, (label, l)) | |
388 | ||
389 | # toggle showing labels | |
390 | l = "L" | |
391 | c = "l" | |
392 | label = _("Toggle displaying labels column") | |
393 | viewmenu.add_command(label=label, underline=1, command=self.toggleLabels) | |
394 | self.bindTop(c, self.toggleLabels) | |
395 | ||
396 | viewmenu.entryconfig(8, accelerator=l) | |
397 | self.add2menu(path, (label, l)) | |
398 | ||
399 | # expand to depth | |
400 | l = "O" | |
401 | c = "o" | |
402 | label = _("Set outline depth") | |
403 | viewmenu.add_command(label=label, underline=1, command=self.expand2Depth) | |
404 | self.bindTop(c, self.expand2Depth) | |
405 | ||
406 | viewmenu.entryconfig(9, accelerator=l) | |
407 | self.add2menu(path, (label, l)) | |
408 | ||
409 | # popup active tree | |
410 | l = "S" | |
411 | c = "s" | |
412 | label = _("Show outline as text") | |
413 | viewmenu.add_command(label=label, underline=1, command=self.popupTree) | |
414 | self.bindTop(c, self.popupTree) | |
415 | ||
416 | viewmenu.entryconfig(10, accelerator=l) | |
417 | self.add2menu(path, (label, l)) | |
418 | ||
419 | # print active tree | |
420 | l = "P" | |
421 | c = "p" | |
422 | label = _("Print outline") | |
423 | viewmenu.add_command(label=label, underline=1, command=self.printTree) | |
424 | self.bindTop("p", self.printTree) | |
425 | viewmenu.entryconfig(11, accelerator=l) | |
426 | self.add2menu(path, (label, l)) | |
427 | ||
428 | # toggle showing finished | |
429 | l = "X" | |
430 | c = "x" | |
431 | label = _("Toggle displaying finished") | |
432 | viewmenu.add_command(label=label, underline=1, command=self.toggleFinished) | |
433 | self.bindTop(c, self.toggleFinished) | |
434 | viewmenu.entryconfig(12, accelerator=l) | |
435 | self.add2menu(path, (label, l)) | |
436 | ||
437 | viewmenu.add_separator() # 13 | |
438 | self.add2menu(path, (SEP, )) | |
439 | ||
440 | l = "Left" | |
441 | label = _("Previous week/month") | |
442 | viewmenu.add_command(label=label, underline=1, command=lambda e=None: self.priorWeekMonth(event=e)) | |
443 | ||
444 | viewmenu.entryconfig(14, accelerator=l) | |
445 | self.add2menu(path, (label, l)) | |
446 | ||
447 | l = "Right" | |
448 | label = _("Next week/month") | |
449 | viewmenu.add_command(label=label, underline=1, command=lambda e=None: self.nextWeekMonth(event=e)) | |
450 | ||
451 | viewmenu.entryconfig(15, accelerator=l) | |
452 | self.add2menu(path, (label, l)) | |
453 | ||
454 | l = "Up" | |
455 | label = _("Previous item/day in week/month") | |
456 | viewmenu.add_command(label=label, underline=1, command=lambda e=None: self.selectId(event=e, d=-1)) | |
457 | ||
458 | viewmenu.entryconfig(16, accelerator=l) | |
459 | self.add2menu(path, (label, l)) | |
460 | ||
461 | l = "Down" | |
462 | label = _("Next item/day in week/month") | |
463 | viewmenu.add_command(label=label, underline=1, command=lambda e=None: self.selectId(event=e, d=1)) | |
464 | ||
465 | viewmenu.entryconfig(17, accelerator=l) | |
466 | self.add2menu(path, (label, l)) | |
467 | ||
468 | l = "B" | |
469 | c = 'b' | |
470 | label = _("List busy times in week/month") | |
471 | viewmenu.add_command(label=label, underline=5, command=self.showBusyPeriods) | |
472 | ||
473 | viewmenu.entryconfig(18, accelerator=l) | |
474 | self.add2menu(path, (label, l)) | |
475 | ||
476 | l = "F" | |
477 | c = 'f' | |
478 | label = _("List free times in week/month") | |
479 | viewmenu.add_command(label=label, underline=5, command=self.showFreePeriods) | |
480 | ||
481 | viewmenu.entryconfig(19, accelerator=l) | |
482 | # set binding in showWeekly | |
483 | self.add2menu(path, (label, l)) | |
484 | ||
485 | for i in range(14, 20): | |
486 | self.viewmenu.entryconfig(i, state="disabled") | |
487 | menubar.add_cascade(label=path, underline=0, menu=viewmenu) | |
488 | ||
489 | # Item menu | |
490 | self.itemmenu = itemmenu = Menu(menubar, tearoff=0) | |
491 | self.itemmenu.bind("<Escape>", self.closeItemMenu) | |
492 | self.itemmenu.bind("<FocusOut>", self.closeItemMenu) | |
493 | path = ITEM | |
494 | self.add2menu(menu, (path, )) | |
495 | self.em_options = [ | |
496 | [_('Copy'), 'c'], | |
497 | [_('Delete'), 'd'], | |
498 | [_('Edit'), 'e'], | |
499 | [_('Edit file'), 'E'], | |
500 | [_('Finish'), 'f'], | |
501 | [_('Move'), 'm'], | |
502 | [_('Reschedule'), 'r'], | |
503 | [_('Schedule new'), 'R'], | |
504 | [_('Open link'), 'g'], | |
505 | [_('Show user details'), 'u']] | |
506 | self.edit2cmd = { | |
507 | 'c': self.copyItem, | |
508 | 'd': self.deleteItem, | |
509 | 'e': self.editItem, | |
510 | 'E': self.editItemFile, | |
511 | 'f': self.finishItem, | |
512 | 'm': self.moveItem, | |
513 | 'r': self.rescheduleItem, | |
514 | 'R': self.scheduleNewItem, | |
515 | 'g': self.openWithDefault, | |
516 | 'u': self.showUserDetails} | |
517 | self.em_opts = [x[0] for x in self.em_options] | |
518 | for i in range(len(self.em_options)): | |
519 | label = self.em_options[i][0] | |
520 | k = self.em_options[i][1] | |
521 | if k == 'd': | |
522 | l = "BackSpace" | |
523 | c = "<BackSpace>" | |
524 | elif k == 'E': | |
525 | l = "Shift-E" | |
526 | c = "E" | |
527 | elif k == 'R': | |
528 | l = "Shift-R" | |
529 | c = "R" | |
530 | else: | |
531 | l = k.upper() | |
532 | c = k | |
533 | itemmenu.add_command(label=label, underline=0, command=self.edit2cmd[k]) | |
534 | if k == 'f': | |
535 | self.tree.bind(c, self.edit2cmd[k]) | |
536 | else: | |
537 | self.bindTop(c, self.edit2cmd[k]) | |
538 | ||
539 | itemmenu.entryconfig(i, accelerator=l) | |
540 | self.add2menu(path, (label, l)) | |
541 | menubar.add_cascade(label=path, underline=0, menu=itemmenu) | |
542 | ||
543 | # tools menu | |
544 | path = TOOLS | |
545 | self.add2menu(menu, (path, )) | |
546 | toolsmenu = Menu(menubar, tearoff=0) | |
547 | ||
548 | # date calculator | |
549 | l = "Shift-D" | |
550 | c = "D" | |
551 | label = _("Date and time calculator") | |
552 | toolsmenu.add_command(label=label, underline=12, command=self.dateCalculator) | |
553 | self.bindTop(c, self.dateCalculator) | |
554 | ||
555 | toolsmenu.entryconfig(1, accelerator=l) | |
556 | self.add2menu(path, (label, l)) | |
557 | ||
558 | # available date calculator | |
559 | l = "Shift-A" | |
560 | c = "A" | |
561 | label = _("Available dates calculator") | |
562 | toolsmenu.add_command(label=label, underline=12, command=self.availableDateCalculator) | |
563 | self.bindTop(c, self.availableDateCalculator) | |
564 | ||
565 | toolsmenu.entryconfig(2, accelerator=l) | |
566 | self.add2menu(path, (label, l)) | |
567 | ||
568 | l = "Shift-Y" | |
569 | c = "Y" | |
570 | ||
571 | label = _("Yearly calendar") | |
572 | toolsmenu.add_command(label=label, underline=8, command=self.showCalendar) | |
573 | self.bindTop(c, self.showCalendar) | |
574 | ||
575 | toolsmenu.entryconfig(3, accelerator=l) | |
576 | self.add2menu(path, (label, l)) | |
577 | ||
578 | # changes | |
579 | if loop.options['vcs_system']: | |
580 | ||
581 | l = 'Shift-H' | |
582 | c = 'H' | |
583 | label = _("History of changes") | |
584 | toolsmenu.add_command(label=label, underline=1, command=self.showChanges) | |
585 | self.bind(c, lambda event: self.after(AFTER, self.showChanges)) | |
586 | ||
587 | toolsmenu.entryconfig(4, accelerator=l) | |
588 | self.add2menu(path, (label, l)) | |
589 | ||
590 | # export | |
591 | l = "Shift-X" | |
592 | c = "X" | |
593 | label = _("Export to iCal") | |
594 | toolsmenu.add_command(label=label, underline=1, command=self.exportToIcal) | |
595 | self.bind(c, self.exportToIcal) | |
596 | ||
597 | toolsmenu.entryconfig(5, accelerator=l) | |
598 | self.add2menu(path, (label, l)) | |
599 | ||
600 | # update subscriptions | |
601 | l = "Shift-M" | |
602 | c = "M" | |
603 | label = _("Update calendar subscriptions") | |
604 | toolsmenu.add_command(label=label, underline=1, command=self.updateSubscriptions) | |
605 | self.bind(c, self.updateSubscriptions) | |
606 | ||
607 | toolsmenu.entryconfig(5, accelerator=l) | |
608 | self.add2menu(path, (label, l)) | |
609 | ||
610 | # load data | |
611 | l = "Shift-L" | |
612 | c = "L" | |
613 | label = _("Reload data from files") | |
614 | toolsmenu.add_command(label=label, underline=1, command=loop.loadData) | |
615 | self.bind(c, loop.loadData) | |
616 | ||
617 | toolsmenu.entryconfig(6, accelerator=l) | |
618 | self.add2menu(path, (label, l)) | |
619 | ||
620 | menubar.add_cascade(label=path, menu=toolsmenu, underline=0) | |
621 | ||
622 | # report | |
623 | path = CUSTOM | |
624 | self.add2menu(menu, (path, )) | |
625 | self.custommenu = reportmenu = Menu(menubar, tearoff=0) | |
626 | ||
627 | self.rm_options = [[MAKE, 'm'], | |
628 | [EXPORTTEXT, 't'], | |
629 | [EXPORTCSV, 'x'], | |
630 | [SAVESPECS, 'w']] | |
631 | ||
632 | self.rm2cmd = {'m': self.makeReport, | |
633 | 't': self.exportText, | |
634 | 'x': self.exportCSV, | |
635 | 'w': self.saveSpecs} | |
636 | ||
637 | self.rm_opts = [x[0] for x in self.rm_options] | |
638 | ||
639 | for i in range(len(self.rm_options)): | |
640 | label = self.rm_options[i][0] | |
641 | k = self.rm_options[i][1] | |
642 | l = k.upper() | |
643 | c = k | |
644 | ||
645 | reportmenu.add_command(label=label, underline=0, command=self.rm2cmd[k]) | |
646 | reportmenu.entryconfig(i, state="disabled") | |
647 | ||
648 | self.add2menu(CUSTOM, (_("Create and display selected report"), "Return")) | |
649 | self.add2menu(CUSTOM, (_("Export report in text format ..."), "Ctrl-T")) | |
650 | self.add2menu(CUSTOM, (_("Export report in csv format ..."), "Ctrl-X")) | |
651 | self.add2menu(CUSTOM, (_("Save changes to report specifications"), "Ctrl-W")) | |
652 | self.add2menu(CUSTOM, (_("Expand report list"), "Down")) | |
653 | ||
654 | menubar.add_cascade(label=path, menu=reportmenu, underline=0) | |
655 | ||
656 | # help | |
657 | helpmenu = Menu(menubar, tearoff=0) | |
658 | path = HELP | |
659 | self.add2menu(menu, (path, )) | |
660 | ||
661 | # search is built in | |
662 | self.add2menu(path, (_("Search"), )) | |
663 | ||
664 | label = _("Shortcuts") | |
665 | helpmenu.add_command(label=label, underline=1, accelerator="?", command=self.showShortcuts) | |
666 | self.add2menu(path, (label, "?")) | |
667 | self.bindTop("?", self.showShortcuts) | |
668 | ||
669 | label = _("User manual") | |
670 | helpmenu.add_command(label=label, underline=1, accelerator="F1", command=self.help) | |
671 | self.add2menu(path, (label, "F1")) | |
672 | self.bind("<F1>", lambda e: self.after(AFTER, self.help)) | |
673 | ||
674 | label = _("About") | |
675 | helpmenu.add_command(label="About", accelerator="F2", command=self .about) | |
676 | self.bind("<F2>", self.about) | |
677 | self.add2menu(path, (label, "F2")) | |
678 | ||
679 | # check for updates | |
680 | ||
681 | label = _("Check for update") | |
682 | helpmenu.add_command(label=label, underline=1, accelerator="F3", command=self.checkForUpdate) | |
683 | self.add2menu(path, (label, "F3")) | |
684 | self.bind("<F3>", lambda e: self.after(AFTER, self.checkForUpdate)) | |
685 | ||
686 | menubar.add_cascade(label="Help", menu=helpmenu) | |
687 | ||
688 | self.config(menu=menubar) | |
689 | ||
690 | self.history = [] | |
691 | self.index = 0 | |
692 | self.count = 0 | |
693 | self.count2id = {} | |
694 | self.now = get_current_time() | |
695 | self.today = self.now.date() | |
696 | self.options = loop.options | |
697 | ||
698 | tkfixedfont = tkFont.nametofont("TkFixedFont") | |
699 | if 'fontsize_fixed' in self.options and self.options['fontsize_fixed']: | |
700 | tkfixedfont.configure(size=self.options['fontsize_fixed']) | |
701 | logger.info("using fixedfont size: {0}".format(tkfixedfont.actual()['size'])) | |
702 | self.tkfixedfont = tkfixedfont | |
703 | ||
704 | tktreefont = tkFont.nametofont("TkDefaultFont") | |
705 | treefontfamily = tktreefont['family'] | |
706 | if 'fontsize_tree' in self.options and self.options['fontsize_tree']: | |
707 | tktreefont.configure(size=self.options['fontsize_tree']) | |
708 | logger.info("using treefont size: {0}".format(tktreefont.actual()['size'])) | |
709 | self.tktreefont = tktreefont | |
710 | ||
711 | self.popup = '' | |
712 | self.value = '' | |
713 | self.firsttime = True | |
714 | self.mode = 'command' # or edit or delete | |
715 | self.item_hsh = {} | |
716 | self.depth2id = {} | |
717 | self.prev_week = None | |
718 | self.next_week = None | |
719 | self.curr_week = None | |
720 | self.week_beg = None | |
721 | self.itemSelected = None | |
722 | self.uuidSelected = None | |
723 | self.dtSelected = None | |
724 | self.rowSelected = None | |
725 | ||
726 | self.title(ETM) | |
727 | ||
728 | # self.wm_iconbitmap(bitmap='etmlogo.gif') | |
729 | # self.wm_iconbitmap('etmlogo-4.xbm') | |
730 | # self.call('wm', 'iconbitmap', self._w, '/Users/dag/etm-tk/etmTk/etmlogo.gif') | |
731 | ||
732 | # root = Tk() | |
733 | # img = PhotoImage(file='/Users/dag/etm-tk/etmTk/etmlogo.icns') | |
734 | # self.tk.call('wm', 'iconphoto', self._w, img) | |
735 | # if not mac: | |
736 | # img = PhotoImage(file='etmlogo.gif') | |
737 | # self.call('wm', 'iconphoto', self._w, img) | |
738 | ||
739 | # if sys_platform == 'Linux': | |
740 | # logger.debug('using linux icon') | |
741 | # self.wm_iconbitmap('@' + 'etmlogo.gif') | |
742 | # # self.wm_iconbitmap('etmlogo-4.xbm') | |
743 | # # self.call('wm', 'iconbitmap', self._w, '/Users/dag/etm-tk/etmlogo_128x128x32.ico') | |
744 | # # self.iconbitmap(ICON_PATH) | |
745 | # elif sys_platform == 'Darwin': | |
746 | # logger.debug('using darwin icon') | |
747 | # self.iconbitmap('/Users/dag/etm-tk/etmTk/etmlogo.ico') | |
748 | # self.wm_iconbitmap('etmlogo.icns') | |
749 | # else: | |
750 | # logger.debug('using windows icon') | |
751 | # self.wm_iconbitmap('etmlogo.ico') | |
752 | ||
753 | self.columnconfigure(0, minsize=300, weight=1) | |
754 | self.rowconfigure(1, weight=2) | |
755 | ||
756 | topbar = Frame(self, bd=0, relief="flat", highlightbackground=BGCOLOR, background=BGCOLOR) | |
757 | topbar.pack(side="top", fill="x", expand=0, padx=0, pady=0) | |
758 | ||
759 | # report | |
760 | self.box_value = StringVar() | |
761 | self.custom_box = ttk.Combobox(self, textvariable=self.box_value, font=self.tkfixedfont) | |
762 | self.custom_box.bind("<<ComboboxSelected>>", self.newselection) | |
763 | self.bind("<Return>", self.makeReport) | |
764 | self.bind("<Control-q>", self.quit) | |
765 | self.saved_specs = [''] | |
766 | self.getSpecs() | |
767 | if self.specs: | |
768 | self.value_of_combo = self.specs[0] | |
769 | self.custom_box['values'] = self.specs | |
770 | self.custom_box.current(0) | |
771 | self.saved_specs = deepcopy(self.specs) | |
772 | self.custom_box.configure(width=30, background=BGCOLOR, takefocus=False) | |
773 | ||
774 | self.vm_options = [[AGENDA, 'a'], | |
775 | ['-', ''], | |
776 | [DAY, 'd'], | |
777 | [WEEK, 'w'], | |
778 | [MONTH, 'm'], | |
779 | ['-', ''], | |
780 | [TAG, 't'], | |
781 | [KEYWORD, 'k'], | |
782 | [PATH, 'p'], | |
783 | ['-', ''], | |
784 | [NOTE, 'n'], | |
785 | [CUSTOM, 'c']] | |
786 | ||
787 | self.view2cmd = {'a': self.agendaView, | |
788 | 'd': self.dayView, | |
789 | 'm': self.showMonthly, | |
790 | 'p': self.pathView, | |
791 | 'k': self.keywordView, | |
792 | 'n': self.noteView, | |
793 | 't': self.tagView, | |
794 | 'c': self.customView, | |
795 | 'w': self.showWeekly} | |
796 | ||
797 | self.view = self.vm_options[0][0] | |
798 | self.currentView = StringVar(self) | |
799 | self.currentView.set(self.view) | |
800 | ||
801 | MAIN = _("Main") | |
802 | self.add2menu(root, (MAIN, )) | |
803 | VIEWS = _("Views") | |
804 | self.add2menu(MAIN, (VIEWS, )) | |
805 | ||
806 | self.vm_opts = [x[0] for x in self.vm_options] | |
807 | # self.vm = OptionMenu(topbar, self.currentView, *self.vm_opts) | |
808 | self.vm = OptionMenu(topbar, self.currentView, []) | |
809 | self.vm["menu"].delete(0, END) | |
810 | self.vm.configure(pady=2) | |
811 | for i in range(len(self.vm_options)): | |
812 | label = self.vm_options[i][0] | |
813 | k = self.vm_options[i][1] | |
814 | if label == "-": | |
815 | self.vm["menu"].add_separator() | |
816 | self.add2menu(VIEWS, (SEP, )) | |
817 | else: | |
818 | l, c = commandShortcut(k) | |
819 | self.vm["menu"].add_command(label=label, command=self.view2cmd[k], accelerator=l) | |
820 | self.bind(c, lambda e, x=k: self.after(AFTER, self.view2cmd[x])) | |
821 | self.add2menu(VIEWS, (self.vm_opts[i], l)) | |
822 | self.vm.pack(side="left", padx=2) | |
823 | self.vm.configure(background=BGCOLOR, takefocus=False) | |
824 | ||
825 | # calendars | |
826 | self.calbutton = Button(topbar, text=CALENDARS, command=self.selectCalendars, highlightbackground=BGCOLOR, bg=BGCOLOR, width=8, pady=2) | |
827 | self.calbutton.pack(side="right", padx=6) | |
828 | if not self.default_calendars: | |
829 | self.calbutton.configure(state="disabled") | |
830 | ||
831 | # filter | |
832 | self.filterValue = StringVar(self) | |
833 | self.filterValue.set('') | |
834 | self.filterValue.trace_variable("w", self.filterView) | |
835 | ||
836 | self.fltr = Entry(topbar, textvariable=self.filterValue, width=14, highlightbackground=BGCOLOR, bg=BGCOLOR) | |
837 | self.fltr.pack(side="left", padx=0, expand=1, fill=X) | |
838 | self.fltr.bind("<FocusIn>", self.setFilter) | |
839 | self.fltr.bind("<Escape>", self.clearFilter) | |
840 | self.fltr.bind('<Tab>', self.leaveFilter) | |
841 | self.bind("<Shift-Control-F>", self.clearFilter) | |
842 | self.filter_active = False | |
843 | self.viewmenu.entryconfig(6, state="normal") | |
844 | self.viewmenu.entryconfig(7, state="disabled") | |
845 | ||
846 | self.weekly = False | |
847 | ||
848 | self.col2_width = tktreefont.measure('abcdgklmprtuX') | |
849 | self.col3_width = tktreefont.measure('10:30pm ~ 11:30pmX') | |
850 | self.text_width = 260 | |
851 | logger.info('column widths: {0}, {1}, {2}'.format(self.text_width, self.col2_width, self.col3_width)) | |
852 | self.tree.column('#0', minwidth=140, width=self.text_width, stretch=1) | |
853 | self.labels = False | |
854 | # don't show the labels column to start with by setting width=0 | |
855 | self.tree.column('#1', minwidth=0, width=0, stretch=0, anchor='center') | |
856 | self.tree.column('#2', width=self.col3_width, stretch=0, anchor='center') | |
857 | self.tree.bind('<<TreeviewSelect>>', self.OnSelect) | |
858 | self.tree.bind('<Double-1>', self.OnActivate) | |
859 | self.tree.bind('<Return>', self.OnActivate) | |
860 | self.tree.bind('<Control-Down>', self.nextItem) | |
861 | self.tree.bind('<Control-Up>', self.prevItem) | |
862 | # self.tree.bind('<space>', self.goHome) | |
863 | ||
864 | for t in tstr2SCI: | |
865 | self.tree.tag_configure(t, foreground=tstr2SCI[t][1]) | |
866 | ||
867 | self.date2id = {} | |
868 | self.root = ('', '_') | |
869 | ||
870 | self.tree.pack(fill="both", expand=1, padx=4, pady=0) | |
871 | panedwindow.add(self.toppane, padx=0, pady=0, stretch="first") | |
872 | ||
873 | self.content = ReadOnlyText(panedwindow, wrap="word", padx=3, bd=2, relief="sunken", font=tkfixedfont, height=loop.options['details_rows'], width=46, takefocus=False) | |
874 | self.content.bind('<Escape>', self.cleartext) | |
875 | self.content.bind('<Tab>', self.focus_next_window) | |
876 | self.content.bind("<FocusIn>", self.editItem) | |
877 | ||
878 | panedwindow.add(self.content, padx=3, pady=0, stretch="never") | |
879 | ||
880 | self.statusbar = Frame(self) | |
881 | ||
882 | self.timerStatus = StringVar(self) | |
883 | self.timerStatus.set("") | |
884 | timer_status = Label(self.statusbar, textvariable=self.timerStatus, bd=0, relief="flat", anchor="w", padx=0, pady=0) | |
885 | timer_status.pack(side="left", expand=0, padx=2) | |
886 | timer_status.configure(background=BGCOLOR, highlightthickness=0) | |
887 | ||
888 | self.pendingAlerts = IntVar(self) | |
889 | self.pendingAlerts.set(0) | |
890 | self.pending = Button(self.statusbar, padx=8, pady=2, | |
891 | ||
892 | takefocus=False, textvariable=self.pendingAlerts, command=self.showAlerts, anchor="center") | |
893 | self.pending.pack(side="right", padx=0, pady=0) | |
894 | self.pending.configure(highlightbackground=BGCOLOR, | |
895 | background=BGCOLOR, | |
896 | highlightthickness=0, | |
897 | state="disabled") | |
898 | self.showPending = True | |
899 | ||
900 | self.currentTime = StringVar(self) | |
901 | currenttime = Label(self.statusbar, textvariable=self.currentTime, bd=1, relief="flat", anchor="e", padx=4, pady=0) | |
902 | currenttime.pack(side="right") | |
903 | currenttime.configure(background=BGCOLOR) | |
904 | ||
905 | self.statusbar.pack(side="bottom", fill="x", expand=0, padx=6, pady=0) | |
906 | self.statusbar.configure(background=BGCOLOR) | |
907 | ||
908 | panedwindow.pack(side="top", fill="both", expand=1, padx=2, pady=0) | |
909 | panedwindow.configure(background=BGCOLOR) | |
910 | self.panedwindow = panedwindow | |
911 | ||
912 | # set cal_regex here and update it in updateCalendars | |
913 | self.cal_regex = None | |
914 | if loop.calendars: | |
915 | cal_pattern = r'^%s' % '|'.join( | |
916 | [x[2] for x in loop.calendars if x[1]]) | |
917 | self.cal_regex = re.compile(cal_pattern) | |
918 | logger.debug("cal_pattern: {0}".format(cal_pattern)) | |
919 | ||
920 | self.default_regex = None | |
921 | if 'calendars' in loop.options and loop.options['calendars']: | |
922 | calendars = loop.options['calendars'] | |
923 | default_pattern = r'^%s' % '|'.join( | |
924 | [x[2] for x in calendars if x[1]]) | |
925 | self.default_regex = re.compile(default_pattern) | |
926 | ||
927 | self.add2menu(root, (EDIT, )) | |
928 | self.add2menu(EDIT, (_("Show completions"), "Ctrl-Space")) | |
929 | self.add2menu(EDIT, (_("Cancel"), "Escape")) | |
930 | self.add2menu(EDIT, (FINISH, "Ctrl-W")) | |
931 | ||
932 | # start clock | |
933 | self.updateClock() | |
934 | self.year_month = [self.today.year, self.today.month] | |
935 | # showView will be called from updateClock | |
936 | self.updateAlerts() | |
937 | self.etmgeo = os.path.normpath(os.path.join(loop.options['etmdir'], ".etmgeo")) | |
938 | self.restoreGeometry() | |
939 | self.tree.focus_set() | |
940 | ||
941 | def bindTop(self, c, cmd, e=None): | |
942 | if e and e.char != c: | |
943 | # ignore Control-c | |
944 | return | |
945 | self.tree.bind(c, lambda e: self.after(AFTER, cmd(e))) | |
946 | self.canvas.bind(c, lambda e: self.after(AFTER, cmd(e))) | |
947 | ||
948 | def toggleLabels(self, e=None): | |
949 | if e and e.char != "l": | |
950 | return | |
951 | if self.weekly or self.monthly: | |
952 | return | |
953 | if self.labels: | |
954 | width0 = self.tree.column('#0')['width'] | |
955 | self.tree.column('#0', width=width0 + self.col2_width) | |
956 | self.tree.column('#1', width=0) | |
957 | self.labels = False | |
958 | else: | |
959 | width0 = self.tree.column('#0')['width'] | |
960 | self.tree.column('#0', width=width0 - self.col2_width) | |
961 | self.tree.column('#1', width=self.col2_width) | |
962 | self.labels = True | |
963 | ||
964 | def toggleFinished(self, e=None): | |
965 | if e and e.char != "x": | |
966 | return | |
967 | if loop.options['hide_finished']: | |
968 | loop.options['hide_finished'] = False | |
969 | else: | |
970 | loop.options['hide_finished'] = True | |
971 | logger.debug('reloading data') | |
972 | # self.updateAlerts() | |
973 | if self.weekly: | |
974 | self.showWeek() | |
975 | elif self.monthly: | |
976 | self.showMonth() | |
977 | else: | |
978 | self.showView() | |
979 | ||
980 | def saveGeometry(self): | |
981 | str = self.geometry() | |
982 | fo = open(self.etmgeo, 'w') | |
983 | fo.write(str) | |
984 | fo.close() | |
985 | ||
986 | def restoreGeometry(self): | |
987 | if os.path.isfile(self.etmgeo): | |
988 | fo = open(self.etmgeo, "r") | |
989 | str = fo.read() | |
990 | fo.close() | |
991 | tup = [x.strip() for x in str.split(',')] | |
992 | if tup: | |
993 | self.geometry(tup[0]) | |
994 | ||
995 | def closeItemMenu(self, event=None): | |
996 | if self.weekly or self.monthly: | |
997 | self.canvas.focus_set() | |
998 | else: | |
999 | self.tree.focus_set() | |
1000 | self.itemmenu.unpost() | |
1001 | ||
1002 | def add2menu(self, parent, child): | |
1003 | if child == (SEP, ): | |
1004 | id = uuid.uuid1() | |
1005 | elif len(child) > 1 and child[1]: | |
1006 | id = uuid.uuid1() | |
1007 | m = LASTLTR.search(child[1]) | |
1008 | if m: | |
1009 | ||
1010 | child = tuple(child) | |
1011 | else: | |
1012 | id = child[0] | |
1013 | if len(child) >= 2: | |
1014 | leaf = "{0}::{1}".format(child[0], child[1]) | |
1015 | else: | |
1016 | leaf = "{0}::".format(child[0]) | |
1017 | self.menutree.create_node(leaf, id, parent=parent) | |
1018 | ||
1019 | def confirm(self, parent=None, title="", prompt="", instance="xyz"): | |
1020 | ok, value = OptionsDialog(parent=parent, title=_("confirm").format(instance), prompt=prompt).getValue() | |
1021 | return ok | |
1022 | ||
1023 | def selectCalendars(self): | |
1024 | if self.default_calendars: | |
1025 | prompt = _("Only items from selected calendars will be displayed.") | |
1026 | title = CALENDARS | |
1027 | if self.weekly or self.monthly: | |
1028 | master = self.canvas | |
1029 | else: | |
1030 | master = self.tree | |
1031 | ||
1032 | values = OptionsDialog(parent=self, master=master, title=title, prompt=prompt, opts=loop.calendars, radio=False, yesno=False).getValue() | |
1033 | ||
1034 | if values != loop.calendars: | |
1035 | loop.calendars = values | |
1036 | loop.options['calendars'] = values | |
1037 | data.setConfig(loop.options) | |
1038 | self.updateCalendars() | |
1039 | else: | |
1040 | prompt = _("No calendars have been specified in etmtk.cfg.") | |
1041 | self.textWindow(self, CALENDARS, prompt, opts=self.options) | |
1042 | ||
1043 | def updateCalendars(self, *args): | |
1044 | cal_pattern = r'^%s' % '|'.join( | |
1045 | [x[2] for x in loop.calendars if x[1]]) | |
1046 | self.cal_regex = re.compile(cal_pattern) | |
1047 | if loop.calendars != self.default_calendars: | |
1048 | self.calbutton.configure(text="{0}*".format(CALENDARS)) | |
1049 | else: | |
1050 | self.calbutton.configure(text=CALENDARS) | |
1051 | self.update() | |
1052 | self.updateAlerts() | |
1053 | if self.weekly: | |
1054 | self.showWeek() | |
1055 | elif self.monthly: | |
1056 | self.showMonth() | |
1057 | else: | |
1058 | self.showView() | |
1059 | ||
1060 | def quit(self, e=None): | |
1061 | ans = self.confirm( | |
1062 | title=_('Quit'), | |
1063 | prompt=_("Do you really want to quit?"), | |
1064 | parent=self) | |
1065 | if ans: | |
1066 | self.saveGeometry() | |
1067 | self.destroy() | |
1068 | ||
1069 | def donothing(self, e=None): | |
1070 | """For testing""" | |
1071 | logger.debug('donothing') | |
1072 | return "break" | |
1073 | ||
1074 | def openWithDefault(self, e=None): | |
1075 | if not self.itemSelected or 'g' not in self.itemSelected: | |
1076 | return(False) | |
1077 | path = self.itemSelected['g'] | |
1078 | ||
1079 | if windoz: | |
1080 | os.startfile(path) | |
1081 | return() | |
1082 | if mac: | |
1083 | cmd = 'open' + " {0}".format(path) | |
1084 | else: | |
1085 | cmd = 'xdg-open' + " {0}".format(path) | |
1086 | subprocess.call(cmd, shell=True) | |
1087 | return | |
1088 | ||
1089 | def printWithDefault(self, s, e=None): | |
1090 | fo = codecs.open(loop.tmpfile, 'w', loop.options['encoding']['file']) | |
1091 | # add a trailing formfeed | |
1092 | # fo.write("{0}".format(s)) | |
1093 | fo.write(s) | |
1094 | fo.close() | |
1095 | if windoz: | |
1096 | os.startfile(loop.tmpfile, "print") | |
1097 | return | |
1098 | else: | |
1099 | cmd = "lp -s -o media='letter' -o cpi=12 -o lpi=8 -o page-left=48 -o page-right=48 -o page-top=48 -o page-bottom=48 {0}\n".format(loop.tmpfile) | |
1100 | # cmd = "lpr -l {0}".format(loop.tmpfile) | |
1101 | subprocess.call(cmd, shell=True) | |
1102 | return | |
1103 | ||
1104 | def showUserDetails(self, e=None): | |
1105 | if not self.itemSelected or 'u' not in self.itemSelected: | |
1106 | return | |
1107 | if not loop.options['user_data']: | |
1108 | return | |
1109 | user = self.itemSelected['u'] | |
1110 | if user in loop.options['user_data']: | |
1111 | detail = "\n".join(loop.options['user_data'][user]) | |
1112 | else: | |
1113 | detail = _("No record was found for {0}".format(user)) | |
1114 | self.textWindow(self, user, detail, opts=loop.options) | |
1115 | return | |
1116 | ||
1117 | def dateCalculator(self, event=None): | |
1118 | prompt = """\ | |
1119 | Enter an expression of the form "x [+-] y" where x is a date and y is | |
1120 | either a date or a time period if "-" is used and a time period if "+" | |
1121 | is used. Both x and y can be followed by timezones, e.g., | |
1122 | 4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai | |
1123 | = 14h25m | |
1124 | 4/20 4:50p Asia/Shanghai + 14h25m US/Central | |
1125 | = 2014-04-20 18:15-0500 | |
1126 | The local timezone is used when none is given.""" | |
1127 | GetString(parent=self, title=_('date and time calculator'), prompt=prompt, opts=loop.options, process=date_calculator) | |
1128 | return | |
1129 | ||
1130 | def availableDateCalculator(self, event=None): | |
1131 | prompt = """\ | |
1132 | Enter an expression of the form | |
1133 | start; end; busy | |
1134 | where start and end are dates and busy is comma separated list of | |
1135 | busy dates or busy intervals, .e.g, | |
1136 | 6/1; 6/30; 6/2, 6/14-6/22, 6/5-6/9, 6/11-6/15, 6/17-6/29 | |
1137 | returns: | |
1138 | Sun Jun 01 | |
1139 | Tue Jun 03 | |
1140 | Wed Jun 04 | |
1141 | Tue Jun 10 | |
1142 | Mon Jun 30\ | |
1143 | """ | |
1144 | GetString(parent=self, title=_('available dates calculator'), prompt=prompt, opts={}, process=availableDates, font=self.tkfixedfont) | |
1145 | return | |
1146 | ||
1147 | def exportToIcal(self, e=None): | |
1148 | if self.itemSelected: | |
1149 | self._exportItemToIcal() | |
1150 | else: | |
1151 | self._exportActiveToIcal() | |
1152 | ||
1153 | def _exportItemToIcal(self): | |
1154 | if 'icsitem_file' in loop.options: | |
1155 | res = export_ical_item(self.itemSelected, loop.options['icsitem_file']) | |
1156 | if res: | |
1157 | prompt = _("Selected item successfully exported to {0}".format(loop.options['icsitem_file'])) | |
1158 | else: | |
1159 | prompt = _("Could not export selected item.") | |
1160 | else: | |
1161 | prompt = "icsitem_file is not set in etmtk.cfg" | |
1162 | MessageWindow(self, 'Selected Item Export', prompt) | |
1163 | ||
1164 | def _exportActiveToIcal(self, event=None): | |
1165 | if 'icscal_file' in loop.options: | |
1166 | res = export_ical_active(loop.file2uuids, loop.uuid2hash, loop.options['icscal_file'], loop.calendars) | |
1167 | if res: | |
1168 | prompt = _("Active calendars successfully exported to {0}".format(loop.options['icscal_file'])) | |
1169 | else: | |
1170 | prompt = _("Could not export active calendars.") | |
1171 | else: | |
1172 | prompt = "icscal_file is not set in etmtk.cfg" | |
1173 | MessageWindow(self, 'Active Calendar Export', prompt) | |
1174 | ||
1175 | def newItem(self, e=None): | |
1176 | # hack to avoid Ctrl-n activation | |
1177 | if e and e.char != "n": | |
1178 | return | |
1179 | if self.weekly or self.monthly: | |
1180 | master = self.canvas | |
1181 | else: | |
1182 | master = self.tree | |
1183 | if self.view in [AGENDA] and self.active_date: | |
1184 | if self.itemSelected: | |
1185 | if 's' in self.itemSelected: | |
1186 | text = " @s {0}".format(self.active_date) | |
1187 | elif 'c' in self.itemSelected: | |
1188 | text = " @c {0}".format(self.itemSelected['c']) | |
1189 | else: | |
1190 | text = " @s {0}".format(self.active_date) | |
1191 | changed = SimpleEditor(parent=self, master=master, start=text, options=loop.options).changed | |
1192 | elif self.view in [DAY, WEEK, MONTH] and self.active_date: | |
1193 | text = " @s {0}".format(self.active_date) | |
1194 | changed = SimpleEditor(parent=self, master=master, start=text, options=loop.options).changed | |
1195 | elif self.view in [KEYWORD, NOTE] and self.itemSelected: | |
1196 | if self.itemSelected and 'k' in self.itemSelected: | |
1197 | text = " @k {0}".format(self.itemSelected['k']) | |
1198 | else: | |
1199 | text = "" | |
1200 | changed = SimpleEditor(parent=self, master=master, start=text, options=loop.options).changed | |
1201 | elif self.view in [TAG]: | |
1202 | if self.itemSelected and 't' in self.itemSelected: | |
1203 | text = " @t {0}".format(", ".join(self.itemSelected['t'])) | |
1204 | else: | |
1205 | text = "" | |
1206 | changed = SimpleEditor(parent=self, master=master, start=text, options=loop.options).changed | |
1207 | else: | |
1208 | changed = SimpleEditor(parent=self, master=master, options=loop.options).changed | |
1209 | if changed: | |
1210 | logger.debug('changed, reloading data') | |
1211 | loop.do_update = True | |
1212 | self.updateAlerts() | |
1213 | if self.weekly: | |
1214 | self.showWeek() | |
1215 | elif self.monthly: | |
1216 | self.showMonth() | |
1217 | else: | |
1218 | self.showView() | |
1219 | ||
1220 | def which(self, act, instance="xyz"): | |
1221 | prompt = "\n".join([ | |
1222 | _("You have selected an instance of a repeating"), | |
1223 | _("item. What do you want to {0}?").format(act)]) | |
1224 | if act == DELETE: | |
1225 | opt_lst = [ | |
1226 | _("this instance"), | |
1227 | _("this and all subsequent instances"), | |
1228 | _("all instances"), | |
1229 | _("all previous instances")] | |
1230 | else: | |
1231 | opt_lst = [ | |
1232 | _("this instance"), | |
1233 | _("this and all subsequent instances"), | |
1234 | _("all instances")] | |
1235 | ||
1236 | if self.weekly or self.monthly: | |
1237 | master = self.canvas | |
1238 | else: | |
1239 | master = self.tree | |
1240 | index, value = OptionsDialog(parent=self, master=master, title=_("instance: {0}").format(instance), prompt=prompt, opts=opt_lst, yesno=False).getValue() | |
1241 | return index, value | |
1242 | ||
1243 | def copyItem(self, e=None): | |
1244 | """ | |
1245 | newhsh = selected, rephsh = None | |
1246 | """ | |
1247 | if not self.itemSelected: | |
1248 | return | |
1249 | if e and e.char != 'c': | |
1250 | return | |
1251 | if 'r' in self.itemSelected: | |
1252 | choice, value = self.which(COPY, self.dtSelected) | |
1253 | logger.debug("{0}: {1}".format(choice, value)) | |
1254 | if not choice: | |
1255 | self.tree.focus_set() | |
1256 | return | |
1257 | self.itemSelected['_dt'] = self.dtSelected | |
1258 | else: | |
1259 | ans = self.confirm( | |
1260 | parent=self.tree, | |
1261 | title=_('Confirm'), | |
1262 | prompt=_("Open a copy of this item?")) | |
1263 | if not ans: | |
1264 | self.tree.focus_set() | |
1265 | return | |
1266 | choice = 3 | |
1267 | hsh_cpy = deepcopy(self.itemSelected) | |
1268 | hsh_cpy['fileinfo'] = None | |
1269 | ||
1270 | title = _("new item") | |
1271 | self.mode = 'new' | |
1272 | if choice in [1, 2]: | |
1273 | # we need to modify the copy according to the choice | |
1274 | dt = hsh_cpy['_dt'].replace( | |
1275 | tzinfo=tzlocal()).astimezone(gettz(hsh_cpy['z'])) | |
1276 | dtn = dt.replace(tzinfo=None) | |
1277 | ||
1278 | if choice == 1: | |
1279 | # this instance | |
1280 | for k in ['_r', 'o', '+', '-']: | |
1281 | if k in hsh_cpy: | |
1282 | del hsh_cpy[k] | |
1283 | hsh_cpy['s'] = dtn | |
1284 | ||
1285 | elif choice == 2: | |
1286 | # this and all subsequent instances | |
1287 | if u'+' in hsh_cpy: | |
1288 | tmp_cpy = [] | |
1289 | for d in hsh_cpy['+']: | |
1290 | if d >= dtn: | |
1291 | tmp_cpy.append(d) | |
1292 | hsh_cpy['+'] = tmp_cpy | |
1293 | if u'-' in hsh_cpy: | |
1294 | tmp_cpy = [] | |
1295 | for d in hsh_cpy['-']: | |
1296 | if d >= dtn: | |
1297 | tmp_cpy.append(d) | |
1298 | hsh_cpy['-'] = tmp_cpy | |
1299 | hsh_cpy['s'] = dtn | |
1300 | ||
1301 | changed = SimpleEditor(parent=self, newhsh=hsh_cpy, rephsh=None, options=loop.options, title=title, modified=True).changed | |
1302 | if changed: | |
1303 | self.updateAlerts() | |
1304 | if self.weekly: | |
1305 | self.showWeek() | |
1306 | elif self.weekly: | |
1307 | self.showMonth() | |
1308 | else: | |
1309 | self.showView(row=self.topSelected) | |
1310 | else: | |
1311 | if self.weekly or self.monthly: | |
1312 | self.canvas.focus_set() | |
1313 | else: | |
1314 | self.tree.focus_set() | |
1315 | ||
1316 | def deleteItem(self, e=None): | |
1317 | if not self.itemSelected: | |
1318 | return | |
1319 | logger.debug('{0}: {1}'.format(self.itemSelected['_summary'], self.dtSelected)) | |
1320 | indx = 3 | |
1321 | if 'r' in self.itemSelected: | |
1322 | indx, value = self.which(DELETE, self.dtSelected) | |
1323 | logger.debug("{0}: {1}/{2}".format(self.dtSelected, indx, value)) | |
1324 | if not indx: | |
1325 | if self.weekly or self.monthly: | |
1326 | self.canvas.focus_set() | |
1327 | else: | |
1328 | self.tree.focus_set() | |
1329 | return | |
1330 | self.itemSelected['_dt'] = self.dtSelected | |
1331 | else: | |
1332 | ans = self.confirm( | |
1333 | title=_('Confirm'), | |
1334 | prompt=_("Delete this item?"), | |
1335 | parent=self.tree) | |
1336 | if not ans: | |
1337 | self.tree.focus_set() | |
1338 | return | |
1339 | loop.item_hsh = self.itemSelected | |
1340 | loop.cmd_do_delete(indx) | |
1341 | ||
1342 | self.updateAlerts() | |
1343 | if self.weekly: | |
1344 | self.canvas.focus_set() | |
1345 | self.showWeek() | |
1346 | elif self.monthly: | |
1347 | self.canvas.focus_set() | |
1348 | self.showMonth() | |
1349 | else: | |
1350 | self.tree.focus_set() | |
1351 | self.showView(row=self.topSelected) | |
1352 | ||
1353 | def moveItem(self, e=None): | |
1354 | if not self.itemSelected: | |
1355 | return | |
1356 | if e and e.char != 'm': | |
1357 | return | |
1358 | logger.debug('{0}: {1}'.format(self.itemSelected['_summary'], self.dtSelected)) | |
1359 | oldrp, begline, endline = self.itemSelected['fileinfo'] | |
1360 | oldfile = os.path.join(loop.options['datadir'], oldrp) | |
1361 | newfile = self.getDataFile(title="moving from {0}:".format(oldrp), start=oldfile) | |
1362 | if not (newfile and os.path.isfile(newfile)): | |
1363 | return | |
1364 | if newfile == oldfile: | |
1365 | return | |
1366 | ret = loop.append_item(self.itemSelected, newfile) | |
1367 | if ret != "break": | |
1368 | # post message and quit | |
1369 | prompt = _("""\ | |
1370 | Adding item to {1} failed - aborted removing item from {2}""".format( | |
1371 | newfile, oldfile)) | |
1372 | MessageWindow(self, 'Error', prompt) | |
1373 | return | |
1374 | loop.item_hsh = self.itemSelected | |
1375 | ret = loop.delete_item() | |
1376 | ||
1377 | self.updateAlerts() | |
1378 | if self.weekly: | |
1379 | self.canvas.focus_set() | |
1380 | self.showWeek() | |
1381 | elif self.monthly: | |
1382 | self.canvas.focus_set() | |
1383 | self.showWeek() | |
1384 | else: | |
1385 | self.tree.focus_set() | |
1386 | self.showView(row=self.topSelected) | |
1387 | ||
1388 | def editItem(self, e=None): | |
1389 | if not self.itemSelected: | |
1390 | return | |
1391 | logger.debug('starting editItem: {0}; {1}, {2}'.format(self.itemSelected['_summary'], self.dtSelected, type(self.dtSelected))) | |
1392 | choice = 3 | |
1393 | title = ETM | |
1394 | start_text = None | |
1395 | filename = None | |
1396 | if self.itemSelected['itemtype'] == '$': | |
1397 | start_text = self.itemSelected['entry'] | |
1398 | hsh_rev = deepcopy(self.itemSelected) | |
1399 | elif 'r' in self.itemSelected: | |
1400 | # repeating | |
1401 | choice, value = self.which(EDIT, self.dtSelected) | |
1402 | logger.debug("{0}: {1}".format(choice, value)) | |
1403 | if self.weekly or self.monthly: | |
1404 | self.canvas.focus_set() | |
1405 | else: | |
1406 | self.tree.focus_set() | |
1407 | logger.debug(('dtSelected: {0}, {1}'.format(type(self.dtSelected), self.dtSelected))) | |
1408 | self.itemSelected['_dt'] = self.dtSelected | |
1409 | if not choice: | |
1410 | self.tree.focus_set() | |
1411 | return | |
1412 | hsh_rev = hsh_cpy = None | |
1413 | self.mode = 2 # replace | |
1414 | if choice in [1, 2]: | |
1415 | self.mode = 3 # new and replace - both newhsh and rephsh | |
1416 | title = _("new item") | |
1417 | hsh_cpy = deepcopy(self.itemSelected) | |
1418 | hsh_rev = deepcopy(self.itemSelected) | |
1419 | # we will be editing and adding hsh_cpy and replacing hsh_rev | |
1420 | ||
1421 | dt = hsh_cpy['_dt'].replace( | |
1422 | tzinfo=tzlocal()).astimezone(gettz(hsh_cpy['z'])) | |
1423 | dtn = dt.replace(tzinfo=None) | |
1424 | ||
1425 | if choice == 1: | |
1426 | # this instance | |
1427 | if '+' in hsh_rev and dtn in hsh_rev['+']: | |
1428 | hsh_rev['+'].remove(dtn) | |
1429 | if not hsh_rev['+'] and hsh_rev['r'] == 'l': | |
1430 | del hsh_rev['r'] | |
1431 | del hsh_rev['_r'] | |
1432 | else: | |
1433 | hsh_rev.setdefault('-', []).append(dt) | |
1434 | for k in ['_r', 'o', '+', '-']: | |
1435 | if k in hsh_cpy: | |
1436 | del hsh_cpy[k] | |
1437 | hsh_cpy['s'] = dtn | |
1438 | ||
1439 | elif choice == 2: | |
1440 | # this and all subsequent instances | |
1441 | tmp = [] | |
1442 | for h in hsh_rev['_r']: | |
1443 | if 'f' in h and h['f'] != u'l': | |
1444 | h['u'] = dt - ONEMINUTE | |
1445 | tmp.append(h) | |
1446 | hsh_rev['_r'] = tmp | |
1447 | if u'+' in hsh_rev: | |
1448 | tmp_rev = [] | |
1449 | tmp_cpy = [] | |
1450 | for d in hsh_rev['+']: | |
1451 | if d < dtn: | |
1452 | tmp_rev.append(d) | |
1453 | else: | |
1454 | tmp_cpy.append(d) | |
1455 | hsh_rev['+'] = tmp_rev | |
1456 | hsh_cpy['+'] = tmp_cpy | |
1457 | if u'-' in hsh_rev: | |
1458 | tmp_rev = [] | |
1459 | tmp_cpy = [] | |
1460 | for d in hsh_rev['-']: | |
1461 | if d < dtn: | |
1462 | tmp_rev.append(d) | |
1463 | else: | |
1464 | tmp_cpy.append(d) | |
1465 | hsh_rev['-'] = tmp_rev | |
1466 | hsh_cpy['-'] = tmp_cpy | |
1467 | hsh_cpy['s'] = dtn | |
1468 | else: # replace | |
1469 | self.mode = 2 | |
1470 | hsh_rev = deepcopy(self.itemSelected) | |
1471 | ||
1472 | logger.debug("mode: {0}; newhsh: {1}; rephsh: {2}".format(self.mode, hsh_cpy is not None, hsh_rev is not None)) | |
1473 | changed = SimpleEditor(parent=self, file=filename, newhsh=hsh_cpy, rephsh=hsh_rev, options=loop.options, title=title, start=start_text).changed | |
1474 | ||
1475 | if changed: | |
1476 | logger.debug("starting if changed") | |
1477 | loop.do_update = True | |
1478 | self.updateAlerts() | |
1479 | if self.weekly: | |
1480 | self.canvas.focus_set() | |
1481 | self.showWeek() | |
1482 | elif self.monthly: | |
1483 | self.canvas.focus_set() | |
1484 | self.showMonth() | |
1485 | else: | |
1486 | self.tree.focus_set() | |
1487 | self.showView(row=self.topSelected) | |
1488 | self.update_idletasks() | |
1489 | logger.debug("leaving if changed") | |
1490 | ||
1491 | else: | |
1492 | if self.weekly or self.monthly: | |
1493 | self.canvas.focus_set() | |
1494 | else: | |
1495 | self.tree.focus_set() | |
1496 | logger.debug('ending editItem') | |
1497 | return | |
1498 | ||
1499 | def editItemFile(self, e=None): | |
1500 | if not self.itemSelected: | |
1501 | return | |
1502 | logger.debug('starting editItemFile: {0}; {1}, {2}, {3}'.format(self.itemSelected['_summary'], self.dtSelected, type(self.dtSelected), self.itemSelected['fileinfo'])) | |
1503 | self.editFile(e, file=os.path.join(loop.options['datadir'], self.itemSelected['fileinfo'][0]), line=self.itemSelected['fileinfo'][1]) | |
1504 | ||
1505 | def editFile(self, e=None, file=None, line=None, config=False): | |
1506 | if e and e.char and e.char not in ["F", "E", "C", "S", "P"]: | |
1507 | logger.debug('e.char: "{0}"'.format(e.char)) | |
1508 | return | |
1509 | titlefile = os.path.normpath(relpath(file, loop.options['datadir'])) | |
1510 | logger.debug('file: {0}; config: {1}'.format(file, config)) | |
1511 | if self.weekly or self.monthly: | |
1512 | master = self.canvas | |
1513 | else: | |
1514 | master = self.tree | |
1515 | changed = SimpleEditor(parent=self, master=master, file=file, line=line, options=loop.options, title=titlefile).changed | |
1516 | logger.debug('changed: {0}'.format(changed)) | |
1517 | if changed: | |
1518 | logger.debug("config: {0}".format(config)) | |
1519 | if config: | |
1520 | current_options = deepcopy(loop.options) | |
1521 | (user_options, options, use_locale) = data.get_options( | |
1522 | d=loop.options['etmdir']) | |
1523 | loop.options = options | |
1524 | if options['calendars'] != current_options['calendars']: | |
1525 | self.updateCalendars() | |
1526 | loop.do_update = True | |
1527 | ||
1528 | logger.debug("changed - calling updateAlerts") | |
1529 | self.updateAlerts() | |
1530 | if self.weekly: | |
1531 | self.canvas.focus_set() | |
1532 | self.showWeek() | |
1533 | elif self.monthly: | |
1534 | self.canvas.focus_set() | |
1535 | self.showMonth() | |
1536 | else: | |
1537 | self.tree.focus_set() | |
1538 | self.showView() | |
1539 | return changed | |
1540 | ||
1541 | def editConfig(self, e=None): | |
1542 | file = loop.options['config'] | |
1543 | self.editFile(e, file=file, config=True) | |
1544 | ||
1545 | def editCfgFiles(self, e=None): | |
1546 | other = [] | |
1547 | if 'cfg_files' in loop.options: | |
1548 | for key in ['completions', 'reports', 'users']: | |
1549 | other.extend(loop.options['cfg_files'][key]) | |
1550 | prefix, tuples = getFileTuples(loop.options['datadir'], include=r'*.cfg', other=other) | |
1551 | ret = FileChoice(self, "open configuration file", prefix=prefix, list=tuples).returnValue() | |
1552 | if not (ret and os.path.isfile(ret)): | |
1553 | return False | |
1554 | self.editFile(e, file=ret, config=True) | |
1555 | ||
1556 | def getReportsFile(self, e=None): | |
1557 | ret = FileChoice(self, "append to reports file", prefix=self.loop.options['etmdir'], list=self.loop.options['report_files']).returnValue() | |
1558 | if not (ret and os.path.isfile(ret)): | |
1559 | return False | |
1560 | return ret | |
1561 | ||
1562 | def getDataFile(self, e=None, title="data file", start=''): | |
1563 | prefix, tuples = getFileTuples(loop.options['datadir'], include=r'*.txt') | |
1564 | ret = FileChoice(self, title, prefix=prefix, list=tuples, start=start).returnValue() | |
1565 | if not (ret and os.path.isfile(ret)): | |
1566 | return False | |
1567 | return ret | |
1568 | ||
1569 | def editScratch(self, e=None): | |
1570 | file = loop.options['scratchpad'] | |
1571 | self.editFile(e, file=file) | |
1572 | ||
1573 | def editData(self, e=None): | |
1574 | if e and e.char != "F": | |
1575 | return | |
1576 | prefix, tuples = getFileTuples(loop.options['datadir'], include=r'*.txt', all=False) | |
1577 | ret = FileChoice(self, "open data file", prefix=prefix, list=tuples).returnValue() | |
1578 | if not (ret and os.path.isfile(ret)): | |
1579 | return False | |
1580 | self.editFile(e, file=ret) | |
1581 | ||
1582 | def newFile(self, e=None): | |
1583 | if e and e.char != "N": | |
1584 | return | |
1585 | other = [os.path.join(loop.options['etmdir'], 'etmtk.cfg')] | |
1586 | if 'cfg_files' in loop.options: | |
1587 | for key in ['completions', 'reports', 'users']: | |
1588 | other.extend(loop.options['cfg_files'][key]) | |
1589 | prefix, tuples = getFileTuples(loop.options['datadir'], include=r'*', other=other, all=True) | |
1590 | filename = FileChoice(self, "create new file", prefix=prefix, list=tuples, new=True).returnValue() | |
1591 | if not filename: | |
1592 | return | |
1593 | if os.path.isfile(filename): | |
1594 | prompt = _("Aborting. File {0} already exists.").format(filename) | |
1595 | MessageWindow(self, title=_("new file"), prompt=prompt) | |
1596 | else: | |
1597 | pth = os.path.split(filename)[0] | |
1598 | if not os.path.isdir(pth): | |
1599 | os.makedirs(pth) | |
1600 | fo = codecs.open(filename, 'w', loop.options['encoding']['file']) | |
1601 | fo.write("") | |
1602 | fo.close() | |
1603 | prompt = _('created: {0}').format(filename) | |
1604 | if self.weekly or self.monthly: | |
1605 | p = self.canvas | |
1606 | else: | |
1607 | p = self.tree | |
1608 | ans = self.confirm( | |
1609 | parent=p, | |
1610 | title=_('etm'), | |
1611 | prompt=_("file: {0}\nhas been created.\nOpen it for editing?").format(filename)) | |
1612 | if ans: | |
1613 | self.editFile(None, filename) | |
1614 | ||
1615 | def finishItem(self, e=None): | |
1616 | if e and e.char != "f": | |
1617 | return | |
1618 | if not (self.itemSelected and self.itemSelected['itemtype'] in ['-', '+', '%']): | |
1619 | return | |
1620 | prompt = _("""\ | |
1621 | Enter the completion date for the item or return an empty string to | |
1622 | use the current date. Relative dates and fuzzy parsing are supported.""") | |
1623 | d = GetDateTime(parent=self, title=_('date'), prompt=prompt) | |
1624 | chosen_day = d.value | |
1625 | if chosen_day is None: | |
1626 | return () | |
1627 | logger.debug('completion date: {0}'.format(chosen_day)) | |
1628 | loop.item_hsh = self.itemSelected | |
1629 | loop.cmd_do_finish(chosen_day, options=loop.options) | |
1630 | ||
1631 | self.updateAlerts() | |
1632 | if self.weekly: | |
1633 | self.canvas.focus_set() | |
1634 | self.showWeek() | |
1635 | elif self.monthly: | |
1636 | self.canvas.focus_set() | |
1637 | self.showMonth() | |
1638 | else: | |
1639 | self.tree.focus_set() | |
1640 | self.showView(row=self.topSelected) | |
1641 | ||
1642 | def rescheduleItem(self, e=None): | |
1643 | if e and e.char != 'r': | |
1644 | return | |
1645 | if not self.itemSelected: | |
1646 | return | |
1647 | loop.item_hsh = self.itemSelected | |
1648 | if self.dtSelected: | |
1649 | loop.old_dt = old_dt = self.dtSelected | |
1650 | title = _('rescheduling {0}').format(old_dt.strftime( | |
1651 | rrulefmt)) | |
1652 | else: | |
1653 | loop.old_dt = None | |
1654 | title = _('scheduling an undated item') | |
1655 | logger.debug('dtSelected: {0}'.format(self.dtSelected)) | |
1656 | prompt = _("""\ | |
1657 | Enter the new date and time for the item or return an empty string to | |
1658 | use the current time. Relative dates and fuzzy parsing are supported.""") | |
1659 | dt = GetDateTime(parent=self, title=title, prompt=prompt) | |
1660 | new_dt = dt.value | |
1661 | if new_dt is None: | |
1662 | return | |
1663 | new_dtn = new_dt.astimezone(gettz(self.itemSelected['z'])).replace(tzinfo=None) | |
1664 | logger.debug('rescheduled from {0} to {1}'.format(self.dtSelected, new_dtn)) | |
1665 | loop.cmd_do_reschedule(new_dtn) | |
1666 | ||
1667 | self.updateAlerts() | |
1668 | if self.weekly: | |
1669 | self.canvas.focus_set() | |
1670 | self.showWeek() | |
1671 | elif self.monthly: | |
1672 | self.canvas.focus_set() | |
1673 | self.showMonthly() | |
1674 | else: | |
1675 | self.tree.focus_set() | |
1676 | self.showView(row=self.topSelected) | |
1677 | ||
1678 | def scheduleNewItem(self, e=None): | |
1679 | if e and e.char != 'i': | |
1680 | return | |
1681 | if not self.itemSelected: | |
1682 | return | |
1683 | loop.item_hsh = self.itemSelected | |
1684 | if self.dtSelected: | |
1685 | loop.old_dt = self.dtSelected | |
1686 | title = _('adding new instance') | |
1687 | else: | |
1688 | loop.old_dt = None | |
1689 | title = _('scheduling an undated item') | |
1690 | logger.debug('dtSelected: {0}'.format(self.dtSelected)) | |
1691 | prompt = _("""\ | |
1692 | Enter the new date and time for the item or return an empty string to | |
1693 | use the current time. Relative dates and fuzzy parsing are supported.""") | |
1694 | dt = GetDateTime(parent=self, title=title, prompt=prompt) | |
1695 | new_dt = dt.value | |
1696 | if new_dt is None: | |
1697 | return | |
1698 | new_dtn = new_dt.astimezone(gettz(self.itemSelected['z'])).replace(tzinfo=None) | |
1699 | logger.debug('scheduled new instance: {0}'.format(new_dtn)) | |
1700 | loop.cmd_do_schedulenew(new_dtn) | |
1701 | ||
1702 | self.updateAlerts() | |
1703 | if self.weekly: | |
1704 | self.canvas.focus_set() | |
1705 | self.showWeek() | |
1706 | elif self.weekly: | |
1707 | self.canvas.focus_set() | |
1708 | self.showMonth() | |
1709 | else: | |
1710 | self.tree.focus_set() | |
1711 | self.showView(row=self.topSelected) | |
1712 | ||
1713 | def showAlerts(self, e=None): | |
1714 | # hack to avoid activating with Ctrl-a | |
1715 | if e and e.char != "a": | |
1716 | return | |
1717 | t = _('remaining alerts for today') | |
1718 | header = "{0:^7}\t{1:^7}\t{2:<8}{3:<26}".format( | |
1719 | _('alert'), | |
1720 | _('event'), | |
1721 | _('type'), | |
1722 | _('summary')) | |
1723 | divider = '-' * 52 | |
1724 | if self.activeAlerts: | |
1725 | ||
1726 | s = '%s\n%s\n%s' % ( | |
1727 | header, divider, "\n".join( | |
1728 | ["{0:^7}\t{1:^7}\t{2:<8}{3:<26}".format( | |
1729 | x[2]['alert_time'], x[2]['_event_time'], | |
1730 | ", ".join(x[2]['_alert_action']), | |
1731 | utf8(x[2]['summary'][:26])) for x in self.activeAlerts])) | |
1732 | else: | |
1733 | s = _("none") | |
1734 | self.textWindow(self, t, s, opts=self.options) | |
1735 | ||
1736 | def agendaView(self, e=None): | |
1737 | self.setView(AGENDA) | |
1738 | ||
1739 | def dayView(self, e=None): | |
1740 | self.setView(DAY) | |
1741 | ||
1742 | def pathView(self, e=None): | |
1743 | self.setView(PATH) | |
1744 | ||
1745 | def keywordView(self, e=None): | |
1746 | self.setView(KEYWORD) | |
1747 | ||
1748 | def tagView(self, e=None): | |
1749 | self.setView(TAG) | |
1750 | ||
1751 | def customView(self, e=None): | |
1752 | # TODO: finish this | |
1753 | self.content.delete("1.0", END) | |
1754 | self.fltr.forget() | |
1755 | self.clearTree() | |
1756 | self.setView(CUSTOM) | |
1757 | pass | |
1758 | ||
1759 | def noteView(self, e=None): | |
1760 | self.setView(NOTE) | |
1761 | ||
1762 | def setView(self, view, row=None): | |
1763 | self.rowSelected = None | |
1764 | if view != WEEK and self.weekly: | |
1765 | self.closeWeekly() | |
1766 | if view != MONTH and self.monthly: | |
1767 | self.closeMonthly() | |
1768 | if view == CUSTOM: | |
1769 | # self.reportbar.pack(side="top") | |
1770 | logger.debug('showing custom_box') | |
1771 | self.custom_box.pack(side="top", fill="x", padx=3) | |
1772 | self.custom_box.focus_set() | |
1773 | for i in range(len(self.rm_options)): | |
1774 | self.custommenu.entryconfig(i, state="normal") | |
1775 | else: | |
1776 | if self.view == CUSTOM: | |
1777 | # we're leaving custom view | |
1778 | logger.debug('removing custom_box') | |
1779 | self.custom_box.forget() | |
1780 | for i in range(len(self.rm_options)): | |
1781 | self.custommenu.entryconfig(i, state="disabled") | |
1782 | self.saveSpecs() | |
1783 | self.fltr.pack(side="left", padx=0, expand=1, fill=X) | |
1784 | self.view = view | |
1785 | logger.debug("setView view: {0}. Calling showView.".format(view)) | |
1786 | self.showView(row=row) | |
1787 | ||
1788 | def filterView(self, e, *args): | |
1789 | if self.weekly or self.monthly: | |
1790 | return | |
1791 | self.depth2id = {} | |
1792 | fltr = self.filterValue.get() | |
1793 | cmd = "{0} {1}".format( | |
1794 | self.vm_options[self.vm_opts.index(self.view)][1], fltr) | |
1795 | self.mode = 'command' | |
1796 | self.process_input(event=e, cmd=cmd) | |
1797 | ||
1798 | def showView(self, e=None, row=None): | |
1799 | tt = TimeIt(loglevel=2, label="{0} view".format(self.view)) | |
1800 | if self.weekly or self.monthly: | |
1801 | return | |
1802 | self.depth2id = {} | |
1803 | self.currentView.set(self.view) | |
1804 | fltr = self.filterValue.get() | |
1805 | if self.view != CUSTOM: | |
1806 | cmd = "{0} {1}".format( | |
1807 | self.vm_options[self.vm_opts.index(self.view)][1], fltr) | |
1808 | self.mode = 'command' | |
1809 | self.process_input(event=e, cmd=cmd) | |
1810 | if row: | |
1811 | row = max(0, row - 1) | |
1812 | self.tree.yview(row) | |
1813 | tt.stop() | |
1814 | ||
1815 | def showBusyPeriods(self, e=None): | |
1816 | if e and e.char != "b": | |
1817 | return | |
1818 | if self.busy_info is None: | |
1819 | return() | |
1820 | theweek, weekdays, busy_lst, occasion_lst = self.busy_info | |
1821 | theweek = _("Busy periods in {0}").format(theweek) | |
1822 | ||
1823 | lines = [theweek, '-' * len(theweek)] | |
1824 | ampm = loop.options['ampm'] | |
1825 | s1 = s2 = '' | |
1826 | for i in range(len(busy_lst)): | |
1827 | times = [] | |
1828 | for tup in busy_lst[i]: | |
1829 | t1 = max(7 * 60, tup[0]) | |
1830 | t2 = min(23 * 60, max(420, tup[1])) | |
1831 | if t1 != t2: | |
1832 | t1h, t1m = (t1 // 60, t1 % 60) | |
1833 | t2h, t2m = (t2 // 60, t2 % 60) | |
1834 | if ampm: | |
1835 | if t1h == 12: | |
1836 | s1 = 'pm' | |
1837 | elif t1h > 12: | |
1838 | t1h -= 12 | |
1839 | s1 = 'pm' | |
1840 | else: | |
1841 | s1 = 'am' | |
1842 | if t2h == 12: | |
1843 | s2 = 'pm' | |
1844 | elif t2h > 12: | |
1845 | t2h -= 12 | |
1846 | s2 = 'pm' | |
1847 | else: | |
1848 | s2 = 'am' | |
1849 | ||
1850 | T1 = "%d:%02d%s" % (t1h, t1m, s1) | |
1851 | T2 = "%d:%02d%s" % (t2h, t2m, s2) | |
1852 | ||
1853 | times.append("%s-%s" % (T1, T2)) | |
1854 | if times: | |
1855 | lines.append("%s: %s" % (weekdays[i], "; ".join(times))) | |
1856 | s = "\n".join(lines) | |
1857 | self.textWindow(parent=self, title=_('busy times'), prompt=s, opts=self.options) | |
1858 | ||
1859 | def showFreePeriods(self, e=None): | |
1860 | if e and e.char != 'f': | |
1861 | return | |
1862 | if self.busy_info is None or 'freetimes' not in loop.options: | |
1863 | return() | |
1864 | ampm = loop.options['ampm'] | |
1865 | om = loop.options['freetimes']['opening'] | |
1866 | cm = loop.options['freetimes']['closing'] | |
1867 | mm = loop.options['freetimes']['minimum'] | |
1868 | bm = loop.options['freetimes']['buffer'] | |
1869 | prompt = _("""\ | |
1870 | Enter the shortest time period you want displayed in minutes.""") | |
1871 | mm = GetInteger(parent=self, title=_("depth"), prompt=prompt, opts=[0], default=mm).value | |
1872 | if mm is None: | |
1873 | self.canvas.focus_set() | |
1874 | return | |
1875 | theweek, weekdays, busy_lst, occasion_lst = self.busy_info | |
1876 | theweek = _("Free periods in {0}").format(theweek) | |
1877 | lines = [theweek, '-' * len(theweek)] | |
1878 | s1 = s2 = '' | |
1879 | for i in range(len(busy_lst)): | |
1880 | times = [] | |
1881 | busy = [] | |
1882 | for tup in busy_lst[i]: | |
1883 | t1 = max(om, tup[0] - bm) | |
1884 | t2 = min(cm, max(om, tup[1]) + bm) | |
1885 | if t2 > t1: | |
1886 | busy.append((t1, t2)) | |
1887 | lastend = om | |
1888 | free = [] | |
1889 | for tup in busy: | |
1890 | if tup[0] - lastend >= mm: | |
1891 | free.append((lastend, tup[0])) | |
1892 | lastend = tup[1] | |
1893 | if cm - lastend >= mm: | |
1894 | free.append((lastend, cm)) | |
1895 | for tup in free: | |
1896 | t1, t2 = tup | |
1897 | if t1 != t2: | |
1898 | t1h, t1m = (t1 // 60, t1 % 60) | |
1899 | t2h, t2m = (t2 // 60, t2 % 60) | |
1900 | if ampm: | |
1901 | if t1h == 12: | |
1902 | s1 = 'pm' | |
1903 | elif t1h > 12: | |
1904 | t1h -= 12 | |
1905 | s1 = 'pm' | |
1906 | else: | |
1907 | s1 = 'am' | |
1908 | if t2h == 12: | |
1909 | s2 = 'pm' | |
1910 | elif t2h > 12: | |
1911 | t2h -= 12 | |
1912 | s2 = 'pm' | |
1913 | else: | |
1914 | s2 = 'am' | |
1915 | T1 = "%d:%02d%s" % (t1h, t1m, s1) | |
1916 | T2 = "%d:%02d%s" % (t2h, t2m, s2) | |
1917 | times.append("%s-%s" % (T1, T2)) | |
1918 | if times: | |
1919 | lines.append("%s: %s" % (weekdays[i], "; ".join(times))) | |
1920 | lines.append('-' * len(theweek)) | |
1921 | lines.append("Only periods of at least {0} minutes are displayed.".format(mm)) | |
1922 | s = "\n".join(lines) | |
1923 | self.textWindow(parent=self, title=_('free times'), prompt=s, opts=self.options) | |
1924 | ||
1925 | def setWeek(self, chosen_day=None): | |
1926 | if chosen_day is None: | |
1927 | chosen_day = get_current_time() | |
1928 | yn, wn, dn = chosen_day.isocalendar() | |
1929 | self.prev_week = chosen_day - 7 * ONEDAY | |
1930 | self.next_week = chosen_day + 7 * ONEDAY | |
1931 | self.curr_week = chosen_day | |
1932 | if dn > 1: | |
1933 | days = dn - 1 | |
1934 | else: | |
1935 | days = 0 | |
1936 | self.week_beg = weekbeg = chosen_day - days * ONEDAY | |
1937 | logger.debug('week_beg: {0}'.format(self.week_beg)) | |
1938 | weekend = chosen_day + (6 - days) * ONEDAY | |
1939 | weekdays = [] | |
1940 | ||
1941 | day = weekbeg | |
1942 | self.active_date = weekbeg.date() | |
1943 | busy_lst = [] | |
1944 | occasion_lst = [] | |
1945 | matching = self.cal_regex is not None and self.default_regex is not None | |
1946 | while day <= weekend: | |
1947 | weekdays.append(fmt_weekday(day)) | |
1948 | isokey = day.isocalendar() | |
1949 | ||
1950 | if isokey in loop.occasions: | |
1951 | bt = [] | |
1952 | for item in loop.occasions[isokey]: | |
1953 | it = list(item) | |
1954 | if matching: | |
1955 | if not self.cal_regex.match(it[-1]): | |
1956 | continue | |
1957 | mtch = (self.default_regex.match(it[-1]) is not None) | |
1958 | else: | |
1959 | mtch = True | |
1960 | it.append(mtch) | |
1961 | item = tuple(it) | |
1962 | bt.append(item) | |
1963 | occasion_lst.append(bt) | |
1964 | else: | |
1965 | occasion_lst.append([]) | |
1966 | ||
1967 | if isokey in loop.busytimes: | |
1968 | bt = [] | |
1969 | for item in loop.busytimes[isokey]: | |
1970 | it = list(item) | |
1971 | if it[0] == it[1]: | |
1972 | # skip reminders | |
1973 | continue | |
1974 | if matching: | |
1975 | if not self.cal_regex.match(it[-1]): | |
1976 | continue | |
1977 | mtch = (self.default_regex.match(it[-1]) is not None) | |
1978 | else: | |
1979 | mtch = True | |
1980 | it.append(mtch) | |
1981 | item = tuple(it) | |
1982 | bt.append(item) | |
1983 | busy_lst.append(bt) | |
1984 | else: | |
1985 | busy_lst.append([]) | |
1986 | day = day + ONEDAY | |
1987 | ||
1988 | ybeg = weekbeg.year | |
1989 | yend = weekend.year | |
1990 | mbeg = weekbeg.month | |
1991 | mend = weekend.month | |
1992 | # busy_lst: list of days 0 (monday) - 6 (sunday) where each day is a list of (start minute, end minute, id, summary-time str and file info) tuples | |
1993 | ||
1994 | if mbeg == mend: | |
1995 | header = "{0} - {1}".format( | |
1996 | fmt_dt(weekbeg, '%b %d'), fmt_dt(weekend, '%d, %Y')) | |
1997 | elif ybeg == yend: | |
1998 | header = "{0} - {1}".format( | |
1999 | fmt_dt(weekbeg, '%b %d'), fmt_dt(weekend, '%b %d, %Y')) | |
2000 | else: | |
2001 | header = "{0} - {1}".format( | |
2002 | fmt_dt(weekbeg, '%b %d, %Y'), fmt_dt(weekend, '%b %d, %Y')) | |
2003 | header = leadingzero.sub('', header) | |
2004 | theweek = _("Week {0}: {1}").format(wn, header) | |
2005 | self.busy_info = (theweek, weekdays, busy_lst, occasion_lst) | |
2006 | return self.busy_info | |
2007 | ||
2008 | def configureCanvas(self, e=None): | |
2009 | if self.weekly: | |
2010 | self.showWeek() | |
2011 | elif self.monthly: | |
2012 | self.showMonth() | |
2013 | else: | |
2014 | return | |
2015 | ||
2016 | def closeWeekly(self, event=None): | |
2017 | self.today_col = None | |
2018 | for i in range(14, 20): | |
2019 | self.viewmenu.entryconfig(i, state="disabled") | |
2020 | self.canvas.pack_forget() | |
2021 | self.weekly = False | |
2022 | self.fltr.pack(side=LEFT, padx=8, pady=0, fill="x", expand=1) | |
2023 | self.tree.pack(fill="both", expand=1, padx=4, pady=0) | |
2024 | self.update_idletasks() | |
2025 | if self.filter_active: | |
2026 | self.viewmenu.entryconfig(6, state="disabled") | |
2027 | self.viewmenu.entryconfig(7, state="normal") | |
2028 | else: | |
2029 | self.viewmenu.entryconfig(6, state="normal") | |
2030 | self.viewmenu.entryconfig(7, state="disabled") | |
2031 | ||
2032 | for i in [4, 5, 8, 9, 10, 11, 12]: | |
2033 | self.viewmenu.entryconfig(i, state="normal") | |
2034 | self.bind("<Control-f>", self.setFilter) | |
2035 | ||
2036 | def showWeekly(self, event=None, chosen_day=None): | |
2037 | """ | |
2038 | Open the canvas at the current week | |
2039 | """ | |
2040 | self.custom_box.forget() | |
2041 | tt = TimeIt(loglevel=2, label="week view") | |
2042 | logger.debug("chosen_day: {0}; active_date: {1}".format(chosen_day, self.active_date)) | |
2043 | if self.weekly: | |
2044 | # we're in weekview already | |
2045 | return | |
2046 | if self.monthly: | |
2047 | self.closeMonthly() | |
2048 | self.content.delete("1.0", END) | |
2049 | self.weekly = True | |
2050 | self.tree.pack_forget() | |
2051 | self.fltr.pack_forget() | |
2052 | for i in range(4, 13): | |
2053 | self.viewmenu.entryconfig(i, state="disabled") | |
2054 | ||
2055 | self.view = WEEK | |
2056 | self.currentView.set(WEEK) | |
2057 | ||
2058 | if chosen_day is not None: | |
2059 | self.chosen_day = chosen_day | |
2060 | elif self.active_date: | |
2061 | self.chosen_day = datetime.combine(self.active_date, time()) | |
2062 | else: | |
2063 | self.chosen_day = get_current_time() | |
2064 | ||
2065 | self.canvas.configure(highlightthickness=0) | |
2066 | self.canvas.pack(side="top", fill="both", expand=1, padx=4, pady=0) | |
2067 | ||
2068 | if self.options['ampm']: | |
2069 | self.hours = ["{0}am".format(i) for i in range(7, 12)] + ['12pm'] + ["{0}pm".format(i) for i in range(1, 12)] | |
2070 | else: | |
2071 | self.hours = ["{0}:00".format(i) for i in range(7, 24)] | |
2072 | for i in range(14, 20): | |
2073 | self.viewmenu.entryconfig(i, state="normal") | |
2074 | self.canvas.focus_set() | |
2075 | self.showWeek() | |
2076 | tt.stop() | |
2077 | ||
2078 | def priorWeekMonth(self, event=None): | |
2079 | if self.weekly: | |
2080 | self.showWeek(event, week=-1) | |
2081 | elif self.monthly: | |
2082 | self.showMonth(event, month=-1) | |
2083 | ||
2084 | def nextWeekMonth(self, event=None): | |
2085 | if self.weekly: | |
2086 | self.showWeek(event, week=1) | |
2087 | elif self.monthly: | |
2088 | self.showMonth(event, month=1) | |
2089 | ||
2090 | def showWeek(self, event=None, week=None): | |
2091 | self.canvas.focus_set() | |
2092 | self.selectedId = None | |
2093 | self.current_day = get_current_time().replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) | |
2094 | logger.debug('self.current_day: {0}, minutes: {1}'.format(self.current_day, self.current_minutes)) | |
2095 | self.x_win = self.canvas.winfo_width() | |
2096 | self.y_win = self.canvas.winfo_height() | |
2097 | logger.debug("win: {0}, {1}".format(self.x_win, self.y_win)) | |
2098 | logger.debug("event: {0}, week: {1}, chosen_day: {2}".format(event, week, self.chosen_day)) | |
2099 | if week in [-1, 0, 1]: | |
2100 | if week == 0: | |
2101 | day = get_current_time() | |
2102 | elif week == 1: | |
2103 | day = self.next_week | |
2104 | elif week == -1: | |
2105 | day = self.prev_week | |
2106 | self.chosen_day = day | |
2107 | elif self.chosen_day: | |
2108 | day = self.chosen_day | |
2109 | else: | |
2110 | return | |
2111 | logger.debug('week active_date: {0}'.format(self.active_date)) | |
2112 | theweek, weekdays, busy_lst, occasion_lst = self.setWeek(day) | |
2113 | self.OnSelect() | |
2114 | self.canvas.delete("all") | |
2115 | l = 50 | |
2116 | r = 8 | |
2117 | t = 56 | |
2118 | b = 8 | |
2119 | if event: | |
2120 | logger.debug('event: {0}'.format(event)) | |
2121 | w, h = event.width, event.height | |
2122 | if type(w) is int and type(h) is int: | |
2123 | self.canvas_width = w | |
2124 | self.canvas_height = h | |
2125 | else: | |
2126 | w = self.canvas.winfo_width() | |
2127 | h = self.canvas.winfo_height() | |
2128 | else: | |
2129 | w = self.canvas.winfo_width() | |
2130 | h = self.canvas.winfo_height() | |
2131 | logger.debug("w: {0}, h: {1}, l: {2}, t: {3}".format(w, h, l, t)) | |
2132 | self.margins = (w, h, l, r, t, b) | |
2133 | self.week_x = x = Decimal(w - 1 - l - r) / Decimal(7) | |
2134 | self.week_y = y = Decimal(h - 1 - t - b) / Decimal(16) | |
2135 | logger.debug("x: {0}, y: {1}".format(x, y)) | |
2136 | ||
2137 | # week | |
2138 | p = int(l + (w - 1 - l - r) / 2), 20 | |
2139 | self.canvas.create_text(p, text=theweek) | |
2140 | self.busyHsh = {} | |
2141 | ||
2142 | # occasions | |
2143 | occasion_ids = [] | |
2144 | for i in range(7): | |
2145 | day = (self.week_beg + i * ONEDAY).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) | |
2146 | if not occasion_lst[i]: | |
2147 | continue | |
2148 | occasions = occasion_lst[i] | |
2149 | start_x = l + i * x | |
2150 | end_x = start_x + x | |
2151 | for tup in occasions: | |
2152 | xy = int(start_x), int(t), int(end_x), int(t + y * 16) | |
2153 | id = self.canvas.create_rectangle(xy, fill=OCCASIONFILL, outline="", width=0, tag='occasion') | |
2154 | tmp = list(tup) | |
2155 | tmp.append(day) | |
2156 | self.busyHsh[id] = tmp | |
2157 | occasion_ids.append(id) | |
2158 | self.y_per_minute = y_per_minute = y / Decimal(60) | |
2159 | busy_ids = [] | |
2160 | conf_ids = [] | |
2161 | self.today_id = None | |
2162 | self.today_col = None | |
2163 | self.timeline = None | |
2164 | self.last_minutes = None | |
2165 | for i in range(7): | |
2166 | day = (self.week_beg + i * ONEDAY).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) | |
2167 | busy_times = busy_lst[i] | |
2168 | start_x = l + i * x | |
2169 | end_x = start_x + x | |
2170 | if day == self.current_day: | |
2171 | self.today_col = i | |
2172 | xy = int(start_x), int(t), int(end_x), int(t + y * 16) | |
2173 | self.canvas.create_rectangle(xy, fill=CURRENTFILL, outline="", width=0, tag='current_day') | |
2174 | if not busy_times and self.today_col is None: | |
2175 | continue | |
2176 | for tup in busy_times: | |
2177 | conf = None | |
2178 | mtch = tup[5] | |
2179 | if mtch: | |
2180 | busyColor = DEFAULTFILL | |
2181 | ttag = 'default' | |
2182 | else: | |
2183 | busyColor = OTHERFILL | |
2184 | ttag = 'other' | |
2185 | daytime = day + tup[0] * ONEMINUTE | |
2186 | ||
2187 | if (tup[0] < 420): | |
2188 | # early | |
2189 | xy = int(start_x), int(t - 1), int(end_x), int(t - 1) | |
2190 | self.canvas.create_line(xy, fill=OUTSIDELINE, width=2, tag='default') | |
2191 | if (tup[1] > 1380): | |
2192 | # late | |
2193 | xy = int(start_x), int(t + y * 16 + 2), int(end_x), int(t + y * 16 + 2) | |
2194 | self.canvas.create_line(xy, fill=OUTSIDELINE, width=2, tag='default') | |
2195 | ||
2196 | if tup[0] > 1380 or tup[1] < 420: | |
2197 | continue | |
2198 | ||
2199 | t1 = t + (max(7 * 60, tup[0]) - 7 * 60) * y_per_minute | |
2200 | ||
2201 | t2 = t + min(16 * 60, max(7 * 60, tup[1]) - 7 * 60) * y_per_minute | |
2202 | ||
2203 | xy = int(start_x), int(max(t, t1)), int(end_x), int(min(t2, t + y * 16)) | |
2204 | conf = self.canvas.find_overlapping(*xy) | |
2205 | id = self.canvas.create_rectangle(xy, fill=busyColor, width=0, tag=ttag) | |
2206 | conf = [z for z in conf if z in busy_ids] | |
2207 | busy_ids.append(id) | |
2208 | conf_ids.extend(conf) | |
2209 | if conf: | |
2210 | bb1 = self.canvas.bbox(id) | |
2211 | bb2 = self.canvas.bbox(*conf) | |
2212 | ||
2213 | # we want the max of bb1[1], bb2[1] | |
2214 | # and the min of bb1[4], bb2[4] | |
2215 | ol = bb1[0], max(bb1[1], bb2[1]), bb1[2], min(bb1[3], bb2[3]) | |
2216 | self.canvas.create_rectangle(ol, fill=CONFLICTFILL, outline="", width=0, tag="conflict") | |
2217 | ||
2218 | tmp = list(tup[2:]) # id, time str, summary and file info | |
2219 | tmp.append(daytime) | |
2220 | self.busyHsh[id] = tmp | |
2221 | if self.today_col is not None: | |
2222 | xy = self.get_timeline() | |
2223 | if xy: | |
2224 | self.canvas.delete('current_time') | |
2225 | self.canvas.create_line(xy, width=2, fill=CURRENTLINE, tag='current_time') | |
2226 | ||
2227 | self.busy_ids = busy_ids | |
2228 | self.conf_ids = conf_ids | |
2229 | for id in occasion_ids + busy_ids + conf_ids: # + conf_ids: | |
2230 | self.canvas.tag_bind(id, '<Any-Enter>', self.on_enter_item) | |
2231 | ||
2232 | self.canvas.bind('<Escape>', self.on_clear_item) | |
2233 | ||
2234 | self.canvas_ids = [z for z in self.busyHsh.keys()] | |
2235 | self.canvas_ids.sort() | |
2236 | self.canvas_idpos = None | |
2237 | # border | |
2238 | xy = int(l), int(t), int(l + x * 7), int(t + y * 16) | |
2239 | self.canvas.create_rectangle(xy, tag="grid") | |
2240 | ||
2241 | # verticals | |
2242 | for i in range(1, 7): | |
2243 | ||
2244 | xy = int(l + x * i), int(t), int(l + x * i), int(t + y * 16) | |
2245 | self.canvas.create_line(xy, fill=LINECOLOR, tag="grid") | |
2246 | # horizontals | |
2247 | for j in range(1, 16): | |
2248 | xy = int(l), int(t + y * j), int(l + x * 7), int(t + y * j) | |
2249 | self.canvas.create_line(xy, fill=LINECOLOR, tag="grid") | |
2250 | # hours | |
2251 | for j in range(17): | |
2252 | if j % 2: | |
2253 | p = int(l - 5), int(t + y * j) | |
2254 | self.canvas.create_text(p, text=self.hours[j], anchor="e") | |
2255 | # days | |
2256 | for i in range(7): | |
2257 | ||
2258 | p = int(l + x / 2 + x * i), int(t - 13) | |
2259 | ||
2260 | if self.today_col is not None and i == self.today_col: | |
2261 | self.canvas.create_text(p, text="{0}".format(weekdays[i]), fill=CURRENTLINE) | |
2262 | else: | |
2263 | self.canvas.create_text(p, text="{0}".format(weekdays[i])) | |
2264 | ||
2265 | def closeMonthly(self, event=None): | |
2266 | self.today_col = None | |
2267 | for i in range(14, 20): | |
2268 | self.viewmenu.entryconfig(i, state="disabled") | |
2269 | self.canvas.pack_forget() | |
2270 | self.monthly = False | |
2271 | self.fltr.pack(side=LEFT, padx=8, pady=0, fill="x", expand=1) | |
2272 | self.tree.pack(fill="both", expand=1, padx=4, pady=0) | |
2273 | self.update_idletasks() | |
2274 | if self.filter_active: | |
2275 | self.viewmenu.entryconfig(6, state="disabled") | |
2276 | self.viewmenu.entryconfig(7, state="normal") | |
2277 | else: | |
2278 | self.viewmenu.entryconfig(6, state="normal") | |
2279 | self.viewmenu.entryconfig(7, state="disabled") | |
2280 | ||
2281 | for i in [4, 5, 8, 9, 10, 11, 12]: | |
2282 | self.viewmenu.entryconfig(i, state="normal") | |
2283 | self.bind("<Control-f>", self.setFilter) | |
2284 | ||
2285 | def showMonthly(self, event=None, chosen_day=None): | |
2286 | """ | |
2287 | Open the canvas at the current week | |
2288 | """ | |
2289 | self.custom_box.forget() | |
2290 | tt = TimeIt(loglevel=2, label="month view") | |
2291 | logger.debug("chosen_day: {0}; active_date: {1}".format(chosen_day, self.active_date)) | |
2292 | if self.monthly: | |
2293 | # we're in weekview already | |
2294 | return | |
2295 | if self.weekly: | |
2296 | self.closeWeekly() | |
2297 | self.content.delete("1.0", END) | |
2298 | self.monthly = True | |
2299 | self.tree.pack_forget() | |
2300 | self.fltr.pack_forget() | |
2301 | for i in range(4, 13): | |
2302 | self.viewmenu.entryconfig(i, state="disabled") | |
2303 | ||
2304 | self.view = MONTH | |
2305 | self.currentView.set(MONTH) | |
2306 | ||
2307 | if chosen_day is not None: | |
2308 | self.chosen_day = chosen_day | |
2309 | elif self.active_date: | |
2310 | self.chosen_day = datetime.combine(self.active_date, time()) | |
2311 | else: | |
2312 | self.chosen_day = get_current_time() | |
2313 | ||
2314 | self.canvas.configure(highlightthickness=0) | |
2315 | self.canvas.pack(side="top", fill="both", expand=1, padx=4, pady=0) | |
2316 | ||
2317 | for i in range(14, 20): | |
2318 | self.viewmenu.entryconfig(i, state="normal") | |
2319 | self.canvas.focus_set() | |
2320 | self.showMonth() | |
2321 | tt.stop() | |
2322 | ||
2323 | def showMonth(self, event=None, month=None): | |
2324 | self.canvas.focus_set() | |
2325 | self.selectedId = None | |
2326 | matching = self.cal_regex is not None and self.default_regex is not None | |
2327 | busy_lst = [] | |
2328 | busy_dates = [] | |
2329 | occasion_lst = [] | |
2330 | ||
2331 | self.current_day = get_current_time().replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) | |
2332 | ||
2333 | logger.debug('self.current_day: {0}, minutes: {1}'.format(self.current_day, self.current_minutes)) | |
2334 | self.x_win = self.canvas.winfo_width() | |
2335 | self.y_win = self.canvas.winfo_height() | |
2336 | if month in [-1, 0, 1]: | |
2337 | if month == 0: | |
2338 | self.year_month = [self.current_day.year, self.current_day.month] | |
2339 | elif month == 1: | |
2340 | self.year_month[1] += 1 | |
2341 | if self.year_month[1] > 12: | |
2342 | self.year_month[1] -= 12 | |
2343 | self.year_month[0] += 1 | |
2344 | elif month == -1: | |
2345 | self.year_month[1] -= 1 | |
2346 | if self.year_month[1] < 1: | |
2347 | self.year_month[1] += 12 | |
2348 | self.year_month[0] -= 1 | |
2349 | elif self.chosen_day: | |
2350 | self.year_month = [self.chosen_day.year, self.chosen_day.month] | |
2351 | else: | |
2352 | return | |
2353 | logger.debug('month active_date: {0}'.format(self.active_date)) | |
2354 | weeks = self.monthly_calendar.monthdatescalendar(*self.year_month) | |
2355 | num_weeks = len(weeks) | |
2356 | weekdays = [x.strftime("%a") for x in weeks[0]] | |
2357 | # weeknumbers = [x[0].strftime("%W") for x in weeks] | |
2358 | weeknumbers = [x[0].isocalendar()[1] for x in weeks] | |
2359 | themonth = weeks[1][0].strftime("%B %Y") | |
2360 | self.canvas.delete("all") | |
2361 | l = 36 | |
2362 | r = 8 | |
2363 | t = 56 | |
2364 | b = 8 | |
2365 | if event: | |
2366 | logger.debug('event: {0}'.format(event)) | |
2367 | w, h = event.width, event.height | |
2368 | if type(w) is int and type(h) is int: | |
2369 | self.canvas_width = w | |
2370 | self.canvas_height = h | |
2371 | else: | |
2372 | w = self.canvas.winfo_width() | |
2373 | h = self.canvas.winfo_height() | |
2374 | else: | |
2375 | w = self.canvas.winfo_width() | |
2376 | h = self.canvas.winfo_height() | |
2377 | logger.debug("w: {0}, h: {1}, l: {2}, t: {3}".format(w, h, l, t)) | |
2378 | ||
2379 | self.margins = (w, h, l, r, t, b) | |
2380 | ||
2381 | self.month_x = x_ = Decimal(w - 1 - l - r) / Decimal(7) | |
2382 | self.month_y = y_ = Decimal(h - 1 - t - b) / Decimal(num_weeks) | |
2383 | ||
2384 | logger.debug("x: {0}, y: {1}".format(x_, y_)) | |
2385 | ||
2386 | # month | |
2387 | p = l + int((w - 1 - l - r) / 2), 20 | |
2388 | self.canvas.create_text(p, text="{0}".format(themonth)) | |
2389 | self.busyHsh = {} | |
2390 | ||
2391 | # occasions | |
2392 | busy_ids = set([]) | |
2393 | monthid2date = {} | |
2394 | ||
2395 | self.canvas.bind('<Escape>', self.on_clear_item) | |
2396 | ||
2397 | # monthdays | |
2398 | intervals = [240, 480, 720, 960] | |
2399 | busywidth = 2 | |
2400 | offset = 6 | |
2401 | indent = 7 | |
2402 | # barcolor = "PaleGreen3" | |
2403 | # barcolor = "SkyBlue3" | |
2404 | # barcolor = "SlateBlue4" | |
2405 | # barcolor = "SlateGray3" | |
2406 | barcolor = "SteelBlue3" | |
2407 | ||
2408 | for j in range(num_weeks): | |
2409 | for i in range(7): | |
2410 | busytimes = 0 | |
2411 | start_x = l + i * x_ | |
2412 | end_x = start_x + x_ | |
2413 | start_y = int(t + y_ * j) | |
2414 | end_y = start_y + y_ | |
2415 | xy = int(start_x), int(start_y), int(end_x), int(end_y) | |
2416 | p = int(l + x_ / 2 + x_ * i), int(t + y_ * j + y_ / 2) | |
2417 | pp = int(l + x_ + x_ * i), int(t + y_ * j + y_ ) | |
2418 | ||
2419 | tl_x = bl_x = int(l + x_ * i) | |
2420 | tl_y = tr_y = int(t + y_ *j) | |
2421 | tr_x = br_x = int(tl_x + x_) | |
2422 | bl_y = br_y = int(tl_y + y_) | |
2423 | w_ = x_ - 12 | |
2424 | h_ = y_ - 12 | |
2425 | ||
2426 | thisdate = weeks[j][i] | |
2427 | isokey = thisdate.isocalendar() | |
2428 | month = thisdate.month | |
2429 | fill = None | |
2430 | tags = [] | |
2431 | if (month != self.year_month[1]): | |
2432 | fill = "gray70" | |
2433 | else: | |
2434 | fill = "SteelBlue4" | |
2435 | id = self.canvas.create_rectangle(xy, outline="", width=0) | |
2436 | busy_ids.add(id) | |
2437 | monthid2date[id] = thisdate | |
2438 | today = (thisdate == self.current_day.date()) | |
2439 | bt = [] | |
2440 | if today: | |
2441 | tags.append('current_day') | |
2442 | if isokey in loop.occasions: | |
2443 | bt = [] | |
2444 | for item in loop.occasions[isokey]: | |
2445 | it = list(item) | |
2446 | if matching: | |
2447 | if not self.cal_regex.match(it[-1]): | |
2448 | continue | |
2449 | mtch = (self.default_regex.match(it[-1]) is not None) | |
2450 | else: | |
2451 | mtch = True | |
2452 | it.append(mtch) | |
2453 | item = tuple(it) | |
2454 | bt.append(item) | |
2455 | occasion_lst.append(bt) | |
2456 | if bt: | |
2457 | if not today: | |
2458 | tags.append('occasion') | |
2459 | self.busyHsh.setdefault(id, []).extend(["^ {0}".format(x[0]) for x in bt]) | |
2460 | else: | |
2461 | occasion_lst.append([]) | |
2462 | ||
2463 | if isokey in loop.busytimes: | |
2464 | bt = [] | |
2465 | for item in loop.busytimes[isokey]: | |
2466 | it = list(item) | |
2467 | if it[0] == it[1]: | |
2468 | # skip reminders | |
2469 | continue | |
2470 | if matching: | |
2471 | if not self.cal_regex.match(it[-1]): | |
2472 | continue | |
2473 | mtch = (self.default_regex.match(it[-1]) is not None) | |
2474 | else: | |
2475 | mtch = True | |
2476 | it.append(mtch) | |
2477 | item = tuple(it) | |
2478 | bt.append(item) | |
2479 | busy_lst.append(bt) | |
2480 | busy_dates.append(thisdate.strftime("%a %d")) | |
2481 | if bt: | |
2482 | for pts in bt: | |
2483 | busytimes += pts[1] - pts[0] | |
2484 | self.busyHsh.setdefault(id, []).append("* {0}".format(pts[2])) | |
2485 | tags.append('busy') | |
2486 | ||
2487 | busylines = [[], [], [], []] | |
2488 | # each side 240 minutes plus 2 times bar width | |
2489 | # 420 - 660 top: tl+(5,-3) -> tr+(-5,-3) | |
2490 | # 660 - 900 right: tr+(-3,-5) -> br+(-3,+5) | |
2491 | # 900 - 1140 bottom: br+(-5,+3) -> bl+(+5,+3) | |
2492 | # 1140 - 1380 left: bl+(+3,+5) -> tl+(+3, -5) | |
2493 | ||
2494 | for pts in bt: | |
2495 | if (pts[0] < 420 or pts[1] > 1380): | |
2496 | # busy time outside display interval | |
2497 | by = tl_y + 3 | |
2498 | ey = tl_y + 8 | |
2499 | bx = tl_x + 3 | |
2500 | ex = tl_x + 8 | |
2501 | self.canvas.create_rectangle((bx, by, ex, ey), fill="red", outline="red", tag="busy") | |
2502 | ||
2503 | pt1 = max(420, pts[0]) - 420 | |
2504 | pt2 = min(pts[1], 1380) - 420 | |
2505 | if pt1 >= 960 or pt2 <= 0: | |
2506 | continue | |
2507 | tmp = [[], [], [], []] | |
2508 | ||
2509 | for ii in range(0, 4): | |
2510 | if pt1 >= intervals[ii]: | |
2511 | continue | |
2512 | tmp[ii].append(pt1) | |
2513 | for jj in range(ii, 4): | |
2514 | if jj > ii: | |
2515 | tmp[jj].append(intervals[jj-1]) | |
2516 | if pt2 <= intervals[jj]: | |
2517 | tmp[jj].append(pt2) | |
2518 | break | |
2519 | else: | |
2520 | tmp[jj].append(intervals[jj]) | |
2521 | break | |
2522 | for ii in range(4): | |
2523 | if tmp[ii]: | |
2524 | busylines[ii].append(tmp[ii]) | |
2525 | ||
2526 | ||
2527 | if busylines: | |
2528 | for side in range(4): | |
2529 | lines = busylines[side] | |
2530 | if lines: | |
2531 | if side == 0: # top | |
2532 | for line in lines: | |
2533 | by = ey = tl_y + offset | |
2534 | bx = tl_x + indent + int(Decimal(line[0]/240) * w_) | |
2535 | ex = tl_x + indent + int(Decimal(line[1]/240) * w_) | |
2536 | self.canvas.create_line((bx, by, ex, ey), fill=barcolor, width=busywidth, tag="busy") | |
2537 | elif side == 1: # right | |
2538 | for line in lines: | |
2539 | bx = ex = tr_x - offset | |
2540 | by = tr_y + indent + int(Decimal((line[0]-240)/240) * h_) | |
2541 | ey = tr_y + indent + int(Decimal((line[1]-240)/240) * h_) | |
2542 | self.canvas.create_line((bx, by, ex, ey), fill=barcolor, width=busywidth, tag="busy") | |
2543 | elif side == 2: # bottom | |
2544 | for line in lines: | |
2545 | by = ey = br_y - offset | |
2546 | bx = br_x - indent - int(Decimal((line[0]-480)/240) * w_) | |
2547 | ex = br_x - indent - int(Decimal((line[1]-480)/240) * w_) | |
2548 | self.canvas.create_line((bx, by, ex, ey), fill=barcolor, width=busywidth, tag="busy") | |
2549 | elif side == 3: # left | |
2550 | for line in lines: | |
2551 | bx = ex = bl_x + offset | |
2552 | by = bl_y - indent - int(Decimal((line[0]-720)/240) * h_) | |
2553 | ey = bl_y - indent - int(Decimal((line[1]-720)/240) * h_) | |
2554 | self.canvas.create_line((bx, by, ex, ey), fill=barcolor, width=busywidth, tag="busy") | |
2555 | else: | |
2556 | busy_lst.append([]) | |
2557 | busy_dates.append(thisdate.strftime("%a %d")) | |
2558 | if 'current_day' in tags: | |
2559 | self.canvas.itemconfig(id, tag='current_day', fill=CURRENTFILL) | |
2560 | elif 'occasion' in tags: | |
2561 | self.canvas.itemconfig(id, tag='occasion', fill=OCCASIONFILL) | |
2562 | elif 'busy' in tags: | |
2563 | self.canvas.itemconfig(id, tag='busy', fill="white") | |
2564 | ||
2565 | if fill: | |
2566 | self.canvas.create_text(p, text="{0}".format(weeks[j][i].day), fill=fill) | |
2567 | busy_ids = list(busy_ids) | |
2568 | for id in busy_ids: | |
2569 | self.canvas.tag_bind(id, '<Any-Enter>', self.on_enter_item) | |
2570 | self.canvas.tag_bind(id, '<Any-Leave>', self.on_leave_item) | |
2571 | ||
2572 | # border | |
2573 | xy = int(l), int(t), int(l + x_ * 7), int(t + y_ * num_weeks + 1) | |
2574 | self.canvas.create_rectangle(xy, tag="grid") | |
2575 | ||
2576 | # verticals | |
2577 | for i in range(1, 7): | |
2578 | xy = int(l + x_ * i), int(t), int(l + x_ * i), int(t + y_ * num_weeks) | |
2579 | self.canvas.create_line(xy, fill=LINECOLOR, tag="grid") | |
2580 | # horizontals | |
2581 | for j in range(1, num_weeks): | |
2582 | xy = int(l), int(t + y_ * j), int(l + x_ * 7), int(t + y_ * j) | |
2583 | self.canvas.create_line(xy, fill=LINECOLOR, tag="grid") | |
2584 | ||
2585 | # week numbers | |
2586 | for j in range(num_weeks): | |
2587 | p = int(l - 5), int(t + y_ * j + y_ / 2) | |
2588 | self.canvas.create_text(p, text=weeknumbers[j], anchor="e") | |
2589 | # days | |
2590 | for i in range(7): | |
2591 | ||
2592 | p = int(l + x_ / 2 + x_ * i), int(t - 13) | |
2593 | ||
2594 | if self.today_col is not None and i == self.today_col: | |
2595 | self.canvas.create_text(p, text="{0}".format(weekdays[i]), fill=CURRENTLINE) | |
2596 | else: | |
2597 | self.canvas.create_text(p, text="{0}".format(weekdays[i])) | |
2598 | ||
2599 | self.busy_info = (themonth, busy_dates, busy_lst, occasion_lst) | |
2600 | self.busy_ids = busy_ids | |
2601 | self.busy_ids.sort() | |
2602 | self.canvas_ids = self.busy_ids | |
2603 | self.monthid2date = monthid2date | |
2604 | self.canvas_idpos = None | |
2605 | ||
2606 | def get_timeline(self): | |
2607 | if not (self.weekly and self.today_col is not None): | |
2608 | return | |
2609 | x = self.week_x | |
2610 | if self.current_minutes < 7 * 60: | |
2611 | return None | |
2612 | elif self.current_minutes > 23 * 60: | |
2613 | return None | |
2614 | else: | |
2615 | current_minutes = self.current_minutes | |
2616 | (w, h, l, r, t, b) = self.margins | |
2617 | start_x = l + self.today_col * x | |
2618 | end_x = start_x + x | |
2619 | ||
2620 | t1 = t + (current_minutes - 7 * 60) * self.y_per_minute | |
2621 | xy = int(start_x), int(t1), int(end_x), int(t1) | |
2622 | return xy | |
2623 | ||
2624 | def selectId(self, event, d=1): | |
2625 | ids = self.busy_ids | |
2626 | if self.canvas_idpos is None: | |
2627 | self.canvas_idpos = 0 | |
2628 | old_id = None | |
2629 | else: | |
2630 | old_id = self.canvas_ids[self.canvas_idpos] | |
2631 | if old_id in ids: | |
2632 | tags = self.canvas.gettags(old_id) | |
2633 | if 'current_day' in tags: | |
2634 | self.canvas.itemconfig(old_id, fill=CURRENTFILL) | |
2635 | elif 'occasion' in tags: | |
2636 | self.canvas.itemconfig(old_id, fill=OCCASIONFILL) | |
2637 | elif 'other' in tags: | |
2638 | self.canvas.itemconfig(old_id, fill=OTHERFILL) | |
2639 | elif self.weekly: | |
2640 | self.canvas.itemconfig(old_id, fill=DEFAULTFILL) | |
2641 | else: | |
2642 | self.canvas.itemconfig(old_id, fill=OCCASIONFILL) | |
2643 | self.canvas.tag_lower(old_id) | |
2644 | if d == -1: | |
2645 | self.canvas_idpos -= 1 | |
2646 | if self.canvas_idpos < 0: | |
2647 | self.canvas_idpos = len(self.canvas_ids) - 1 | |
2648 | elif d == 1: | |
2649 | self.canvas_idpos += 1 | |
2650 | if self.canvas_idpos > len(self.canvas_ids) - 1: | |
2651 | self.canvas_idpos = 0 | |
2652 | if self.weekly: | |
2653 | self.selectedId = id = self.canvas_ids[self.canvas_idpos] | |
2654 | self.canvas.itemconfig(id, fill=ACTIVEFILL) | |
2655 | self.canvas.tag_raise('conflict') | |
2656 | self.canvas.tag_raise(id) | |
2657 | self.canvas.tag_lower('occasion') | |
2658 | self.canvas.tag_lower('current_day') | |
2659 | self.canvas.tag_raise('current_time') | |
2660 | if id in self.busyHsh: | |
2661 | self.OnSelect(uuid=self.busyHsh[id][-4], dt=self.busyHsh[id][-1]) | |
2662 | self.active_date = self.busyHsh[id][-1].date() | |
2663 | ||
2664 | elif self.monthly: | |
2665 | if old_id is not None and old_id in self.busy_ids: | |
2666 | tags = self.canvas.gettags(old_id) | |
2667 | if 'current_day' in tags: | |
2668 | self.canvas.itemconfig(old_id, fill=CURRENTFILL) | |
2669 | elif 'occasion' in tags: | |
2670 | self.canvas.itemconfig(old_id, fill=OCCASIONFILL) | |
2671 | elif 'busy' in tags: | |
2672 | self.canvas.itemconfig(old_id, fill="white") | |
2673 | else: | |
2674 | self.canvas.itemconfig(old_id, fill="white") | |
2675 | self.selectedId = id = self.canvas_ids[self.canvas_idpos] | |
2676 | self.active_date = self.monthid2date[id] | |
2677 | self.canvas_idpos = self.canvas_ids.index(id) | |
2678 | if id in self.busy_ids: | |
2679 | self.canvas.itemconfig(id, fill=ACTIVEFILL) | |
2680 | if id in self.busyHsh: | |
2681 | txt = "\n".join(self.busyHsh[id]) | |
2682 | self.content.delete("1.0", END) | |
2683 | self.content.insert("1.0", txt) | |
2684 | else: | |
2685 | self.content.delete("1.0", END) | |
2686 | ||
2687 | def setFocus(self, e): | |
2688 | self.canvas.focus() | |
2689 | self.canvas.focus_set() | |
2690 | ||
2691 | def on_enter_item(self, e): | |
2692 | if self.weekly: | |
2693 | old_id = None | |
2694 | if self.canvas_idpos is not None: | |
2695 | old_id = self.canvas_ids[self.canvas_idpos] | |
2696 | if old_id in self.busy_ids: | |
2697 | tags = self.canvas.gettags(old_id) | |
2698 | if 'other' in tags: | |
2699 | self.canvas.itemconfig(old_id, fill=OTHERFILL) | |
2700 | else: | |
2701 | self.canvas.itemconfig(old_id, fill=DEFAULTFILL) | |
2702 | else: | |
2703 | self.canvas.itemconfig(old_id, fill=OCCASIONFILL) | |
2704 | self.canvas.tag_lower(old_id) | |
2705 | ||
2706 | self.selectedId = id = self.canvas.find_withtag(CURRENT)[0] | |
2707 | if id in self.busyHsh: | |
2708 | self.active_date = self.busyHsh[id][-1].date() | |
2709 | ||
2710 | self.canvas.itemconfig(id, fill=ACTIVEFILL) | |
2711 | self.canvas.tag_raise('conflict') | |
2712 | self.canvas.tag_raise('grid') | |
2713 | self.canvas.tag_raise(id) | |
2714 | self.canvas.tag_lower('occasion') | |
2715 | self.canvas.tag_lower('current_day') | |
2716 | self.canvas.tag_raise('current_time') | |
2717 | ||
2718 | if id in self.busyHsh: | |
2719 | self.canvas_idpos = self.canvas_ids.index(id) | |
2720 | ||
2721 | self.OnSelect(uuid=self.busyHsh[id][-4], dt=self.busyHsh[id][-1]) | |
2722 | elif self.monthly: | |
2723 | if self.canvas_idpos is not None: | |
2724 | old_id = self.canvas_ids[self.canvas_idpos] | |
2725 | if old_id in self.busy_ids: | |
2726 | tags = self.canvas.gettags(old_id) | |
2727 | if 'current_day' in tags: | |
2728 | self.canvas.itemconfig(old_id, fill=CURRENTFILL) | |
2729 | elif 'occasion' in tags: | |
2730 | self.canvas.itemconfig(old_id, fill=OCCASIONFILL) | |
2731 | else: | |
2732 | self.canvas.itemconfig(old_id, fill="white") | |
2733 | self.selectedId = id = self.canvas.find_withtag(CURRENT)[0] | |
2734 | self.active_date = self.monthid2date[id] | |
2735 | self.canvas_idpos = self.canvas_ids.index(id) | |
2736 | if id in self.busy_ids: | |
2737 | self.canvas.itemconfig(id, fill=ACTIVEFILL) | |
2738 | if id in self.busyHsh: | |
2739 | txt = "\n".join(self.busyHsh[id]) | |
2740 | self.content.delete("1.0", END) | |
2741 | self.content.insert("1.0", txt) | |
2742 | else: | |
2743 | self.content.delete("1.0", END) | |
2744 | ||
2745 | def on_leave_item(self, e): | |
2746 | if self.weekly or self.monthly: | |
2747 | return | |
2748 | self.content.delete("1.0", END) | |
2749 | id = self.canvas.find_withtag(CURRENT)[0] | |
2750 | if id in self.busy_ids: | |
2751 | tags = self.canvas.gettags(id) | |
2752 | if 'current_date' in tags: | |
2753 | self.canvas.itemconfig(id, fill=CURRENTFILL) | |
2754 | elif 'occasion' in tags: | |
2755 | self.canvas.itemconfig(id, fill=OCCASIONFILL) | |
2756 | else: | |
2757 | self.canvas.itemconfig(id, fill="white") | |
2758 | else: | |
2759 | self.canvas.itemconfig(id, fill="white") | |
2760 | ||
2761 | def on_clear_item(self, e=None): | |
2762 | if not self.weekly: | |
2763 | return | |
2764 | if self.selectedId: | |
2765 | id = self.selectedId | |
2766 | if id in self.busy_ids: | |
2767 | tags = self.canvas.gettags(id) | |
2768 | if 'other' in tags: | |
2769 | self.canvas.itemconfig(id, fill=OTHERFILL) | |
2770 | else: | |
2771 | self.canvas.itemconfig(id, fill=DEFAULTFILL) | |
2772 | else: | |
2773 | self.canvas.itemconfig(id, fill=OCCASIONFILL) | |
2774 | self.canvas.tag_raise('conflict') | |
2775 | self.canvas.tag_raise('grid') | |
2776 | self.canvas.tag_lower('occasion') | |
2777 | self.selectedId = None | |
2778 | self.OnSelect() | |
2779 | self.canvas.focus("") | |
2780 | ||
2781 | def on_select_item(self, event): | |
2782 | if self.monthly: | |
2783 | self.newItem() | |
2784 | else: | |
2785 | current = self.canvas.find_withtag(CURRENT) | |
2786 | logger.debug('current: {0}'.format(current)) | |
2787 | if current and current[0] in self.busy_ids: | |
2788 | self.selectedId = current[0] | |
2789 | self.on_activate_item(event) | |
2790 | else: | |
2791 | self.newEvent(event) | |
2792 | return "break" | |
2793 | ||
2794 | def on_activate_item(self, event): | |
2795 | if self.monthly: | |
2796 | self.newItem() | |
2797 | else: | |
2798 | x = self.winfo_rootx() + 350 | |
2799 | y = self.winfo_rooty() + 50 | |
2800 | id = self.selectedId | |
2801 | if not id: | |
2802 | return | |
2803 | ||
2804 | logger.debug("id: {0}, coords: {1}, {2}\n {3}".format(id, x, y, self.busyHsh[id])) | |
2805 | self.uuidSelected = uuid = self.busyHsh[id][1] | |
2806 | self.itemSelected = loop.uuid2hash[uuid] | |
2807 | self.dtSelected = self.busyHsh[id][-1] | |
2808 | self.itemmenu.post(x, y) | |
2809 | self.itemmenu.focus_set() | |
2810 | ||
2811 | def newEvent(self, event): | |
2812 | logger.debug("event: {0}".format(event)) | |
2813 | self.canvas.focus_set() | |
2814 | min_round = 15 | |
2815 | px = event.x | |
2816 | py = event.y | |
2817 | (w, h, l, r, t, b) = self.margins | |
2818 | x = Decimal(w - 1 - l - r) / Decimal(7) # x per day intervals | |
2819 | y = Decimal(h - 1 - t - b) / Decimal(16 * 60) # y per minute intervals | |
2820 | if px < l: | |
2821 | px = l | |
2822 | elif px > l + 7 * x: | |
2823 | py = l + 7 * x | |
2824 | if py < t: | |
2825 | py = t | |
2826 | elif py > t + 16 * 60 * y: | |
2827 | py = t + 16 * 60 * y | |
2828 | ||
2829 | rx = int(round(Decimal(px - l) / x - Decimal(0.5))) # number of days | |
2830 | ry = int(7 * 60 + round(Decimal(py - t) / y)) # number of minutes | |
2831 | ryr = round(Decimal(ry) / min_round) * min_round | |
2832 | ||
2833 | hours = int(ryr // 60) | |
2834 | minutes = int(ryr % 60) | |
2835 | dt = (self.week_beg + rx * ONEDAY).replace(hour=hours, minute=minutes, second=0, microsecond=0, tzinfo=None) | |
2836 | ||
2837 | tfmt = fmt_time(dt, options=loop.options) | |
2838 | dfmt = dt.strftime("%a %b %d") | |
2839 | dtfmt = "{0} {1}".format(tfmt, dfmt) | |
2840 | s = "* @s {0}".format(dtfmt) | |
2841 | changed = SimpleEditor(parent=self, master=self.canvas, start=s, options=loop.options).changed | |
2842 | ||
2843 | if changed: | |
2844 | logger.debug('changed, updating alerts, ...') | |
2845 | ||
2846 | self.updateAlerts() | |
2847 | if self.weekly: | |
2848 | self.showWeek() | |
2849 | elif self.monthly: | |
2850 | self.showMonth() | |
2851 | else: | |
2852 | self.showView() | |
2853 | ||
2854 | def showCalendar(self, e=None): | |
2855 | cal_year = 0 | |
2856 | opts = loop.options | |
2857 | cal_pastcolor = '#FFCCCC' | |
2858 | cal_currentcolor = '#FFFFCC' | |
2859 | cal_futurecolor = '#99CCFF' | |
2860 | ||
2861 | def showYear(x=0): | |
2862 | global cal_year | |
2863 | if x: | |
2864 | cal_year += x | |
2865 | else: | |
2866 | cal_year = 0 | |
2867 | cal = "\n".join(calyear(cal_year, options=opts)) | |
2868 | if cal_year > 0: | |
2869 | col = cal_futurecolor | |
2870 | elif cal_year < 0: | |
2871 | col = cal_pastcolor | |
2872 | else: | |
2873 | col = cal_currentcolor | |
2874 | t.configure(bg=col) | |
2875 | t.delete("0.0", END) | |
2876 | t.insert("0.0", cal) | |
2877 | ||
2878 | win = Toplevel() | |
2879 | win.title(_("Calendar")) | |
2880 | f = Frame(win) | |
2881 | # pack the button first so that it doesn't disappear with resizing | |
2882 | b = Button(win, text=_('OK'), width=10, command=win.destroy, default='active', pady=2) | |
2883 | b.pack(side='bottom', fill=tkinter.NONE, expand=0, pady=0) | |
2884 | win.bind('<Return>', (lambda e, b=b: b.invoke())) | |
2885 | win.bind('<Escape>', (lambda e, b=b: b.invoke())) | |
2886 | ||
2887 | t = ReadOnlyText(f, wrap="word", padx=2, pady=2, bd=2, relief="sunken", | |
2888 | ||
2889 | font=self.tkfixedfont, | |
2890 | ||
2891 | takefocus=False) | |
2892 | win.bind('<Left>', (lambda e: showYear(-1))) | |
2893 | win.bind('<Right>', (lambda e: showYear(1))) | |
2894 | win.bind('<space>', (lambda e: showYear())) | |
2895 | showYear() | |
2896 | t.pack(side='left', fill=tkinter.BOTH, expand=1, padx=0, pady=0) | |
2897 | ysb = Scrollbar(f, orient='vertical', command=t.yview, width=8) | |
2898 | ysb.pack(side='right', fill=tkinter.Y, expand=0, padx=0, pady=0) | |
2899 | ||
2900 | t.configure(yscroll=ysb.set) | |
2901 | f.pack(padx=2, pady=2, fill=tkinter.BOTH, expand=1) | |
2902 | win.focus_set() | |
2903 | win.grab_set() | |
2904 | win.transient(self) | |
2905 | win.wait_window(win) | |
2906 | ||
2907 | def newCommand(self, e=None): | |
2908 | self.newValue.set(self.newLabel) | |
2909 | ||
2910 | def showShortcuts(self, e=None): | |
2911 | if e and e.char != "?": | |
2912 | return | |
2913 | res = self.menutree.showMenu("_") | |
2914 | self.textWindow(parent=self, title='etm', opts=self.options, prompt=res, modal=False) | |
2915 | ||
2916 | def help(self, event=None): | |
2917 | path = USERMANUAL | |
2918 | if windoz: | |
2919 | os.startfile(USERMANUAL) | |
2920 | return() | |
2921 | if mac: | |
2922 | cmd = 'open' + " {0}".format(path) | |
2923 | else: | |
2924 | cmd = 'xdg-open' + " {0}".format(path) | |
2925 | subprocess.call(cmd, shell=True) | |
2926 | return True | |
2927 | ||
2928 | def about(self, event=None): | |
2929 | res = loop.do_v("") | |
2930 | self.textWindow(parent=self, title='etm', opts=self.options, prompt=res, modal=False) | |
2931 | ||
2932 | def checkForUpdate(self, event=None): | |
2933 | res = checkForNewerVersion()[1] | |
2934 | self.textWindow(parent=self, title='etm', prompt=res, opts=self.options) | |
2935 | ||
2936 | def showChanges(self, event=None): | |
2937 | if not loop.options['vcs_system']: | |
2938 | prompt = """An entry for 'vcs_system' in etmtk.cfg is required but missing.""" | |
2939 | self.textWindow(parent=self, title="etm", prompt=prompt, opts=loop.options) | |
2940 | return | |
2941 | ||
2942 | if self.itemSelected: | |
2943 | f = self.itemSelected['fileinfo'][0] | |
2944 | fn = ' {0}"{1}"'.format(self.options['vcs']['file'], os.path.normpath(os.path.join(self.options['datadir'], f))) | |
2945 | title = _("Showing changes for {0}.").format(f) | |
2946 | ||
2947 | else: | |
2948 | fn = "" | |
2949 | title = _("Showing changes for all files.") | |
2950 | logger.debug('fn: {0}'.format(fn)) | |
2951 | prompt = _("""\ | |
2952 | {0} | |
2953 | ||
2954 | If an item is selected, changes will be shown for the file containing | |
2955 | the item. Otherwise, changes will be shown for all files. | |
2956 | ||
2957 | Enter an integer number of changes to display | |
2958 | or 0 to display all changes.""").format(title) | |
2959 | depth = GetInteger( | |
2960 | parent=self, | |
2961 | title=_("Changes"), | |
2962 | prompt=prompt, opts=[0], default=10).value | |
2963 | if depth is None: | |
2964 | return () | |
2965 | if depth == 0: | |
2966 | # all changes | |
2967 | numstr = "" | |
2968 | else: | |
2969 | numstr = "{0} {1}".format(loop.options['vcs']['limit'], depth) | |
2970 | command = loop.options['vcs']['history'].format( | |
2971 | repo=loop.options['vcs']['repo'], | |
2972 | work=loop.options['vcs']['work'], | |
2973 | numchanges=numstr, rev="{rev}", desc="{desc}", file=fn) | |
2974 | logger.debug('vcs history command: {0}'.format(command)) | |
2975 | tt = TimeIt(loglevel=2, label="showChanges") | |
2976 | s = subprocess.check_output(command, shell=True, universal_newlines=True) | |
2977 | tt.stop() | |
2978 | p = s2or3(s) | |
2979 | if not p: | |
2980 | p = 'no output from command:\n {0}'.format(command) | |
2981 | ||
2982 | p = "\n".join([x for x in p.split('\n') if not (x.startswith('index') or x.startswith('diff') or x.startswith('\ No newline'))]) | |
2983 | ||
2984 | self.textWindow(parent=self, title=title, prompt=s2or3(p), opts=self.options) | |
2985 | ||
2986 | def focus_next_window(self, event): | |
2987 | event.widget.tk_focusNext().focus() | |
2988 | return "break" | |
2989 | ||
2990 | def goHome(self, event=None): | |
2991 | if self.weekly: | |
2992 | self.showWeek(week=0) | |
2993 | elif self.monthly: | |
2994 | self.showMonth(month=0) | |
2995 | elif self.view == DAY: | |
2996 | today = get_current_time().date() | |
2997 | self.scrollToDate(today) | |
2998 | else: | |
2999 | self.tree.focus_set() | |
3000 | self.tree.focus(1) | |
3001 | self.tree.selection_set(1) | |
3002 | self.tree.yview(0) | |
3003 | ||
3004 | def nextItem(self, e=None): | |
3005 | item = self.tree.selection()[0] | |
3006 | if item: | |
3007 | next = self.tree.next(item) | |
3008 | if next: | |
3009 | ||
3010 | next = int(next) | |
3011 | next -= 1 | |
3012 | self.tree.focus(next) | |
3013 | self.tree.selection_set(next) | |
3014 | ||
3015 | def prevItem(self, e=None): | |
3016 | item = self.tree.selection()[0] | |
3017 | if item: | |
3018 | prev = self.tree.prev(item) | |
3019 | if prev: | |
3020 | ||
3021 | prev = int(prev) | |
3022 | prev += 1 | |
3023 | self.tree.focus(prev) | |
3024 | self.tree.selection_set(prev) | |
3025 | ||
3026 | def OnSelect(self, event=None, uuid=None, dt=None): | |
3027 | """ | |
3028 | Tree row has gained selection. | |
3029 | """ | |
3030 | logger.debug("starting OnSelect with uuid: {0}".format(uuid)) | |
3031 | self.content.delete("1.0", END) | |
3032 | if self.weekly: # week view | |
3033 | if uuid: | |
3034 | # an item is selected, enable clear selection | |
3035 | hsh = loop.uuid2hash[uuid] | |
3036 | type_chr = hsh['itemtype'] | |
3037 | elif uuid is None: # tree view | |
3038 | item = self.tree.selection()[0] | |
3039 | self.rowSelected = int(item) | |
3040 | logger.debug('rowSelected: {0}'.format(self.rowSelected)) | |
3041 | # type_chr is the actual type, e.g., "-" | |
3042 | # show_chr is what's displayed in the tree, e.g., "X" | |
3043 | type_chr = show_chr = self.tree.item(item)['text'][0] | |
3044 | uuid, dt, hsh = self.getInstance(item) | |
3045 | logger.debug('tree rowSelected: {0}; {1}; {2}'.format(self.rowSelected, self.tree.item(item)['text'], dt)) | |
3046 | if self.view in [AGENDA, DAY]: | |
3047 | if self.rowSelected in self.id2date: | |
3048 | if dt is None: | |
3049 | # we have the date selected | |
3050 | self.active_date = self.id2date[self.rowSelected] | |
3051 | logger.debug("active_date from id2date: {0}; {1}".format(self.active_date, self.tree.item(item)['text'])) | |
3052 | else: | |
3053 | # we have an item | |
3054 | self.active_date = dt.date() | |
3055 | logger.debug('active date from dt: {0}; {1}'.format(self.active_date, self.tree.item(item))) | |
3056 | else: | |
3057 | if dt: | |
3058 | self.active_date = dt.date() | |
3059 | logger.debug('active date from dt: {0}; {1}'.format(self.active_date, self.tree.item(item))) | |
3060 | else: | |
3061 | self.active_date = None | |
3062 | logger.debug('active_date: {0}'.format(self.active_date)) | |
3063 | if hsh: | |
3064 | type_chr = hsh['itemtype'] | |
3065 | self.update_idletasks() | |
3066 | ||
3067 | if uuid is not None: | |
3068 | isRepeating = ('r' in hsh and dt) | |
3069 | if isRepeating: | |
3070 | logger.debug('selected: {0}, {1}'.format(dt, type(dt))) | |
3071 | item = "{0} {1}".format(_('selected'), dt) | |
3072 | self.itemmenu.entryconfig(1, label="{0} ...".format(self.em_opts[1])) | |
3073 | self.itemmenu.entryconfig(2, label="{0} ...".format(self.em_opts[2])) | |
3074 | else: | |
3075 | self.itemmenu.entryconfig(1, label=self.em_opts[1]) | |
3076 | self.itemmenu.entryconfig(2, label=self.em_opts[2]) | |
3077 | item = _('selected') | |
3078 | isUnfinished = (type_chr in ['-', '+', '%'] and show_chr != 'X') | |
3079 | hasLink = ('g' in hsh and hsh['g']) | |
3080 | # hasUser = ('u' in hsh and hsh['u'] and hsh['u'] in loop.options['user_data']) | |
3081 | hasUser = ('u' in hsh and hsh['u']) | |
3082 | l1 = hsh['fileinfo'][1] | |
3083 | l2 = hsh['fileinfo'][2] | |
3084 | if l1 == l2: | |
3085 | lines = "{0} {1}".format(_('line'), l1) | |
3086 | else: | |
3087 | lines = "{0} {1}-{2}".format(_('lines'), l1, l2) | |
3088 | self.filetext = filetext = "{0}, {1}".format(hsh['fileinfo'][0], lines) | |
3089 | if 'errors' in hsh and hsh['errors']: | |
3090 | text = "{1}\n\n{2}: {3}\n\n{4}: {5}".format(item, hsh['entry'].lstrip(), _("Errors"), hsh['errors'], _("file"), filetext) | |
3091 | else: | |
3092 | text = "{1}\n\n{2}: {3}".format(item, hsh['entry'].lstrip(), _("file"), filetext) | |
3093 | for i in [0, 1, 2, 3, 5, 6, 7]: # everything except finish (4), open link (8) and show user (9) | |
3094 | self.itemmenu.entryconfig(i, state='normal') | |
3095 | if isUnfinished: | |
3096 | self.itemmenu.entryconfig(4, state='normal') | |
3097 | else: | |
3098 | self.itemmenu.entryconfig(4, state='disabled') | |
3099 | if hasLink: | |
3100 | self.itemmenu.entryconfig(8, state='normal') | |
3101 | else: | |
3102 | self.itemmenu.entryconfig(8, state='disabled') | |
3103 | if hasUser: | |
3104 | self.itemmenu.entryconfig(9, state='normal') | |
3105 | else: | |
3106 | self.itemmenu.entryconfig(9, state='disabled') | |
3107 | self.uuidSelected = uuid | |
3108 | self.itemSelected = hsh | |
3109 | logger.debug('dt selected: {0}, {1}'.format(dt, type(dt))) | |
3110 | self.dtSelected = dt | |
3111 | else: | |
3112 | text = "" | |
3113 | for i in range(10): | |
3114 | self.itemmenu.entryconfig(i, state='disabled') | |
3115 | self.itemSelected = None | |
3116 | self.uuidSelected = None | |
3117 | self.dtSelected = None | |
3118 | r = self.tree.identify_row(1) | |
3119 | if r: | |
3120 | self.topSelected = int(r) | |
3121 | else: | |
3122 | self.topSelected = 1 | |
3123 | logger.debug("row: {0}; uuid: {1}; instance: {2}, {3}; top: {4}".format(self.rowSelected, self.uuidSelected, self.dtSelected, type(self.dtSelected), self.topSelected)) | |
3124 | self.content.insert(INSERT, text) | |
3125 | self.update_idletasks() | |
3126 | logger.debug('ending OnSelect') | |
3127 | return | |
3128 | ||
3129 | def OnActivate(self, event): | |
3130 | """ | |
3131 | Return pressed with tree row selected | |
3132 | """ | |
3133 | if not self.itemSelected: | |
3134 | return "break" | |
3135 | item = self.tree.selection()[0] | |
3136 | uuid, dt, hsh = self.getInstance(item) | |
3137 | x = self.winfo_rootx() + 350 | |
3138 | y = self.winfo_rooty() + 50 | |
3139 | logger.debug("id: {0}, coords: {1}, {2}".format(id, x, y)) | |
3140 | self.itemmenu.post(x, y) | |
3141 | self.itemmenu.focus_set() | |
3142 | return "break" | |
3143 | ||
3144 | def getInstance(self, item): | |
3145 | instance = self.count2id[item] | |
3146 | logger.debug('starting getInstance: {0}; {1}'.format(item, instance)) | |
3147 | if instance is not None: | |
3148 | uuid, dt = self.count2id[item].split("::") | |
3149 | hsh = loop.uuid2hash[uuid] | |
3150 | dt = parse(dt) | |
3151 | logger.debug('returning uuid: {0}, dt: {1}'.format(uuid, dt)) | |
3152 | return uuid, dt, hsh | |
3153 | else: | |
3154 | logger.debug('returning None') | |
3155 | return None, None, None | |
3156 | ||
3157 | def updateClock(self): | |
3158 | tt = TimeIt(loglevel=2, label="updateClock") | |
3159 | self.now = get_current_time() | |
3160 | self.current_minutes = self.now.hour * 60 + self.now.minute | |
3161 | nxt = (60 - self.now.second) * 1000 - self.now.microsecond // 1000 | |
3162 | logger.debug('next update in {0} milliseconds.'.format(nxt)) | |
3163 | self.after(nxt, self.updateClock) | |
3164 | nowfmt = "{0} {1}".format( | |
3165 | s2or3(self.now.strftime(loop.options['reprtimefmt']).lower()), | |
3166 | s2or3(self.now.strftime("%a %b %d"))) | |
3167 | ||
3168 | nowfmt = leadingzero.sub("", nowfmt) | |
3169 | self.currentTime.set("{0}".format(nowfmt)) | |
3170 | today = self.now.date() | |
3171 | newday = (today != self.today) | |
3172 | self.today = today | |
3173 | ||
3174 | new, modified, deleted = get_changes( | |
3175 | self.options, loop.file2lastmodified) | |
3176 | ||
3177 | if newday or new or modified or deleted: | |
3178 | if newday: | |
3179 | logger.info('newday') | |
3180 | logger.info("new: {0}; modified: {1}; deleted: {2}".format(len(new), len(modified), len(deleted))) | |
3181 | logger.debug('calling loadData') | |
3182 | loop.loadData() | |
3183 | # we now have file2uuids ... | |
3184 | ||
3185 | if self.weekly: | |
3186 | logger.debug('calling showWeek') | |
3187 | self.showWeek() | |
3188 | elif self.monthly: | |
3189 | logger.debug('calling showMonth') | |
3190 | self.showMonth() | |
3191 | else: | |
3192 | logger.debug('calling showView') | |
3193 | self.showView() | |
3194 | elif self.today_col is not None: | |
3195 | xy = self.get_timeline() | |
3196 | if xy: | |
3197 | self.canvas.delete('current_time') | |
3198 | self.canvas.create_line(xy, width=2, fill=CURRENTLINE, tag='current_time') | |
3199 | self.update_idletasks() | |
3200 | ||
3201 | if self.current_minutes % loop.options['update_minutes'] == 0: | |
3202 | if loop.do_update: | |
3203 | updateCurrentFiles(loop.rows, loop.file2uuids, loop.uuid2hash, loop.options) | |
3204 | loop.do_update = False | |
3205 | ||
3206 | if loop.options['icssync_folder']: | |
3207 | fullpath = os.path.join(loop.options['datadir'], loop.options['icssync_folder']) | |
3208 | prefix, files = getAllFiles(fullpath, include="*") | |
3209 | base_files = set([]) | |
3210 | # file_lst = [] | |
3211 | for tup in files: | |
3212 | base, ext = os.path.splitext(tup[0]) | |
3213 | if ext in [".txt", ".ics"]: | |
3214 | base_files.add(base) | |
3215 | file_lst = list(base_files) | |
3216 | datadir = loop.options['datadir'] | |
3217 | for file in file_lst: | |
3218 | relfile = relpath(file, datadir) | |
3219 | logger.debug('calling syncTxt: {0}; {1}'.format(datadir, relfile)) | |
3220 | syncTxt(self.loop.file2uuids, self.loop.uuid2hash, datadir, relfile) | |
3221 | # any updated txt files will be reloaded in the next update | |
3222 | ||
3223 | self.updateAlerts() | |
3224 | ||
3225 | if self.actionTimer.idle_active or self.actionTimer.timer_status != STOPPED: | |
3226 | self.timerStatus.set(self.actionTimer.get_time()) | |
3227 | if self.actionTimer.timer_minutes >= 1: | |
3228 | if (self.options['action_interval'] and self.actionTimer.timer_minutes % loop.options['action_interval'] == 0): | |
3229 | logger.debug('action_minutes trigger: {0} {1}'.format(self.actionTimer.timer_minutes, self.actionTimer.timer_status)) | |
3230 | if self.actionTimer.timer_status == 'running': | |
3231 | if ('running' in loop.options['action_timer'] and | |
3232 | loop.options['action_timer']['running']): | |
3233 | tcmd = loop.options['action_timer']['running'] | |
3234 | logger.debug('running: {0}'.format(tcmd)) | |
3235 | ||
3236 | subprocess.call(tcmd, shell=True) | |
3237 | ||
3238 | elif self.actionTimer.timer_status == 'paused': | |
3239 | if ('paused' in loop.options['action_timer'] and | |
3240 | loop.options['action_timer']['paused']): | |
3241 | tcmd = loop.options['action_timer']['paused'] | |
3242 | ||
3243 | logger.debug('paused: {0}'.format(tcmd)) | |
3244 | subprocess.call(tcmd, shell=True) | |
3245 | ||
3246 | tt.stop() | |
3247 | ||
3248 | def updateAlerts(self): | |
3249 | self.update_idletasks() | |
3250 | if loop.alerts: | |
3251 | logger.debug('updateAlerts: {0}'.format(len(loop.alerts))) | |
3252 | alerts = deepcopy(loop.alerts) | |
3253 | if alerts and loop.options['calendars']: | |
3254 | alerts = [x for x in alerts if self.cal_regex.match(x[-1])] | |
3255 | if alerts: | |
3256 | curr_minutes = datetime2minutes(self.now) | |
3257 | td = -1 | |
3258 | # pop old alerts | |
3259 | while td < 0 and alerts: | |
3260 | td = alerts[0][0] - curr_minutes | |
3261 | if td < 0: | |
3262 | a = alerts.pop(0) | |
3263 | # alerts for this minute will have td's < 1.0 | |
3264 | if td < 1.0: | |
3265 | if ('alert_wakecmd' in loop.options and | |
3266 | loop.options['alert_wakecmd']): | |
3267 | cmd = s2or3(loop.options['alert_wakecmd']) | |
3268 | subprocess.call(cmd, shell=True) | |
3269 | while td < 1.0 and alerts: | |
3270 | hsh = alerts[0][2] | |
3271 | alerts.pop(0) | |
3272 | actions = hsh['_alert_action'] | |
3273 | if 's' in actions: | |
3274 | if ('alert_soundcmd' in self.options and | |
3275 | self.options['alert_soundcmd']): | |
3276 | scmd = s2or3(expand_template( | |
3277 | self.options['alert_soundcmd'], hsh)) | |
3278 | subprocess.call(scmd, shell=True) | |
3279 | else: | |
3280 | self.textWindow(parent=self, title="etm", prompt=_("""\ | |
3281 | A sound alert failed. The setting for 'alert_soundcmd' is missing from your etmtk.cfg."""), opts=self.options) | |
3282 | if 'd' in actions: | |
3283 | if ('alert_displaycmd' in self.options and | |
3284 | self.options['alert_displaycmd']): | |
3285 | dcmd = s2or3(expand_template( | |
3286 | self.options['alert_displaycmd'], hsh)) | |
3287 | subprocess.call(dcmd.encode(loop.options['encoding']['gui']), shell=True) | |
3288 | else: | |
3289 | self.textWindow(parent=self, title="etm", prompt=_("""\ | |
3290 | A display alert failed. The setting for 'alert_displaycmd' is missing \ | |
3291 | from your etmtk.cfg."""), opts=self.options) | |
3292 | if 'v' in actions: | |
3293 | if ('alert_voicecmd' in self.options and | |
3294 | self.options['alert_voicecmd']): | |
3295 | vcmd = s2or3(expand_template( | |
3296 | self.options['alert_voicecmd'], hsh)) | |
3297 | subprocess.call(vcmd, shell=True) | |
3298 | else: | |
3299 | self.textWindow(parent=self, title="etm", prompt=_("""\ | |
3300 | An email alert failed. The setting for 'alert_voicecmd' is missing from \ | |
3301 | your etmtk.cfg."""), opts=self.options) | |
3302 | if 'e' in actions: | |
3303 | missing = [] | |
3304 | for field in ['smtp_from', 'smtp_id', 'smtp_pw', 'smtp_server', 'smtp_to']: | |
3305 | if not self.options[field]: | |
3306 | missing.append(field) | |
3307 | if missing: | |
3308 | self.textWindow(parent=self, title="etm", prompt=_("""\ | |
3309 | An email alert failed. Settings for the following variables are missing \ | |
3310 | from your etmtk.cfg: %s.""" % ", ".join(["'%s'" % x for x in missing])), opts=self.options) | |
3311 | else: | |
3312 | subject = hsh['summary'] | |
3313 | message = expand_template( | |
3314 | self.options['email_template'], hsh) | |
3315 | arguments = hsh['_alert_argument'] | |
3316 | recipients = [str(x).strip() for x in arguments[0]] | |
3317 | if len(arguments) > 1: | |
3318 | attachments = [str(x).strip() | |
3319 | for x in arguments[1]] | |
3320 | else: | |
3321 | attachments = [] | |
3322 | if subject and message and recipients: | |
3323 | send_mail( | |
3324 | smtp_to=recipients, | |
3325 | subject=subject, | |
3326 | message=message, | |
3327 | files=attachments, | |
3328 | smtp_from=self.options['smtp_from'], | |
3329 | smtp_server=self.options['smtp_server'], | |
3330 | smtp_id=self.options['smtp_id'], | |
3331 | smtp_pw=self.options['smtp_pw']) | |
3332 | if 't' in actions: | |
3333 | missing = [] | |
3334 | for field in ['sms_from', 'sms_message', 'sms_phone', 'sms_pw', 'sms_server', 'sms_subject']: | |
3335 | if not self.options[field]: | |
3336 | missing.append(field) | |
3337 | if missing: | |
3338 | self.textWindow(parent=self, title="etm", prompt=_("""\ | |
3339 | A text alert failed. Settings for the following variables are missing \ | |
3340 | from your 'emt.cfg': %s.""" % ", ".join(["'%s'" % x for x in missing])), opts=self.options) | |
3341 | else: | |
3342 | message = expand_template( | |
3343 | self.options['sms_message'], hsh) | |
3344 | subject = expand_template( | |
3345 | self.options['sms_subject'], hsh) | |
3346 | arguments = hsh['_alert_argument'] | |
3347 | if arguments: | |
3348 | sms_phone = ",".join([str(x).strip() for x in | |
3349 | arguments[0]]) | |
3350 | else: | |
3351 | sms_phone = self.options['sms_phone'] | |
3352 | if message: | |
3353 | send_text( | |
3354 | sms_phone=sms_phone, | |
3355 | subject=subject, | |
3356 | message=message, | |
3357 | sms_from=self.options['sms_from'], | |
3358 | sms_server=self.options['sms_server'], | |
3359 | sms_pw=self.options['sms_pw']) | |
3360 | if 'p' in actions: | |
3361 | arguments = hsh['_alert_argument'] | |
3362 | proc = str(arguments[0][0]).strip() | |
3363 | cmd = s2or3(expand_template(proc, hsh)) | |
3364 | subprocess.call(cmd, shell=True) | |
3365 | if 'm' in actions: | |
3366 | # put this last since the internal message window is modal and thus blocking | |
3367 | MessageWindow( | |
3368 | self, | |
3369 | title=expand_template('!summary!', hsh), | |
3370 | prompt=expand_template( | |
3371 | self.options['alert_template'], hsh)) | |
3372 | ||
3373 | if not alerts: | |
3374 | break | |
3375 | td = alerts[0][0] - curr_minutes | |
3376 | if alerts and len(alerts) > 0: | |
3377 | self.pendingAlerts.set(len(alerts)) | |
3378 | self.pending.configure(state="normal") | |
3379 | self.activeAlerts = alerts | |
3380 | else: | |
3381 | self.pendingAlerts.set(0) | |
3382 | self.activeAlerts = [] | |
3383 | self.pending.configure(state="disabled") | |
3384 | ||
3385 | def textWindow(self, parent, title=None, prompt=None, opts=None, modal=True): | |
3386 | TextDialog(parent, title=title, prompt=prompt, opts=opts, modal=modal) | |
3387 | ||
3388 | def goToDate(self, e=None): | |
3389 | """ | |
3390 | :param e: | |
3391 | :return: | |
3392 | """ | |
3393 | if e and e.char != "j": | |
3394 | return | |
3395 | prompt = _("""\ | |
3396 | Return an empty string for the current date or a date to be parsed. | |
3397 | Relative dates and fuzzy parsing are supported.""") | |
3398 | if self.view not in [DAY, WEEK]: | |
3399 | self.view = DAY | |
3400 | self.showView() | |
3401 | d = GetDateTime(parent=self, title=_('date'), prompt=prompt) | |
3402 | day = d.value | |
3403 | ||
3404 | logger.debug('day: {0}'.format(day)) | |
3405 | if day is not None: | |
3406 | self.chosen_day = day | |
3407 | ||
3408 | if self.weekly: | |
3409 | self.showWeek(event=e, week=None) | |
3410 | elif self.monthly: | |
3411 | self.showMonth(event=e, month=None) | |
3412 | else: | |
3413 | self.scrollToDate(day.date()) | |
3414 | return | |
3415 | ||
3416 | def setFilter(self, *args): | |
3417 | if self.view in [WEEK, MONTH, CUSTOM]: | |
3418 | return | |
3419 | self.filter_active = True | |
3420 | self.viewmenu.entryconfig(6, state="disabled") | |
3421 | self.viewmenu.entryconfig(7, state="normal") | |
3422 | self.fltr.configure(bg="white", state="normal") | |
3423 | self.fltr.focus_set() | |
3424 | ||
3425 | def clearFilter(self, e=None): | |
3426 | if self.view in [WEEK, MONTH, CUSTOM]: | |
3427 | return | |
3428 | self.filter_active = False | |
3429 | self.viewmenu.entryconfig(6, state="normal") | |
3430 | self.viewmenu.entryconfig(7, state="disabled") | |
3431 | self.filterValue.set('') | |
3432 | self.fltr.configure(bg=BGCOLOR) | |
3433 | self.tree.focus_set() | |
3434 | if self.rowSelected: | |
3435 | self.tree.focus(self.rowSelected) | |
3436 | self.tree.selection_set(self.rowSelected) | |
3437 | self.tree.see(self.rowSelected) | |
3438 | ||
3439 | def leaveFilter(self, e=None): | |
3440 | self.tree.focus_set() | |
3441 | if self.rowSelected: | |
3442 | self.tree.focus(self.rowSelected) | |
3443 | self.tree.selection_set(self.rowSelected) | |
3444 | self.tree.see(self.rowSelected) | |
3445 | ||
3446 | def startIdleTimer(self, e=None): | |
3447 | if self.actionTimer.timer_status != STOPPED: | |
3448 | prompt = "The active action timer must be stopped before starting the idle timer." | |
3449 | MessageWindow(self, title="error", prompt=prompt) | |
3450 | return | |
3451 | if self.actionTimer.idle_active: | |
3452 | self.actionTimer.idle_resolve() | |
3453 | else: | |
3454 | self.actionTimer.idle_start() | |
3455 | self.newmenu.entryconfig(4, state="disabled") | |
3456 | self.newmenu.entryconfig(5, state="normal") | |
3457 | ||
3458 | def stopIdleTimer(self, e=None): | |
3459 | if not self.actionTimer.idle_active: | |
3460 | return | |
3461 | if self.actionTimer.timer_status != STOPPED: | |
3462 | prompt = "The active action timer must be stopped before stopping the idle timer." | |
3463 | MessageWindow(self, title="error", prompt=prompt) | |
3464 | return | |
3465 | self.actionTimer.idle_stop() | |
3466 | self.timerStatus.set("") | |
3467 | self.newmenu.entryconfig(4, state="normal") | |
3468 | self.newmenu.entryconfig(5, state="disabled") | |
3469 | ||
3470 | def startActionTimer(self, e=None): | |
3471 | """ | |
3472 | Prompt for a summary and start action timer. | |
3473 | if uuid: | |
3474 | if ~ | |
3475 | restart timer? | |
3476 | else: | |
3477 | enter summary or empty | |
3478 | """ | |
3479 | # hack to avoid activating with Ctrl-t | |
3480 | if e and e.char != "t": | |
3481 | return | |
3482 | if self.actionTimer.idle_active and self.actionTimer.timer_status in [STOPPED, PAUSED] and self.actionTimer.idle_delta > int(loop.options['idle_minutes']) * ONEMINUTE: | |
3483 | self.actionTimer.idle_resolve() | |
3484 | if self.actionTimer.timer_status == STOPPED: | |
3485 | if self.uuidSelected: | |
3486 | nullok = True | |
3487 | sel_hsh = loop.uuid2hash[self.uuidSelected] | |
3488 | prompt = _("""\ | |
3489 | Enter a summary for the new action timer or return an empty string | |
3490 | to create a timer based on the selected item.""") | |
3491 | else: | |
3492 | nullok = False | |
3493 | ||
3494 | prompt = _("""\ | |
3495 | Enter a summary for the new action timer.""") | |
3496 | value = GetString(parent=self, title=_('action timer'), prompt=prompt, opts={'nullok': nullok}, font=self.tkfixedfont).value | |
3497 | ||
3498 | self.tree.focus_set() | |
3499 | logger.debug('value: {0}'.format(value)) | |
3500 | if value is None: | |
3501 | return | |
3502 | ||
3503 | if value: | |
3504 | self.timerItem = None | |
3505 | hsh, msg = str2hsh(value, options=loop.options) | |
3506 | elif nullok: | |
3507 | self.timerItem = self.uuidSelected | |
3508 | # Based on item, 'entry' will be in hsh | |
3509 | hsh = sel_hsh | |
3510 | ('hsh', hsh) | |
3511 | for k in ['_r', 'o', '+', '-']: | |
3512 | if k in hsh: | |
3513 | del hsh[k] | |
3514 | hsh['e'] = 0 * ONEMINUTE | |
3515 | else: | |
3516 | # shouldn't happen | |
3517 | return "break" | |
3518 | logger.debug('item: {0}'.format(hsh)) | |
3519 | ||
3520 | self.actionTimer.timer_start(hsh) | |
3521 | if ('running' in loop.options['action_timer'] and | |
3522 | loop.options['action_timer']['running']): | |
3523 | tcmd = loop.options['action_timer']['running'] | |
3524 | logger.debug('command: {0}'.format(tcmd)) | |
3525 | subprocess.call(tcmd, shell=True) | |
3526 | elif self.actionTimer.timer_status in [PAUSED, RUNNING]: | |
3527 | self.actionTimer.timer_toggle() | |
3528 | if (self.actionTimer.timer_status == RUNNING and 'running' in loop.options['action_timer'] and loop.options['action_timer']['running']): | |
3529 | tcmd = loop.options['action_timer']['running'] | |
3530 | logger.debug('command: {0}'.format(tcmd)) | |
3531 | subprocess.call(tcmd, shell=True) | |
3532 | elif (self.actionTimer.timer_status == PAUSED and 'paused' in loop.options['action_timer'] and loop.options['action_timer']['paused']): | |
3533 | tcmd = loop.options['action_timer']['paused'] | |
3534 | logger.debug('command: {0}'.format(tcmd)) | |
3535 | subprocess.call(tcmd, shell=True) | |
3536 | self.timerStatus.set(self.actionTimer.get_time()) | |
3537 | self.newmenu.entryconfig(3, state="normal") | |
3538 | return | |
3539 | ||
3540 | def finishActionTimer(self, e=None): | |
3541 | if e and e.char != "T": | |
3542 | return | |
3543 | if self.actionTimer.timer_status not in [RUNNING, PAUSED]: | |
3544 | logger.info('stopping already stopped timer') | |
3545 | return "break" | |
3546 | self.actionTimer.timer_stop() | |
3547 | self.timerStatus.set(self.actionTimer.get_time()) | |
3548 | hsh = self.actionTimer.timer_hsh | |
3549 | changed = SimpleEditor(parent=self, newhsh=hsh, rephsh=None, options=loop.options, title=_("new action"), modified=True).changed | |
3550 | if changed: | |
3551 | # clear status and reload | |
3552 | self.actionTimer.timer_clear() | |
3553 | # self.timerStatus.set("") | |
3554 | self.newmenu.entryconfig(3, state="disabled") | |
3555 | ||
3556 | self.updateAlerts() | |
3557 | if self.weekly: | |
3558 | self.showWeek() | |
3559 | elif self.monthly: | |
3560 | self.showMonth() | |
3561 | else: | |
3562 | self.showView(row=self.topSelected) | |
3563 | else: | |
3564 | # edit canceled | |
3565 | ans = self.confirm( | |
3566 | title=_('timer'), | |
3567 | prompt=_('Retain the timer for "{0}"').format(self.actionTimer.timer_hsh['_summary']), | |
3568 | parent=self) | |
3569 | if ans: | |
3570 | # restore timer with the old status | |
3571 | self.actionTimer.timer_start(hsh=hsh, toggle=False) | |
3572 | else: | |
3573 | if self.actionTimer.idle_active: | |
3574 | # add the time back into idle | |
3575 | self.actionTimer.idle_delta += self.actionTimer.timer_delta | |
3576 | self.actionTimer.timer_clear() | |
3577 | # self.timerStatus.set("") | |
3578 | self.newmenu.entryconfig(3, state="disabled") | |
3579 | self.timerStatus.set(self.actionTimer.get_time()) | |
3580 | self.tree.focus_set() | |
3581 | ||
3582 | def gettext(self, event=None): | |
3583 | s = self.e.get() | |
3584 | if s is not None: | |
3585 | return s | |
3586 | else: | |
3587 | return '' | |
3588 | ||
3589 | def cleartext(self, event=None): | |
3590 | self.showView() | |
3591 | return 'break' | |
3592 | ||
3593 | def process_input(self, event=None, cmd=None): | |
3594 | """ | |
3595 | """ | |
3596 | ||
3597 | if not cmd: | |
3598 | return True | |
3599 | if self.mode == 'command': | |
3600 | cmd = cmd.strip() | |
3601 | # if cmd[0] in ['a', 'c']: | |
3602 | if cmd[0] in ['a']: | |
3603 | # simple command history for report commands | |
3604 | if cmd in self.history: | |
3605 | self.history.remove(cmd) | |
3606 | self.history.append(cmd) | |
3607 | self.index = len(self.history) - 1 | |
3608 | else: | |
3609 | parts = cmd.split(' ') | |
3610 | if len(parts) == 2: | |
3611 | try: | |
3612 | i = int(parts[0]) | |
3613 | except: | |
3614 | i = None | |
3615 | if i: | |
3616 | parts.pop(0) | |
3617 | parts.append(str(i)) | |
3618 | cmd = " ".join(parts) | |
3619 | try: | |
3620 | res = loop.do_command(cmd) | |
3621 | except: | |
3622 | return _('could not process command "{0}"').format(cmd) | |
3623 | ||
3624 | elif self.mode == 'delete': | |
3625 | loop.cmd_do_delete(cmd) | |
3626 | res = '' | |
3627 | ||
3628 | elif self.mode == 'finish': | |
3629 | loop.cmd_do_finish(cmd) | |
3630 | res = '' | |
3631 | ||
3632 | elif self.mode == 'new_date': | |
3633 | res = loop.new_date(cmd) | |
3634 | ||
3635 | if not res: | |
3636 | res = _('command "{0}" returned no output').format(cmd) | |
3637 | ||
3638 | logger.debug('no output') | |
3639 | ||
3640 | self.clearTree() | |
3641 | return () | |
3642 | ||
3643 | if type(res) == dict: | |
3644 | self.showTree(res, event=event) | |
3645 | else: | |
3646 | # not a hash => not a tree | |
3647 | self.textWindow(self, title='etm', prompt=res, opts=self.options) | |
3648 | return 0 | |
3649 | ||
3650 | def getDepths(self, e=None): | |
3651 | for k in self.depth2id: | |
3652 | print(k) | |
3653 | for item in self.depth2id[k]: | |
3654 | print(item, self.tree.item(item)) | |
3655 | ||
3656 | ||
3657 | def expand2Depth(self, e=None): | |
3658 | self.getDepths() | |
3659 | ||
3660 | if e and e.char != "o": | |
3661 | return | |
3662 | prompt = _("""\ | |
3663 | Enter an integer depth to expand branches | |
3664 | or 0 to expand all branches completely.""") | |
3665 | depth = GetInteger( | |
3666 | parent=self, | |
3667 | title=_("depth"), prompt=prompt, opts=[0], default=0).value | |
3668 | if depth is None: | |
3669 | return () | |
3670 | maxdepth = max([k for k in self.depth2id]) | |
3671 | logger.debug('expand2Depth {0}: {1}/{2}'.format(self.view, depth, maxdepth)) | |
3672 | if self.view in [AGENDA, DAY, KEYWORD, NOTE, TAG, PATH, CUSTOM]: | |
3673 | self.outline_depths[self.view] = depth | |
3674 | logger.debug('outline_depths: {0}'.format(self.outline_depths)) | |
3675 | if depth == 0: | |
3676 | # expand all | |
3677 | for k in self.depth2id: | |
3678 | for item in self.depth2id[k]: | |
3679 | self.tree.item(item, open=True) | |
3680 | else: | |
3681 | depth -= 1 | |
3682 | depth = max(depth, 0) | |
3683 | logger.debug('using depth: {0}; {1}'.format(depth, maxdepth)) | |
3684 | for i in range(depth): | |
3685 | if i in self.depth2id: | |
3686 | for item in self.depth2id[i]: | |
3687 | try: | |
3688 | self.tree.item(item, open=True) | |
3689 | except: | |
3690 | logger.exception('open: {0}, {1}'.format(i, item)) | |
3691 | for i in range(depth, maxdepth + 1): | |
3692 | if i in self.depth2id: | |
3693 | for item in self.depth2id[i]: | |
3694 | try: | |
3695 | self.tree.item(item, open=False) | |
3696 | except: | |
3697 | logger.exception('open: {0}, {1}'.format(i, item)) | |
3698 | ||
3699 | def scrollToDate(self, date): | |
3700 | # only makes sense for schedule | |
3701 | logger.debug("DAY: {0}; date: {1}".format(self.view == DAY, date)) | |
3702 | if self.view != DAY or date not in loop.prevnext: | |
3703 | return | |
3704 | active_date = loop.prevnext[date][1] | |
3705 | if active_date not in self.date2id: | |
3706 | return | |
3707 | uid = self.date2id[active_date] | |
3708 | self.active_date = active_date | |
3709 | self.scrollToId(uid) | |
3710 | ||
3711 | def scrollToId(self, uid): | |
3712 | self.update_idletasks() | |
3713 | self.tree.focus_set() | |
3714 | self.tree.focus(uid) | |
3715 | self.tree.selection_set(uid) | |
3716 | self.tree.yview(int(uid) - 1) | |
3717 | ||
3718 | def showTree(self, tree, event=None): | |
3719 | self.date2id = {} | |
3720 | self.id2date = {} | |
3721 | self.clearTree() | |
3722 | self.count = 0 | |
3723 | self.count2id = {} | |
3724 | self.active_tree = tree | |
3725 | self.depth2id = {} | |
3726 | self.add2Tree(u'', tree[self.root], tree) | |
3727 | loop.count2id = self.count2id | |
3728 | self.tree.tag_configure('treefont', font=self.tktreefont) | |
3729 | ||
3730 | self.content.delete("0.0", END) | |
3731 | ||
3732 | if event is None: | |
3733 | ||
3734 | if self.view == DAY and self.active_date: | |
3735 | self.scrollToDate(self.active_date) | |
3736 | else: | |
3737 | if self.view in [AGENDA, DAY, TAG, KEYWORD, NOTE, PATH]: | |
3738 | if self.filter_active: | |
3739 | depth = 0 | |
3740 | else: | |
3741 | depth = self.outline_depths[self.view] | |
3742 | if depth == 0: | |
3743 | # expand all | |
3744 | for k in self.depth2id: | |
3745 | for item in self.depth2id[k]: | |
3746 | self.tree.item(item, open=True) | |
3747 | else: | |
3748 | maxdepth = max([k for k in self.depth2id]) | |
3749 | depth -= 1 | |
3750 | depth = max(depth, 0) | |
3751 | for i in range(depth): | |
3752 | for item in self.depth2id[i]: | |
3753 | self.tree.item(item, open=True) | |
3754 | for i in range(depth, maxdepth + 1): | |
3755 | for item in self.depth2id[i]: | |
3756 | self.tree.item(item, open=False) | |
3757 | self.goHome() | |
3758 | ||
3759 | def popupTree(self, e=None): | |
3760 | if self.weekly or self.monthly: | |
3761 | return | |
3762 | if not self.active_tree: | |
3763 | return | |
3764 | depth = self.outline_depths[self.view] | |
3765 | if loop.options: | |
3766 | if 'report_indent' in loop.options: | |
3767 | indent = loop.options['report_indent'] | |
3768 | if 'report_width1' in loop.options: | |
3769 | width1 = loop.options['report_width1'] | |
3770 | if 'report_width2' in loop.options: | |
3771 | width2 = loop.options['report_width2'] | |
3772 | else: | |
3773 | indent = 4 | |
3774 | width1 = 43 | |
3775 | width2 = 20 | |
3776 | res = tree2Text(self.active_tree, indent=indent, width1=width1, width2=width2, depth=depth) | |
3777 | if not res[0][0]: | |
3778 | res[0].pop(0) | |
3779 | prompt = "\n".join(res[0]) | |
3780 | self.textWindow(parent=self, title='etm', opts=self.options, prompt=prompt, modal=False) | |
3781 | ||
3782 | def printTree(self, e=None): | |
3783 | if e and e.char != "p": | |
3784 | return | |
3785 | if self.weekly or self.monthly: | |
3786 | return | |
3787 | if not self.active_tree: | |
3788 | return | |
3789 | ans = self.confirm(parent=self.tree, prompt=_("""Print current outline?""")) | |
3790 | if not ans: | |
3791 | return False | |
3792 | depth = self.outline_depths[self.view] | |
3793 | ||
3794 | if loop.options: | |
3795 | if 'report_indent' in loop.options: | |
3796 | indent = loop.options['report_indent'] | |
3797 | if 'report_width1' in loop.options: | |
3798 | width1 = loop.options['report_width1'] | |
3799 | if 'report_width2' in loop.options: | |
3800 | width2 = loop.options['report_width2'] | |
3801 | else: | |
3802 | indent = 4 | |
3803 | width1 = 43 | |
3804 | width2 = 20 | |
3805 | res = tree2Text(self.active_tree, indent=indent, width1=width1, width2=width2, depth=depth) | |
3806 | if not res[0][0]: | |
3807 | res[0].pop(0) | |
3808 | res[0].append('') | |
3809 | s = "{0}".format("\n".join(res[0])) | |
3810 | self.printWithDefault(s) | |
3811 | ||
3812 | def clearTree(self): | |
3813 | """ | |
3814 | Remove all items from the tree | |
3815 | """ | |
3816 | self.active_tree = {} | |
3817 | for child in self.tree.get_children(): | |
3818 | self.tree.delete(child) | |
3819 | ||
3820 | def add2Tree(self, parent, elements, tree, depth=0): | |
3821 | max_depth = 100 | |
3822 | for text in elements: | |
3823 | self.count += 1 | |
3824 | # text is a key in the element (tree) hash | |
3825 | # these keys are (parent, item) tuples | |
3826 | if text in tree: | |
3827 | # this is a branch | |
3828 | item = " {0}".format(text[1]) # this is the label of the parent | |
3829 | children = tree[text] # these are the children tuples of item | |
3830 | oid = self.tree.insert(parent, 'end', iid=self.count, text=item, | |
3831 | open=(depth <= max_depth)) | |
3832 | self.depth2id.setdefault(depth, set([])).add(oid) | |
3833 | # recurse to get children | |
3834 | self.count2id[oid] = None | |
3835 | self.add2Tree(oid, children, tree, depth=depth + 1) | |
3836 | else: | |
3837 | # this is a leaf | |
3838 | if len(text[1]) == 4: | |
3839 | uuid, item_type, col1, col3 = text[1] | |
3840 | dt = '' | |
3841 | else: # len 5 day view with datetime appended | |
3842 | uuid, item_type, col1, col3, dt = text[1] | |
3843 | ||
3844 | if item_type: | |
3845 | # This hack avoids encoding issues under python 2 | |
3846 | col1 = "{0} {1}".format(id2Type[item_type], col1) | |
3847 | ||
3848 | if type(col3) == int: | |
3849 | col3 = '%s' % col3 | |
3850 | else: | |
3851 | col3 = s2or3(col3) | |
3852 | ||
3853 | # Drop the instance information from the id | |
3854 | id = uuid.split(':')[0] | |
3855 | if id in loop.uuid2labels: | |
3856 | col2 = loop.uuid2labels[id] | |
3857 | else: | |
3858 | col2 = "***" | |
3859 | if item_type not in ["=", "ib"]: | |
3860 | logger.warn('Missing key {0} for {1} {2}'.format(id, col1, col3)) | |
3861 | oid = self.tree.insert(parent, 'end', iid=self.count, text=col1, open=(depth <= max_depth), values=[col2, col3], tags=(item_type, 'treefont')) | |
3862 | self.count2id[oid] = "{0}::{1}".format(uuid, dt) | |
3863 | if dt: | |
3864 | if item_type == 'by': | |
3865 | # we want today, not the starting date for this | |
3866 | d = get_current_time().date() | |
3867 | else: | |
3868 | if type(dt) is datetime: | |
3869 | d = dt.date() | |
3870 | else: | |
3871 | d = parse(dt).date() | |
3872 | if d and d not in self.date2id: | |
3873 | # logger.debug('date2id[{0}] = {1}'.format(d, parent)) | |
3874 | self.date2id[d] = int(parent) | |
3875 | if int(parent) not in self.id2date: | |
3876 | # logger.debug('id2date[{0}] = {1}'.format(int(parent), d)) | |
3877 | self.id2date[int(parent)] = d | |
3878 | ||
3879 | def makeReport(self, event=None): | |
3880 | if self.view != CUSTOM: | |
3881 | return | |
3882 | self.outline_depths[CUSTOM] = 0 | |
3883 | self.value_of_combo = self.custom_box.get() | |
3884 | if not self.value_of_combo.strip(): | |
3885 | return | |
3886 | try: | |
3887 | res = getReportData( | |
3888 | self.value_of_combo, | |
3889 | self.loop.file2uuids, | |
3890 | self.loop.uuid2hash, | |
3891 | self.loop.options, | |
3892 | cli=False) | |
3893 | if not res: | |
3894 | res = _("Report contains no output.") | |
3895 | if self.value_of_combo not in self.specs: | |
3896 | self.specs.append(self.value_of_combo) | |
3897 | self.specs.sort() | |
3898 | self.specs = [x for x in self.specs if x] | |
3899 | self.custom_box["values"] = self.specs | |
3900 | self.specsModified = True | |
3901 | logger.debug("spec: {0}".format(self.value_of_combo)) | |
3902 | except: | |
3903 | logger.exception("could not process: {0}".format(self.value_of_combo)) | |
3904 | res = _("'{0}' could not be processed".format(self.value_of_combo)) | |
3905 | if type(res) == dict: | |
3906 | self.showTree(res, event=event) | |
3907 | else: | |
3908 | # not a hash => not a tree | |
3909 | self.textWindow(self, title='etm', prompt=res, opts=self.options) | |
3910 | self.custom_box.focus_set() | |
3911 | return 0 | |
3912 | ||
3913 | def getSpecs(self, e=None): | |
3914 | self.specs = [] | |
3915 | if 'reports' in loop.options: | |
3916 | self.specs = loop.options['reports'] | |
3917 | ||
3918 | def saveSpecs(self, e=None): | |
3919 | # called when changing from custom view or | |
3920 | # when calling save changes to specs | |
3921 | if self.view != CUSTOM: | |
3922 | return | |
3923 | if not self.specsModified: | |
3924 | return | |
3925 | # remove duplicates | |
3926 | self.specs = list(set(self.specs)) | |
3927 | self.specs.sort() | |
3928 | added = [x for x in self.specs if x not in self.saved_specs] | |
3929 | if not added: | |
3930 | self.specsModified = False | |
3931 | return | |
3932 | ans = self.confirm(parent=self, prompt=_("""Save the additions to your report specifications? {0} """.format("\n ".join(added)))) | |
3933 | if ans: | |
3934 | file = self.getReportsFile() | |
3935 | if not (file and os.path.isfile(file)): | |
3936 | return | |
3937 | with codecs.open(file, 'r', loop.options['encoding']['file']) as fo: | |
3938 | lines = fo.readlines() | |
3939 | lines.extend(added) | |
3940 | lines.sort() | |
3941 | content = "\n".join([x.strip() for x in lines if x.strip()]) | |
3942 | with codecs.open(file, 'w', loop.options['encoding']['file']) as fo: | |
3943 | fo.write(content) | |
3944 | logger.debug("saved: {0}".format(file)) | |
3945 | self.getSpecs() | |
3946 | self.custom_box['values'] = self.specs | |
3947 | self.value_of_combo = self.specs[0] | |
3948 | self.specsModified = False | |
3949 | ||
3950 | def exportText(self): | |
3951 | if self.view != CUSTOM: | |
3952 | return | |
3953 | logger.debug("spec: {0}".format(self.value_of_combo)) | |
3954 | tree = getReportData( | |
3955 | self.value_of_combo, | |
3956 | self.loop.file2uuids, | |
3957 | self.loop.uuid2hash, | |
3958 | self.loop.options, | |
3959 | export=False) | |
3960 | text = "\n".join([x for x in tree2Text(tree)[0]]) | |
3961 | prefix, tuples = getFileTuples(loop.options['etmdir'], include=r'*.text', all=True) | |
3962 | filename = FileChoice(self, "cvs file", prefix=prefix, list=tuples, ext="text", new=False).returnValue() | |
3963 | if not filename: | |
3964 | return False | |
3965 | fo = codecs.open(filename, 'w', self.options['encoding']['file']) | |
3966 | fo.write(text) | |
3967 | fo.close() | |
3968 | MessageWindow(self, "etm", "Exported text to {0}".format(filename)) | |
3969 | ||
3970 | def exportCSV(self): | |
3971 | if self.view != CUSTOM: | |
3972 | return | |
3973 | logger.debug("spec: {0}".format(self.value_of_combo)) | |
3974 | data = getReportData( | |
3975 | self.value_of_combo, | |
3976 | self.loop.file2uuids, | |
3977 | self.loop.uuid2hash, | |
3978 | self.loop.options, | |
3979 | export=True) | |
3980 | prefix, tuples = getFileTuples(loop.options['etmdir'], include=r'*.csv', all=True) | |
3981 | filename = FileChoice(self, "cvs file", prefix=prefix, list=tuples, ext="csv", new=False).returnValue() | |
3982 | if not filename: | |
3983 | return | |
3984 | import csv as CSV | |
3985 | c = CSV.writer(open(filename, "w"), delimiter=",") | |
3986 | for line in data: | |
3987 | c.writerow(line) | |
3988 | MessageWindow(self, "etm", "Exported CSV to {0}".format(filename)) | |
3989 | ||
3990 | def updateSubscriptions(self, e=None): | |
3991 | if not self.loop.options['ics_subscriptions']: | |
3992 | MessageWindow(self, 'etm', "A configuration setting for 'ics_subscriptions' is required but missing.") | |
3993 | return | |
3994 | good = [] | |
3995 | bad = [] | |
3996 | msg = [] | |
3997 | for url, rp in self.loop.options['ics_subscriptions']: | |
3998 | fp = os.path.join(self.loop.options['datadir'], rp) | |
3999 | logger.debug('updating: {0}, {1}'.format(rp, fp)) | |
4000 | res = update_subscription(url, fp) | |
4001 | if res: | |
4002 | good.append(rp) | |
4003 | else: | |
4004 | bad.append(rp) | |
4005 | ||
4006 | if good: | |
4007 | msg.append(_("Succesfully updated:\n {0}").format("\n ".join(good))) | |
4008 | if bad: | |
4009 | msg.append(_("Not updated:\n {0}").format("\n ".join(bad))) | |
4010 | MessageWindow(self, "etm", "\n".join(msg)) | |
4011 | ||
4012 | def newselection(self, event=None): | |
4013 | self.value_of_combo = self.custom_box.get() | |
4014 | ||
4015 | loop = None | |
4016 | ||
4017 | log_levels = { | |
4018 | '1': logging.DEBUG, | |
4019 | '2': logging.INFO, | |
4020 | '3': logging.WARN, | |
4021 | '4': logging.ERROR, | |
4022 | '5': logging.CRITICAL | |
4023 | } | |
4024 | ||
4025 | ||
4026 | def main(dir=None): # debug, info, warn, error, critical | |
4027 | global loop | |
4028 | etmdir = '' | |
4029 | logger.debug('in view.main with dir: {0}'.format(dir)) | |
4030 | # For testing override etmdir: | |
4031 | if dir is not None: | |
4032 | etmdir = dir | |
4033 | logger.debug('using etmdir: {0}'.format(etmdir)) | |
4034 | init_localization() | |
4035 | (user_options, options, use_locale) = data.get_options(etmdir) | |
4036 | loop = data.ETMCmd(options=options) | |
4037 | loop.tkversion = tkversion | |
4038 | ||
4039 | app = App() | |
4040 | app.mainloop() | |
4041 | ||
4042 | ||
4043 | if __name__ == "__main__": | |
4044 | setup_logging('3') | |
4045 | main() |
0 | Metadata-Version: 1.1 | |
1 | Name: etmtk | |
2 | Version: 3.0.40 | |
3 | Summary: event and task manager | |
4 | Home-page: http://people.duke.edu/~dgraham/etmtk | |
5 | Author: Daniel A Graham | |
6 | Author-email: daniel.graham@duke.edu | |
7 | License: License :: OSI Approved :: GNU General Public License (GPL) | |
8 | Description: manage events and tasks using simple text files | |
9 | Platform: Any | |
10 | Classifier: Development Status :: 5 - Production/Stable | |
11 | Classifier: Environment :: Console | |
12 | Classifier: Environment :: MacOS X | |
13 | Classifier: Environment :: Win32 (MS Windows) | |
14 | Classifier: Environment :: X11 Applications | |
15 | Classifier: Intended Audience :: End Users/Desktop | |
16 | Classifier: License :: OSI Approved :: GNU General Public License (GPL) | |
17 | Classifier: Operating System :: MacOS :: MacOS X | |
18 | Classifier: Operating System :: Microsoft :: Windows :: Windows Vista | |
19 | Classifier: Operating System :: Microsoft :: Windows :: Windows 7 | |
20 | Classifier: Operating System :: OS Independent | |
21 | Classifier: Operating System :: POSIX | |
22 | Classifier: Operating System :: POSIX :: Linux | |
23 | Classifier: Programming Language :: Python | |
24 | Classifier: Programming Language :: Python :: 2.7 | |
25 | Classifier: Programming Language :: Python :: 3.3 | |
26 | Classifier: Programming Language :: Python :: 3.4 | |
27 | Classifier: Topic :: Office/Business | |
28 | Classifier: Topic :: Office/Business :: News/Diary | |
29 | Classifier: Topic :: Office/Business :: Scheduling |
0 | MANIFEST.in | |
1 | README.txt | |
2 | etm | |
3 | etmtk | |
4 | setup.py | |
5 | etm-tk.wiki/help/UserManual.html | |
6 | etmTk/CHANGES | |
7 | etmTk/__init__.py | |
8 | etmTk/data.py | |
9 | etmTk/dialog.py | |
10 | etmTk/edit.py | |
11 | etmTk/etm.1 | |
12 | etmTk/etm.appdata.xml | |
13 | etmTk/etm.desktop | |
14 | etmTk/etm.xpm | |
15 | etmTk/etmlogo.gif | |
16 | etmTk/etmlogo.icns | |
17 | etmTk/etmlogo.ico | |
18 | etmTk/v.py | |
19 | etmTk/version.py | |
20 | etmTk/view.py | |
21 | etmTk/help/UserManual.html | |
22 | etmtk/data.py | |
23 | etmtk/v.py | |
24 | etmtk.egg-info/PKG-INFO | |
25 | etmtk.egg-info/SOURCES.txt | |
26 | etmtk.egg-info/dependency_links.txt | |
27 | etmtk.egg-info/not-zip-safe | |
28 | etmtk.egg-info/top_level.txt | |
29 | test/test.py⏎ |
0 | etmTk |
0 | # from distutils.core import setup | |
1 | from setuptools import setup, find_packages | |
2 | from etmTk.v import version | |
3 | import glob | |
4 | ||
5 | import sys | |
6 | if sys.version_info >= (3, 2): | |
7 | REQUIRES = ["tkinter>=8.5.11", "python-dateutil>=1.5", "PyYaml>=3.10","icalendar>=3.6", "pytz"] | |
8 | else: | |
9 | REQUIRES = ["Tkinter>=8.5.11", "python>=2.7.6,<3.0", "python-dateutil>=1.5", "PyYaml>=3.10", "icalendar>=3.5", "pytz"] | |
10 | ||
11 | APP = ['etm'] | |
12 | ||
13 | # includefiles = ["etmTk/etmlogo.gif", "etmTk/etmlogo.icns", "etmTk/etmlogo.ico"] | |
14 | ||
15 | OPTIONS = {'build': {'build_exe': 'releases/etmtk-{0}'.format(version)}, | |
16 | 'build_exe': {'icon': 'etmTk/etmlogo.gif', 'optimize': '2', | |
17 | 'compressed': 1}, | |
18 | 'build_mac': {'iconfile': 'etmTk/etmlogo.gif', | |
19 | 'bundle_name': 'etm'}, | |
20 | 'Executable': {'targetDir': 'releases/etmtk-{0}'.format(version)} | |
21 | } | |
22 | ||
23 | setup( | |
24 | name='etmtk', | |
25 | version=version, | |
26 | include_package_data=True, | |
27 | zip_safe=False, | |
28 | url='http://people.duke.edu/~dgraham/etmtk', | |
29 | description='event and task manager', | |
30 | long_description='manage events and tasks using simple text files', | |
31 | platforms='Any', | |
32 | license='License :: OSI Approved :: GNU General Public License (GPL)', | |
33 | author='Daniel A Graham', | |
34 | author_email='daniel.graham@duke.edu', | |
35 | # options=OPTIONS, | |
36 | classifiers=[ | |
37 | 'Development Status :: 5 - Production/Stable', | |
38 | 'Environment :: Console', | |
39 | 'Environment :: MacOS X', | |
40 | 'Environment :: Win32 (MS Windows)', | |
41 | 'Environment :: X11 Applications', | |
42 | 'Intended Audience :: End Users/Desktop', | |
43 | 'License :: OSI Approved :: GNU General Public License (GPL)', | |
44 | 'Operating System :: MacOS :: MacOS X', | |
45 | 'Operating System :: Microsoft :: Windows :: Windows Vista', | |
46 | 'Operating System :: Microsoft :: Windows :: Windows 7', | |
47 | 'Operating System :: OS Independent', | |
48 | 'Operating System :: POSIX', | |
49 | 'Operating System :: POSIX :: Linux', | |
50 | 'Programming Language :: Python', | |
51 | 'Programming Language :: Python :: 2.7', | |
52 | 'Programming Language :: Python :: 3.3', | |
53 | 'Programming Language :: Python :: 3.4', | |
54 | 'Topic :: Office/Business', | |
55 | 'Topic :: Office/Business :: News/Diary', | |
56 | 'Topic :: Office/Business :: Scheduling'], | |
57 | packages=['etmTk'], | |
58 | scripts=['etm'], | |
59 | # install_requires=REQUIRES, | |
60 | # extras_require={"icalendar": EXTRAS}, | |
61 | # package_data={'etmTk': ['etmlogo.*', 'CHANGES', 'etmtk.desktop', 'etmtk.1', 'etmtk.xpm']}, | |
62 | package_data={'etmTk': ['etm.desktop', 'etm.appdata.xml', 'CHANGES', 'etm.1', 'etm.xpm'], | |
63 | 'etmTk/help' : ['help/UserManual.html', ]}, | |
64 | data_files=[ | |
65 | ('share/man/man1', ['etmTk/etm.1']), | |
66 | ('share/doc/etm', ['etmTk/CHANGES']), | |
67 | ('share/pixmaps', ['etmTk/etm.xpm']), | |
68 | ('share/applications', ['etmTk/etm.desktop']), | |
69 | ('share/appdata', ['etmTk/etm.appdata.xml']), | |
70 | ] | |
71 | ) |
0 | #Copyright (c) 2009, Julian Aloofi | |
1 | #All rights reserved. | |
2 | ||
3 | #Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | |
4 | ||
5 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | |
6 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | |
7 | ||
8 | #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
9 | ||
10 | import Tkinter | |
11 | from PIL import Image, ImageTk | |
12 | import tkSnack | |
13 | ||
14 | ##initializing some stuff | |
15 | #new Tk window | |
16 | root = Tkinter.Tk() | |
17 | #initialize sound | |
18 | tkSnack.initializeSnack(root) | |
19 | #create and open sound file | |
20 | crapalert = tkSnack.Sound(load='../Resources/crapalert.wav') | |
21 | #open the image, Tkinter just can read gifs, so we need PIL | |
22 | crapimg = Image.open('../Resources/crapalert.png') | |
23 | crapbuttimg = ImageTk.PhotoImage(crapimg) | |
24 | #creating the window | |
25 | mainframe = Tkinter.Frame(root) | |
26 | mainframe.pack(side=Tkinter.TOP, fill = Tkinter.X) | |
27 | ||
28 | #let's create a button handler | |
29 | def crapAlert(): | |
30 | """ | |
31 | Results in a crap-alert sound and a notification | |
32 | """ | |
33 | #play the sound | |
34 | crapalert.play() | |
35 | ||
36 | #create the button and connect to the handler | |
37 | crapbutton = Tkinter.Button(mainframe, compound = Tkinter.TOP, width=400, height=400, image=crapbuttimg, command=crapAlert) | |
38 | crapbutton.pack(side=Tkinter.LEFT, padx=2, pady=2) | |
39 | ||
40 | #let's go | |
41 | root.mainloop() |