Codebase list etm / 5a0d139
Imported Upstream version 3.0.40 SVN-Git Migration 6 years ago
29 changed file(s) with 16877 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
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.
+44
-0
etm less more
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 &amp;i 14 @o r</code></pre></li>
187 <li><p>Payday (an occasion) on the last week day of each month. The <code>&amp;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 &amp;w MO, TU, WE, TH, FR
189 &amp;m -1, -2, -3 &amp;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 &amp;h 10, 14, 18, 22 &amp;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 &amp;w SU &amp;h 14, 15, 16, 17 &amp;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 &amp;w SU &amp;h 14, 15, 16, 17 &amp;n 0, 30 &amp;M 4, 5, 6, 7, 8, 9</code></pre>
196 <p>or this:</p>
197 <pre><code>@r n &amp;i 30 &amp;w SU &amp;h 14, 15, 16, 17 &amp;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 &amp;i 4 &amp;M 11 &amp;m 2, 3, 4, 5, 6, 7, 8 &amp;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 = &#39;&#39; or,
246 if ARGS = X where X is one of the above commands, then display
247 details about command X. &#39;X ?&#39; is equivalent to &#39;? X&#39;.</code></pre>
248 <p>For example, you can print your agenda to the terminal window by adding the letter &quot;a&quot;:</p>
249 <pre><code>$ etm a
250 Sun Apr 06, 2014
251 &gt; 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 &gt; 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 &lt;type&gt; &lt;groupby&gt; [options]
284
285 Generate a custom view where type is either &#39;a&#39; (action) or &#39;c&#39; (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 &#39;c ddd, MMM dd yyyy -b 1 -e +1/1&#39;</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 &quot;N&quot; command. For example,</p>
327 <pre><code> etm N &#39;123 456-7890&#39;</code></pre>
328 <p>would create an entry in your inbox with this phone number. (With no type character an &quot;$&quot; 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 &quot;jump&quot; 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 &quot;adu&quot;.</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>&amp;u</code> (until) or <code>&amp;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 &quot;jones&quot; 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 &quot;commented out&quot; 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> [&#39;https://www.google.com/calendar/ical/.../basic.ics&#39;, &#39;personal/google.txt&#39;]
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 &quot;escaped&quot; 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 &amp;M 7 &amp;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 &amp;q 1
612 @j cut pieces &amp;q 2
613 @j assemble &amp;q 3
614 @j paint &amp;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>&amp;q</code>, set the order --- tasks with smaller &amp;q values are prerequisites for subsequent tasks with larger &amp;q values. In the example above, neither &quot;pickup lumber&quot; nor &quot;pickup paint&quot; have any prerequisites. &quot;Pickup lumber&quot;, however, is a prerequisite for &quot;cut pieces&quot; which, in turn, is a prerequisite for &quot;assemble&quot;. Both &quot;assemble&quot; and &quot;pickup paint&quot; are prerequisites for &quot;paint&quot;.</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 &quot;project_a&quot; for &quot;client_1&quot;. 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&amp;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 &lt;trigger times&gt; [: 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 &quot;next view&quot; supports this usage by showing undated tasks, grouped by context. If you're about to run errands, for example, you can open the &quot;next view&quot;, look under &quot;errands&quot; and be sure that you will have no &quot;wish I had remembered&quot; 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>&amp;f done</code> or <code>&amp;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>&amp;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 &amp;w MO,TU,WE,TH,FR &amp;m 23,24,25 &amp;s -1
692 @j organize bills &amp;q 1
693 @j pay on-line bills &amp;q 3
694 @j get stamps, envelopes, checkbook &amp;q 1
695 @j write checks &amp;q 2
696 @j mail checks &amp;q 3</code></pre>
697 <p>Here &quot;organize bills&quot; and &quot;get stamps, envelopes, checkbook&quot; have no prerequisites. &quot;Organize bills&quot;, however, is a prerequisite for &quot;pay on-line bills&quot; and both &quot;organize bills&quot; and &quot;get stamps, envelops, checkbook&quot; are prerequisites for &quot;write checks&quot; which, in turn, is a prerequisite for &quot;mail checks&quot;.</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>&amp;key value</code> entries given in jobs use <code>&amp;</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>&amp;</code>:</p>
709 <pre><code>@c or &amp;c context
710 @d or &amp;d description
711 @e or &amp;e extent
712 @f or &amp;f done[; due] datetime
713 @k or &amp;k keyword
714 @l or &amp;l location
715 @u or &amp;u user</code></pre>
716 <p>The key-value pair <code>&amp;h</code> is used internally to track job done;due completions in task groups.</p>
717 <p>The key-value pair <code>&amp;q</code> (queue position) can <em>only</em> be given in component jobs where it is required. Key-values other than <code>&amp;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>&amp;key value</code> pairs:</p>
755 <pre><code>&amp;i: interval (positive integer, default = 1) E.g, with frequency w, interval 3 would repeat every three weeks.
756 &amp;t: total (positive integer) Include no more than this total number of repetitions.
757 &amp;s: bysetpos (integer). When multiple dates satisfy the rule, take the date from this position in the list, e.g, &amp;s 1 would choose the first element and &amp;s -1 the last. See the payday example below for an illustration of bysetpos.
758 &amp;u: until (datetime) Only include repetitions falling **before** (not including) this datetime.
759 &amp;M: bymonth (1, 2, ..., 12)
760 &amp;m: bymonthday (1, 2, ..., 31) Use, e.g., -1 for the last day of the month.
761 &amp;W: byweekno (1, 2, ..., 53)
762 &amp;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 &amp;h: byhour (0 ... 23)
764 &amp;n: byminute (0 ... 59)
765 &amp;E: byeaster (integer number of days before, &lt; 0, or after, &gt; 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 &amp;w 1WE, 3WE</code></pre></li>
771 <li><p>Payday (an occasion) on the last week day of each month. (The <code>&amp;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 &amp;w MO, TU, WE, TH, FR &amp;m -1, -2, -3 &amp;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 &amp;u 11p 27 &amp;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>&amp;m range(2,9)</code> requires the month day to fall within 2 ... 8 and thus, combined with <code>&amp;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 &amp;i 4 &amp;M 11 &amp;m range(2,9) &amp;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 &amp;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 &amp;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 &lt;k|s|r&gt;</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>&amp;E</code>, e.g.,</p>
867 <pre><code>^ Easter Sunday @s 2010-01-01 @r y &amp;E 0
868 ^ Ash Wednesday @s 2010-01-01 @r y &amp;E -46
869 ^ Rose Monday @s 2010-01-01 @r y &amp;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 : &#39;2 days 3 hours 15 minutes&#39;
905 when : &#39;2 days 3 hours 15 minutes from now&#39;</code></pre></li>
906 <li><p><code>20m</code></p>
907 <pre><code>time_left : &#39;20 minutes&#39;
908 when : &#39;20 minutes from now&#39;</code></pre></li>
909 <li><p><code>0m</code></p>
910 <pre><code>time_left : &#39;&#39;
911 when : &#39;now&#39;</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: &#39;!hours!h) !label! (!count!)&#39;</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: &#39;play ~/.etm/sounds/timer_paused.wav&#39;
979 running: &#39;play ~/.etm/sounds/timer_running.wav&#39;
980 idle: &#39;play ~/.etm/sounds/timer_idle.wav&#39;</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 &#39;!time_span!&#39;</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: &#39;play ~/.etm/sounds/etm_alert.wav&#39;</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: &#39;!time_span!\n!l!\n\n!d!&#39;</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 &#39;Alex&#39; &#39;!summary! begins !when!.&#39;</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 &quot;wake up the display&quot; 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 &quot;week view&quot; 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, &quot;<span class="citation">@c</span>&quot; 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 - &#39;home: 123 456-7890&#39;
1051 - &#39;birthday: 1978-12-14&#39;
1052 dcharles:
1053 - Charles, Debbie
1054 - dch@sometime.com
1055 - &#39;cell: 456 789-0123&#39;
1056 - &#39;spouse: Rebecca&#39;</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 &quot;u&quot; 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: &#39;&#39;
1066 current_textfile: &#39;&#39;
1067 current_icsfolder: &#39;&#39;
1068 current_indent: 3
1069 current_opts: &#39;&#39;
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 &lt;current_textfile&gt;</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: &#39;Time: !time_span!
1092 Locaton: !l!
1093
1094
1095 !d!&#39;</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 &lt;contents of @d&gt;</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: &#39;play ~/.etm/sounds/etm_alert.wav&#39;</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: &#39;&#39;</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 - [&#39;https://www.google.com/calendar/ical/.../basic.ics&#39;, &#39;personal/dag/google.txt&#39;]
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: &quot;\n &quot;
1183 prefix_uses: &quot;rj+-tldm&quot;</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 &amp;f 20140510T1411;20140509T0000 &amp;q 1
1188 @j job 2 &amp;f 20140510T1412;20140509T0000 &amp;q 2
1189 @j job 3 &amp;q 3
1190 @d description</code></pre>
1191 <h3 id="report"><a href="#report">report</a></h3>
1192 <pre><code>report_begin: &#39;1&#39;
1193 report_end: &#39;+1/1&#39;
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: &#39;!summary!&#39;
1218 sms_subject: &#39;!time_span!&#39;
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&amp;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: &#39;&#39;
1241 commit: &#39;&#39;
1242 dir: &#39;&#39;
1243 file: &#39;&#39;
1244 history: &#39;&#39;
1245 init: &#39;&#39;
1246 limit: &#39;&#39;</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: &#39;/usr/bin/git --git-dir {repo} --work-dir {work}&#39;
1252 commit: &#39;/usr/bin/git --git-dir {repo} --work-dir {work} add */\*.txt
1253 &amp;&amp; /usr/bin/git --git-dir {repo} --work-dir {work} commit -a -m &quot;{mesg}&quot;&#39;
1254 dir: &#39;.git&#39;
1255 file: &#39;&#39;
1256 history: &#39;/usr/bin/git -git-dir {repo} --work-dir {work} log
1257 --pretty=format:&quot;- %ar: %an%n%w(70,0,4)%s&quot; -U1 {numchanges}
1258 {file}&#39;
1259 init: &#39;/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 &quot;{mesg}&quot;&#39;
1262 limit: &#39;-n&#39;</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: &#39;&#39;</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>&amp;u</code> (until) or <code>&amp;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 &quot;a&quot; or &quot;c&quot;, followed by a <em>groupby setting</em> and, perhaps, by one or more <em>report options</em>:</p>
1280 <pre><code>&lt;a|c&gt; &lt;groupby setting&gt; [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.
(New empty file)
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 &amp;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 &amp;i 4 &amp;M 11 &amp;m 2, 3, 4, 5, 6, 7, 8 &amp;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 &amp;i 14 @o r</code></pre></li>
187 <li><p>Payday (an occasion) on the last week day of each month. The <code>&amp;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 &amp;w MO, TU, WE, TH, FR
189 &amp;m -1, -2, -3 &amp;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 &amp;h 10, 14, 18, 22 &amp;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 &amp;w SU &amp;h 14, 15, 16, 17 &amp;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 &amp;w SU &amp;h 14, 15, 16, 17 &amp;n 0, 30 &amp;M 4, 5, 6, 7, 8, 9</code></pre>
196 <p>or this:</p>
197 <pre><code>@r n &amp;i 30 &amp;w SU &amp;h 14, 15, 16, 17 &amp;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 &amp;i 4 &amp;M 11 &amp;m 2, 3, 4, 5, 6, 7, 8 &amp;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 = &#39;&#39; or,
246 if ARGS = X where X is one of the above commands, then display
247 details about command X. &#39;X ?&#39; is equivalent to &#39;? X&#39;.</code></pre>
248 <p>For example, you can print your agenda to the terminal window by adding the letter &quot;a&quot;:</p>
249 <pre><code>$ etm a
250 Sun Apr 06, 2014
251 &gt; 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 &gt; 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 &lt;type&gt; &lt;groupby&gt; [options]
284
285 Generate a custom view where type is either &#39;a&#39; (action) or &#39;c&#39; (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 &#39;c ddd, MMM dd yyyy -b 1 -e +1/1&#39;</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 &quot;N&quot; command. For example,</p>
327 <pre><code> etm N &#39;123 456-7890&#39;</code></pre>
328 <p>would create an entry in your inbox with this phone number. (With no type character an &quot;$&quot; 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 &quot;jump&quot; 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 &quot;adu&quot;.</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>&amp;u</code> (until) or <code>&amp;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 &quot;jones&quot; 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 &quot;commented out&quot; 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> [&#39;https://www.google.com/calendar/ical/.../basic.ics&#39;, &#39;personal/google.txt&#39;]
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 &quot;escaped&quot; 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 &amp;M 7 &amp;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 &amp;q 1
612 @j cut pieces &amp;q 2
613 @j assemble &amp;q 3
614 @j paint &amp;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>&amp;q</code>, set the order --- tasks with smaller &amp;q values are prerequisites for subsequent tasks with larger &amp;q values. In the example above, neither &quot;pickup lumber&quot; nor &quot;pickup paint&quot; have any prerequisites. &quot;Pickup lumber&quot;, however, is a prerequisite for &quot;cut pieces&quot; which, in turn, is a prerequisite for &quot;assemble&quot;. Both &quot;assemble&quot; and &quot;pickup paint&quot; are prerequisites for &quot;paint&quot;.</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 &quot;project_a&quot; for &quot;client_1&quot;. 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&amp;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 &lt;trigger times&gt; [: 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 &quot;next view&quot; supports this usage by showing undated tasks, grouped by context. If you're about to run errands, for example, you can open the &quot;next view&quot;, look under &quot;errands&quot; and be sure that you will have no &quot;wish I had remembered&quot; 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>&amp;f done</code> or <code>&amp;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>&amp;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 &amp;w MO,TU,WE,TH,FR &amp;m 23,24,25 &amp;s -1
692 @j organize bills &amp;q 1
693 @j pay on-line bills &amp;q 3
694 @j get stamps, envelopes, checkbook &amp;q 1
695 @j write checks &amp;q 2
696 @j mail checks &amp;q 3</code></pre>
697 <p>Here &quot;organize bills&quot; and &quot;get stamps, envelopes, checkbook&quot; have no prerequisites. &quot;Organize bills&quot;, however, is a prerequisite for &quot;pay on-line bills&quot; and both &quot;organize bills&quot; and &quot;get stamps, envelops, checkbook&quot; are prerequisites for &quot;write checks&quot; which, in turn, is a prerequisite for &quot;mail checks&quot;.</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>&amp;key value</code> entries given in jobs use <code>&amp;</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>&amp;</code>:</p>
709 <pre><code>@c or &amp;c context
710 @d or &amp;d description
711 @e or &amp;e extent
712 @f or &amp;f done[; due] datetime
713 @k or &amp;k keyword
714 @l or &amp;l location
715 @u or &amp;u user</code></pre>
716 <p>The key-value pair <code>&amp;h</code> is used internally to track job done;due completions in task groups.</p>
717 <p>The key-value pair <code>&amp;q</code> (queue position) can <em>only</em> be given in component jobs where it is required. Key-values other than <code>&amp;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>&amp;key value</code> pairs:</p>
755 <pre><code>&amp;i: interval (positive integer, default = 1) E.g, with frequency w, interval 3 would repeat every three weeks.
756 &amp;t: total (positive integer) Include no more than this total number of repetitions.
757 &amp;s: bysetpos (integer). When multiple dates satisfy the rule, take the date from this position in the list, e.g, &amp;s 1 would choose the first element and &amp;s -1 the last. See the payday example below for an illustration of bysetpos.
758 &amp;u: until (datetime) Only include repetitions falling **before** (not including) this datetime.
759 &amp;M: bymonth (1, 2, ..., 12)
760 &amp;m: bymonthday (1, 2, ..., 31) Use, e.g., -1 for the last day of the month.
761 &amp;W: byweekno (1, 2, ..., 53)
762 &amp;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 &amp;h: byhour (0 ... 23)
764 &amp;n: byminute (0 ... 59)
765 &amp;E: byeaster (integer number of days before, &lt; 0, or after, &gt; 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 &amp;w 1WE, 3WE</code></pre></li>
771 <li><p>Payday (an occasion) on the last week day of each month. (The <code>&amp;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 &amp;w MO, TU, WE, TH, FR &amp;m -1, -2, -3 &amp;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 &amp;u 11p 27 &amp;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>&amp;m range(2,9)</code> requires the month day to fall within 2 ... 8 and thus, combined with <code>&amp;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 &amp;i 4 &amp;M 11 &amp;m range(2,9) &amp;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 &amp;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 &amp;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 &lt;k|s|r&gt;</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>&amp;E</code>, e.g.,</p>
867 <pre><code>^ Easter Sunday @s 2010-01-01 @r y &amp;E 0
868 ^ Ash Wednesday @s 2010-01-01 @r y &amp;E -46
869 ^ Rose Monday @s 2010-01-01 @r y &amp;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 : &#39;2 days 3 hours 15 minutes&#39;
905 when : &#39;2 days 3 hours 15 minutes from now&#39;</code></pre></li>
906 <li><p><code>20m</code></p>
907 <pre><code>time_left : &#39;20 minutes&#39;
908 when : &#39;20 minutes from now&#39;</code></pre></li>
909 <li><p><code>0m</code></p>
910 <pre><code>time_left : &#39;&#39;
911 when : &#39;now&#39;</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: &#39;!hours!h) !label! (!count!)&#39;</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: &#39;play ~/.etm/sounds/timer_paused.wav&#39;
979 running: &#39;play ~/.etm/sounds/timer_running.wav&#39;
980 idle: &#39;play ~/.etm/sounds/timer_idle.wav&#39;</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 &#39;!time_span!&#39;</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: &#39;play ~/.etm/sounds/etm_alert.wav&#39;</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: &#39;!time_span!\n!l!\n\n!d!&#39;</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 &#39;Alex&#39; &#39;!summary! begins !when!.&#39;</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 &quot;wake up the display&quot; 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 &quot;week view&quot; 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, &quot;<span class="citation">@c</span>&quot; 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 - &#39;home: 123 456-7890&#39;
1051 - &#39;birthday: 1978-12-14&#39;
1052 dcharles:
1053 - Charles, Debbie
1054 - dch@sometime.com
1055 - &#39;cell: 456 789-0123&#39;
1056 - &#39;spouse: Rebecca&#39;</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 &quot;u&quot; 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: &#39;&#39;
1066 current_textfile: &#39;&#39;
1067 current_icsfolder: &#39;&#39;
1068 current_indent: 3
1069 current_opts: &#39;&#39;
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 &lt;current_textfile&gt;</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: &#39;Time: !time_span!
1092 Locaton: !l!
1093
1094
1095 !d!&#39;</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 &lt;contents of @d&gt;</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: &#39;play ~/.etm/sounds/etm_alert.wav&#39;</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: &#39;&#39;</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 - [&#39;https://www.google.com/calendar/ical/.../basic.ics&#39;, &#39;personal/dag/google.txt&#39;]
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: &quot;\n &quot;
1183 prefix_uses: &quot;rj+-tldm&quot;</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 &amp;f 20140510T1411;20140509T0000 &amp;q 1
1188 @j job 2 &amp;f 20140510T1412;20140509T0000 &amp;q 2
1189 @j job 3 &amp;q 3
1190 @d description</code></pre>
1191 <h3 id="report"><a href="#report">report</a></h3>
1192 <pre><code>report_begin: &#39;1&#39;
1193 report_end: &#39;+1/1&#39;
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: &#39;!summary!&#39;
1218 sms_subject: &#39;!time_span!&#39;
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&amp;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: &#39;&#39;
1241 commit: &#39;&#39;
1242 dir: &#39;&#39;
1243 file: &#39;&#39;
1244 history: &#39;&#39;
1245 init: &#39;&#39;
1246 limit: &#39;&#39;</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: &#39;/usr/bin/git --git-dir {repo} --work-dir {work}&#39;
1252 commit: &#39;/usr/bin/git --git-dir {repo} --work-dir {work} add */\*.txt
1253 &amp;&amp; /usr/bin/git --git-dir {repo} --work-dir {work} commit -a -m &quot;{mesg}&quot;&#39;
1254 dir: &#39;.git&#39;
1255 file: &#39;&#39;
1256 history: &#39;/usr/bin/git -git-dir {repo} --work-dir {work} log
1257 --pretty=format:&quot;- %ar: %an%n%w(70,0,4)%s&quot; -U1 {numchanges}
1258 {file}&#39;
1259 init: &#39;/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 &quot;{mesg}&quot;&#39;
1262 limit: &#39;-n&#39;</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: &#39;&#39;</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>&amp;u</code> (until) or <code>&amp;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 &quot;a&quot; or &quot;c&quot;, followed by a <em>groupby setting</em> and, perhaps, by one or more <em>report options</em>:</p>
1280 <pre><code>&lt;a|c&gt; &lt;groupby setting&gt; [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 [egg_info]
1 tag_date = 0
2 tag_svn_revision = 0
3 tag_build =
4
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()