Codebase list etm / upstream/3.1.18
Imported Upstream version 3.1.18 SVN-Git Migration 6 years ago
29 changed file(s) with 2990 addition(s) and 3747 deletion(s). Raw diff Collapse all Expand all
33 include etmTk/etm.desktop
44 include etmTk/etm.appdata.xml
55 include etmTk/help/UserManual.html
6 include etmTk/icons/*.gif
00 Metadata-Version: 1.1
11 Name: etmtk
2 Version: 3.0.43
2 Version: 3.1.18
33 Summary: event and task manager
44 Home-page: http://people.duke.edu/~dgraham/etmtk
55 Author: Daniel A Graham
+0
-1498
etm-tk.wiki/help/UserManual.html less more
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 Sun Jan 18 12:05:58 EST 2015:
1 - 2015-01-18 12:06:03 -0500 (HEAD, tag: 3.0.43, master): Dan Graham
2 tagged version 3.0.43
3 - 2015-01-18 12:01:34 -0500 (origin/master): Dan Graham
4 Fixed bug in delete this and all subsequent repetitions
0 # Changes in the 4 weeks preceding Sun Jun 21 11:27:42 EDT 2015:
1 - 2015-06-21 11:27:44 -0400 (HEAD, tag: 3.1.18, master): Dan Graham
2 tagged version 3.1.18
3 - 2015-06-21 10:07:44 -0400 (origin/master): Dan Graham
4 Added early_hours to options and a warning to the save confirmation
5 for events or actions with starting times before this 24-hour time.
6 - 2015-06-20 11:48:00 -0400 (tag: 3.1.17): Dan Graham
7 tagged version 3.1.17
8 - 2015-06-20 11:42:10 -0400: Dan Graham
9 Restored Radiobutton.
10 - 2015-06-20 08:24:20 -0400 (tag: 3.1.16): Dan Graham
11 tagged version 3.1.16
12 - 2015-06-20 08:24:06 -0400: Dan Graham
13 Added ttk import to dialog. Removed unused Radiobutton.
14 - 2015-06-20 07:28:37 -0400: Dan Graham
15 Added unicode = str to the >= 3 part of dialog, Removed unused
16 utf8.
17 - 2015-06-19 10:48:50 -0400 (tag: 3.1.15): Dan Graham
18 tagged version 3.1.15
19 - 2015-06-19 08:05:05 -0400: Dan Graham
20 Fixed bug in cli under python2 comparing unicode and non-unicode
21 strings.
22 - 2015-06-17 13:52:58 -0400 (tag: 3.1.14): Dan Graham
23 tagged version 3.1.14
24 - 2015-06-17 13:51:37 -0400: Dan Graham
25 Fixed bug in which scrollToDate would move the selection in month
26 view to the current weekday number instead of the current month day
27 number.
28 - 2015-06-16 15:07:21 -0400 (tag: 3.1.13, braeburn/master): Dan Graham
29 tagged version 3.1.13
30 - 2015-06-16 13:51:47 -0400: Dan Graham
31 Split timer status into title and status and pack the status part
32 first to avoid it disappearing with resize. Allow @e 0m in actions.
33 In kloneTimer use summary as the starting value in selectTimer and
34 allow editing.
35 - 2015-06-15 13:31:14 -0400 (tag: 3.1.12): Dan Graham
36 tagged version 3.1.12
37 - 2015-06-15 13:29:04 -0400: Dan Graham
38 Added klone as timer (kloneTimer) command to item menu. Confirm
39 quit even without running timer. Pause timer when quitting to avoid
40 accumulating down time in running timer.
41 - 2015-06-14 18:22:22 -0400 (tag: 3.1.11): Dan Graham
42 tagged version 3.1.11
43 - 2015-06-14 18:21:48 -0400: Dan Graham
44 removed edit.py
45 - 2015-06-14 15:03:45 -0400 (tag: 3.1.10): Dan Graham
46 tagged version 3.1.10
47 - 2015-06-14 15:01:54 -0400: Dan Graham
48 removed erroneous call to getStatus.
49 - 2015-06-14 14:17:41 -0400 (timers): Dan Graham
50 Removed old timer class.
51 - 2015-06-14 13:59:19 -0400 (origin/timers): Dan Graham
52 Move timer items to a new menu under File. Update menu states
53 based on active timers. Bail on selection if new is false and
54 there are no active timers. Make selection window title reflect
55 purpose of selection, e.g., delete, finish, ... Set minsize for
56 selection window. Update status line on Save and Load timers.
57 - 2015-06-14 12:22:02 -0400: Dan Graham
58 Call newDay on load if active date has changed. Save old timers to
59 inbox. Restart a running timer with 0 minutes for the new day.
60 Cleaned up print statements.
61 - 2015-06-14 09:39:00 -0400: Dan Graham
62 Save timer state on quit. Reload on start. On new day, save timers
63 as inbox items and, if there is a currently running timer, also
64 start a new timer with the same summary for the new day. Use the
65 wm_delete_window protocol to call quit in view.py. Warn if a timer
66 is running and if affirmed, pause timer and save timers state.
67 - 2015-06-13 21:41:43 -0400: Dan Graham
68 Fixed import statements. Added wait_window to selectTimers.
69 - 2015-06-13 16:29:10 -0400 (braeburn/timers): Dan Graham
70 Timer completion window works. Moved contents of edit.py to
71 dialog.py.
72 - 2015-06-13 10:31:42 -0400: Dan Graham
73 Added class Timers - first step to multiple timers.
74 - 2015-06-12 17:07:09 -0400: Dan Graham
75 Added self.canvas_date to use date selected in week and month view
76 for new items.
77 - 2015-06-11 12:38:17 -0400 (tag: 3.1.9): Dan Graham
78 tagged version 3.1.9
79 - 2015-06-11 12:37:54 -0400: Dan Graham
80 Removed ETMLOGO and the use of self.iconphoto.
81 - 2015-06-11 10:06:21 -0400 (tag: 3.1.8): Dan Graham
82 tagged version 3.1.8
83 - 2015-06-11 10:05:53 -0400: Dan Graham
84 Moved conflict identification from data.py to view.py where it
85 respects active calendar setting.
86 - 2015-06-11 09:03:26 -0400 (tag: 3.1.7): Dan Graham
87 tagged version 3.1.7
88 - 2015-06-11 09:02:48 -0400: Dan Graham
89 Added updateDay to reschedule and schedule new.
90 - 2015-06-09 08:36:36 -0400: Dan Graham
91 Added ICONLOGO - doesn't work on OSX. Improved debug logging for
92 showWeek parameters.
93 - 2015-06-08 14:30:39 -0400 (tag: 3.1.6): Dan Graham
94 tagged version 3.1.6
95 - 2015-06-08 14:03:14 -0400: Dan Graham
96 Fixed bug in CLI ignoring -w and -W options.
97 - 2015-06-08 08:32:53 -0400: Dan Graham
98 Allow toggle labels and print tree in week and month view.
99 - 2015-06-07 21:48:27 -0400 (tag: 3.1.5): Dan Graham
100 tagged version 3.1.5
101 - 2015-06-07 21:45:43 -0400: Dan Graham
102 When reporting item hsh errors, add file name and line number
103 information.
104 - 2015-06-07 10:59:55 -0400 (tag: 3.1.4): Dan Graham
105 tagged version 3.1.4
106 - 2015-06-07 10:59:34 -0400: Dan Graham
107 Set focus on mouse click in week or month view canvas.
108 - 2015-06-07 10:40:17 -0400: Dan Graham
109 Added new screenshots
110 - 2015-06-07 09:52:30 -0400 (tag: 3.1.3): Dan Graham
111 tagged version 3.1.3
112 - 2015-06-07 09:52:07 -0400: Dan Graham
113 In custom view, replace the filter with the combo box.
114 - 2015-06-07 08:09:23 -0400 (tag: 3.1.2): Dan Graham
115 tagged version 3.1.2
116 - 2015-06-07 08:09:05 -0400 (tag: 3.1.1): Dan Graham
117 tagged version 3.1.1
118 - 2015-06-07 08:08:04 -0400 (tag: 3.1.0-5): Dan Graham
119 Add icons to the etmTk directory itself for python2.
120 - 2015-06-07 07:42:35 -0400: Dan Graham
121 tagged version 3.1.2
122 - 2015-06-07 07:42:06 -0400: Dan Graham
123 Fixed bug in setting path to icons.
124 - 2015-06-07 00:01:57 -0400 (tag: 3.1.0-2): Dan Graham
125 tagged version 3.1.1
126 - 2015-06-07 00:01:34 -0400: Dan Graham
127 Corrected icon path.
128 - 2015-06-06 23:42:57 -0400 (tag: 3.1.0): Dan Graham
129 tagged version 3.1.0
130 - 2015-06-06 23:33:31 -0400 (tag: 3.0.74-7): Dan Graham
131 tagged version 3.1.1
132 - 2015-06-06 23:32:56 -0400: Dan Graham
133 Added icons to MANIFEST.in
134 - 2015-06-06 18:33:47 -0400 (tag: 3.1.1-4): Dan Graham
135 tagged version 3.1.0
136 - 2015-06-06 18:30:54 -0400: Dan Graham
137 Merged dayweek
138 - 2015-06-06 18:22:20 -0400 (tag: 3.0.62): Dan Graham
139 tagged version 3.0.62
140 - 2015-06-06 18:17:00 -0400 (dayweek): Dan Graham
141 tagged version 3.1.0
142 - 2015-06-06 18:15:51 -0400: Dan Graham
143 tagged version 3.1.1
144 - 2015-06-06 16:21:31 -0400 (tag: 3.0.74, origin/dayweek, braeburn/dayweek): Dan Graham
145 tagged version 3.0.74
146 - 2015-06-06 16:20:49 -0400: Dan Graham
147 Added space bar binding for goHome to canvas.
148 - 2015-06-06 12:43:24 -0400 (tag: 3.0.73): Dan Graham
149 tagged version 3.0.73
150 - 2015-06-06 12:39:28 -0400: Dan Graham
151 Fixed bug in setting canvas height in week and month views. Fixed
152 bug in creating new items with empty data directory.
153 - 2015-06-05 13:06:34 -0400 (tag: 3.0.72): Dan Graham
154 tagged version 3.0.72
155 - 2015-06-05 13:06:03 -0400: Dan Graham
156 Removed day from menu and key bindings.
157 - 2015-06-05 12:05:44 -0400 (tag: 3.0.71): Dan Graham
158 tagged version 3.0.71
159 - 2015-06-05 12:04:02 -0400: Dan Graham
160 Don't try to set new item time from week view.
161 - 2015-06-05 11:41:28 -0400: Dan Graham
162 Added updateDay to refresh the day list in week and month views.
163 - 2015-06-03 20:39:05 -0400 (tag: 3.0.70): Dan Graham
164 tagged version 3.0.70
165 - 2015-06-03 20:37:55 -0400: Dan Graham
166 Make right/left cursor keys at end/beginning of week or month move
167 to next/prior week or month.
168 - 2015-06-03 20:24:21 -0400 (tag: 3.0.69): Dan Graham
169 tagged version 3.0.69
170 - 2015-06-03 20:23:49 -0400: Dan Graham
171 Removed border from week and month views and extended verticals -
172 simpler and nicer.
173 - 2015-06-03 18:37:46 -0400 (tag: 3.0.68): Dan Graham
174 tagged version 3.0.68
175 - 2015-06-03 18:35:43 -0400: Dan Graham
176 Refresh day list on item/file change.
177 - 2015-06-03 14:39:06 -0400 (tag: 3.0.67): Dan Graham
178 tagged version 3.0.67
179 - 2015-06-03 14:24:46 -0400 (tag: 3.0.66): Dan Graham
180 tagged version 3.0.66
181 - 2015-06-03 14:00:56 -0400 (tag: 3.0.65): Dan Graham
182 tagged version 3.0.65
183 - 2015-06-03 13:58:51 -0400 (tag: 3.0.64): Dan Graham
184 tagged version 3.0.64
185 - 2015-06-03 13:40:33 -0400 (tag: 3.0.63): Dan Graham
186 tagged version 3.0.63
187 - 2015-06-03 13:16:34 -0400: Dan Graham
188 tagged version 3.0.62
189 - 2015-06-03 12:41:30 -0400: Dan Graham
190 Tweaked idpos in week and month views.
191 - 2015-06-03 09:02:56 -0400: Dan Graham
192 Tweaked scrolling in week view.
193 - 2015-06-02 20:56:59 -0400: Dan Graham
194 Replaced "x" and "y" (tuples) with "x_" and "y_" in showWeek.
195 - 2015-06-02 19:08:25 -0400: Dan Graham
196 Much menu refactoring and comment removal.
197 - 2015-06-01 16:17:02 -0400: Dan Graham
198 Removed refresh button (toggle active) and search button.
199 - 2015-06-01 15:56:35 -0400: Dan Graham
200 Removed configureCanvas and binding to <Configure>. Added binding
201 to newItem for newbutton. Removed call to configure calbutton from
202 updateCalendars.
203 - 2015-06-01 11:06:41 -0400: Dan Graham
204 Selected item in week view now shows details.
205 - 2015-06-01 10:57:15 -0400: Dan Graham
206 Mouse click selection now works in week view.
207 - 2015-06-01 06:59:18 -0400: Dan Graham
208 Only nag about the oldest pastdue instance of repeating tasks with
209 overdue restart. Prevent buttons from taking focus so tab switches
210 back and forth from canvas to tree in week and month views.
211 - 2015-05-31 17:58:39 -0400: Dan Graham
212 In week and month view, up/down cursor keys now move through dates
213 and select them in the day list.
214 - 2015-05-31 14:08:08 -0400: Dan Graham
215 added icons
216 - 2015-05-31 14:05:59 -0400: Dan Graham
217 Remove leading zeros from month days (day, week, month views). Week
218 and month views now sync with their day lists. Jump to date selects
219 day in both week and month but cursor keys select week and month
220 begin.
221 - 2015-05-30 22:53:43 -0400: Dan Graham
222 Without conficts, draw the conflict square in the background color
223 (white, OCCASIONFILL or CURRENTFILL).
224 - 2015-05-30 22:30:22 -0400: Dan Graham
225 Only draw the conflict square in week view if flagcolor is not
226 None.
227 - 2015-05-30 19:55:16 -0400: Dan Graham
228 Week view now displays occasions and today. Adjusted day in week
229 view to be monday of the selected week. Cleaned up comments and
230 print statements.
231 - 2015-05-30 16:40:14 -0400: Dan Graham
232 Refactored paned window setup and sizing. Removed leading zeros in
233 week view.
234 - 2015-05-30 14:23:53 -0400: Dan Graham
235 Added 2nd panedwindow for a total of 3 windows. The top holds the
236 week and month canvases. The middle holds the tree and the bottom
237 holds the details. Display ok but no commands yet.
238 - 2015-05-28 21:33:17 -0400: Dan Graham
239 Tweaked week and month layouts.
240 - 2015-05-28 18:11:20 -0400 (tag: 3.0.61-13): Dan Graham
241 added icon_refresh.gif
242 - 2015-05-28 18:10:55 -0400: Dan Graham
243 Updated gettext usage. Import _() from data in dialog, edit and
244 view.
245 - 2015-05-28 14:34:52 -0400: Dan Graham
246 Put current time in the title bar. In week and month views, move
247 the week (month) information up to currentView.
248 - 2015-05-27 21:41:12 -0400: Dan Graham
249 Added toggleActiveView bound to check button. Removed filter
250 entry.
251 - 2015-05-27 12:46:32 -0400: Dan Graham
252 Added red and green check icons
253 - 2015-05-27 12:45:12 -0400: Dan Graham
254 Red and Green check icons
255 - 2015-05-27 10:09:56 -0400: Dan Graham
256 removed unused add icon
257 - 2015-05-27 10:08:58 -0400: Dan Graham
258 removed unused png icons
259 - 2015-05-27 10:07:33 -0400: Dan Graham
260 new 16x16 gif icons
261 - 2015-05-27 10:06:48 -0400: Dan Graham
262 More icon tweaks.
263 - 2015-05-26 16:30:07 -0400: Dan Graham
264 added button icons
265 - 2015-05-26 16:23:00 -0400: Dan Graham
266 Icon adjustments.
267 - 2015-05-26 15:43:14 -0400: Dan Graham
268 Initial tests for display changes.
269 - 2015-05-24 16:14:03 -0400 (tag: 3.0.61): Dan Graham
270 tagged version 3.0.61
271 - 2015-05-24 16:02:28 -0400: Dan Graham
272 Reset self.conflicts in updateDataFromFile.
273 - 2015-05-24 15:17:19 -0400 (tag: 3.0.60): Dan Graham
274 tagged version 3.0.60
275 - 2015-05-24 15:12:04 -0400 (tag: 3.0.59): Dan Graham
276 tagged version 3.0.59
277 - 2015-05-24 15:11:36 -0400: Dan Graham
278 Added conflicts in ETMCmd to store a list of isodates with
279 overlapping busy times. Display a rectangular red flag in the
280 lower, left-hand corner of month view cells with conflicts.
281 - 2015-05-24 13:40:18 -0400 (tag: 3.0.58): Dan Graham
282 tagged version 3.0.58
283 - 2015-05-24 13:39:23 -0400: Dan Graham
284 Changed month view busy border to cover all 24 hours with a break
285 for midnight in the lower, left-hand corner.
8080 import subprocess
8181
8282 import gettext
83 t = gettext.translation('etm', 'locale', fallback=True)
8384
8485 if platform.python_version() >= '3':
8586 python_version = 3
8687 python_version2 = False
8788 from io import StringIO
88 from gettext import gettext as _
89 _ = t.gettext
90 # from gettext import gettext as _
8991 unicode = str
9092 u = lambda x: x
9193 raw_input = input
94 from urllib.parse import quote
9295 else:
9396 python_version = 2
9497 python_version2 = True
9598 from cStringIO import StringIO
96 _ = gettext.lgettext
97
99 _ = t.ugettext
100 from urllib2 import quote
98101
99102 from random import random
100103 from math import log
135138 NIL = Node(End(), [], [])
136139
137140 # default for items without a tag or keyword entry
138 NONE = '~~~'
139
141 # the leading ~ makes them sort last
142 NONE = '~ {0} ~'.format(_("none"))
143
144 YESTERDAY = _('Yesterday')
145 TODAY = _('Today')
146 TOMORROW = _('Tomorrow')
140147
141148 class IndexableSkiplist:
142149 """Sorted collection supporting O(lg n) insertion, removal, and lookup by rank."""
280287
281288 dayfirst = False
282289 yearfirst = True
283 # bgclr = "#e9e9e9"
284 # BGCOLOR = "#ebebeb"
285290
286291 FINISH = _("Finish ...")
287292
550555 header = "{0} - {1}".format(
551556 fmt_dt(weekbeg, '%b %d, %Y'), fmt_dt(weekend, '%b %d, %Y'))
552557 header = leadingzero.sub('', header)
553 theweek = "{0} {1}: {2}".format(_("{0} Week".format(yn)), "{0:02d}".format(wn), header)
558 theweek = "{0} {1}: {2}".format(_("Week"), "{0:02d}".format(wn), header)
554559 return theweek
555560
556561
971976 if ny:
972977 y, yzs = ny.groups()
973978 yz = gettz(yzs)
974 windoz_epoch = _("Warning: under Windows with dates prior to 1970,\nany timezone information is ignored.")
979 windoz_epoch = _("Warning: any timezone information in dates prior to 1970 is ignored under Windows.")
975980 warn = ""
976981 try:
977982 dt_x = parse(parse_dtstr(x, timezone=xzs))
13461351 'action_template': '!hours!h $!value!) !label! (!count!)',
13471352
13481353 'agenda_colors': 2,
1349 'agenda_days': 4,
1354 'agenda_days': 2,
13501355 'agenda_indent': 3,
13511356 'agenda_width1': 32,
13521357 'agenda_width2': 18,
13781383
13791384 'details_rows': 4,
13801385
1386 'early_hour': 6,
1387
13811388 'edit_cmd': '',
13821389 'email_template': "!time_span!\n!l!\n\n!d!",
13831390 'etmdir': etmdir,
1391 'exportdir': etmdir,
13841392 'encoding': {'file': dfile_encoding, 'gui': dgui_encoding,
13851393 'term': dterm_encoding},
13861394 'filechange_alert': '',
14811489 # logger.debug("user_options: {0}".format(user_options))
14821490
14831491 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']:
1492 if key in ['show_finished', 'fontsize_busy', 'fontsize_fixed', 'fontsize_tree', 'outline_depth', 'prefix', 'prefix_uses', 'icssyc_folder', 'ics_subscriptions', 'agenda_days']:
14851493 if key not in user_options:
14861494 # we want to allow 0 as an entry
14871495 options[key] = default_options[key]
19111919 return unicode(dt)
19121920 if short:
19131921 tdy = datetime.today()
1914 if dt.date() == tdy.date():
1915 dt_fmt = "%s" % _('today')
1916 elif dt.year == tdy.year:
1922 if type(dt) == datetime:
1923 dt = dt.date()
1924 if dt == tdy.date():
1925 dt_fmt = "%s" % TODAY
1926 elif dt == tdy.date() - oneday:
1927 dt_fmt = "%s" % YESTERDAY
1928 elif dt == tdy.date() + oneday:
1929 dt_fmt = "%s" % TOMORROW
1930 elif dt == tdy.year:
19171931 dt_fmt = dt.strftime(shortyearlessfmt)
19181932 else:
19191933 dt_fmt = dt.strftime(shortdatefmt)
19201934 else:
1921 dt_fmt = dt.strftime(reprdatefmt)
1935 if python_version2:
1936 dt_fmt = unicode(dt.strftime(reprdatefmt), term_encoding)
1937 else:
1938 dt_fmt = dt.strftime(reprdatefmt)
1939 dt_fmt = leadingzero.sub('', dt_fmt)
19221940 return s2or3(dt_fmt)
19231941
19241942
19291947 return unicode(dt)
19301948 tdy = datetime.today()
19311949 if dt.date() == tdy.date():
1932 dt_fmt = "%s %s" % (fmt_time(dt, options=options), _('today'))
1950 dt_fmt = "%s %s" % (fmt_time(dt, options=options), TODAY)
1951 elif dt.date() == tdy.date() - oneday:
1952 dt_fmt = "%s %s" % (fmt_time(dt, options=options), YESTERDAY)
1953 elif dt.date() == tdy.date() + oneday:
1954 dt_fmt = "%s %s" % (fmt_time(dt, options=options), TOMORROW)
19331955 elif dt.year == tdy.year:
19341956 try:
19351957 x1 = unicode(fmt_time(dt, options=options))
20452067 'u', # user
20462068 'f', # finish date
20472069 'h', # history (task group)
2070 'i', # invitees
20482071 'g', # goto
20492072 'j', # job
20502073 'p', # priority
20552078 'd', # description
20562079 'm', # memo
20572080 'z', # time zone
2058 'i', # id',
2081 'I', # id',
20592082 'v', # action rate key
20602083 'w', # expense markup key
20612084 ]
20622085
20632086 all_keys = at_keys + ['entry', 'fileinfo', 'itemtype', 'rrule', '_summary', '_group_summary', '_a', '_j', '_p', '_r', 'prereqs']
2087
2088 all_types = [u'=', u'^', u'*', u'-', u'+', u'%', u'~', u'$', u'?', u'!', u'#']
2089 job_types = [u'-', u'+', u'%', u'$', u'?', u'#']
2090 any_types = [u'=', u'$', u'?', u'#']
2091
2092 # @key to item types - used to check for valid key usage
2093 key2type = {
2094 u'+': all_types,
2095 u'-': all_types,
2096 u'a': all_types,
2097 u'b': all_types,
2098 u'c': all_types,
2099 u'd': all_types,
2100 u'e': all_types,
2101 u'f': job_types + any_types,
2102 u'g': all_types + any_types,
2103 u'h': [u'+'] + any_types,
2104 u'i': [u'*', u'^'] + any_types,
2105 u'I': all_types,
2106 u'j': [u'+'] + any_types,
2107 u'k': all_types,
2108 u'l': all_types,
2109 u'm': all_types,
2110 u'o': job_types + any_types,
2111 u'p': job_types + any_types,
2112 u'r': all_types,
2113 u's': all_types,
2114 u't': all_types,
2115 u'u': all_types,
2116 u'v': [u'~'] + any_types,
2117 u'w': [u'~'] + any_types,
2118 u'x': [u'~'] + any_types,
2119 u'z': all_types,
2120 }
20642121
20652122 label_keys = [
20662123 # 'f', # finish date
20692126 'c', # context
20702127 'd', # description
20712128 'g', # goto
2129 'i', # invitees
20722130 'k', # keyword
20732131 'l', # location
20742132 'm', # memo
22732331 def tree2Text(tree, indent=4, width1=43, width2=20, colors=0,
22742332 number=False, count=0, count2id=None, depth=0):
22752333 global text_lst
2334 logger.debug("data.tree2Text: width1={0}, width2={1}, colors={2}".format(width1, width2, colors))
22762335 args = [count, count2id]
22772336 text_lst = []
22782337 if colors:
27172776 hsh['_summary'] = ''
27182777 if '_group_summary' in hsh:
27192778 sl = ["%s %s" % (hsh['itemtype'], hsh['_group_summary'])]
2720 if 'i' in hsh:
2779 if 'I' in hsh:
27212780 # fix the item index
2722 hsh['i'] = hsh['i'].split(':')[0]
2781 hsh['I'] = hsh['I'].split(':')[0]
27232782 else:
27242783 sl = ["%s %s" % (hsh['itemtype'], hsh['_summary'])]
2725 if 'i' not in hsh or not hsh['i']:
2726 hsh['i'] = uniqueId()
2784 if 'I' not in hsh or not hsh['I']:
2785 hsh['I'] = uniqueId()
27272786 bad_keys = [x for x in hsh.keys() if x not in all_keys]
27282787 if bad_keys:
27292788 omitted = []
27632822 else:
27642823 keys = []
27652824
2766 if key in hsh and hsh[key]:
2825 if key in hsh and hsh[key] is not None:
27672826 # since r and j can repeat, value will be a list
27682827 value = hsh[key]
27692828 if keys:
28282887 for pair in hsh['f']:
28292888 tmp.append(";".join([x.strftime(zfmt) for x in pair if x]))
28302889 sl.append("%s@f %s" % (prefix, ", {0}".format(prefix).join(tmp)))
2831 elif key == 'i':
2890 elif key == 'I':
28322891 if include_uid and hsh['itemtype'] != "=":
28332892 sl.append("prefix@i {0}".format(prefix, value))
28342893 elif key == 'h':
28662925 for hsh in hashes:
28672926 if hsh['itemtype'] == '=':
28682927 continue
2869 uid = hsh['i']
2928 uid = hsh['I']
28702929 uuid2hashes[uid] = hsh
28712930 file2uuids.setdefault(r, []).append(uid)
28722931 except Exception:
29983057 linenum += 1
29993058 # preserve new lines and leading whitespace within logical lines
30003059 stripped = line.rstrip()
3001 m = item_regex.match(stripped)
3002 if m:
3060 m = item_regex.match(stripped) # this requires item char to be followed by a whitespace char
3061 if m or stripped=='=':
30033062 if logical_line:
30043063 yield (''.join(logical_line))
30053064 logical_line = []
30383097 # preserve new lines and leading whitespace within logical lines
30393098 stripped = line.rstrip()
30403099 m = item_regex.match(stripped)
3041 if m:
3100 if m is not None or stripped == '=':
30423101 if logical_line:
30433102 yield (''.join(logical_line), rel_name, linenums)
30443103 logical_line = []
30693128 # in_task_group = False
30703129 for item, rel_name, linenums in list_of_items:
30713130 hsh, msg = str2hsh(item, options=options)
3131 logger.debug("items2Hashes:\n item='{0}' hsh={1}\n msg={2}".format(item, hsh, msg))
3132
3133 if item.strip() == "=":
3134 # reset defaults
3135 defaults = {}
3136
30723137 tmp_hsh = {}
30733138 tmp_hsh.update(defaults)
30743139 tmp_hsh.update(hsh)
30863151 else:
30873152 lines.append(item)
30883153 for line in lines:
3089 messages.append(" %s" % line)
3154 messages.append("{0}".format(line))
30903155 for m in msg:
3091 messages.append(' %s' % m)
3092
3156 messages.append('{0}'.format(m))
3157 msg.append(' {0}'.format(hsh['fileinfo']))
30933158 # put the bad item in the inbox for repairs
30943159 hsh['_summary'] = "{0} {1}".format(hsh['itemtype'], hsh['_summary'])
30953160 hsh['itemtype'] = "$"
3096 hsh['i'] = uniqueId()
3097 hsh['errors'] = "\n".join(msg)
3098 logger.warn("hsh errors: {0}".format(hsh['errors']))
3161 hsh['I'] = uniqueId()
3162 hsh['errors'] = "\n ".join(msg)
3163 logger.warn("{0}".format(hsh['errors']))
30993164 # no more processing
31003165 # ('hsh:', hsh)
31013166 hashes.append(hsh)
31123177 elif itemtype == '=':
31133178 # set group defaults
31143179 # hashes.append(this so that default entries are in file2uuids
3180 logger.debug("items2Hashes defaults: {0}".format(hsh))
31153181 defaults = hsh
31163182 hashes.append(hsh)
31173183 elif itemtype == '+':
31293195 # Here we assume that one or more jobs are unfinished.
31303196 queue_hsh = {}
31313197 tmp_hsh = {}
3132 tmp_hsh.update(defaults)
3198 for at_key in defaults:
3199 if at_key in key2type and itemtype in key2type[at_key]:
3200 tmp_hsh[at_key] = defaults[at_key]
3201
3202 # tmp_hsh.update(defaults)
31333203 tmp_hsh.update(hsh)
31343204 group_defaults = tmp_hsh
31353205 group_task = deepcopy(group_defaults)
31413211 del group_defaults['rrule']
31423212 prereqs = []
31433213 last_level = 1
3144 uid = hsh['i']
3214 uid = hsh['I']
31453215 summary = hsh['_summary']
31463216 if 'j' not in hsh:
31473217 continue
31803250 except:
31813251 logger.warn('error: bad value for q', job['q'])
31823252 continue
3183 job['i'] = current_id
3253 job['I'] = current_id
31843254
31853255 queue_hsh.setdefault(current_level, set([])).add(current_id)
31863256
32033273 hashes.append(job)
32043274 else:
32053275 tmp_hsh = {}
3206 tmp_hsh.update(defaults)
3276 for at_key in defaults:
3277 if at_key in key2type and itemtype in key2type[at_key]:
3278 tmp_hsh[at_key] = defaults[at_key]
3279
3280 # tmp_hsh.update(defaults)
32073281 tmp_hsh.update(hsh)
32083282 hsh = tmp_hsh
32093283 try:
32213295 tmp.append(key)
32223296 # else:
32233297 # tmp.append(' ')
3224 uuid2labels[hsh['i']] = "".join(tmp)
3298 uuid2labels[hsh['I']] = "".join(tmp)
32253299 return messages, hashes, uuid2labels
32263300
32273301
32383312 else:
32393313 start = done
32403314 else:
3241 start = parse(parse_dtstr(hsh['s'])).replace(tzinfo=None)
3315 start = hsh['s'].replace(tzinfo=None)
32423316 tmp = []
32433317 if not start:
32443318 return False, []
33453419 if 'z' not in hsh:
33463420 hsh['z'] = local_timezone
33473421 if 'o' in hsh and hsh['o'] == 'r' and 'f' in hsh:
3348 # restart
33493422 dtstart = hsh['f'][-1][0].replace(tzinfo=gettz(hsh['z']))
33503423 elif 's' in hsh:
33513424 dtstart = parse(parse_dtstr(
34233496 cal_pattern = r'^%s' % '|'.join(
34243497 [x[2] for x in options['calendars'] if x[1]])
34253498 filters['cal_regex'] = re.compile(cal_pattern)
3426 s = str(s)
3499 s = s2or3(s)
34273500 op_str = s.split('#')[0]
34283501 parts = minus_regex.split(op_str)
34293502 head = parts.pop(0)
34443517 if groupdate_regex.search(part):
34453518 dated['grpby'] = True
34463519 filters['dates'] = True
3447 elif part not in ['c', 'u'] and part[0] not in ['k', 'f', 't']:
3520 elif part not in ['c', 'u', 'l'] and part[0] not in ['k', 'f', 't']:
34483521 term_print(
34493522 str(_('Ignoring invalid grpby part: "{0}"'.format(part))))
34503523 groupbylst.remove(part)
34553528 grpby['fmts'] = []
34563529 grpby['tuples'] = []
34573530 filters['grpby'] = ['_summary']
3531 filters['missing'] = False
34583532 # include = {'y', 'm', 'w', 'd'}
34593533 include = {'y', 'm', 'd'}
34603534 for group in groupbylst:
35463620 dt = parse_date_period(part[1:])
35473621 dated[key] = dt.replace(tzinfo=None)
35483622
3623 elif key == 'm':
3624 value = unicode(part[1:].strip())
3625 if value == '1':
3626 filters['missing'] = True
3627
35493628 elif key == 'f':
35503629 value = unicode(part[1:].strip())
35513630 if value[0] == '!':
36413720 'g': '+',
36423721 'o': '^',
36433722 'n': '!',
3644 't': '-'
3723 't': '-',
3724 's': '?',
36453725 }
36463726 uuids = []
36473727
36633743 hsh = uuid2hash[uid]
36643744 skip = False
36653745 type_char = hsh['itemtype']
3666 if type_char in ['=', '#', '$', '?']:
3746 if type_char in ['=', '#', '$']:
36673747 # omit defaults, hidden, inbox and someday
36683748 continue
36693749 if filters['dates'] and 's' not in hsh:
37103790 skip = True
37113791 if not tf and res:
37123792 skip = True
3713 for t in ['c', 'k', 'u']:
3714 if t in filters['grpby'] and t not in hsh:
3715 # t is missing from hsh
3716 skip = True
3717 break
3793 for t in ['c', 'k', 'u', 'l']:
3794 if t in filters['grpby']:
3795 if filters['missing']:
3796 if t not in hsh:
3797 hsh[t] = NONE
3798 else:
3799 if t in hsh and hsh[t] == NONE:
3800 # we added this on an earlier report
3801 del hsh[t]
3802 if t not in hsh:
3803 skip = True
3804 break
37183805 if skip:
37193806 # try the next uid
37203807 continue
39144001 else:
39154002 bb = ""
39164003 eb = ""
4004 # show day items starting with beg and ending with lst
39174005 beg = datetime.today()
4006 tom = beg + oneday
4007 lst = beg + (days - 1)*oneday
39184008 beg_fmt = beg.strftime("%Y%m%d")
3919 beg + days * oneday
3920 day_count = 0
3921 last_day = ''
4009 tom_fmt = tom.strftime("%Y%m%d")
4010 lst_fmt = lst.strftime("%Y%m%d")
39224011 if not items:
3923 return "no output"
4012 return {}
39244013 for item in items:
39254014 if item[0][0] == 'day':
3926 if item[0][1] >= beg_fmt and day_count <= days + 1:
3927 # process day items until we get to days+1 so that all items
3928 # from days are included
4015 if item[0][1] >= beg_fmt and item[0][1] <= lst_fmt:
39294016 if item[2][1] in ['fn', 'ac', 'ns']:
39304017 # skip finished tasks, actions and notes
39314018 continue
3932 if item[0][1] != last_day:
3933 last_day = item[0][1]
3934 day_count += 1
3935 if day_count <= days:
3936 day.append(item)
4019 if item[0][1] == beg_fmt:
4020 item[1] = TODAY
4021 elif item[0][1] == tom_fmt:
4022 item[1] = TOMORROW
4023 day.append(item)
39374024 elif item[0][0] == 'inbasket':
39384025 item.insert(1, "%sIn Basket%s" % (bb, eb))
39394026 inbasket.append(item)
39764063 try:
39774064 grpby, dated, filters = str2opts(s, options, cli)
39784065 except:
3979 e = _("Could not process: {0}").format(s)
4066 e = "{0}: {1}".format(_("Could not process"), s)
39804067 logger.exception(e)
39814068 return e
39824069 if not grpby:
3983 return [str(_('invalid grpby setting'))]
4070 return ["{0}: grpby".format(_('invalid setting'))]
39844071 uuids = applyFilters(file2uuids, uuid2hash, filters)
39854072 tups = makeReportTuples(uuids, uuid2hash, grpby, dated, options)
39864073 items = []
41024189 hsh['itemtype'] = itemtype
41034190 hsh['_summary'] = summary
41044191 if uid:
4105 hsh['i'] = uid
4192 hsh['I'] = uid
41064193 if itemtype == u'+':
41074194 hsh['_group_summary'] = summary
41084195 # drop the @i line
41114198 for at_part in at_parts:
41124199 at_key = unicode(at_part[0])
41134200 at_val = at_part[1:].strip()
4201 if itemtype not in key2type[at_key]:
4202 msg.append("An entry for @{0} is not allowed in items of type '{1}'.".format(at_key, itemtype))
4203 continue
4204 # print('bad key', at_key, itemtype)
41144205 if at_key == 'a':
41154206 actns = options['alert_default']
41164207 arguments = []
41314222 tmp = action_part.split(',')
41324223 arguments.append(tmp)
41334224 alerts.append([triggers, actns, arguments])
4134 elif at_key in ['+', '-']:
4225 elif at_key in ['+', '-', 'i']:
41354226 parts = comma_regex.split(at_val)
41364227 tmp = []
41374228 for part in parts:
43624453 msg.extend(warn)
43634454 except:
43644455 logger.exception("exception processing rrule: {0}".format(hsh['_r']))
4365 if 'i' not in hsh:
4366 hsh['i'] = uniqueId()
4456 if 'I' not in hsh:
4457 hsh['I'] = uniqueId()
43674458
43684459 except:
43694460 logger.exception('exception processing "{0}"'.format(s))
43754466 if not lbls:
43764467 lbls = {}
43774468 marker = '!'
4469 if hsh and "_summary" in hsh and "summary" not in hsh:
4470 hsh["summary"] = hsh["_summary"]
43784471
43794472 def lookup(w):
43804473 if w == '':
43814474 return marker
43824475 l1, l2 = lbls.get(w, ('', ''))
43834476 v = hsh.get(w, None)
4477 if template.startswith("mailto"):
4478 v = quote(v)
43844479 if v is None:
43854480 if complain:
43864481 return w
47764871 def getDataFromFile(f, file2data, bef, file2uuids=None, uuid2hash=None, options=None):
47774872 if not options:
47784873 options = {}
4874 if file2data is None:
4875 file2data = {}
47794876 if not file2uuids:
47804877 file2uuids = {}
47814878 if not uuid2hash:
47914888 alerts = []
47924889 alert_minutes = {}
47934890 folders = expandPath(f)
4891 pastduerepeating = []
47944892 for uid in file2uuids[f]:
47954893 # this will give the items in file order!
47964894 if uuid2hash[uid]['itemtype'] in ['=']:
49585056 else:
49595057 dt = parse(parse_dtstr(hsh['s'],
49605058 hsh['z'])).replace(tzinfo=None)
5059 # dt = hsh['s'].replace(tzinfo=None)
49615060 else:
49625061 dt = None
49635062 # dts = "none"
49655064
49665065 if dt:
49675066 if hsh['itemtype'] == '*':
4968 # sdt = "%s %s" % (
4969 # fmt_time(dt, True, options=options),
4970 # fmt_date(dt, True))
49715067 sdt = fmt_shortdatetime(dt, options=options)
49725068 elif hsh['itemtype'] == '~':
49735069 if 'e' in hsh:
50775173
50785174 elif 's' in hsh and hsh['s'] and 'f' not in hsh:
50795175 thisdate = parse(
5080 parse_dtstr(
5081 hsh['s'], hsh['z'])).astimezone(
5176 parse_dtstr(
5177 hsh['s'], hsh['z'])).astimezone(
50825178 tzlocal()).replace(tzinfo=None)
50835179 dates.append(thisdate)
50845180 # add2list("datetimes", (thisdate, f))
50925188 else:
50935189 st_fmt = fmt_time(st, options=options)
50945190 summary = setSummary(hsh, dtl)
5095 tmpl_hsh = {'i': uid, 'summary': summary,
5191 tmpl_hsh = {'I': uid, 'summary': summary,
50965192 'start_date': fmt_date(dtl, True),
50975193 'start_time': fmt_time(dtl, True, options=options)}
50985194 if 't' in hsh:
51465242 alert_minutes[amn] += .1
51475243 else:
51485244 alert_minutes[amn] = amn
5149 # add2list(alerts, (amn, this_hsh, f), False)
5150 # add2list("alerts", (alert_minutes[amn], this_hsh['i'], this_hsh, f), False)
5151 alerts.append((alert_minutes[amn], this_hsh['i'], this_hsh, f))
5245 alerts.append((alert_minutes[amn], this_hsh['I'], this_hsh, f))
51525246 if (hsh['itemtype'] in ['+', '-', '%'] and dtl < today_datetime):
51535247 time_diff = (dtl - today_datetime).days
51545248 if time_diff == 0:
51905284 if 'f' in hsh and 'rrule' not in hsh:
51915285 continue
51925286 else:
5287 if 'rrule' in hsh and 'o' in hsh and hsh['o'] == 'r':
5288 # only nag about the oldest instance
5289 if uid in pastduerepeating:
5290 continue
5291 pastduerepeating.append(uid)
51935292 item = [
51945293 ('now', sn, dtl, hsh['_p'], summary, f), (cat,),
51955294 (uid, typ, summary, time_str, dtl)]
52075306 time_diff,
52085307 hsh['_p'],
52095308 f),
5210 (fmt_date(today_datetime),),
5309 (fmt_date(today_datetime, ),),
52115310 (uid, 'by', summary, extstr, dtl)]
52125311 items.append(item)
52135312
52255324 item = [
52265325 ('day', sd.strftime(sortdatefmt),
52275326 tstr2SCI[typ][0], hsh['_p'], '', f),
5228 (fmt_date(dt),),
5327 (fmt_date(dt, ),),
52295328 (uid, typ, summary, '', dtl)]
52305329 items.append(item)
52315330 occasions.append([sd, summary, uid, f])
52395338 item = [
52405339 ('day', sd.strftime(sortdatefmt),
52415340 tstr2SCI[typ][0], hsh['_p'], '', f),
5242 (fmt_date(dt),),
5341 (fmt_date(dt, ),),
52435342 (uid, 'ac', summary,
52445343 sdt, dtl)]
52455344 items.append(item)
52635362 ('day', sd.strftime(sortdatefmt),
52645363 tstr2SCI[typ][0], hsh['_p'],
52655364 st.strftime(sorttimefmt), f),
5266 (fmt_date(sd),),
5365 (fmt_date(sd, ),),
52675366 (uid, typ, summary, '%s ~ %s' %
52685367 (st_fmt,
52695368 options['dayend_fmt']), dtl)]
53125411 ('day', sd.strftime(sortdatefmt),
53135412 tstr2SCI[typ][0], hsh['_p'],
53145413 st.strftime(sorttimefmt), f),
5315 (fmt_date(sd),),
5414 (fmt_date(sd, ),),
53165415 (uid, typ, summary, '%s%s' % (
53175416 st_fmt,
53185417 et_fmt), dtl)]
53215420 continue
53225421 # other dated items
53235422 if hsh['itemtype'] in ['+', '-', '%']:
5324 if 'e' in hsh:
5325 extstr = fmt_period(hsh['e'])
5326 else:
5327 extstr = ''
53285423 if 'f' in hsh and hsh['f'][-1][1] == dtl:
53295424 typ = 'fn'
53305425 else:
53375432 typ = 'cs'
53385433 else:
53395434 typ = 'av'
5340 item = [
5341 ('day', sd.strftime(sortdatefmt), tstr2SCI[typ][0],
5342 hsh['_p'], '', f),
5343 (fmt_date(dt),),
5344 (uid, typ, summary, extstr, dtl)]
5345 items.append(item)
5346 continue
5347 if hsh['itemtype'] == '%':
5348 if 'f' in hsh:
5349 typ = 'fn'
5350 else:
5351 typ = 'ds'
5352 item = [
5353 ('day', sd.strftime(sortdatefmt), tstr2SCI[typ][0],
5354 hsh['_p'], '', f),
5355 (fmt_date(dt),),
5356 (uid, typ, summary, extstr, dtl)]
5357 items.append(item)
5358 continue
5359 if hsh['itemtype'] == '+':
5360 if 'prereqs' in hsh and hsh['prereqs']:
5361 typ = 'cu'
5362 else:
5363 if 'f' in hsh and hsh['f'][-1][1] == dtl:
5364 typ = 'fn'
5435 sm = st.hour * 60 + st.minute
5436 if sm != 0:
5437 ed = etl.date()
5438 et = etl.time()
5439 em = et.hour * 60 + et.minute
5440
5441 # make tasks with set starting times highest priority
5442 hsh['_p'] = 0
5443
5444 evnt_summary = "%s: %s" % (tmpl_hsh['summary'], tmpl_hsh['busy_span'])
5445 if et != st:
5446 et_fmt = " ~ %s" % fmt_time(et, options=options)
53655447 else:
5366 typ = 'cs'
5367 item = [
5368 ('day', sd.strftime(sortdatefmt), tstr2SCI[typ][0],
5369 hsh['_p'], '', f),
5370 (fmt_date(dt),),
5371 (uid, typ, summary, extstr, dtl)]
5372 items.append(item)
5373 continue
5448 et_fmt = ''
5449 if ed > sd:
5450 # this task overlaps more than one day
5451 # first_min = 24*60 - sm
5452 # last_min = em
5453 # the first day tuple
5454 item = [
5455 ('day', sd.strftime(sortdatefmt),
5456 tstr2SCI[typ][0], hsh['_p'],
5457 st.strftime(sorttimefmt), f),
5458 (fmt_date(sd, ),),
5459 (uid, typ, summary, '%s ~ %s' %
5460 (st_fmt,
5461 options['dayend_fmt']), dtl)]
5462 items.append(item)
5463 busytimes.append([sd, sm, day_end_minutes, evnt_summary, uid, f])
5464 sd += oneday
5465 i = 0
5466 item_copy = []
5467 while sd < ed:
5468 item_copy.append([x for x in item])
5469 item_copy[i][0] = list(item_copy[i][0])
5470 item_copy[i][1] = list(item_copy[i][1])
5471 item_copy[i][2] = list(item_copy[i][2])
5472 item_copy[i][0][1] = sd.strftime(sortdatefmt)
5473 item_copy[i][1][0] = fmt_date(sd)
5474 item_copy[i][2][3] = '%s ~ %s' % (
5475 options['daybegin_fmt'],
5476 options['dayend_fmt'])
5477 item_copy[i][0] = tuple(item_copy[i][0])
5478 item_copy[i][1] = tuple(item_copy[i][1])
5479 item_copy[i][2] = tuple(item_copy[i][2])
5480 # add2list("items", item_copy[i])
5481 items.append(item_copy[i])
5482 busytimes.append([sd, 0, day_end_minutes, evnt_summary, uid, f])
5483 sd += oneday
5484 i += 1
5485 # the last day tuple
5486 if em:
5487 item_copy.append([x for x in item])
5488 item_copy[i][0] = list(item_copy[i][0])
5489 item_copy[i][1] = list(item_copy[i][1])
5490 item_copy[i][2] = list(item_copy[i][2])
5491 item_copy[i][0][1] = sd.strftime(sortdatefmt)
5492 item_copy[i][1][0] = fmt_date(sd)
5493 item_copy[i][2][3] = '%s%s' % (
5494 options['daybegin_fmt'], et_fmt)
5495 item_copy[i][0] = tuple(item_copy[i][0])
5496 item_copy[i][1] = tuple(item_copy[i][1])
5497 item_copy[i][2] = tuple(item_copy[i][2])
5498 # add2list("items", item_copy[i])
5499 items.append(item_copy[i])
5500 busytimes.append([sd, 0, em, evnt_summary, uid, f])
5501 else:
5502 # single day task
5503 item = [
5504 ('day', sd.strftime(sortdatefmt),
5505 tstr2SCI[typ][0], hsh['_p'],
5506 st.strftime(sorttimefmt), f),
5507 (fmt_date(sd, ),),
5508 (uid, typ, summary, '%s%s' % (
5509 st_fmt,
5510 et_fmt), dtl)]
5511 items.append(item)
5512 busytimes.append([sd, sm, em, evnt_summary, uid, f])
5513 continue
5514 else: # sm == 0
5515 # midnight task - show extent only
5516 # use 11:59pm as the sorting datetime
5517 dtl = dtl + 1439 * oneminute
5518 item = [
5519 ('day', dtl.strftime(sortdatefmt), tstr2SCI[typ][0],
5520 hsh['_p'], '', f),
5521 (fmt_date(dt, ),),
5522 (uid, typ, summary, tmpl_hsh['e'], dtl)]
5523 items.append(item)
5524 continue
53745525 file2data[f] = [items, alerts, busytimes, datetimes, occasions]
53755526
53765527
54025553
54035554
54045555 def updateViewFromFile(f, file2data):
5556 if not file2data:
5557 file2data = {}
5558 if f not in file2data:
5559 file2data[f] = [[], [], [], [], []]
54055560 _items, _alerts, _busytimes, _datetimes, _occasions = file2data[f]
54065561 # logger.debug('file: {0}'.format(f))
54075562 for item in _items:
54335588 uuid2hash = {}
54345589 if not options:
54355590 options = {}
5591 if file2data is None:
5592 file2data = {}
54365593 # clear data for this file
54375594 _items = _alerts = _busytimes = _datetimes = _occasions = []
5438 if f in file2data:
5595 if file2data is not None and f in file2data:
54395596 _items, _alerts, _busytimes, _datetimes, _occasions = file2data[f]
54405597 if _items:
54415598 for item in _items:
55965753 else:
55975754 return False, 'Cannot export item type "%s"' % hsh['itemtype']
55985755
5599 element.add('uid', hsh[u'i'])
5756 element.add('uid', hsh[u'I'])
56005757 if 'z' in hsh:
56015758 # pytz is required to get the proper tzid into datetimes
56025759 tz = pytz.timezone(hsh['z'])
56705827 element.add('comment', hsh['m'])
56715828 if 'u' in hsh:
56725829 element.add('organizer', hsh['u'])
5830 if 'i' in hsh:
5831 for x in hsh['i']:
5832 element.add('attendee', "MAILTO:{0}".format(x))
5833
56735834
56745835 if hsh['itemtype'] in ['-', '+', '%']:
56755836 done, due, following = getDoneAndTwo(hsh)
59366097
59376098
59386099 def import_ical(ics="", txt="", vcal=""):
6100 if not has_icalendar:
6101 logger.error("Could not import icalendar")
6102 return False
59396103 logger.debug("ics: {0}, txt: {1}, vcal:{2}".format(ics, txt, vcal))
59406104 if vcal:
59416105 cal = Calendar.from_ical(vcal)
60056169 clst.append("@z %s" % tzid.to_ical().decode())
60066170 logger.debug("Using tzid: {0}".format(tzid.to_ical().decode()))
60076171 else:
6008 logger.debug("Using tzid: UTC")
6009 clst.append("@z UTC")
6172 logger.debug("Using tzid: {0}".format(local_timezone))
6173 clst.append("@z {0}".format(local_timezone))
60106174
60116175 tmp = comp.get('description')
60126176 if tmp:
60316195 clst.append("@t %s" % u', '.join(tags))
60326196 else:
60336197 clst.append("@t %s" % tags)
6198
6199 invitees = comp.get('attendee')
6200 if invitees:
6201 if type(invitees) is list:
6202 invitees = [x.to_ical().decode() for x in invitees]
6203 ilst = []
6204 for x in invitees:
6205 if x.startswith("MAILTO:"):
6206 x = x[7:]
6207 ilst.append(x)
6208 clst.append("@i %s" % u', '.join(ilst))
6209 else:
6210 clst.append("@i %s" % invitee)
6211
60346212 tmp = comp.get('organizer')
60356213 if tmp:
60366214 clst.append("@u %s" % tmp.to_ical().decode())
62226400 cmd = arg_str[0]
62236401 ret = []
62246402 views = {
6225 # everything but agenda and week
6403 # everything but agenda, week and month
62266404 'd': 'day',
62276405 'p': 'folder',
62286406 't': 'tag',
62536431 else:
62546432 f = None
62556433 if not self.rows:
6256 return "no output"
6434 return {}
62576435 rows = deepcopy(self.rows)
62586436 return (makeTree(rows, view=view, calendars=self.calendars, fltr=f, hide_finished=self.options['hide_finished']))
62596437 else:
62976475 self.busytimes = {}
62986476 for key in busytimesSL:
62996477 self.busytimes[key] = list(busytimesSL[key])
6478
63006479 self.occasions = {}
63016480 for key in occasionsSL:
63026481 self.occasions[key] = list(occasionsSL[key])
63466525 for hsh in loh:
63476526 if hsh['itemtype'] == '=':
63486527 continue
6349 logger.debug('adding: {0}, {1}'.format(hsh['i'], hsh['_summary']))
6350 id = hsh['i']
6528 logger.debug('adding: {0}, {1}'.format(hsh['I'], hsh['_summary']))
6529 id = hsh['I']
63516530 self.uuid2hash[id] = hsh
63526531 self.file2uuids.setdefault(rp, []).append(id)
63536532 mtime = os.path.getmtime(fp)
63546533 self.file2lastmodified[(fp, rp)] = mtime
63556534 (self.rows, self.alerts, self.busytimes, self.datetimes, self.occasions, self.file2data) = updateViewData(rp, bef, self.file2uuids, self.uuid2hash, self.options, self.file2data)
6535
63566536 logger.debug('ended updateDataFromFile')
63576537
63586538 def edit_tmp(self):
66406820 gettz(hsh['z'])).replace(tzinfo=None)
66416821 else:
66426822 ddn = ''
6823 if 's' in hsh and 'o' in hsh and hsh['o'] == 'r':
6824 hours = hsh['s'].hour
6825 minutes = hsh['s'].minute
6826 dt = dt.replace(hour=hours, minute=minutes, second=0, microsecond=0)
66436827 if hsh['itemtype'] == u'+':
66446828 m = group_regex.match(hsh['_summary'])
66456829 if m:
69477131 use_locale = ()
69487132 (user_options, options, use_locale) = get_options(etmdir)
69497133 ARGS = ['a', 'k', 'm', 'n', 'N', 'p', 'c', 'd', 't', 'v']
7134 QUESTION = s2or3("?")
69507135 if len(argv) > 1:
7136 for i in range(len(argv)-1):
7137 j = i+1
7138 argv[j] = s2or3(argv[j])
69517139 c = ETMCmd(options)
69527140 c.loop = False
69537141 c.number = False
69547142 args = []
6955 if len(argv) == 2 and argv[1] == "?":
7143 if len(argv) == 2 and argv[1] == QUESTION:
69567144 term_print(USAGE)
69577145 elif len(argv) == 2 and argv[1] == 'v':
69587146 term_print(c.do_v(""))
6959 elif len(argv) == 3 and '?' in argv:
6960 if argv[1] == '?':
6961 args = ['?', argv[2]]
7147 elif len(argv) == 3 and QUESTION in argv:
7148 if argv[1] == QUESTION:
7149 args = [QUESTION, argv[2]]
69627150 else:
6963 args = ['?', argv[1]]
7151 args = [QUESTION, argv[1]]
69647152 if args[1] not in ARGS:
69657153 term_print(USAGE)
69667154 else:
69847172 tt = TimeIt(loglevel=2, label="cmd '{0}'".format(argstr))
69857173 c.loadData()
69867174 res = c.do_command(argstr)
7175 width1 = 43
69877176 if opts and 'width1' in opts:
69887177 width1 = opts['width1']
6989 elif options and 'report_width1' in options:
6990 width1 = options['report_width1']
6991 else:
6992 width1 = 43
7178 elif options:
7179 if 'report_width1' in options:
7180 width1 = options['report_width1']
7181 elif 'agenda_width2' in options:
7182 width2 = options['agenda_width2']
7183
7184 width2 = 20
69937185 if opts and 'width2' in opts:
69947186 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:
7187 elif options:
7188 if 'report_width2' in options:
7189 width2 = options['report_width2']
7190 elif 'agenda_width2' in options:
7191 width2 = options['agenda_width2']
7192
7193 indent = 4
7194 if options:
7195 if 'report_indent' in options:
70017196 indent = options['report_indent']
7002 else:
7003 indent = 4
7197 elif 'agenda_indent' in options:
7198 indent = options['agenda_indent']
7199
7200 colors = 0
7201 if options:
7202 if 'report_colors' in options:
7203 colors = options['report_colors']
7204 elif 'agenda_colors' in options:
7205 colors = options['agenda_colors']
70047206
70057207 if type(res) is dict:
7006 lines = tree2Text(res, indent=indent, width1=width1, width2=width2)[0]
7208 logger.debug("data.main res is dict; calling tree2Text width1={0}, width2={1}".format(width1, width2))
7209 lines = tree2Text(res, indent=indent, width1=width1, width2=width2, colors=colors)[0]
70077210 if lines and not lines[0]:
70087211 lines.pop(0)
70097212 res = "\n".join(lines)
99 import os.path
1010
1111 logger = logging.getLogger()
12 import codecs
13 import yaml
1214
1315 import platform
1416
1517 if platform.python_version() >= '3':
1618 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
19 from tkinter import (
20 ACTIVE,
21 BooleanVar,
22 BOTH,
23 BROWSE,
24 Button,
25 Checkbutton,
26 END,
27 Entry,
28 FLAT,
29 Frame,
30 INSERT,
31 IntVar,
32 Label,
33 LEFT,
34 Listbox,
35 Radiobutton,
36 RIGHT,
37 Scrollbar,
38 StringVar,
39 TclError,
40 Text,
41 Toplevel,
42 W,
43 X,
44 )
45 from tkinter import ttk
1846 from tkinter import font as tkFont
19 utf8 = lambda x: x
47 unicode = str
2048 else:
2149 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
50 from Tkinter import (
51 ACTIVE,
52 BooleanVar,
53 BOTH,
54 BROWSE,
55 Button,
56 Checkbutton,
57 END,
58 Entry,
59 FLAT,
60 Frame,
61 INSERT,
62 IntVar,
63 Label,
64 LEFT,
65 Listbox,
66 Radiobutton,
67 RIGHT,
68 Scrollbar,
69 StringVar,
70 TclError,
71 Text,
72 Toplevel,
73 W,
74 X,
75 )
76 import ttk
2377 import tkFont
2478
25 def utf8(s):
26 return s
27
2879 from datetime import datetime, timedelta
2980
30 from etmTk.data import fmt_period, parse_dt, get_current_time, relpath, ensureMonthly, parse_period
31
32 import gettext
33
34 _ = gettext.gettext
81 from etmTk.data import (_,
82 commandShortcut,
83 completion_regex,
84 ensureMonthly,
85 FINISH,
86 fmt_date,
87 fmt_period,
88 fmt_shortdatetime,
89 fmt_time,
90 get_current_time,
91 get_reps,
92 getFileTuples,
93 hsh2str,
94 import_ical,
95 parse_dt,
96 parse_period,
97 relpath,
98 rrulefmt,
99 str2hsh,
100 uniqueId,
101 )
102
103
104 SOMEREPS = _('selected repetitions')
105 ALLREPS = _('all repetitions')
106 MESSAGES = _('Error messages')
107 VALID = _("Valid entry")
108 FOUND = "found" # for found text marking
109
110 MAKE = _("Make")
111 PRINT = _("Print")
112 EXPORTTEXT = _("Export report in text format ...")
113 EXPORTCSV = _("Export report in CSV format ...")
114 SAVESPECS = _("Save changes to report specifications")
115 CLOSE = _("Close")
116
117 # VALID = _("Valid {0}").format(u"\u2714")
118 SAVEANDEXIT = _("Save changes and exit?")
119 UNCHANGEDEXIT = _("Item is unchanged. Exit?")
120 CREATENEW = _("creating a new item")
121 EDITEXISTING = _("editing an existing item")
122
123 type2Text = {
124 '$': _("In Basket item"),
125 '^': _("Occasion"),
126 '*': _("Event"),
127 '~': _("Action"),
128 '!': _("Note"), # undated only appear in folders
129 '-': _("Task"), # for next view
130 '+': _("Task Group"), # for next view
131 '%': _("Delegated Task"),
132 '?': _("Someday Maybe item"),
133 '#': _("Hidden item")
134 }
135
136 # import gettext
137 # _ = gettext.gettext
35138
36139
37140 def sanitize_id(id):
56159
57160 BGLCOLOR = "#f2f2f2"
58161 BGCOLOR = "#ebebeb"
162
163
164 class SimpleEditor(Toplevel):
165
166 def __init__(self, parent=None, master=None, file=None, line=None, newhsh=None, rephsh=None, options=None, title=None, start=None, modified=False):
167 """
168 If file is given, open file for editing.
169 Otherwise, we are creating a new item and/or replacing an item
170 mode:
171 1: new: edit newhsh, replace none
172 2: replace: edit and replace rephsh
173 3: new and replace: edit newhsh, replace rephsh
174
175 :param parent:
176 :param file: path to file to be edited
177 """
178 # self.frame = frame = Frame(parent)
179 if master is None:
180 master = parent
181 self.master = master
182 Toplevel.__init__(self, master)
183 self.minsize(400, 300)
184 self.geometry('500x200')
185 self.transient(parent)
186 self.configure(background=BGCOLOR, highlightbackground=BGCOLOR)
187 self.parent = parent
188 self.loop = parent.loop
189 self.messages = self.loop.messages
190 self.messages = []
191 self.mode = None
192 self.changed = False
193
194 self.scrollbar = None
195 self.listbox = None
196 self.autocompletewindow = None
197 self.line = None
198 self.match = None
199
200 self.file = file
201 self.initfile = None
202 self.fileinfo = None
203 self.repinfo = None
204 self.title = title
205 self.edithsh = {}
206 self.newhsh = newhsh
207 self.rephsh = rephsh
208 self.value = ''
209 self.options = options
210 self.tkfixedfont = tkFont.nametofont("TkFixedFont")
211 self.tkfixedfont.configure(size=self.options['fontsize_fixed'])
212 # self.text_value.trace_variable("w", self.setSaveStatus)
213 frame = Frame(self, bd=0, relief=FLAT)
214 frame.pack(side="bottom", fill=X, padx=4, pady=0)
215 frame.configure(background=BGCOLOR, highlightbackground=BGCOLOR)
216
217 # quit with a warning prompt if modified
218 Button(frame, text=_("Cancel"), highlightbackground=BGCOLOR, pady=2, command=self.quit).pack(side=LEFT, padx=4)
219 self.bind("<Escape>", self.quit)
220
221 l, c = commandShortcut('q')
222 self.bind(c, self.quit)
223 self.bind("<Escape>", self.cancel)
224
225 # finish will evaluate the item entry and, if repeating, show reps
226 finish = Button(frame, text=FINISH, highlightbackground=BGCOLOR, command=self.onFinish, pady=2)
227 # self.bind("<Control-w>", self.onCheck)
228 self.bind("<Control-w>", self.onFinish)
229
230 finish.pack(side=RIGHT, padx=4)
231
232 # find
233 Button(frame, text='x', command=self.clearFind, highlightbackground=BGCOLOR, padx=8, pady=2).pack(side=LEFT, padx=0)
234 self.find_text = StringVar(frame)
235 self.e = Entry(frame, textvariable=self.find_text, width=10, highlightbackground=BGCOLOR)
236 self.e.pack(side=LEFT, padx=0, expand=1, fill=X)
237 self.e.bind("<Return>", self.onFind)
238 Button(frame, text='>', command=self.onFind, highlightbackground=BGCOLOR, padx=8, pady=2).pack(side=LEFT, padx=0)
239
240 text = Text(self, wrap="word", bd=2, relief="sunken", padx=3, pady=2, font=self.tkfixedfont, undo=True, width=70)
241 text.configure(highlightthickness=0)
242 text.tag_configure(FOUND, background="lightskyblue")
243
244 text.pack(side="bottom", padx=4, pady=3, expand=1, fill=BOTH)
245 self.text = text
246
247 self.completions = self.loop.options['completions']
248
249 if start is not None:
250 # we have the starting text but will need a new uid
251 text = start
252 if self.rephsh is None:
253 self.edithsh = {}
254 self.mode = 1
255 self.title = CREATENEW
256 else:
257 self.edithsh = self.rephsh
258 self.mode = 2
259 self.title = EDITEXISTING
260
261 elif file is not None:
262 # we're editing a file - if it's a data file we will add uid's
263 # as necessary when saving
264 self.mode = 'file'
265 if not os.path.isfile(file):
266 logger.warn('could not open: {0}'.format(file))
267 text = ""
268 else:
269 with codecs.open(file, 'r', self.options['encoding']['file']) as f:
270 text = f.read()
271 else:
272 # we are creating a new item and/or replacing an item
273 # mode:
274 # 1: new
275 # 2: replace
276 # 3: new and replace
277 initfile = ensureMonthly(options=self.options, date=datetime.now())
278 # set the mode
279 if newhsh is None and rephsh is None:
280 # we are creating a new item from scratch and will need
281 # a new uid
282 self.mode = 1
283 self.title = CREATENEW
284 self.edithsh = {}
285 self.edithsh['I'] = uniqueId()
286 text = ''
287 elif rephsh is None: # newhsh is not None
288 # we are creating a new item as a copy and will need
289 # a new uid
290 self.mode = 1
291 self.title = CREATENEW
292 self.edithsh = self.newhsh
293 self.edithsh['I'] = uniqueId()
294 if ('fileinfo' in newhsh and newhsh['fileinfo']):
295 initfile = newhsh['fileinfo'][0]
296 text, msg = hsh2str(self.edithsh, self.options)
297 elif newhsh is None:
298 # we are editing and replacing rephsh - no file prompt
299 # using existing uid
300 self.title = EDITEXISTING
301 self.mode = 2
302 # self.repinfo = rephsh['fileinfo']
303 self.edithsh = self.rephsh
304 text, msg = hsh2str(self.edithsh, self.options)
305 else: # neither is None
306 # we are changing some instances of a repeating item
307 # we will be writing but not editing rephsh using its fileinfo
308 # and its existing uid
309 # we will be editing and saving newhsh using self.initfile
310 # we will need a new uid for newhsh
311 self.mode = 3
312 self.title = CREATENEW
313 self.edithsh = self.newhsh
314 self.edithsh['I'] = uniqueId()
315 if 'fileinfo' in newhsh and newhsh['fileinfo'][0]:
316 initfile = self.newhsh['fileinfo'][0]
317 text, msg = hsh2str(self.edithsh, self.options)
318 self.initfile = initfile
319 logger.debug('mode: {0}; initfile: {1}; edit: {2}'.format(self.mode, self.initfile, self.edithsh))
320 if self.title is not None:
321 self.wm_title(self.title)
322 self.settext(text)
323
324 # clear the undo buffer
325 if not modified:
326 self.text.edit_reset()
327 self.setmodified(False)
328 self.text.bind('<<Modified>>', self.updateSaveStatus)
329
330 self.text.focus_set()
331 self.protocol("WM_DELETE_WINDOW", self.quit)
332 if parent:
333 self.geometry("+%d+%d" % (parent.winfo_rootx() + 50,
334 parent.winfo_rooty() + 50))
335 self.configure(background=BGCOLOR)
336 l, c = commandShortcut('f')
337 self.bind(c, lambda e: self.e.focus_set())
338 l, c = commandShortcut('g')
339 self.bind(c, lambda e: self.onFind())
340 if start:
341 # self.text.tag_add("sel", "1.1", "1.{0}".format(len(start)))
342 self.text.mark_set(INSERT, 0.0)
343 elif line:
344 self.text.mark_set(INSERT, "{0}.0".format(line))
345 else:
346 self.text.mark_set(INSERT, END)
347 self.text.see(INSERT)
348 # l, c = commandShortcut('/')
349 logger.debug("/: {0}, {1}".format(l, c))
350 self.text.bind("<Control-space>", self.showCompletions)
351 self.grab_set()
352 self.wait_window(self)
353
354 def settext(self, text=''):
355 self.text.delete('1.0', END)
356 self.text.insert(INSERT, text)
357 self.text.mark_set(INSERT, '1.0')
358 self.text.focus()
359 logger.debug("modified: {0}".format(self.checkmodified()))
360
361 def gettext(self):
362 return self.text.get('1.0', END + '-1c')
363
364 def setCompletions(self, *args):
365 match = self.filterValue.get()
366 self.matches = matches = [x for x in self.completions if x and x.lower().startswith(match.lower())]
367 self.listbox.delete(0, END)
368 for item in matches:
369 self.listbox.insert(END, item)
370 self.listbox.select_set(0)
371 self.listbox.see(0)
372 self.fltr.focus_set()
373
374 def showCompletions(self, e=None):
375 if not self.completions:
376 return "break"
377 if self.autocompletewindow:
378 return "break"
379 line = self.text.get("insert linestart", INSERT)
380 m = completion_regex.search(line)
381 if not m:
382 logger.debug("no match in {0}".format(line))
383 return "break"
384
385 # set self.match here since it determines the characters to be replaced
386 self.match = match = m.groups()[0]
387 logger.debug("found match '{0}' in line '{1}'".format(match, line))
388
389 self.autocompletewindow = acw = Toplevel(master=self.text)
390 acw.geometry("+%d+%d" % (self.text.winfo_rootx() + 50, self.text.winfo_rooty() + 50))
391
392 self.autocompletewindow.wm_attributes("-topmost", 1)
393
394 self.filterValue = StringVar(self)
395 self.filterValue.set(match)
396 self.filterValue.trace_variable("w", self.setCompletions)
397 self.fltr = Entry(acw, textvariable=self.filterValue)
398 self.fltr.pack(side="top", fill="x")
399 self.fltr.icursor(END)
400
401 self.listbox = listbox = Listbox(acw, exportselection=False, width=self.loop.options['completions_width'])
402 listbox.pack(side="bottom", fill=BOTH, expand=True)
403
404 self.autocompletewindow.bind("<Double-1>", self.completionSelected)
405 self.autocompletewindow.bind("<Return>", self.completionSelected)
406 self.autocompletewindow.bind("<Escape>", self.hideCompletions)
407 self.autocompletewindow.bind("<Up>", self.cursorUp)
408 self.autocompletewindow.bind("<Down>", self.cursorDown)
409 self.fltr.bind("<Up>", self.cursorUp)
410 self.fltr.bind("<Down>", self.cursorDown)
411 self.setCompletions()
412
413 def is_active(self):
414 return self.autocompletewindow is not None
415
416 def hideCompletions(self, e=None):
417 if not self.is_active():
418 return
419 # destroy widgets
420 self.listbox.destroy()
421 self.listbox = None
422 self.autocompletewindow.destroy()
423 self.autocompletewindow = None
424
425 def completionSelected(self, event):
426 # Put the selected completion in the text, and close the list
427 modified = False
428 if self.matches:
429 cursel = self.matches[int(self.listbox.curselection()[0])]
430 else:
431 cursel = self.filterValue.get()
432 modified = True
433
434 start = "insert-{0}c".format(len(self.match))
435 end = "insert-1c wordend"
436 logger.debug("cursel: {0}; match: {1}; start: {2}; insert: {3}".format(
437 cursel, self.match, start, INSERT))
438 self.text.delete(start, end)
439 self.text.insert(INSERT, cursel)
440 self.hideCompletions()
441 if modified:
442 file = FileChoice(self, "append completion to file", prefix=self.loop.options['etmdir'], list=self.loop.options['completion_files']).returnValue()
443 if (file and os.path.isfile(file)):
444 with codecs.open(file, 'r', self.loop.options['encoding']['file']) as fo:
445 lines = fo.readlines()
446 lines.append(cursel)
447 lines.sort()
448 content = "\n".join([x.strip() for x in lines if x.strip()])
449 with codecs.open(file, 'w', self.loop.options['encoding']['file']) as fo:
450 fo.write(content)
451 self.completions.append(cursel)
452 self.completions.sort()
453
454 def cursorUp(self, event=None):
455 cursel = int(self.listbox.curselection()[0])
456 # newsel = max(0, cursel=1)
457 newsel = max(0, cursel - 1)
458 self.listbox.select_clear(cursel)
459 self.listbox.select_set(newsel)
460 self.listbox.see(newsel)
461 return "break"
462
463 def cursorDown(self, event=None):
464 cursel = int(self.listbox.curselection()[0])
465 newsel = min(len(self.matches) - 1, cursel + 1)
466 self.listbox.select_clear(cursel)
467 self.listbox.select_set(newsel)
468 self.listbox.see(newsel)
469 return "break"
470
471 def setmodified(self, bool):
472 if bool is not None:
473 self.text.edit_modified(bool)
474
475 def checkmodified(self):
476 return self.text.edit_modified()
477
478 def updateSaveStatus(self, event=None):
479 # Called by <<Modified>>
480 if self.checkmodified():
481 self.wm_title("{0} (modified)".format(self.title))
482 else:
483 self.wm_title("{0}".format(self.title))
484
485 def onFinish(self, e=None):
486 if self.mode == 'file':
487 self.onSave()
488 else:
489 self.onCheck()
490
491 def onSave(self, e=None, v=0):
492 if not self.checkmodified():
493 self.quit()
494 elif self.file is not None:
495 # we are editing a file
496 alltext = self.gettext()
497 self.loop.safe_save(self.file, alltext)
498 self.setmodified(False)
499 self.changed = True
500 self.quit()
501 else:
502 # we are editing an item
503 if self.mode in [1, 3]: # new
504 dir = self.options['datadir']
505 if 's' in self.edithsh and self.edithsh['s']:
506 dt = self.edithsh['s']
507 file = ensureMonthly(self.options, dt.date())
508 else:
509 dt = None
510 file = ensureMonthly(self.options)
511 dir, initfile = os.path.split(file)
512 # we need a filename for the new item
513 # make datadir the root
514 prefix, tuples = getFileTuples(self.options['datadir'], include=r'*.txt')
515 if v == 2:
516 filename = file
517 else:
518 ret = FileChoice(self, "etm data files", prefix=prefix, list=tuples, start=file).returnValue()
519 if not ret:
520 return False
521 filename = os.path.join(prefix, ret)
522 if not os.path.isfile(filename):
523 return False
524 filename = os.path.normpath(filename)
525 logger.debug('saving to: {0}'.format(filename))
526 self.text.focus_set()
527 logger.debug('edithsh: {0}'.format(self.edithsh))
528 if self.mode == 1:
529 if self.loop.append_item(self.edithsh, filename):
530 logger.debug('append mode: {0}'.format(self.mode))
531 elif self.mode == 2:
532 if self.loop.replace_item(self.edithsh):
533 logger.debug('replace mode: {0}'.format(self.mode))
534 else: # self.mode == 3
535 if self.loop.append_item(self.edithsh, filename):
536 logger.debug('append mode: {0}'.format(self.mode))
537 if self.loop.replace_item(self.rephsh):
538 logger.debug('replace mode: {0}'.format(self.mode))
539
540 # update the return value so that when it is not null then modified
541 # is false and when modified is true then it is null
542 self.setmodified(False)
543 self.changed = True
544 self.quit()
545 return "break"
546
547 def onCheck(self, event=None, showreps=True, showres=True):
548 # only called when editing an item and finish is pressed
549 self.loop.messages = []
550 text = self.gettext()
551 msg = []
552 reps = []
553 if text.startswith("BEGIN:VCALENDAR"):
554 text = import_ical(vcal=text)
555 logger.debug("text: {0} '{01}'".format(type(text), text))
556 if self.edithsh and 'i' in self.edithsh:
557 uid = self.edithsh['i']
558 else:
559 uid = None
560 hsh, msg = str2hsh(text, options=self.options, uid=uid)
561
562 if not msg:
563 # we have a good hsh
564 pre = post = warn = ""
565 if 'r' in hsh:
566 pre = _("Repeating ")
567 elif 's' in hsh:
568 dt = hsh['s']
569 if hsh['itemtype'] in ['*', '~']:
570 if self.options['early_hour'] and dt.hour < self.options['early_hour']:
571 warn = _("Is {0} the starting time you intended?".format(fmt_time(dt, options=self.options)))
572 dtfmt = fmt_shortdatetime(hsh['s'], self.options)
573 else:
574 if not dt.hour and not dt.minute:
575 dtfmt = fmt_date(dt, short=True)
576 else:
577 dtfmt = fmt_shortdatetime(hsh['s'], self.options)
578 post = _(" starting {0}.").format(dtfmt)
579 else: # unscheduled
580 pre = _("Unscheduled ")
581
582 prompt = "{0}{1}{2}".format(pre, type2Text[hsh['itemtype']], post)
583 if warn:
584 prompt = prompt + "\n\n" + warn
585
586 if self.edithsh and 'fileinfo' in self.edithsh:
587 fileinfo = self.edithsh['fileinfo']
588 self.edithsh = hsh
589 self.edithsh['fileinfo'] = fileinfo
590 else:
591 # we have a new item without fileinfo
592 self.edithsh = hsh
593 # update missing fields
594 logger.debug('calling hsh2str with {0}'.format(hsh))
595 str, msg = hsh2str(hsh, options=self.options)
596
597 self.loop.messages.extend(msg)
598 if self.loop.messages:
599 messages = "{0}".format("\n".join(self.loop.messages))
600 logger.debug("messages: {0}".format(messages))
601 self.messageWindow(MESSAGES, messages, opts=self.options)
602 return False
603
604 logger.debug("back from hsh2str with: {0}".format(str))
605 if 'r' in hsh:
606 showing_all, reps = get_reps(self.loop.options['bef'], hsh)
607 if reps:
608 if showreps:
609 try:
610 repsfmt = [unicode(x.strftime(rrulefmt)) for x in reps]
611 except:
612 repsfmt = [unicode(x.strftime("%X %x")) for x in reps]
613 logger.debug("{0}: {1}".format(showing_all, repsfmt))
614 if showing_all:
615 reps = ALLREPS
616 else:
617 reps = SOMEREPS
618 prompt = "{0}, {1}:\n\n {2}".format(prompt, reps, "\n ".join(repsfmt))
619 else:
620 repetitions = "No repetitions were generated."
621 self.loop.messages.append(repetitions)
622 if self.loop.messages:
623 messages = "{0}".format("\n".join(self.loop.messages))
624 logger.debug("messages: {0}".format(messages))
625 self.messageWindow(MESSAGES, messages, opts=self.options)
626 return False
627
628 if self.checkmodified():
629 prompt += "\n\n{0}".format(SAVEANDEXIT)
630 else:
631 prompt += "\n\n{0}".format(UNCHANGEDEXIT)
632
633 if str != text:
634 self.settext(str)
635 ans, value = OptionsDialog(parent=self, title=self.title, prompt=prompt, yesno=False, list=True).getValue()
636 if ans:
637 self.onSave(v=value)
638 return
639
640 def clearFind(self, *args):
641 self.text.tag_remove(FOUND, "0.0", END)
642 self.find_text.set("")
643
644 def onFind(self, *args):
645 target = self.find_text.get()
646 logger.debug('target: {0}'.format(target))
647 if target:
648 where = self.text.search(target, INSERT, nocase=1)
649 if where:
650 pastit = where + ('+%dc' % len(target))
651 self.text.tag_add(FOUND, where, pastit)
652 self.text.mark_set(INSERT, pastit)
653 self.text.see(INSERT)
654 self.text.focus()
655
656 def cancel(self, e=None):
657 t = self.find_text.get()
658 if t.strip():
659 self.clearFind()
660 return "break"
661 if self.autocompletewindow:
662 self.hideCompletions()
663 return "break"
664 if self.text.tag_ranges("sel"):
665 self.text.tag_remove('sel', "1.0", END)
666 return
667 logger.debug(('calling quit'))
668 self.quit()
669
670 def quit(self, e=None):
671 if self.checkmodified():
672 ans = self.confirm(parent=self, title=_('Quit'), prompt=_("There are unsaved changes.\nDo you really want to quit?"))
673 else:
674 ans = True
675 if ans:
676 if self.master:
677 logger.debug('setting focus')
678 self.master.focus()
679 self.master.focus_set()
680 logger.debug('focus set')
681 self.destroy()
682 logger.debug('done')
683
684 def messageWindow(self, title, prompt, opts=None, height=8, width=52):
685 win = Toplevel(self)
686 win.title(title)
687 win.geometry("+%d+%d" % (self.text.winfo_rootx() + 50, self.text.winfo_rooty() + 50))
688 f = Frame(win)
689 # pack the button first so that it doesn't disappear with resizing
690 b = Button(win, text=_('OK'), width=10, command=win.destroy, default='active', pady=2)
691 b.pack(side='bottom', fill=tkinter.NONE, expand=0, pady=0)
692 win.bind('<Return>', (lambda e, b=b: b.invoke()))
693 win.bind('<Escape>', (lambda e, b=b: b.invoke()))
694 tkfixedfont = tkFont.nametofont("TkFixedFont")
695 if 'fontsize_fixed' in self.loop.options and self.loop.options['fontsize_fixed']:
696 tkfixedfont.configure(size=self.loop.options['fontsize_fixed'])
697
698 t = ReadOnlyText(
699 f, wrap="word", padx=2, pady=2, bd=2, relief="sunken",
700 font=tkfixedfont,
701 height=height,
702 width=width,
703 takefocus=False)
704 t.insert("0.0", prompt)
705 t.pack(side='left', fill=tkinter.BOTH, expand=1, padx=0, pady=0)
706 if height > 1:
707 ysb = ttk.Scrollbar(f, orient='vertical', command=t.yview)
708 ysb.pack(side='right', fill=tkinter.Y, expand=0, padx=0, pady=0)
709 t.configure(state="disabled", yscroll=ysb.set)
710 t.configure(yscroll=ysb.set)
711 f.pack(padx=2, pady=2, fill=tkinter.BOTH, expand=1)
712
713 win.focus_set()
714 win.grab_set()
715 win.transient(self)
716 win.wait_window(win)
717
718 def confirm(self, parent=None, title="", prompt="", instance="xyz"):
719 ok, value = OptionsDialog(parent=parent, title=_("confirm").format(instance), prompt=prompt).getValue()
720 return ok
59721
60722
61723 class OriginalCommand:
195857
196858
197859 class MenuTree:
860 """
861 Used for the shortcuts menu
862 """
198863
199864 def __init__(self):
200865 self.nodes = []
240905 def __getitem__(self, key):
241906 return self.nodes[self.get_index(key)]
242907
243
244908 class Timer():
245909 def __init__(self, parent=None, options={}):
246910 """
247 Methods providing the action timer
911 Methods providing timers
248912 """
249 self.timer_clear()
250913 self.parent = parent
251914 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:
915 self.loop = parent.loop
916 self.timermenu = parent.timermenu
917 self.match = ""
918 self.etmtimers = os.path.normpath(os.path.join(options['etmdir'], ".etmtimers"))
919 self.dfile_encoding = options['encoding']['file']
920
921 self.resetTimers()
922
923 def updateMenu(self, e=None):
924 if self.activeTimers:
925 self.timermenu.entryconfig(1, state="active")
926 if self.currentTimer:
927 self.timermenu.entryconfig(2, state="active")
928 else:
929 self.timermenu.entryconfig(2, state="disabled")
930 self.timermenu.entryconfig(3, state="active")
931 else:
932 self.timermenu.entryconfig(1, state="disabled")
933 self.timermenu.entryconfig(2, state="disabled")
934 self.timermenu.entryconfig(3, state="disabled")
935
936 def resetTimers(self):
937 try:
938 self.loadTimers()
939 logger.info("reloaded saved timer data")
940 except:
941 self.activeDate = datetime.now().date()
942 self.activeTimers = {} # summary -> { total, start, stop }
943 self.currentTimer = None # summary
944 self.currentStatus = STOPPED
945 self.currentMinutes = 0
946 logger.info("reset timer data")
947
948 def selectTimer(self, e=None, new=True, title=None, name=None):
949 """
950 Combo box with list of active timer summaries and option to create a new, unique summary.
951 """
952 self.selected = None
953 self.new = new
954 if not self.activeTimers:
955 if not new:
956 return False
957 self.completions = []
958 if title is None:
959 title = _("Create Timer")
960 else:
961 if title is None:
962 title = _("Create or Choose Timer")
963 self.completions = []
964 tmp = [(self.activeTimers[x]['stop'], x) for x in self.activeTimers]
965 # put the most recently stopped timers at the top
966 sort = sorted(tmp, reverse=True)
967 self.completions = [x[1] for x in sort]
968
969 # return the focus to the right place
970 if self.parent.weekly or self.parent.monthly:
971 master = self.parent.canvas
972 else:
973 master = self.parent.tree
974 master=self.parent
975 self.timerswindow = win = Toplevel(master=master)
976 self.timerswindow.title(title)
977
978 self.filterValue = StringVar(master)
979 self.timerswindow.geometry("+%d+%d" % (master.winfo_rootx() + 50, master.winfo_rooty() + 50))
980 self.timerswindow.minsize(240, 30)
981
982 self.timerswindow.wm_attributes("-topmost", 1)
983
984 self.filterValue = StringVar(master)
985 self.filterValue.set("")
986 self.filterValue.trace_variable("w", self.setCompletions)
987 self.fltr = Entry(self.timerswindow, textvariable=self.filterValue)
988 self.fltr.pack(side="top", fill="x")
989 self.fltr.icursor(END)
990
991 self.listbox = listbox = Listbox(self.timerswindow, exportselection=False)
992 listbox.pack(side="bottom", fill=BOTH, expand=True)
993
994 self.timerswindow.bind("<Double-1>", self.completionSelected)
995 self.timerswindow.bind("<Return>", self.completionSelected)
996 self.timerswindow.bind("<Escape>", self.hideCompletions)
997 self.timerswindow.bind("<Up>", self.cursorUp)
998 self.timerswindow.bind("<Down>", self.cursorDown)
999 self.fltr.bind("<Up>", self.cursorUp)
1000 self.fltr.bind("<Down>", self.cursorDown)
1001 if name is not None:
1002 self.filterValue.set(name)
1003 self.setCompletions()
1004 win.wait_window(win)
1005
1006
1007 def setCompletions(self, *args):
1008 match = self.filterValue.get()
1009 self.matches = matches = [x for x in self.completions if x and x.lower().startswith(match.lower())]
1010 self.listbox.delete(0, END)
1011 for item in matches:
1012 self.listbox.insert(END, item)
1013 self.listbox.select_set(0)
1014 self.listbox.see(0)
1015 self.fltr.focus_set()
1016
1017 def is_active(self):
1018 return self.timerswindow is not None
1019
1020 def hideCompletions(self, e=None):
1021 # destroy widgets
1022 if not self.is_active():
2661023 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:
1024 self.fltr.destroy()
1025 self.fltr = None
1026 self.listbox.destroy()
1027 self.listbox = None
1028 self.timerswindow.destroy()
1029 self.timerswindow = None
1030
1031 def completionSelected(self, event):
1032 # Put the selected completion in the text, and close the list
1033 cursel = None
1034 if self.matches:
1035 cursel = self.matches[int(self.listbox.curselection()[0])]
1036 else:
1037 tmp = self.filterValue.get()
1038 if tmp in self.activeTimers or self.new:
1039 cursel = tmp
1040 logger.debug("cursel: {0}; match: {1}".format(cursel, self.match))
1041 self.hideCompletions(e=event)
1042 if cursel is not None:
1043 self.selected = cursel
1044 if self.new:
1045 self.startTimer()
1046
1047 def cursorUp(self, event=None):
1048 cursel = int(self.listbox.curselection()[0])
1049 # newsel = max(0, cursel=1)
1050 newsel = max(0, cursel - 1)
1051 self.listbox.select_clear(cursel)
1052 self.listbox.select_set(newsel)
1053 self.listbox.see(newsel)
1054 return "break"
1055
1056 def cursorDown(self, event=None):
1057 cursel = int(self.listbox.curselection()[0])
1058 newsel = min(len(self.matches) - 1, cursel + 1)
1059 self.listbox.select_clear(cursel)
1060 self.listbox.select_set(newsel)
1061 self.listbox.see(newsel)
1062 return "break"
1063
1064 def saveTimers(self):
1065 """
1066 dump activeTimers, ...
1067 """
1068 tmp = (
1069 self.activeDate,
1070 self.activeTimers,
1071 self.currentTimer,
1072 self.currentStatus,
1073 self.currentMinutes,
1074 )
1075 fo = codecs.open(self.etmtimers, 'w', self.dfile_encoding)
1076 yaml.dump(tmp, fo)
1077 fo.close()
1078 self.updateMenu()
1079
1080 def loadTimers(self):
1081 """
1082 load activeTimers
1083 """
1084 fo = codecs.open(self.etmtimers, 'r', self.dfile_encoding)
1085 tmp = yaml.load(fo)
1086 fo.close()
1087 (self.activeDate, self.activeTimers, self.currentTimer, self.currentStatus, self.currentMinutes) = tmp
1088 if self.activeDate != datetime.now().date():
1089 self.newDay()
1090 self.updateMenu()
1091
1092 def startTimer(self, e=None):
1093 self.pauseTimer()
1094 if not self.selected:
2741095 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):
1096
1097 summary = self.selected
1098 if summary not in self.activeTimers:
1099 # new timer
1100 hsh = {}
1101 hsh['total'] = 0 * ONEMINUTE
1102 hsh['stop'] = datetime.now()
1103 else:
1104 hsh = self.activeTimers[summary]
1105
1106 hsh['start'] = datetime.now()
1107 self.activeTimers[summary] = hsh
1108 self.currentTimer = summary
1109 self.currentStatus = RUNNING
1110
1111 self.saveTimers()
1112
1113 if self.parent:
1114 self.parent.updateTimerStatus()
1115 self.parent.update_idletasks()
1116
1117 def finishTimer(self, e=None):
1118 self.pauseTimer()
1119 self.selectTimer(new=False, title="Finish Timer")
1120 if not self.selected:
1121 return
1122
1123 self.currentStatus = STOPPED
1124 self.currentTimer = None
1125
1126 hsh = self.activeTimers[self.selected]
1127 hsh['summary'] = self.selected
1128
1129 self.saveTimers()
1130
1131 if self.parent:
1132 self.parent.updateTimerStatus()
1133 self.parent.update_idletasks()
1134
1135 return hsh
1136
1137 def deleteTimer(self, e=None, timer=None):
1138 # self.timerswindow.title("Delete")
1139 if timer is None:
1140 self.selectTimer(new=False, title="Delete Timer")
1141 timer = self.selected
1142 if not timer or timer not in self.activeTimers:
1143 return
1144 if self.currentTimer == timer:
1145 self.pauseTimer()
1146 self.currentTimer = None
1147 self.currentMinutes = 0
1148 self.currentStatus = STOPPED
1149 del self.activeTimers[timer]
1150 self.saveTimers()
1151 if self.parent:
1152 self.parent.updateTimerStatus()
1153 self.parent.update_idletasks()
1154
1155
1156 def newDay(self, e=None):
1157 now = datetime.now()
1158 self.activeDate = now.date()
1159 if not self.activeTimers:
1160 self.activeTimers = {} # summary -> { total, start, stop }
1161 self.currentTimer = None # summary
1162 self.currentStatus = STOPPED
1163 self.currentMinutes = 0
1164
1165 running = (self.currentTimer and self.currentStatus == RUNNING)
1166
1167 curfile = ensureMonthly(self.options, date=now.date())
1168 tmp = []
1169 for timer in self.activeTimers:
1170 # create inbox entries
1171 thsh = self.activeTimers[timer]
1172 hsh = {"itemtype": "$", "_summary": timer, "s": thsh['start'], "e": thsh['total']}
1173 tmp.append([timer, hsh])
1174
1175 for timer, hsh in tmp:
1176 res = self.loop.append_item(hsh, curfile)
1177 if res:
1178 del self.activeTimers[timer]
1179
1180 if running:
1181 # currentStatus == RUNNING
1182 hsh = {}
1183 hsh['total'] = 0 * ONEMINUTE
1184 hsh['start'] = hsh['stop'] = now
1185 self.activeTimers[self.currentTimer] = hsh
1186 else:
1187 self.currentTimer = None
1188 self.currentStatus = STOPPED
1189
1190 self.currentMinutes = 0
1191 self.saveTimers()
1192 if self.parent:
1193 self.parent.updateTimerStatus()
1194 self.parent.update_idletasks()
1195
1196 def toggleCurrent(self, e=None):
2831197 """
284 Called when action timer is started or restarted
1198
2851199 """
286 if not self.idle_active or self.idle_delta < ONEMINUTE:
1200 if not self.activeTimers or not self.currentTimer:
2871201 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()
1202
1203 hsh = self.activeTimers[self.currentTimer]
1204
1205 if self.currentStatus == RUNNING:
1206 hsh['total'] += datetime.now() - hsh['start']
1207 hsh['stop'] = datetime.now()
1208 self.currentStatus = PAUSED
1209
1210 elif self.currentStatus == PAUSED:
1211 hsh['start'] = datetime.now()
1212 self.currentStatus = RUNNING
1213
1214 self.activeTimers[self.currentTimer] = hsh
1215
1216 self.saveTimers()
1217
3451218 if self.parent:
1219 self.parent.updateTimerStatus()
3461220 self.parent.update_idletasks()
3471221
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
1222
1223 def pauseTimer(self):
1224 """
1225 Pause the running timer
1226 """
1227 if self.activeTimers and self.currentTimer and self.currentStatus == RUNNING:
1228 self.toggleCurrent()
1229
1230 self.saveTimers()
1231 if self.parent:
1232 self.parent.updateTimerStatus()
1233 self.parent.update_idletasks()
1234
1235 return False
1236
1237
1238
1239 def getStatus(self):
1240 """
1241 Return the status of the timers for the status bar
1242 """
1243 if self.currentTimer and self.currentStatus:
1244 hsh = self.activeTimers[self.currentTimer]
1245 now = datetime.now()
1246 if self.currentStatus == RUNNING:
1247 hsh['total'] = hsh['total'] + (now - hsh['start'])
1248 hsh['start'] = now
1249 self.activeTimers[self.currentTimer] = hsh
1250 total = hsh['total']
1251 self.currentMinutes = total.seconds // 60
1252
1253 ret1 = "{0}".format(self.currentTimer)
1254 ret2 = "{0} - {1} ({2})".format(self.currentStatus, fmt_period(hsh['total']), len(self.activeTimers.keys()))
1255 elif self.activeTimers:
1256 ret1 = "all paused ({0})".format(len(self.activeTimers))
1257 ret2 = ""
1258 else:
1259 ret1 = ret2 = ""
1260 logger.debug("timer: {0} {1}".format(ret1, ret2))
1261 return ret1, ret2
3771262
3781263
3791264 class ReadOnlyText(Text):
11021987 logger.debug('res: {0}'.format(res))
11031988 ok = False
11041989 if not res.strip():
1105 # return the current time if ok is pressed with no entry
1990 # return the current date if ok is pressed with no entry
1991 # val = get_current_time().replace(hour=0, minute=0, second=0, microsecond=0)
11061992 val = get_current_time()
11071993 ok = True
11081994 else:
+0
-631
etmTk/edit.py less more
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.')
00 .\" Text automatically generated by txt2man
1 .TH etm 1 "18 January 2015" "version 3.0.43" "Unix user's manual"
1 .TH etm 1 "21 June 2015" "version 3.1.18" "Unix user's manual"
22 .SH NAME
33 \fBetm \fP- manage events and tasks using simple text files
44 .SH SYNOPSIS
104104 <li><a href="#editing-existing-items">Editing Existing Items</a></li>
105105 <li><a href="#sharing-with-other-calendar-applications">Sharing with other calendar applications</a></li>
106106 <li><a href="#tools">Tools</a></li>
107 <li><a href="#data-organization-and-calendars">Data Organization and Calendars</a></li>
107 <li><a href="#calendars">Calendars</a></li>
108 <li><a href="#data-organization">Data Organization</a></li>
108109 </ul></li>
109110 <li><a href="#item-types">Item types</a><ul>
110111 <li><a href="#action">~ Action</a></li>
111112 <li><a href="#event">* Event</a></li>
112113 <li><a href="#occasion">^ Occasion</a></li>
113114 <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>
115 <li><a href="#tasks">Tasks</a></li>
117116 <li><a href="#in-basket">$ In basket</a></li>
118117 <li><a href="#someday-maybe">? Someday maybe</a></li>
119118 <li><a href="#hidden"># Hidden</a></li>
128127 <li><a href="#f-done-due"><span class="citation">@f</span> done[; due]</a></li>
129128 <li><a href="#g-goto"><span class="citation">@g</span> goto</a></li>
130129 <li><a href="#h-history"><span class="citation">@h</span> history</a></li>
130 <li><a href="#i-invitees"><span class="citation">@i</span> invitees</a></li>
131131 <li><a href="#j-job"><span class="citation">@j</span> job</a></li>
132132 <li><a href="#k-keyword"><span class="citation">@k</span> keyword</a></li>
133133 <li><a href="#l-location"><span class="citation">@l</span> location</a></li>
156156 <li><a href="#template-expansions">Template expansions</a></li>
157157 <li><a href="#options">Options</a></li>
158158 </ul></li>
159 <li><a href="#reports">Reports</a><ul>
160 <li><a href="#report-type-characters">Report type characters</a></li>
159 <li><a href="#custom-view">Custom view</a><ul>
160 <li><a href="#view-type">View type</a></li>
161161 <li><a href="#groupby-setting">Groupby setting</a></li>
162 <li><a href="#options-1">Options</a></li>
162 <li><a href="#groupby-examples">Groupby examples</a></li>
163 <li><a href="#view-options">View Options</a></li>
164 <li><a href="#saving-view-specifications">Saving view specifications</a></li>
163165 </ul></li>
164166 <li><a href="#shortcuts">Shortcuts</a><ul>
165167 <li><a href="#menubar">Menubar</a></li>
166 <li><a href="#main">Main</a></li>
167168 <li><a href="#edit">Edit</a></li>
168169 </ul></li>
169170 </ul>
206207 <p>To start the etm GUI open a terminal window and enter <code>etm</code> at the prompt:</p>
207208 <pre><code>$ etm</code></pre>
208209 <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>
210 <p>Note: if you change the window size and/or position of the etm window on your display and quit etm from the etm file menu, then the closing size and position will be restored when you restart etm.</p>
209211 <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>
210212 <pre><code>$ etm ?
211213 Usage:
231233 expansion.)
232234 d ARG display the day view using ARG, if given, as a filter.
233235 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.
236 m INT display a custom view using the remaining argument, which
237 must be a positive integer, to display a custom view using the
238 corresponding entry from the file given by report_specifications in etmtk.cfg.
237239 Use ? m to display the numbered list of entries from this file.
238240 n ARG display the notes view using ARG, if given, as a filter.
239241 N ARGS Create a new item using the remaining arguments as the item
247249 details about command X. &#39;X ?&#39; is equivalent to &#39;? X&#39;.</code></pre>
248250 <p>For example, you can print your agenda to the terminal window by adding the letter &quot;a&quot;:</p>
249251 <pre><code>$ etm a
250 Sun Apr 06, 2014
252 Today
251253 &gt; set up luncheon meeting with Joe Smith 4d
252 Mon Apr 07, 2014
254 Tomorrow
253255 * test command line event 3pm ~ 4pm
254256 * Aerobics 5pm ~ 6pm
255257 - 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
263258 Now
264259 Available
265260 - Hair cut -1d
271266 Someday
272267 ? lose weight and exercise more</code></pre>
273268 <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>
269 <pre><code>$ etm a hair
270 Now
271 Available
272 - Hair cut -1d</code></pre>
279273 <p>or <code>etm d mar .*2014</code> to show your items for March, 2014.</p>
280274 <p>You can add a question mark to a command to get details about the commmand, e.g.:</p>
281275 <pre><code>Usage:
306300 Options include:
307301 -b begin date
308302 -c context regex
309 -d depth (CLI a reports only)
303 -d depth (CLI type a only)
310304 -e end date
311305 -f file regex
312306 -k keyword regex
313307 -l location regex
314 -o omit (r reports only)
308 -o omit (type c only)
315309 -s summary regex
316310 -S search regex
317311 -t tags regex
327321 <pre><code> etm N &#39;123 456-7890&#39;</code></pre>
328322 <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>
329323 <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>
324 <p>All views display only items consistent with the current choices of active calendars. Click the settings icon on the left side of the top menu bar to choose active calendars.</p>
325 <p>Week and month views have three panes. The top one displays a graphic illustration of scheduled times for the relevant period, the middle one displays an tree view of items grouped by date and the bottom one displays detail information. Custom view also has three panes but the top one is an entry area for providing the specification for the custom view. All other views have two panes - a tree view in the top pane and details in the bottom pane.</p>
326 <p>If a (case-insensitive) filter is entered then the display in the tree 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>
327 <p>In week and month views, <em>Home</em> selects the current date. In all views other than week and month, <em>Home</em> selects the first item in the tree pane.</p>
328 <p>In all views, pressing <em>Return</em> with an item selected will open a context menu with options to copy, delete, edit and so forth.</p>
336329 <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>
330 <p>In all views, pressing <em>O</em> will open a dialog to choose the outline depth. 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;. 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>
331 <p>In custom view it is possible to export the current view in either text or CSV (comma separated values) format to a file of your choosing.</p>
332 <p>Note. In custom view you need to move the focus from the view specification entry field in order for the shortcuts <em>O</em>, <em>L</em> and <em>S</em> to work.</p>
342333 <p>In all views:</p>
343334 <ul>
344335 <li><p>if an item is selected:</p>
361352 <li><p><strong>Someday</strong>: Someday (maybe) items for periodic review.</p></li>
362353 </ul>
363354 <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>
355 <h3 id="week-and-month-views"><a href="#week-and-month-views">Week and Month Views</a></h3>
356 <p>These views only differ in whether the graphic in the top pane describes a week or a month. All dated items appear in the middle, tree pane in these view, grouped by date and sorted by starting time and item type. Displayed items include:</p>
366357 <ul>
367358 <li><p>All non-repeating, dated items.</p></li>
368359 <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>
369360 <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>
370361 </ul>
362 <p>The graphic display in the top pane has a square cell for each week/month date. Within this cell, scheduled, <em>busy</em> times are indicated by segments of a square busy border that surrounds the date. This border can be regarded as a 24-hour clock face that proceeds clockwise from 12am at the lower, left-hand corner with 6 hour segments for each side: 12am - 6am moving upward on the left side, 6am - 12pm moving rightward along the top, 12pm - 6pm moving downward along the right side and, finally, 6pm - 12pm moving leftward along the bottom. When nothing is scheduled for a date, the border is blank. When only one event is scheduled for a date, say from 9am until 3pm, then the border would be colored from the middle of the top side (9am) around the top, right-hand corner and downward to the middle of the right side (3pm). When other periods are scheduled, corresponding portions of the border would be colored. If two or more scheduled periods overlap, then a small, red box appears in the lower, left-hand corner of the border to warn of the conflict.</p>
363 <p>When the top pane has the focus, the left/right cursor keys move to the previous/subsequent week or month and the up/down cursor keys move to the previous/subsequent date. Either <em>Home</em> or <em>Space</em> moves the display to the current date. Pressing <em>J</em> will first prompt for a fuzzy-parsed date and then &quot;jump&quot; to the specified date. Whenever a date is selected in the top pane, the date tree in the middle pane is scrolled, if necessary, to display the selected date first. Whenever a date is selected in either week or month view, the same date is automatically becomes the selection in the other view as well.</p>
364 <p>Note: If a date is selected for which no items are scheduled, then the last date with scheduled items on or before the selected date will become the selected date in the middle, tree pane.</p>
371365 <p>Tip: Want to see your next appointment with Dr. Jones? Switch to day view and enter &quot;jones&quot; in the filter.</p>
366 <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 accommodate the requirement within the selected week/month. Both options are in the <em>View</em> menu.</p>
372367 <h3 id="week-view"><a href="#week-view">Week View</a></h3>
373368 <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>
374369 <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>
376370 <h3 id="month-view"><a href="#month-view">Month View</a></h3>
377371 <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>
378372 <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>
373 <p>Days with scheduled events have an <em>active times</em> border that wraps clockwise around the date box with 12am - 6am upward along the left side, 6am - 12pm to the right along the top, 12pm - 6pm down the right side and 6pm - 12am to the left along the bottom. Days with conflicting (overlapping) busy times have a red box in the lower, left-hand corner of the date box.</p>
374 <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 accommodate the requirement within the selected month. Both options are in the <em>View</em> menu.</p>
381375 <h3 id="tag-view"><a href="#tag-view">Tag View</a></h3>
382376 <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>
383377 <p>Tip: Use the filter to limit the display to items with a particular tag.</p>
389383 <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>
390384 <h3 id="note-view"><a href="#note-view">Note View</a></h3>
391385 <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>
386 <h3 id="custom"><a href="#custom">Custom</a></h3>
387 <p>Design your own view. See <a href="#custom-view">Custom view</a> for details.</p>
394388 <h2 id="creating-new-items"><a href="#creating-new-items">Creating New Items</a></h2>
395389 <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>
390 <p>An event can also be created by double-clicking in a date cell in either Week or Month View - the corresponding date will be entered as the starting date when the dialog opens.</p>
391 <p>For people who bill their time or just like to keep track of how they spend their time, etm allows you to create an action by pressing <em>T</em> to start a timer. You will see an entry area with a list of any existing timers below. As you enter characters in the entry area, the list below will shrink to those whose beginnings match the characters you've entered. You can either select a timer from the list to start that timer or enter new name in the entry area to create and start a new timer. If a timer is running, it will automatically be paused when you start a new timer or switch to another timer.</p>
392 <p>Tip. Devoting time to two different clients this morning? Create two timers, one for each client and just switch back and forth using <em>T</em> when you switch from one client to the other. The timers are ordered in the list so that the most recently paused will be at the top.</p>
393 <p>While a timer is selected, the name, elapsed time and status - running or paused - is displayed in the status bar along with the number of active timers in parentheses. Pressing <em>I</em> toggles the timer between running and paused. You can configure etm's options to, for example, play one sound at intervals when a timer is running and another sound when the selected timer is paused and you can also specify the length of the interval and the volume.</p>
394 <p>When you have one or more active timers, you can press <em>Shift-T</em> to select one to finish. The selected timer will be paused if it is running and you will be presented with an entry area to create a new action with the following details already filled in: <code>~ timer name @s starting datetime @e elapsed time</code>. You can edit this entry in any way you like and then save it. When you do so, this timer will be removed from your list of active timers. Alternatively, you can press <em>Shift-I</em> to select a timer to be deleted from your list of active timers.</p>
395 <p>A final way to start a timer is to select an item, event, note, task or whatever, from one of <em>etm</em>'s Views and then choose <em>Item/Klone as timer</em> from the menu or press <em>K</em>. A start timer dialog will be opened with the summary of the item you selected as the name. You can edit this name if you like or just press <em>Return</em> to accept it and start the timer. If you already have an active timer with this name, it will be restarted. Otherwise a new timer will be created and started.</p>
396 <p>The state of your active timers is saved whenever you quit etm using by choosing <em>Quit</em> from the file menu or using the shortcut so that whenever you restart etm on the same day, the active timers will be restored.</p>
397 <p>If etm is running when a new day begins (midnight local time) or if you stop etm and start it again on a later date, in-basket entries for each of your active timers will be created in the relevant monthly file. These entries will be exactly the same as if you had finished each of the timers save for the use of <code>$</code> (in basket) rather than <code>~</code> (action) as the type character. You can edit or delete these as you wish. If a timer is selected (displayed in the status bar), then a new timer with the same name will be created for the new date but with zero elapsed time. If the timer was running at midnight, then the new timer will also be running.</p>
400398 <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>
401399 <pre><code>Task scheduled for Tue Jun 03
402400
404402 <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>
405403 <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>
406404 <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>
412405 <h2 id="editing-existing-items"><a href="#editing-existing-items">Editing Existing Items</a></h2>
413406 <p>Double-clicking an item or pressing <em>Return</em> when an item is selected will open a context menu of possible actions:</p>
414407 <ul>
419412 <li>Finish (unfinished tasks only)</li>
420413 <li>Reschedule</li>
421414 <li>Schedule new</li>
415 <li>Klone as timer</li>
422416 <li>Open link (items with <code>@g</code> entries only)</li>
423417 <li>Show user details (items with <code>@u</code> entries only)</li>
424418 </ul>
494488 <pre><code> 4/20 4:50p Asia/Shanghai + 14h25m US/Central:
495489
496490 2014-04-20 18:15-0500</code></pre>
497 <p>The local timezone is assumed when none is given.</p>
491 <p>Fuzzy dates (other than relative date expressions using <code>+</code> or <code>-</code>) can be used to specify date entries. The local timezone is assumed when none is given.</p>
498492 <h3 id="available-dates-calculator"><a href="#available-dates-calculator">Available dates calculator</a></h3>
499 <p>Enter an expression of the form</p>
493 <p>Need to see a list of possible dates for a meeting? Get a list of busy dates from each of the members of the group and then use an expression of the form</p>
500494 <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>
495 <p>where start and end are dates and busy is a comma separated list of the busy dates or intervals for the members. E.g., if your group needs to meet between 6/1 and 6/30 and the members indicate that they cannot meet on 6/2, 6/14-6/22, 6/5-6/9, 6/11-6/15 or 6/17-6/29, then entering</p>
502496 <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>
503497 <p>would give:</p>
504498 <pre><code>Sun Jun 01
506500 Wed Jun 04
507501 Tue Jun 10
508502 Mon Jun 30</code></pre>
503 <p>as the possible dates for the meeting.</p>
509504 <h3 id="yearly-calendar"><a href="#yearly-calendar">Yearly calendar</a></h3>
510505 <p>Gives a display such as</p>
511506 <pre><code> January 2014 February 2014 March 2014
541536 13 14 15 16 17 18 19 10 11 12 13 14 15 16 15 16 17 18 19 20 21
542537 20 21 22 23 24 25 26 17 18 19 20 21 22 23 22 23 24 25 26 27 28
543538 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>
539 <p>Left and right cursor keys move backward and forward a year at a time, respectively, and pressing the Home key returns to the current year.</p>
545540 <h3 id="history-of-changes"><a href="#history-of-changes">History of changes</a></h3>
546541 <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>
542 <h2 id="calendars"><a href="#calendars">Calendars</a></h2>
543 <p><em>etm</em> supports using the directory structure in your data directory to create separate <em>calendars</em>. For example, my wife, <em>erp</em>, and I, <em>dag</em>, separate personal and shared items with this structure:</p>
551544 <pre><code>root etm data directory
552545 personal
553546 dag
556549 holidays
557550 birthdays
558551 events</code></pre>
559 <p>Here</p>
552 <p>Here, our etm configuration files are located in our home directories:</p>
560553 <pre><code>~dag/.etm/etm.cfg
561554 ~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
555 <p>Both contain <code>datadir</code> entries specifying the common root data directory mentioned above with these additional entries, respectively:</p>
556 <p>In <code>~dag/.etm/etm.cfg</code>:</p>
557 <pre><code> calendars
565558 - [dag, true, personal/dag]
566559 - [erp, false, personal/erp]
567560 - [shared, true, shared]</code></pre>
568 <p>and</p>
569 <pre><code>~erp/.etm/etm.cfg
570 calendars
561 <p>In <code>~erp/.etm/etm.cfg</code>:</p>
562 <pre><code> calendars
571563 - [erp, true, personal/erp]
572564 - [dag, false, personal/dag]
573565 - [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>
566 <p>Thus, by default, both <em>dag</em> and <em>erp</em> see the entries from their personal files as well as the shared entries and each can 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>
575567 <p>Note for Windows users. The path separator needs to be &quot;escaped&quot; in the calendar paths, e.g., you should enter</p>
576568 <pre><code> - [dag, true, personal\\dag]</code></pre>
577569 <p>instead of</p>
578570 <pre><code> - [dag, true, personal\dag]</code></pre>
571 <h2 id="data-organization"><a href="#data-organization">Data Organization</a></h2>
572 <p><em>etm</em> offers many ways of organizing your data. Perhaps, the most obvious is by <em>path</em>, i.e., the directory structure inside your data directory. <em>Path View</em> presents your data using this organization and, as noted above, calendars can be specified using this structure to allow you to choose quickly the calendars whose items will appear other etm views as well.</p>
573 <p>The other hierarchical way of organizing your data uses the keywords you specify in your items. <em>Keyword View</em> presents your data using this organization. E.g.,</p>
574 <pre><code>- my task @k A:B:C
575 - my other task</code></pre>
576 <p>would appear in <em>Keyword View</em> as:</p>
577 <pre><code>A
578 B
579 C
580 - my task
581 ~ none ~
582 - my other task</code></pre>
583 <p>There are no hard and fast rules about how to use these hierarchies but the goal is a system that makes complementary uses of path and keyword and fits your needs. As with any filing system, planning and consistency are paramount. For example, one pattern of use for a business might be to use folders for departments and people and keywords for client and project.</p>
584 <p>It is also possible to add one or more tags to items and use <em>Tag View</em> to see the resulting organization. For example</p>
585 <pre><code>- item 1 @t red, white, blue
586 - item 2 @t red @t white
587 - item 3 @t white @t blue
588 - item 4 @t red, blue
589 - item 5 @t white</code></pre>
590 <p>would appear in <em>Tag View</em> as</p>
591 <pre><code>blue
592 - item 1
593 - item 3
594 - item 4
595 red
596 - item 1
597 - item 2
598 - item 4
599 white
600 - item 1
601 - item 2
602 - item 3
603 - item 5</code></pre>
604 <p>A final important way of organizing your data is provided by <em>context</em>. This is designed to support a GTD (Getting Things Done) common practice where possible contexts includes things like phone, errands, email and so forth. Undated tasks such as</p>
605 <pre><code>- pick up milk @c errands
606 - call Saul @c phone
607 - confirm schedule with Bob @c email</code></pre>
608 <p>would appear <em>Agenda View</em> as</p>
609 <pre><code>Next
610 email
611 - confirm schedule with Bob
612 errands
613 - pick up milk
614 phone
615 - call Saul
616 </code></pre>
617 <p>When you are next checking email, running errands, using the phone or whatever, you can check <em>Agenda View</em> to see what else might be accomplished at the same time. Note that, unlike tags, items can have at most a single context.</p>
579618 <h1 id="item-types"><a href="#item-types">Item types</a></h1>
580619 <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>
581620 <h2 id="action"><a href="#action">~ Action</a></h2>
582621 <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>
583622 <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>
623 <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 type custom views - see <a href="#custom-view">Custom view</a> for details.</p>
585624 <p>Tip: You can use either path or keyword or a combination of the two to organize your actions.</p>
586625 <h2 id="event"><a href="#event">* Event</a></h2>
587626 <p>Something that will happen on particular day(s) and time(s). Event lines begin with an asterick, <code>*</code>.</p>
598637 <p>A record of some useful information. Note lines begin with an exclamation point, <code>!</code>.</p>
599638 <pre><code>! xyz software @k software:passwords @d user: dnlg, pw: abc123def</code></pre>
600639 <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>
640 <h2 id="tasks"><a href="#tasks">Tasks</a></h2>
641 <p>Tasks are reminders of something that needs to be done. There are three possible type characters for tasks: <code>-</code>, <code>%</code> and <code>+</code>; these are discussed below. Each of these can be further distinguished by whether or not the task has entries for <code>@e</code> and/or <code>@s</code>.</p>
642 <p>When an <code>@e</code> (extent) is provided for any type of task, it is regarded as an estimate of the time required to complete the task.</p>
643 <ul>
644 <li><p>Tasks without an <code>@s</code> entry have no due date and are to be done whenever convenient.</p></li>
645 <li><p>Tasks with an <code>@s</code> entry that specifies <code>12am</code> (or <code>0h</code>) as the starting time are to be completed on or before the date specified.</p></li>
646 <li><p>Tasks with an <code>@s</code> entry that specifies a starting time <em>other than</em> <code>12am</code> (or <code>0h</code>) but without an <code>@e</code> entry are to be completed on or before the date <em>and time</em> specified. These will be displayed in etm Agenda and Day views before other tasks, sorted by and displaying the starting time.</p></li>
647 <li><p>Tasks with an <code>@s</code> entry that specifies a starting time <em>other than</em> <code>12am</code> (or <code>0h</code>) and with an <code>@e</code> entry are to be completed during the period that extends from the starting time until <code>@e</code> after the starting time. These will be displayed in etm Agenda and Day views before other tasks, sorted by and displaying the time period in the same manner as events. This period will be regarded as busy time and treated as such by etm in, e.g., Week view. [Tip: Use a non-midnight starting time and an extent when you want to block off a specific period to complete a task.]</p></li>
648 </ul>
649 <h3 id="task"><a href="#task">- Task</a></h3>
650 <p>This is the basic task and begins with a minus sign, <code>-</code>.</p>
603651 <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>
652 <p>A task with an <code>@s</code> entry becomes due on that date and time 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.</p>
653 <h3 id="delegated-task"><a href="#delegated-task">% Delegated task</a></h3>
606654 <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>
607655 <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>
656 <h3 id="task-group"><a href="#task-group">+ Task group</a></h3>
609657 <p>A collection of related tasks, some of which may be prerequisite for others. Task groups begin with a plus sign, <code>+</code>.</p>
610658 <pre><code> + dog house
611659 @j pickup lumber and paint &amp;q 1
619667 <pre><code> $ joe 919 123-4567</code></pre>
620668 <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>
621669 <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>
670 <p>Something 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>. They are displayed under the heading <em>Someday</em> in Agenda view so that you can easily review them whenever you like.</p>
623671 <pre><code> ? lose weight and exercise more</code></pre>
624672 <h2 id="hidden"><a href="#hidden"># Hidden</a></h2>
625673 <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>
678726 <p>An elaboration of the details of the item to complement the summary.</p>
679727 <h2 id="e-extent"><a href="#e-extent"><span class="citation">@e</span> extent</a></h2>
680728 <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>
729 <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 Shift-D) will accept timezone information so that, e.g., entering the arrival time minus the departure time</p>
682730 <pre><code>4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai</code></pre>
683731 <p>into the calculator would give</p>
684732 <pre><code>14h25m</code></pre>
699747 <h2 id="g-goto"><a href="#g-goto"><span class="citation">@g</span> goto</a></h2>
700748 <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>
701749 <pre><code>- join the etm discussion group @s +1/1
702 @g http://groups.google.com/group/eventandtaskmanager/topics</code></pre>
750 @g http://groups.google.com/group/eventandtaskmanager/topics
751 </code></pre>
752 <p>Template expansion is supported so it is also possible to use a <code>mailto</code> link such as the following:</p>
753 <pre><code>- the subject of the email @d The body of the email
754 @g mailto:sam@what.com?cc=joe@when.net\&amp;subject=!summary!\&amp;body=!d!
755 </code></pre>
756 <p>Pressing <em>G</em> with this item selected would create a new message in your email application with &quot;To: sam@what.com&quot;, &quot;Cc: joe@when.net&quot;, &quot;Subject: The subject of the email&quot; and &quot;The body of the email&quot; already entered.</p>
703757 <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>
704758 <h2 id="h-history"><a href="#h-history"><span class="citation">@h</span> history</a></h2>
705759 <p>Used internally with task groups to track completion done;due pairs.</p>
760 <h2 id="i-invitees"><a href="#i-invitees"><span class="citation">@i</span> invitees</a></h2>
761 <p>An email address or a list of email addresses for people participating in the item.</p>
706762 <h2 id="j-job"><a href="#j-job"><span class="citation">@j</span> job</a></h2>
707763 <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>
708764 <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>
781837 <li><p>Easter Sunday (an occasion).</p>
782838 <p>^ Easter Sunday 2010-01-01 <span class="citation">@r</span> y &amp;E 0</p></li>
783839 </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>
789840 <p>A repeating <em>task</em> may optionally also include an <code>@o &lt;k|s|r&gt;</code> entry (default = k):</p>
790841 <ul>
791842 <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>
819870 <pre><code>* Sydney to New York @s 2014-04-23 9pm @e 14h30m @z Australia/Sydney</code></pre>
820871 <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>
821872 <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>
873 <p>A datetime, e.g., <code>@+ 20150420T0930</code>, or list of datetimes to be added to the repetitions generated by <code>@r rrule</code> entries. If only a date is provided, 12:00am is assumed.</p>
823874 <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>
875 <p>A datetime or list of datetimes to be removed from the repetitions generated by <code>@r rrule</code> entries. If only a date is provided, 12:00am is assumed.</p>
876 <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 one of the <code>@r rrule</code> entries.</p>
877 <p>Example of using <code>@-</code> and <code>@+</code>:</p>
878 <pre><code>@s 2014-02-19 4pm
879 @r m &amp;w 3WE
880 @+ 20140924T1600, 20141029T1600
881 @- 20140917T1600, 20141015T1600</code></pre>
826882 <h1 id="dates"><a href="#dates">Dates</a></h1>
827883 <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>
884 <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> and jumping to a date using Ctrl-J.</p>
829885 <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>
830886 <pre><code> mon 2p or mon 14h 2:00pm Monday, February 19
831887 fri 12:00am Friday, February 15
834890 8p +7 or 20h +7 8:00pm Friday, February 22
835891 -14 12:00am Friday, February 1
836892 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>
893 <p>Note that expressions using <code>+</code> or <code>-</code> give datetimes relative to the current datetime.</p>
894 <p>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>
838895 <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>
839896 <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>
897 <p>Time periods are entered using the format <code>WwDdHhMm</code> where W, D, H and M are integers and w, d, h and m refer to weeks, days, hours and minutes respectively. For example:</p>
841898 <pre><code> 2h30m 2 hours, 30 minutes
842 7d 7 days
899 2w3d 2 weeks, 3 days
843900 45m 45 minutes</code></pre>
844901 <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>
845902 <h2 id="time-zones"><a href="#time-zones">Time zones</a></h2>
849906 <p>then the data file would contain</p>
850907 <pre><code>@s 2013-02-16 12:50am @z Australia/Sydney</code></pre>
851908 <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>
909 <p>Tip. Need to determine the flight time when the departing timezone is different that the arriving timezone? The date calculator (shortcut Shift-D) will accept timezone information so that, e.g., entering the arrival time minus the departure time</p>
853910 <pre><code>4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai</code></pre>
854911 <p>into the calculator would give</p>
855912 <pre><code>14h25m</code></pre>
871928 <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>
872929 <h2 id="template-expansions"><a href="#template-expansions">Template expansions</a></h2>
873930 <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>
931 <ul>
932 <li><code>!summary!</code></li>
933 </ul>
875934 <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>
935 <ul>
936 <li><code>!start_date!</code></li>
937 </ul>
877938 <p>the starting date of the event</p>
878 <h3 id="start_time"><a href="#start_time"><code>!start_time!</code></a></h3>
939 <ul>
940 <li><code>!start_time!</code></li>
941 </ul>
879942 <p>the starting time of the event</p>
880 <h3 id="time_span"><a href="#time_span"><code>!time_span!</code></a></h3>
943 <ul>
944 <li><code>!time_span!</code></li>
945 </ul>
881946 <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>
947 <ul>
948 <li><code>!alert_time!</code></li>
949 </ul>
883950 <p>the time the alert is triggered</p>
884 <h3 id="time_left"><a href="#time_left"><code>!time_left!</code></a></h3>
951 <ul>
952 <li><code>!time_left!</code></li>
953 </ul>
885954 <p>the time remaining until the event starts</p>
886 <h3 id="when"><a href="#when"><code>!when!</code></a></h3>
955 <ul>
956 <li><code>!when!</code></li>
957 </ul>
887958 <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>
959 <ul>
960 <li><code>!d!</code></li>
961 </ul>
889962 <p>the item's <code>@d</code> (description)</p>
890 <h3 id="l"><a href="#l"><code>!l!</code></a></h3>
963 <ul>
964 <li><code>!l!</code></li>
965 </ul>
891966 <p>the item's <code>@l</code> (location)</p>
892967 <p>The value of <code>!time_span!</code> depends on the starting and ending datetimes. Here are some examples:</p>
893968 <ul>
9271002 !charge! = 38.70</code></pre>
9281003 <h3 id="action_minutes"><a href="#action_minutes">action_minutes</a></h3>
9291004 <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>
1005 <p>Round action times up to the nearest <code>action_minutes</code> in action custom view. 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>
9311006 <h3 id="action_rates"><a href="#action_rates">action_rates</a></h3>
9321007 <pre><code>action_rates:
9331008 default: 30.0
9471022 !value! = 118.50 (= 1.3 * 45.0 + 1 * 60.0)</code></pre>
9481023 <h3 id="action_template"><a href="#action_template">action_template</a></h3>
9491024 <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>
1025 <p>Used for action type custom view. With the above settings for <code>action_minutes</code> and <code>action_template</code>, a custom view might appear as follows:</p>
9511026 <pre><code>27.5h) Client 1 (3)
9521027 4.9h) Project A (1)
9531028 15h) Project B (1)
9721047 <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>
9731048 <li><p><code>!total!</code>: the sum of <code>!value!</code> and <code>!charge!</code>.</p></li>
9741049 </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>
1050 <p>Note: when aggregating amounts in action type custom view, 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>
9761051 <h3 id="action_timer"><a href="#action_timer">action_timer</a></h3>
9771052 <pre><code>action_timer:
9781053 paused: &#39;play ~/.etm/sounds/timer_paused.wav&#39;
9801055 idle: &#39;play ~/.etm/sounds/timer_idle.wav&#39;</code></pre>
9811056 <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>
9821057 <h3 id="agenda"><a href="#agenda">agenda</a></h3>
983 <pre><code>agenda_days: 4,
1058 <pre><code>agenda_days: 2,
9841059 agenda_colors: 2,
9851060 agenda_indent: 2,
9861061 agenda_width1: 43,
9871062 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>
1063 <p>Sets the number of days to display in agenda view and other parameters affecting the display in the CLI. The colors setting only affects output to current_html.</p>
9891064 <h3 id="alert_default"><a href="#alert_default">alert_default</a></h3>
9901065 <pre><code>alert_default: [m]</code></pre>
9911066 <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>
10101085 <h3 id="completions_width"><a href="#completions_width">completions_width</a></h3>
10111086 <pre><code>completions_width: 36</code></pre>
10121087 <p>The width in characters of the auto completions popup window.</p>
1013 <h3 id="calendars"><a href="#calendars">calendars</a></h3>
1088 <h3 id="calendars-1"><a href="#calendars-1">calendars</a></h3>
10141089 <pre><code>calendars:
10151090 - [dag, true, personal/dag]
10161091 - [erp, false, personal/erp]
10411116 <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>
10421117 <p>Up and down arrow keys change the selection and either <em>Tab</em> or <em>Return</em> inserts the selection.</p></li>
10431118 <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>
1119 <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="#custom-view">Custom view</a> for details.</p></li>
10451120 <li><p>Users</p>
10461121 <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>
10471122 <pre><code>jbrown:
10811156 <h3 id="details_rows"><a href="#details_rows">details_rows</a></h3>
10821157 <pre><code>details_rows: 4</code></pre>
10831158 <p>The number of rows to display in the bottom, details panel of the main window.</p>
1159 <h3 id="early_hour"><a href="#early_hour">early_hour</a></h3>
1160 <pre><code>early_hour: 6</code></pre>
1161 <p>When scheduling an event or action with a starting time that begins before this hour, append the query &quot;Is __ the starting time you intended?&quot; to the confirmation. Use 0 to disable this warning altogether. The default, 6, will warn for starting times <em>before</em> 6am.</p>
10841162 <h3 id="edit_cmd"><a href="#edit_cmd">edit_cmd</a></h3>
10851163 <pre><code>edit_cmd: ~/bin/vim !file! +!line!</code></pre>
10861164 <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>
11021180 <h3 id="etmdir"><a href="#etmdir">etmdir</a></h3>
11031181 <pre><code>etmdir: ~/.etm</code></pre>
11041182 <p>Absolute path to the directory for etm.cfg and other etm configuration files.</p>
1183 <h3 id="exportdir"><a href="#exportdir">exportdir</a></h3>
1184 <pre><code>exportdir: ~/.etm</code></pre>
1185 <p>Absolute path to the directory for exported CSV files.</p>
11051186 <h3 id="encoding"><a href="#encoding">encoding</a></h3>
11061187 <pre><code>encoding: {file: utf-8, gui: utf-8, term: utf-8}</code></pre>
11071188 <p>The encodings to be used for file IO, the GUI and terminal IO.</p>
11991280 a k, MMM yyyy -b -1/1 -e 1
12001281 c ddd MMM d yyyy
12011282 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>
1283 <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="#custom-view">Custom view</a> for details. See also the <a href="#agenda">agenda</a> settings above.</p>
12031284 <h3 id="retain_ids"><a href="#retain_ids">retain_ids</a></h3>
12041285 <pre><code>retain_ids: false</code></pre>
12051286 <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>
12721353 <h3 id="yearfirst"><a href="#yearfirst">yearfirst</a></h3>
12731354 <pre><code>yearfirst: true</code></pre>
12741355 <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>
1356 <h1 id="custom-view"><a href="#custom-view">Custom view</a></h1>
1357 <p>You can create a custom display of your items using either the <em>custom view</em> in the GUI or the &quot;c&quot; command in the CLI. In both cases, you enter a specification that determines which items will be displayed and how they will be grouped and sorted.</p>
1358 <p>A view <em>specification</em> begins with a <em>type character</em>, either <code>a</code> or <code>c</code>, followed by a <em>groupby setting</em> and then, perhaps, by one or more view <em>options</em>.</p>
1359 <h2 id="view-type"><a href="#view-type">View type</a></h2>
1360 <ul>
1361 <li><p><strong>a</strong>: action</p>
1362 <p>Actions only. 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 actions output. Note that only <em>actions</em> are included in this view and, by default, all actions in your active calendars will be included.</p></li>
1363 <li><p><strong>c</strong>: composite</p>
1364 <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. By default, all items from your active calendars will be included.</p></li>
12881365 </ul>
12891366 <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>
1367 <p>A semicolon separated list that determines how items will be grouped and sorted. Possible elements include elements from</p>
12911368 <ul>
12921369 <li><p>c: context</p></li>
12931370 <li><p>f: file path</p></li>
12941371 <li><p>k: keyword</p></li>
1372 <li><p>l: location</p></li>
12951373 <li><p>t: tag</p></li>
12961374 <li><p>u: user</p></li>
12971375 </ul>
1298 <p>A <em>date specification</em> is either</p>
1376 <p>and/or a <em>date specifications</em> where a <em>date specification</em> is either</p>
12991377 <ul>
13001378 <li>w: week number</li>
13011379 </ul>
13101388 <li><p>ddd: locale specific abbreviated week day: Mon - Sun</p></li>
13111389 <li><p>dddd: locale specific week day: Monday - Sunday</p></li>
13121390 </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>
1391 <p>Note that the groupby specification affects which items will be displayed. 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. The latter behavior depends upon the value of the <a href="#m-missing">-m MISSING</a> option.</p>
1392 <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>
1393 <p>Within groups, items are automatically sorted by date, type and time.</p>
1394 <h2 id="groupby-examples"><a href="#groupby-examples">Groupby examples</a></h2>
1395 <p>For example, the specification <code>c ddd, MMM dd yyyy</code> would group by year, month and day together to give output such as</p>
13141396 <pre><code>Fri, Apr 1 2011
13151397 items for April 1
13161398 Sat, Apr 2 2011
13171399 items for April 2
13181400 ...</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>
1401 <p>On the other hand, the specification <code>a w; u; k[0]; k[1:]</code> would group by week number, user and keywords to give output such as</p>
13201402 <pre><code>13.1) 2014 Week 14: Mar 31 - Apr 6
13211403 6.3) agent 1
13221404 1.3) client 1
13421424 f[2:] = C
13431425 f = A/B/C</code></pre>
13441426 <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
1427 <pre><code>specification: a MMM yyyy; u; k[0]; k[1] -b 1 -e +1/1
13461428
13471429 13.1) Feb 2014
13481430 6.3) agent 1
13631445 3.9) Activity 23
13641446 0.7) project 2
13651447 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>
1448 <h2 id="view-options"><a href="#view-options">View Options</a></h2>
1449 <p>View options are listed below. View type <code>c</code> supports all options except <code>-d</code>. Type <code>a</code> supports all options except <code>-o</code>. These options can be used to further limit which items are displayed.</p>
13711450 <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>
1451 <p>Fuzzy parsed date. When a date specification is provided, 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>
13731452 <h3 id="c-context-1"><a href="#c-context-1">-c CONTEXT</a></h3>
13741453 <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>
1454 <h3 id="d-depth-currently-broken"><a href="#d-depth-currently-broken">-d DEPTH (currently broken)</a></h3>
13761455 <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
1456 <p>For example, modifying the specification above by adding <code>-d 3</code> would give the following:</p>
1457 <pre><code>specification: a MMM yyyy; u; k[0]; k[1] -b 1 -e +1/1 -d 3
13791458
13801459 13.1) Feb 2014
13811460 6.3) agent 1
13851464 2.2) client 1
13861465 4.6) client 2</code></pre>
13871466 <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>
1467 <p>Fuzzy parsed date. When a date specification is provided, 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>
13891468 <h3 id="f-file"><a href="#f-file">-f FILE</a></h3>
13901469 <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>
13911470 <h3 id="k-keyword-1"><a href="#k-keyword-1">-k KEYWORD</a></h3>
13921471 <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>
13931472 <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>
1473 <p>Regular expression. Limit the display to items with a 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>
1474 <h3 id="m-missing"><a href="#m-missing">-m MISSING</a></h3>
1475 <p>Either 0 (the default) or 1. When 1 include items that would otherwise be excluded because of a <em>non-date</em>, groupby specification. E.g., <code>c k</code> would omit items without a keyword entry, but <code>c k -m 1</code> would include such items under a &quot;None&quot; heading. This option does not apply to date specifications, i.e., if a date specification is part of the groupby setting, then undated items will be excluded whatever the value of <code>-m</code>.</p>
13951476 <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>
1477 <p>String. Composite type only. Show/hide a)ctions, d)elegated tasks, e)vents, g)roup tasks, n)otes, o)ccasions, s)omeday items and/or 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>
13971478 <h3 id="s-summary"><a href="#s-summary">-s SUMMARY</a></h3>
13981479 <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>
13991480 <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>
1481 <p>Regular expression. Composite type only. Limit the display to items containing SEARCH (ignoring case) anywhere in the <em>item</em> 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>
14011482 <h3 id="t-tags-1"><a href="#t-tags-1">-t TAGS</a></h3>
14021483 <p>Comma separated list of case insensitive regular expressions. E.g., use</p>
14031484 <pre><code>-t tag1, !tag2</code></pre>
14061487 <p>to display items with one or more tags that match 'tag1' but none that match 'tag2'.</p>
14071488 <h3 id="u-user-1"><a href="#u-user-1">-u USER</a></h3>
14081489 <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>
1490 <h2 id="saving-view-specifications"><a href="#saving-view-specifications">Saving view specifications</a></h2>
1491 <p>You can save view specifications in your specifications file, <code>~./etm/reports.cfg</code> by default, and then select them in the selection box at the bottom of the custom view window in the GUI or from a list in the CLI.</p>
1492 <p>You can also add specifications to file in the GUI 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>
14091493 <h1 id="shortcuts"><a href="#shortcuts">Shortcuts</a></h1>
14101494 <h2 id="menubar"><a href="#menubar">Menubar</a></h2>
14111495 <pre><code>File
14121496 New
14131497 Item N
14141498 File Shift-N
1415 Begin/Pause Action Timer T
1499 Timer
1500 Start Action Timer T
14161501 Finish Action Timer Shift-T
1417 Start/Resolve Idle Timer I
1418 Stop Idle Timer Shift-I
1502 Toggle Current Timer I
1503 Delete Action Timer Shift-I
14191504 Open
14201505 Data file ... Shift-F
14211506 Configuration file ... Shift-C
14241509 ----
14251510 Quit Ctrl-Q
14261511 View
1427 Home Space
1428 Show remaining alerts for today A
1429 Jump to date J
1512 Agenda Ctrl-A
1513 Week Ctrl-W
1514 Month Ctrl-M
1515 Tag Ctrl-T
1516 Keyword Ctrl-K
1517 Path Ctrl-P
1518 Note Ctrl-N
1519 Custom Ctrl-C
14301520 ----
1431 Next sibling Control-Down
1432 Previous sibling Control-Up
14331521 Set outline filter Ctrl-F
14341522 Clear outline filter Shift-Ctrl-F
1435 Toggle displaying labels column L
1523 Toggle labels L
14361524 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
1525 Toggle displayings finished X
14471526 Item
14481527 Copy C
14491528 Delete BackSpace
14531532 Move M
14541533 Reschedule R
14551534 Schedule new Shift-R
1535 Klone as timer K
14561536 Open link G
14571537 Show user details U
14581538 Tools
1539 Home Home
1540 Jump to date J
1541 ----
1542 Show remaining alerts for today A
1543 List busy times in week/month B
1544 List free times in week/month F
14591545 Date and time calculator Shift-D
14601546 Available dates calculator Shift-A
14611547 Yearly calendar Shift-Y
1462 History of changes Shift-H
1548 ----
1549 Show outline as text S
1550 Print outline P
14631551 Export to iCal Shift-X
14641552 Update calendar subscriptions Shift-M
14651553 Reload data from files Shift-L
1554 History of changes Shift-H
14661555 Custom
14671556 Create and display selected report Return
14681557 Export report in text format ... Ctrl-T
14751564 User manual F1
14761565 About F2
14771566 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>
14921567 <h2 id="edit"><a href="#edit">Edit</a></h2>
14931568 <pre><code>Show completions Ctrl-Space
14941569 Cancel Escape
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
0 version = "3.0.43"
0 version = "3.1.18"
0 version = "3.0.43 [2015-01-18 12:01:34 -0500]"
0 version = "3.1.18 [2015-06-21 10:07:44 -0400]"
2020
2121 if platform.python_version() >= '3':
2222 import tkinter
23 from tkinter import Tk, Entry, INSERT, END, Label, Toplevel, Button, Frame, LEFT, PanedWindow, OptionMenu, StringVar, IntVar, Menu, X, Canvas, CURRENT, Scrollbar
23 from tkinter import (
24 BOTH,
25 Button,
26 Canvas,
27 CENTER,
28 CURRENT,
29 END,
30 Entry,
31 FLAT,
32 Frame,
33 INSERT,
34 IntVar,
35 Label,
36 LEFT,
37 Menu,
38 OptionMenu,
39 PanedWindow,
40 PhotoImage,
41 RAISED,
42 Scrollbar,
43 StringVar,
44 Tk,
45 TOP,
46 Toplevel,
47 W,
48 X,
49 )
2450 from tkinter import ttk
2551 from tkinter import font as tkFont
2652 utf8 = lambda x: x
2854
2955 else:
3056 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
57 from Tkinter import (
58 BOTH,
59 Button,
60 Canvas,
61 CENTER,
62 CURRENT,
63 END,
64 Entry,
65 FLAT,
66 Frame,
67 INSERT,
68 IntVar,
69 Label,
70 LEFT,
71 Menu,
72 OptionMenu,
73 PanedWindow,
74 PhotoImage,
75 RAISED,
76 Scrollbar,
77 StringVar,
78 Tk,
79 TOP,
80 Toplevel,
81 W,
82 X,
83 )
3284 import ttk
3385 import tkFont
3486
4698 from decimal import Decimal
4799
48100 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
101 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, fmt_period, TimeIt, getReportData, getFileTuples, getAllFiles, updateCurrentFiles, FINISH, availableDates, syncTxt, update_subscription, _)
102
103 from etmTk.dialog import MenuTree, Timer, ReadOnlyText, MessageWindow, TextDialog, OptionsDialog, GetInteger, GetDateTime, GetString, FileChoice, STOPPED, PAUSED, RUNNING, BGCOLOR, ONEDAY, ONEMINUTE, SimpleEditor
104
105 # from etmTk.edit import SimpleEditor
106
107 # import gettext
108
109 # _ = gettext.gettext
110
111
112 from datetime import datetime, time, date
61113
62114 ETM = "etm"
63115
90142
91143 FILE = _("File")
92144 NEW = _("New")
145 TIMER = _("Timer")
93146 OPEN = _("Open")
94147 VIEW = _("View")
95148 ITEM = _("Item")
106159
107160 SEP = "----"
108161
109 ACTIVEFILL = "#FAFCAC"
162 # ACTIVEFILL = "#FAFCAC"
163 ACTIVEFILL = "#FCFCD9"
110164 ACTIVEOUTLINE = "gray40"
111165
112166 DEFAULTFILL = "#D4DCFC" # blue
115169 BUSYOUTLINE = ""
116170
117171 CONFLICTFILL = "#C1C4C9"
118 CURRENTFILL = "#FCF2F0"
172 # CURRENTFILL = "#FCF2F0"
173 CURRENTFILL = "#DCEAFC"
119174 # CURRENTLINE = "#D053E0"
120175 CURRENTLINE = "#3C3FDE"
121176 OUTSIDELINE = "#E0535C"
122177 LASTLTR = re.compile(r'([a-z])$')
123178 LINECOLOR = "gray80"
124179
125 OCCASIONFILL = "gray96"
180 # OCCASIONFILL = "gray96"
181 OCCASIONFILL = "gray92"
126182
127183 this_dir, this_filename = os.path.split(__file__)
128184 USERMANUAL = os.path.normpath(os.path.join(this_dir, "help", "UserManual.html"))
129
185 ICONSETTINGS = os.path.normpath(os.path.join(this_dir, "icons", "icon_settings.gif"))
186 ICONPLUS = os.path.normpath(os.path.join(this_dir, "icons", "icon_plus.gif"))
187 # ICONLOGO = os.path.normpath(os.path.join(this_dir, "icons", "etmlogo.gif"))
130188
131189 class App(Tk):
132190 def __init__(self, path=None):
133191 Tk.__init__(self)
134192
135 self.minsize(460, 460)
193 self.minsize(400, 460)
136194 self.uuidSelected = None
137195 self.timerItem = None
138196 self.loop = loop
139 self.actionTimer = Timer(self, options=loop.options)
140197 self.monthly_calendar = Calendar()
141198 self.activeAlerts = []
142199 self.configure(background=BGCOLOR)
145202 self.menutree = MenuTree()
146203 self.chosen_day = None
147204 self.active_date = None
205 self.canvas_date = None
148206 self.busy_info = None
149207 self.weekly = False
150208 self.monthly = False
151 self.today_col = None
152209 self.specsModified = False
153210 self.active_tree = {}
211 self.protocol("WM_DELETE_WINDOW", self.quit)
154212 root = "_"
213
214 self.week_height = 80
215 self.month_height = 260
216
217 self.options = loop.options
218
219 tkfixedfont = tkFont.nametofont("TkFixedFont")
220 if 'fontsize_fixed' in self.options and self.options['fontsize_fixed']:
221 tkfixedfont.configure(size=self.options['fontsize_fixed'])
222 logger.info("using fixedfont size: {0}".format(tkfixedfont.actual()['size']))
223 self.tkfixedfont = tkfixedfont
224
225 tktreefont = tkFont.nametofont("TkDefaultFont")
226 treefontfamily = tktreefont['family']
227 if 'fontsize_tree' in self.options and self.options['fontsize_tree']:
228 tktreefont.configure(size=self.options['fontsize_tree'])
229 logger.info("using treefont size: {0}".format(tktreefont.actual()['size']))
230 self.tktreefont = tktreefont
155231
156232 ef = "%a %b %d"
157233 if 'ampm' in loop.options and loop.options['ampm']:
165241 self.menutree.create_node(root, root)
166242
167243 # leaf: (parent, (option, [accelerator])
168
169244 self.outline_depths = {}
170 for view in DAY, TAG, KEYWORD, NOTE, PATH:
245 for view in DAY, WEEK, MONTH, TAG, KEYWORD, NOTE, PATH:
171246 # set all to the default
172247 logger.debug('Setting depth for {0} to {1}'.format(view, loop.options['outline_depth']))
173248 self.outline_depths[view] = loop.options['outline_depth']
175250 self.outline_depths[AGENDA] = 0
176251 self.outline_depths[CUSTOM] = 0
177252
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
253 self.topbar = topbar = Frame(self, bd=0, relief="flat", highlightbackground=BGCOLOR, background=BGCOLOR, takefocus=False)
254 topbar.pack(side="top", fill="x", expand=0, padx=0, pady=0)
255
256 self.box_value = StringVar()
257 self.custom_box = ttk.Combobox(self.topbar, textvariable=self.box_value, font=self.tkfixedfont)
258
259 self.statusbar = Frame(self, bd=0, relief="flat", highlightbackground=BGCOLOR, background=BGCOLOR, takefocus=False)
260 self.statusbar.pack(side="bottom", fill="x", expand=0, padx=4, pady=2)
261
262 self.topwindow = topwindow = PanedWindow(self, orient="vertical", sashwidth=4, sashrelief='flat')
263 self.topwindow.pack(side="top", fill=BOTH, expand=1)
264
265
266 self.toppane = toppane = Frame(
267 topwindow, bd=0, relief="flat",
268 highlightthickness=0,
269 highlightbackground=BGCOLOR, background=BGCOLOR)
270 self.toppane.pack(side="top", fill=BOTH, expand=1)
271
272 self.canvas = Canvas(
273 self.toppane, background="white", bd=1, relief="flat",
274 highlightthickness=2, highlightbackground=BGCOLOR)
275 self.canvas.pack(fill=BOTH, expand=1, padx=0, pady=0)
276
277
278 self.botwindow = botwindow = PanedWindow(topwindow, orient="vertical", sashwidth=4, sashrelief='flat')
279 topwindow.add(botwindow)
280
281 self.treepane = treepane = Frame(
282 botwindow, bd=0, relief="flat",
283 highlightthickness=2,
284 highlightbackground=BGCOLOR,
285 background=BGCOLOR)
286 botwindow.add(treepane, padx=0, pady=0, stretch="first")
287
288 self.content = ReadOnlyText(
289 botwindow, font=self.tkfixedfont,
290 wrap="word", padx=3, bd=2, relief="sunken",
291 height=loop.options['details_rows'],
292 width=46, takefocus=False)
293 botwindow.add(self.content, padx=3, pady=0, stretch="never")
294
295
296 self.tree = ttk.Treeview(treepane, show='tree', columns=["#1", "#2"], selectmode='browse')
297 self.tree.pack(fill="both", expand=1, padx=4, pady=0)
298
299
300 self.canvas.bind('<Button-1>', (lambda e: self.selectId(event=e, d=0)))
183301 self.canvas.bind("<Control-Button-1>", self.on_select_item)
184302 self.canvas.bind("<Double-1>", self.on_select_item)
185 self.canvas.bind("<Configure>", self.configureCanvas)
186303
187304 self.canvas.bind("<Return>", lambda e: self.on_activate_item(event=e))
188305 self.canvas.bind('<Left>', (lambda e: self.priorWeekMonth(event=e)))
189306 self.canvas.bind('<Right>', (lambda e: self.nextWeekMonth(event=e)))
190307 self.canvas.bind('<Up>', (lambda e: self.selectId(event=e, d=-1)))
191308 self.canvas.bind('<Down>', (lambda e: self.selectId(event=e, d=1)))
309 self.canvas.bind('<space>', self.goHome)
192310
193311 self.canvas.bind("b", lambda event: self.after(AFTER, self.showBusyPeriods))
194312 self.canvas.bind("f", lambda event: self.after(AFTER, self.showFreePeriods))
204322 self.add2menu(menu, (path, ))
205323
206324 self.newmenu = newmenu = Menu(filemenu, tearoff=0)
325 filemenu.add_cascade(label=NEW, menu=newmenu)
326
207327 self.add2menu(path, (NEW, ))
208328 path = NEW
209329
226346 newmenu.entryconfig(1, accelerator=l)
227347 self.add2menu(path, (label, l))
228348
229 label = _("Begin/Pause Action Timer")
349 path = FILE
350
351 self.timermenu = timermenu = Menu(filemenu, tearoff=0)
352 filemenu.add_cascade(label=TIMER, menu=timermenu)
353 self.add2menu(path, (TIMER, ))
354 path = TIMER
355
356 self.actionTimer = Timer(self, options=loop.options)
357
358 label = _("Start Action Timer")
230359 l = "T"
231360 c = 't'
232 newmenu.add_command(label=label, command=self.startActionTimer)
233 self.bindTop(c, self.startActionTimer)
234 newmenu.entryconfig(2, accelerator=l)
361 timermenu.add_command(label=label, command=self.actionTimer.selectTimer)
362 self.bindTop(c, self.actionTimer.selectTimer)
363 timermenu.entryconfig(0, accelerator=l)
235364 self.add2menu(path, (label, l))
236365
237366 label = _("Finish Action Timer")
238367 l = "Shift-T"
239368 c = "T"
240 newmenu.add_command(label=label, command=self.finishActionTimer)
369 timermenu.add_command(label=label, command=self.finishActionTimer)
241370 self.bind(c, self.finishActionTimer)
242 filemenu.add_cascade(label=NEW, menu=newmenu)
243 newmenu.entryconfig(3, state="disabled")
371 timermenu.entryconfig(1, accelerator=l)
244372 self.add2menu(path, (label, l))
245373
246 label = _("Start/Resolve Idle Timer")
374 label = _("Toggle Current Timer")
247375 l = "I"
248376 c = 'i'
249 newmenu.add_command(label=label, command=self.startIdleTimer)
250 self.bindTop(c, self.startIdleTimer)
251 newmenu.entryconfig(4, accelerator=l)
377 timermenu.add_command(label=label, command=self.actionTimer.toggleCurrent)
378 self.bindTop(c, self.actionTimer.toggleCurrent)
379 timermenu.entryconfig(2, accelerator=l)
252380 self.add2menu(path, (label, l))
253381
254 label = _("Stop Idle Timer")
382 label = _("Delete Action Timer")
255383 l = "Shift-I"
256384 c = "I"
257 newmenu.add_command(label=label, command=self.stopIdleTimer)
258 self.bind(c, self.stopIdleTimer)
259 newmenu.entryconfig(5, accelerator=l)
385 timermenu.add_command(label=label, command=self.actionTimer.deleteTimer)
386 self.bind(c, self.actionTimer.deleteTimer)
387 timermenu.entryconfig(3, accelerator=l)
260388 self.add2menu(path, (label, l))
261 newmenu.entryconfig(5, state="disabled")
389
390 self.actionTimer.updateMenu()
262391
263392 path = FILE
264393
317446 self.add2menu(path, (label, l))
318447
319448 menubar.add_cascade(label=path, underline=0, menu=filemenu)
320
321 # View menu
449 self.toolsmenu = viewmenu = Menu(menubar, tearoff=0)
450
322451 self.viewmenu = viewmenu = Menu(menubar, tearoff=0)
323452 path = VIEW
324453 self.add2menu(menu, (path, ))
325454
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
455 # # agenda
456 # l = label = AGENDA
457 # toolsmenu.add_command(label=label, command=self.agendaView)
458
459 self.vm_options = [[AGENDA, 'a'],
460 [DAY, 'd'],
461 [WEEK, 'w'],
462 [MONTH, 'm'],
463 [TAG, 't'],
464 [KEYWORD, 'k'],
465 [PATH, 'p'],
466 [NOTE, 'n'],
467 [CUSTOM, 'c'],
468 ]
469
470 self.view2cmd = {'a': self.agendaView,
471 'd': self.dayView,
472 'm': self.showMonthly,
473 'p': self.pathView,
474 'k': self.keywordView,
475 'n': self.noteView,
476 't': self.tagView,
477 'c': self.customView,
478 'w': self.showWeekly}
479
480 self.viewname2cmd = {}
481
482 self.view = self.vm_options[0][0]
483 self.currentView = StringVar(self)
484 self.currentView.set(self.view)
485
486 self.vm_opts = [x[0] for x in self.vm_options]
487 for i in range(len(self.vm_options)):
488 label = self.vm_options[i][0]
489 k = self.vm_options[i][1]
490 if label == DAY:
491 continue
492 elif label == "-":
493 self.viewmenu.add_separator()
494 # self.add2menu(VIEW, (SEP, ))
495 else:
496 l, c = commandShortcut(k)
497 viewmenu.add_command(label=label, command=self.view2cmd[k])
498 self.bind(c, lambda e, x=k: self.after(AFTER, self.view2cmd[x]))
499 viewmenu.entryconfig(i, accelerator=l)
500 self.add2menu(path, (label, l))
501
502
503 viewmenu.add_separator()
356504 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))
371505
372506 # apply filter
373507 l, c = commandShortcut('f')
375509 viewmenu.add_command(label=label, underline=1, command=self.setFilter)
376510 self.bind(c, self.setFilter)
377511
378 viewmenu.entryconfig(6, accelerator=l)
512 viewmenu.entryconfig(10, accelerator=l)
379513 self.add2menu(path, (label, l))
380514
381515 # clear filter
383517 label = _("Clear outline filter")
384518 viewmenu.add_command(label=label, underline=1, command=self.clearFilter)
385519
386 viewmenu.entryconfig(7, accelerator=l)
520 viewmenu.entryconfig(11, accelerator=l)
387521 self.add2menu(path, (label, l))
388522
389523 # toggle showing labels
390524 l = "L"
391525 c = "l"
392 label = _("Toggle displaying labels column")
526 label = _("Toggle labels")
393527 viewmenu.add_command(label=label, underline=1, command=self.toggleLabels)
394528 self.bindTop(c, self.toggleLabels)
395529
396 viewmenu.entryconfig(8, accelerator=l)
530 viewmenu.entryconfig(12, accelerator=l)
397531 self.add2menu(path, (label, l))
398532
399533 # expand to depth
403537 viewmenu.add_command(label=label, underline=1, command=self.expand2Depth)
404538 self.bindTop(c, self.expand2Depth)
405539
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)
540 viewmenu.entryconfig(13, accelerator=l)
426541 self.add2menu(path, (label, l))
427542
428543 # toggle showing finished
429544 l = "X"
430545 c = "x"
431 label = _("Toggle displaying finished")
546 label = _("Toggle displayings finished")
432547 viewmenu.add_command(label=label, underline=1, command=self.toggleFinished)
433548 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
444549 viewmenu.entryconfig(14, accelerator=l)
445550 self.add2menu(path, (label, l))
446551
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")
487552 menubar.add_cascade(label=path, underline=0, menu=viewmenu)
488553
489554 # 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, ))
495555 self.em_options = [
496556 [_('Copy'), 'c'],
497557 [_('Delete'), 'd'],
501561 [_('Move'), 'm'],
502562 [_('Reschedule'), 'r'],
503563 [_('Schedule new'), 'R'],
564 [_('Klone as timer'), 'k'],
504565 [_('Open link'), 'g'],
505 [_('Show user details'), 'u']]
566 [_('Show user details'), 'u'],
567 ]
568 self.itemmenu = itemmenu = Menu(menubar, tearoff=0)
569 self.itemmenu.bind("<Escape>", self.closeItemMenu)
570 self.itemmenu.bind("<FocusOut>", self.closeItemMenu)
571 path = ITEM
572 self.add2menu(menu, (path, ))
506573 self.edit2cmd = {
507574 'c': self.copyItem,
508575 'd': self.deleteItem,
513580 'r': self.rescheduleItem,
514581 'R': self.scheduleNewItem,
515582 'g': self.openWithDefault,
516 'u': self.showUserDetails}
583 'u': self.showUserDetails,
584 'k': self.kloneTimer}
517585 self.em_opts = [x[0] for x in self.em_options]
518586 for i in range(len(self.em_options)):
519587 label = self.em_options[i][0]
543611 # tools menu
544612 path = TOOLS
545613 self.add2menu(menu, (path, ))
546 toolsmenu = Menu(menubar, tearoff=0)
614 self.toolsmenu = toolsmenu = Menu(menubar, tearoff=0)
615
616 # go home
617 l = "Home"
618 label = _("Home")
619 toolsmenu.add_command(label=label, command=self.goHome)
620
621 toolsmenu.entryconfig(0, accelerator=l)
622 self.add2menu(path, (label, l))
623 self.bindTop('<Home>', self.goHome)
624
625 # go to date
626 l = "J"
627 c = "j"
628 label = _("Jump to date")
629 toolsmenu.add_command(label=label, command=self.goToDate)
630 self.bindTop(c, self.goToDate)
631
632 toolsmenu.entryconfig(1, accelerator=l)
633 self.add2menu(path, (label, l))
634
635 toolsmenu.add_separator() # 2
636 self.add2menu(path, (SEP, ))
637
638 # show alerts
639 l = "A"
640 c = "a"
641 label = _("Show remaining alerts for today")
642 toolsmenu.add_command(label=label, underline=1, command=self.showAlerts)
643 self.bindTop(c, self.showAlerts)
644
645 toolsmenu.entryconfig(3, accelerator=l)
646 self.add2menu(path, (label, l))
647
648 l = "B"
649 c = 'b'
650 label = _("List busy times in week/month")
651 toolsmenu.add_command(label=label, underline=5, command=self.showBusyPeriods)
652
653 toolsmenu.entryconfig(4, accelerator=l)
654 self.add2menu(path, (label, l))
655
656 l = "F"
657 c = 'f'
658 label = _("List free times in week/month")
659 toolsmenu.add_command(label=label, underline=5, command=self.showFreePeriods)
660
661 toolsmenu.entryconfig(5, accelerator=l)
662 # set binding in showWeekly
663 self.add2menu(path, (label, l))
664
547665
548666 # date calculator
549667 l = "Shift-D"
552670 toolsmenu.add_command(label=label, underline=12, command=self.dateCalculator)
553671 self.bindTop(c, self.dateCalculator)
554672
555 toolsmenu.entryconfig(1, accelerator=l)
673 toolsmenu.entryconfig(6, accelerator=l)
556674 self.add2menu(path, (label, l))
557675
558676 # available date calculator
562680 toolsmenu.add_command(label=label, underline=12, command=self.availableDateCalculator)
563681 self.bindTop(c, self.availableDateCalculator)
564682
565 toolsmenu.entryconfig(2, accelerator=l)
683 toolsmenu.entryconfig(7, accelerator=l)
566684 self.add2menu(path, (label, l))
567685
568686 l = "Shift-Y"
572690 toolsmenu.add_command(label=label, underline=8, command=self.showCalendar)
573691 self.bindTop(c, self.showCalendar)
574692
575 toolsmenu.entryconfig(3, accelerator=l)
693 toolsmenu.entryconfig(8, accelerator=l)
576694 self.add2menu(path, (label, l))
577695
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))
696 toolsmenu.add_separator() # 9
697 self.add2menu(path, (SEP, ))
698
699 # popup active tree
700 l = "S"
701 c = "s"
702 label = _("Show outline as text")
703 toolsmenu.add_command(label=label, underline=1, command=self.popupTree)
704 self.bindTop(c, self.popupTree)
705
706 toolsmenu.entryconfig(10, accelerator=l)
707 self.add2menu(path, (label, l))
708
709 # print active tree
710 l = "P"
711 c = "p"
712 label = _("Print outline")
713 toolsmenu.add_command(label=label, underline=1, command=self.printTree)
714 self.bindTop("p", self.printTree)
715 toolsmenu.entryconfig(11, accelerator=l)
716 self.add2menu(path, (label, l))
589717
590718 # export
591719 l = "Shift-X"
594722 toolsmenu.add_command(label=label, underline=1, command=self.exportToIcal)
595723 self.bind(c, self.exportToIcal)
596724
597 toolsmenu.entryconfig(5, accelerator=l)
725 toolsmenu.entryconfig(12, accelerator=l)
598726 self.add2menu(path, (label, l))
599727
600728 # update subscriptions
604732 toolsmenu.add_command(label=label, underline=1, command=self.updateSubscriptions)
605733 self.bind(c, self.updateSubscriptions)
606734
607 toolsmenu.entryconfig(5, accelerator=l)
735 toolsmenu.entryconfig(13, accelerator=l)
608736 self.add2menu(path, (label, l))
609737
610738 # load data
614742 toolsmenu.add_command(label=label, underline=1, command=loop.loadData)
615743 self.bind(c, loop.loadData)
616744
617 toolsmenu.entryconfig(6, accelerator=l)
745 toolsmenu.entryconfig(14, accelerator=l)
618746 self.add2menu(path, (label, l))
619747
748 # changes
749 if loop.options['vcs_system']:
750
751 l = 'Shift-H'
752 c = 'H'
753 label = _("History of changes")
754 toolsmenu.add_command(label=label, underline=1, command=self.showChanges)
755 self.bind(c, lambda event: self.after(AFTER, self.showChanges))
756
757 toolsmenu.entryconfig(15, accelerator=l)
758 self.add2menu(path, (label, l))
759
620760 menubar.add_cascade(label=path, menu=toolsmenu, underline=0)
761
762 self.toolsmenu.entryconfig(1, state="disabled")
763 for i in range(4, 6):
764 self.toolsmenu.entryconfig(i, state="disabled")
621765
622766 # report
623767 path = CUSTOM
693837 self.count2id = {}
694838 self.now = get_current_time()
695839 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
710840
711841 self.popup = ''
712842 self.value = ''
723853 self.dtSelected = None
724854 self.rowSelected = None
725855
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')
856 self.currentTime = StringVar(self)
857 self.currentTime.set("")
858
859 # self.title(ETM)
860 self.title(self.currentTime.get())
752861
753862 self.columnconfigure(0, minsize=300, weight=1)
754863 self.rowconfigure(1, weight=2)
755864
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)
865 # self.etmlogo = PhotoImage(file=ICONLOGO)
866 # self.iconphoto(True, self.etmlogo)
758867
759868 # report
760 self.box_value = StringVar()
761 self.custom_box = ttk.Combobox(self, textvariable=self.box_value, font=self.tkfixedfont)
762869 self.custom_box.bind("<<ComboboxSelected>>", self.newselection)
763870 self.bind("<Return>", self.makeReport)
764871 self.bind("<Control-q>", self.quit)
771878 self.saved_specs = deepcopy(self.specs)
772879 self.custom_box.configure(width=30, background=BGCOLOR, takefocus=False)
773880
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")
881
882 iconsize = "22"
883 self.settingsIcon = PhotoImage(file=ICONSETTINGS)
884 self.settingsbutton = Button(
885 topbar, command=self.selectCalendars,
886 highlightbackground=BGCOLOR,
887 bg=BGCOLOR, pady=0, bd=0,
888 highlightthickness=0, takefocus=False
889 )
890 self.settingsbutton.config(image=self.settingsIcon, width=iconsize, height=iconsize)
891 self.settingsbutton.pack(side="left", padx=4, pady=2)
892
893
894 self.newIcon = PhotoImage(file=ICONPLUS)
895 self.newbutton = Button(topbar, command=self.newItem, highlightbackground=BGCOLOR, bg=BGCOLOR, pady=0, highlightthickness=0, takefocus=False)
896 self.newbutton.config(image=self.newIcon, width=iconsize, height=iconsize)
897 self.newbutton.pack(side="right", padx=4, pady=2)
898
899 windowtitle = Label(topbar, textvariable=self.currentView, bd=1, relief="flat", padx=8, pady=0)
900 windowtitle.pack(side="left")
901 windowtitle.configure(background=BGCOLOR)
902 self.currentView.set("Agenda")
903
830904
831905 # filter
832906 self.filterValue = StringVar(self)
833907 self.filterValue.set('')
834908 self.filterValue.trace_variable("w", self.filterView)
835909
836 self.fltr = Entry(topbar, textvariable=self.filterValue, width=14, highlightbackground=BGCOLOR, bg=BGCOLOR)
910 self.fltr = Entry(topbar, textvariable=self.filterValue, width=14, highlightbackground=BGCOLOR, bg=BGCOLOR, takefocus=False)
837911 self.fltr.pack(side="left", padx=0, expand=1, fill=X)
838912 self.fltr.bind("<FocusIn>", self.setFilter)
839913 self.fltr.bind("<Escape>", self.clearFilter)
840914 self.fltr.bind('<Tab>', self.leaveFilter)
841915 self.bind("<Shift-Control-F>", self.clearFilter)
842916 self.filter_active = False
843 self.viewmenu.entryconfig(6, state="normal")
844 self.viewmenu.entryconfig(7, state="disabled")
845917
846918 self.weekly = False
847919
859931 self.tree.bind('<Return>', self.OnActivate)
860932 self.tree.bind('<Control-Down>', self.nextItem)
861933 self.tree.bind('<Control-Up>', self.prevItem)
862 # self.tree.bind('<space>', self.goHome)
934
863935
864936 for t in tstr2SCI:
865937 self.tree.tag_configure(t, foreground=tstr2SCI[t][1])
867939 self.date2id = {}
868940 self.root = ('', '_')
869941
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)
874942 self.content.bind('<Escape>', self.cleartext)
875943 self.content.bind('<Tab>', self.focus_next_window)
876944 self.content.bind("<FocusIn>", self.editItem)
877945
878 panedwindow.add(self.content, padx=3, pady=0, stretch="never")
879
880 self.statusbar = Frame(self)
946
947 self.pendingAlerts = IntVar(self)
948 self.pendingAlerts.set(0)
949 self.pending = Button(self.statusbar,
950 textvariable=self.pendingAlerts,
951 command=self.showAlerts,
952 highlightbackground=BGCOLOR,
953 bg=BGCOLOR, highlightthickness=0, takefocus=False,
954 width=4, pady=2,
955 )
956 self.pending.pack(side="right", expand=0, padx=2, pady=2)
881957
882958 self.timerStatus = StringVar(self)
883959 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)
960 self.timer_status = timer_status = Label(self.statusbar, textvariable=self.timerStatus, bd=0, relief="flat", anchor=W, justify=LEFT, padx=2, pady=0)
961 timer_status.pack(side="right", expand=1, fill=X, padx=6)
886962 timer_status.configure(background=BGCOLOR, highlightthickness=0)
887963
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
964 self.timerTitle = StringVar(self)
965 self.timerTitle.set("")
966 timer_title = Label(self.statusbar, textvariable=self.timerTitle, bd=0, relief="flat", anchor=W, justify=LEFT, padx=2, pady=0)
967
968 timer_title.pack(side="left", expand=1, fill=X, padx=0)
969 timer_title.configure(background=BGCOLOR, highlightthickness=0)
970
911971
912972 # set cal_regex here and update it in updateCalendars
913973 self.cal_regex = None
936996 self.updateAlerts()
937997 self.etmgeo = os.path.normpath(os.path.join(loop.options['etmdir'], ".etmgeo"))
938998 self.restoreGeometry()
999 self.etmtimers = os.path.normpath(os.path.join(loop.options['etmdir'], ".etmtimers"))
9391000 self.tree.focus_set()
9401001
9411002 def bindTop(self, c, cmd, e=None):
9471008
9481009 def toggleLabels(self, e=None):
9491010 if e and e.char != "l":
950 return
951 if self.weekly or self.monthly:
9521011 return
9531012 if self.labels:
9541013 width0 = self.tree.column('#0')['width']
9711030 logger.debug('reloading data')
9721031 # self.updateAlerts()
9731032 if self.weekly:
1033 self.updateDay()
9741034 self.showWeek()
9751035 elif self.monthly:
1036 self.updateDay()
9761037 self.showMonth()
9771038 else:
9781039 self.showView()
10221083
10231084 def selectCalendars(self):
10241085 if self.default_calendars:
1025 prompt = _("Only items from selected calendars will be displayed.")
1086 prompt = _("Display items from calendars selected below.")
10261087 title = CALENDARS
10271088 if self.weekly or self.monthly:
10281089 master = self.canvas
10441105 cal_pattern = r'^%s' % '|'.join(
10451106 [x[2] for x in loop.calendars if x[1]])
10461107 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)
10511108 self.update()
10521109 self.updateAlerts()
10531110 if self.weekly:
1111 self.updateDay()
10541112 self.showWeek()
10551113 elif self.monthly:
1114 self.updateDay()
10561115 self.showMonth()
10571116 else:
10581117 self.showView()
10591118
10601119 def quit(self, e=None):
1061 ans = self.confirm(
1062 title=_('Quit'),
1063 prompt=_("Do you really want to quit?"),
1064 parent=self)
1120 ans = True
1121 if self.actionTimer.currentStatus == RUNNING:
1122 ans = self.confirm(
1123 title=_('Quit'),
1124 prompt=_("An action timer is running.\nDo you really want to quit?"),
1125 parent=self)
1126 else:
1127 ans = self.confirm(
1128 title=_('Quit'),
1129 prompt=_("Do you really want to quit?"),
1130 parent=self)
10651131 if ans:
1132 self.actionTimer.pauseTimer()
10661133 self.saveGeometry()
10671134 self.destroy()
10681135
10741141 def openWithDefault(self, e=None):
10751142 if not self.itemSelected or 'g' not in self.itemSelected:
10761143 return(False)
1077 path = self.itemSelected['g']
1144 # path = self.itemSelected['g']
1145 path = expand_template(self.itemSelected['g'], self.itemSelected)
10781146
10791147 if windoz:
10801148 os.startfile(path)
10811149 return()
1150
10821151 if mac:
10831152 cmd = 'open' + " {0}".format(path)
10841153 else:
10891158 def printWithDefault(self, s, e=None):
10901159 fo = codecs.open(loop.tmpfile, 'w', loop.options['encoding']['file'])
10911160 # add a trailing formfeed
1092 # fo.write("{0}".format(s))
10931161 fo.write(s)
10941162 fo.close()
10951163 if windoz:
11861254 text = " @s {0}".format(self.active_date)
11871255 elif 'c' in self.itemSelected:
11881256 text = " @c {0}".format(self.itemSelected['c'])
1189 else:
1257 else:
1258 text = " "
1259 elif self.active_date:
11901260 text = " @s {0}".format(self.active_date)
11911261 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)
1262 elif self.view in [DAY, WEEK, MONTH] and self.canvas_date:
1263 text = " @s {0}".format(self.canvas_date)
11941264 changed = SimpleEditor(parent=self, master=master, start=text, options=loop.options).changed
11951265 elif self.view in [KEYWORD, NOTE] and self.itemSelected:
11961266 if self.itemSelected and 'k' in self.itemSelected:
12111281 loop.do_update = True
12121282 self.updateAlerts()
12131283 if self.weekly:
1284 self.updateDay()
12141285 self.showWeek()
12151286 elif self.monthly:
1287 self.updateDay()
12161288 self.showMonth()
12171289 else:
12181290 self.showView()
13021374 if changed:
13031375 self.updateAlerts()
13041376 if self.weekly:
1377 self.updateDay()
13051378 self.showWeek()
13061379 elif self.weekly:
1380 self.updateDay()
13071381 self.showMonth()
13081382 else:
13091383 self.showView(row=self.topSelected)
13421416 self.updateAlerts()
13431417 if self.weekly:
13441418 self.canvas.focus_set()
1419 self.updateDay()
13451420 self.showWeek()
13461421 elif self.monthly:
13471422 self.canvas.focus_set()
1423 self.updateDay()
13481424 self.showMonth()
13491425 else:
13501426 self.tree.focus_set()
13771453 self.updateAlerts()
13781454 if self.weekly:
13791455 self.canvas.focus_set()
1456 self.updateDay()
13801457 self.showWeek()
13811458 elif self.monthly:
13821459 self.canvas.focus_set()
1460 self.updateDay()
13831461 self.showWeek()
13841462 else:
13851463 self.tree.focus_set()
14781556 self.updateAlerts()
14791557 if self.weekly:
14801558 self.canvas.focus_set()
1559 self.updateDay()
14811560 self.showWeek()
14821561 elif self.monthly:
14831562 self.canvas.focus_set()
1563 self.updateDay()
14841564 self.showMonth()
14851565 else:
14861566 self.tree.focus_set()
15291609 self.updateAlerts()
15301610 if self.weekly:
15311611 self.canvas.focus_set()
1612 self.updateDay()
15321613 self.showWeek()
15331614 elif self.monthly:
15341615 self.canvas.focus_set()
1616 self.updateDay()
15351617 self.showMonth()
15361618 else:
15371619 self.tree.focus_set()
16311713 self.updateAlerts()
16321714 if self.weekly:
16331715 self.canvas.focus_set()
1716 self.updateDay()
16341717 self.showWeek()
16351718 elif self.monthly:
16361719 self.canvas.focus_set()
1720 self.updateDay()
16371721 self.showMonth()
16381722 else:
16391723 self.tree.focus_set()
16671751 self.updateAlerts()
16681752 if self.weekly:
16691753 self.canvas.focus_set()
1754 self.updateDay()
16701755 self.showWeek()
16711756 elif self.monthly:
16721757 self.canvas.focus_set()
1758 self.updateDay()
16731759 self.showMonthly()
16741760 else:
16751761 self.tree.focus_set()
17021788 self.updateAlerts()
17031789 if self.weekly:
17041790 self.canvas.focus_set()
1791 self.updateDay()
17051792 self.showWeek()
1706 elif self.weekly:
1793 elif self.monthly:
17071794 self.canvas.focus_set()
1795 self.updateDay()
17081796 self.showMonth()
17091797 else:
17101798 self.tree.focus_set()
17301818 ", ".join(x[2]['_alert_action']),
17311819 utf8(x[2]['summary'][:26])) for x in self.activeAlerts]))
17321820 else:
1733 s = _("none")
1821 s = _("None ")
17341822 self.textWindow(self, t, s, opts=self.options)
17351823
17361824 def agendaView(self, e=None):
17511839 def customView(self, e=None):
17521840 # TODO: finish this
17531841 self.content.delete("1.0", END)
1754 self.fltr.forget()
1842 # self.fltr.forget()
17551843 self.clearTree()
17561844 self.setView(CUSTOM)
17571845 pass
17591847 def noteView(self, e=None):
17601848 self.setView(NOTE)
17611849
1850 def updateDay(self, e=None):
1851 self.mode = "command"
1852 self.process_input(event=e, cmd='d')
1853
17621854 def setView(self, view, row=None):
17631855 self.rowSelected = None
1764 if view != WEEK and self.weekly:
1856 if view in [DAY, WEEK, MONTH]:
1857 self.toolsmenu.entryconfig(1, state="normal")
1858 else:
1859 self.toolsmenu.entryconfig(1, state="disabled")
1860 if self.weekly and view not in [DAY, WEEK]:
17651861 self.closeWeekly()
1766 if view != MONTH and self.monthly:
1862 if self.monthly and view not in [DAY, MONTH]:
17671863 self.closeMonthly()
17681864 if view == CUSTOM:
1769 # self.reportbar.pack(side="top")
17701865 logger.debug('showing custom_box')
1771 self.custom_box.pack(side="top", fill="x", padx=3)
1866 self.fltr.forget()
1867 self.custom_box.pack(side="left", fill=X, padx=0, expand=1)
17721868 self.custom_box.focus_set()
17731869 for i in range(len(self.rm_options)):
17741870 self.custommenu.entryconfig(i, state="normal")
17771873 # we're leaving custom view
17781874 logger.debug('removing custom_box')
17791875 self.custom_box.forget()
1876 self.fltr.pack(side="left", padx=0, expand=1, fill=X)
17801877 for i in range(len(self.rm_options)):
17811878 self.custommenu.entryconfig(i, state="disabled")
17821879 self.saveSpecs()
1783 self.fltr.pack(side="left", padx=0, expand=1, fill=X)
17841880 self.view = view
17851881 logger.debug("setView view: {0}. Calling showView.".format(view))
17861882 self.showView(row=row)
17871883
17881884 def filterView(self, e, *args):
1789 if self.weekly or self.monthly:
1790 return
17911885 self.depth2id = {}
17921886 fltr = self.filterValue.get()
1793 cmd = "{0} {1}".format(
1794 self.vm_options[self.vm_opts.index(self.view)][1], fltr)
1887 cn = self.vm_options[self.vm_opts.index(self.view)][1]
1888 if cn in ['w', 'm']:
1889 # with week or month views use the day view command
1890 cn = 'd'
1891 cmd = "{0} {1}".format(cn, fltr)
17951892 self.mode = 'command'
17961893 self.process_input(event=e, cmd=cmd)
17971894
17981895 def showView(self, e=None, row=None):
17991896 tt = TimeIt(loglevel=2, label="{0} view".format(self.view))
1800 if self.weekly or self.monthly:
1801 return
18021897 self.depth2id = {}
18031898 self.currentView.set(self.view)
18041899 fltr = self.filterValue.get()
19222017 s = "\n".join(lines)
19232018 self.textWindow(parent=self, title=_('free times'), prompt=s, opts=self.options)
19242019
1925 def setWeek(self, chosen_day=None):
2020 def getWeek(self, chosen_day=None):
19262021 if chosen_day is None:
19272022 chosen_day = get_current_time()
19282023 yn, wn, dn = chosen_day.isocalendar()
19372032 logger.debug('week_beg: {0}'.format(self.week_beg))
19382033 weekend = chosen_day + (6 - days) * ONEDAY
19392034 weekdays = []
2035 weekdates = []
19402036
19412037 day = weekbeg
19422038 self.active_date = weekbeg.date()
19442040 occasion_lst = []
19452041 matching = self.cal_regex is not None and self.default_regex is not None
19462042 while day <= weekend:
1947 weekdays.append(fmt_weekday(day))
2043 weekdays.append(s2or3(day.strftime("%a")))
2044 weekdates.append(leadingzero.sub("", day.strftime("%d")))
19482045 isokey = day.isocalendar()
19492046
1950 if isokey in loop.occasions:
2047 day = day + ONEDAY
2048
2049 ybeg = weekbeg.year
2050 yend = weekend.year
2051 mbeg = weekbeg.month
2052 mend = weekend.month
2053 # 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
2054
2055 if mbeg == mend:
2056 header = "{0} - {1}".format(
2057 fmt_dt(weekbeg, '%b %d'), fmt_dt(weekend, '%d, %Y'))
2058 elif ybeg == yend:
2059 header = "{0} - {1}".format(
2060 fmt_dt(weekbeg, '%b %d'), fmt_dt(weekend, '%b %d, %Y'))
2061 else:
2062 header = "{0} - {1}".format(
2063 fmt_dt(weekbeg, '%b %d, %Y'), fmt_dt(weekend, '%b %d, %Y'))
2064 header = leadingzero.sub('', header)
2065 theweek = _("{0} {1}: {2}").format(_("Week"), wn, header)
2066 return theweek, weekdays, weekdates
2067
2068
2069 def closeWeekly(self, event=None):
2070 self.week_height = self.topwindow.panecget(self.toppane, "height")
2071 self.topwindow.forget(self.toppane)
2072 self.weekly = False
2073 self.tree.pack(fill="both", expand=1, padx=4, pady=0)
2074 self.update_idletasks()
2075 for i in range(4, 6):
2076 self.toolsmenu.entryconfig(i, state="disabled")
2077
2078 self.bind("<Control-f>", self.setFilter)
2079
2080 def showWeekly(self, event=None, chosen_day=None):
2081 """
2082 Open the canvas at the current week
2083 """
2084 self.custom_box.forget()
2085 tt = TimeIt(loglevel=2, label="week view")
2086 logger.debug("chosen_day: {0}; active_date: {1}".format(chosen_day, self.active_date))
2087 if self.weekly:
2088 # we're in weekview already
2089 return
2090 if self.monthly:
2091 self.closeMonthly()
2092 self.content.delete("1.0", END)
2093 self.weekly = True
2094
2095 self.setView(DAY)
2096
2097 self.view = WEEK
2098
2099 if chosen_day is not None:
2100 self.chosen_day = chosen_day
2101 elif self.active_date:
2102 self.chosen_day = datetime.combine(self.active_date, time())
2103 else:
2104 self.chosen_day = get_current_time()
2105
2106 self.topwindow.add(self.toppane, padx=0, pady=0, before=self.botwindow, height=self.week_height)
2107
2108 if self.options['ampm']:
2109 self.hours = ["{0}am".format(i) for i in range(7, 12)] + ['12pm'] + ["{0}pm".format(i) for i in range(1, 12)]
2110 else:
2111 self.hours = ["{0}:00".format(i) for i in range(7, 24)]
2112 for i in range(4, 6):
2113 self.toolsmenu.entryconfig(i, state="normal")
2114 self.showWeek(event=event, week=0)
2115 tt.stop()
2116
2117 def priorWeekMonth(self, event=None):
2118 if self.weekly:
2119 self.showWeek(event, week=-1)
2120 elif self.monthly:
2121 self.showMonth(event, month=-1)
2122
2123 def nextWeekMonth(self, event=None):
2124 if self.weekly:
2125 self.showWeek(event=event, week=1)
2126 elif self.monthly:
2127 self.showMonth(event=event, month=1)
2128
2129 def showWeek(self, event=None, week=None):
2130 self.canvas.focus_set()
2131 self.selectedId = None
2132
2133 matching = self.cal_regex is not None and self.default_regex is not None
2134
2135 busy_dates = []
2136
2137 self.current_day = get_current_time().replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
2138 logger.debug('self.current_day: {0}, minutes: {1}'.format(self.current_day, self.current_minutes))
2139 self.x_win = self.toppane.winfo_width()
2140 # self.y_win = self.canvas.winfo_height()
2141 self.y_win = self.toppane.winfo_height()
2142 logger.debug("win: {0}, {1}".format(self.x_win, self.y_win))
2143 logger.debug("event: {0}, week: {1}, chosen_day: {2}".format(event, week, self.chosen_day))
2144 use_chosen = False
2145 if week in [-1, 0, 1]:
2146 if week == 0:
2147 day = get_current_time()
2148 elif week == 1:
2149 day = self.next_week
2150 elif week == -1:
2151 day = self.prev_week
2152 self.chosen_day = day
2153 elif self.chosen_day:
2154 use_chosen = True
2155 self.year_month = [self.chosen_day.year, self.chosen_day.month]
2156 day = self.chosen_day
2157 else:
2158 return
2159 logger.debug('week active_date: {0}'.format(self.active_date))
2160 theweek, weekdays, weekdates = self.getWeek(day)
2161 busy_lst = []
2162 occasion_lst = []
2163 weekdaynum = day.isocalendar()[2]
2164 # reset day to Monday of the current week
2165 day = day - (weekdaynum - 1) * ONEDAY
2166 if use_chosen:
2167 scrolldate = self.chosen_day.date()
2168 self.canvas_idpos = weekdaynum - 1
2169 else:
2170 scrolldate = day.date()
2171 self.canvas_idpos = 0
2172 self.scrollToDate(scrolldate)
2173 self.OnSelect()
2174 self.canvas.delete("all")
2175 l = 4
2176 r = 4
2177 t = 22
2178 b = 4
2179 if event:
2180 logger.debug('event: {0}'.format(event))
2181 w, h = event.width, event.height
2182 if type(w) is int and type(h) is int:
2183 self.canvas_width = w
2184 self.canvas_height = h
2185 else:
2186 w = self.canvas.winfo_width()
2187 h = self.canvas.winfo_height()
2188 else:
2189 w = self.canvas.winfo_width()
2190 h = self.week_height
2191 logger.debug("w: {0} {1}; h: {2} {3}, l: {4} {5}, t: {6} {7}".format(w, type(w), h, type(h), l, type(l), t, type(t)))
2192 self.margins = (w, h, l, r, t, b)
2193 self.week_x = x_ = Decimal(w - 1 - l - r) / Decimal(7)
2194 self.week_y = y_ = Decimal(h - 1 - t - b)
2195 logger.debug("x: {0}, y: {1}".format(x_, y_))
2196
2197 # week
2198 self.currentView.set(theweek)
2199 self.busyHsh = {}
2200
2201 # occasions
2202 busy_ids = set([])
2203 monthid2date = {}
2204
2205 # self.canvas.bind('<Escape>', self.on_leave_item)
2206
2207 # weekdays
2208 intervals = [360, 720, 1080, 1440]
2209 busywidth = 2
2210 offset = 6
2211 indent = 7
2212
2213 barcolor = "SteelBlue3"
2214 nightcolor = barcolor
2215 morningcolor = barcolor
2216 afternooncolor = barcolor
2217 eveningcolor = barcolor
2218
2219 conf_ids = []
2220 self.today_id = None
2221 self.timeline = None
2222 self.last_minutes = None
2223
2224 # x_ = x
2225 # y_ = y
2226
2227 for i in range(7):
2228 fill = "SteelBlue4"
2229 flagcolor = "white"
2230 busytimes = 0
2231 start_x = l + i * x_
2232 end_x = start_x + x_
2233 start_y = int(t)
2234 end_y = start_y + y_
2235 xy = int(start_x), int(start_y), int(end_x), int(end_y)
2236 p = int(l + x_ / 2 + x_ * i), int(t + y_ / 2)
2237 tl_x = bl_x = int(l + x_ * i)
2238 tl_y = tr_y = int(t)
2239 tr_x = br_x = int(tl_x + x_)
2240 bl_y = br_y = int(tl_y + y_)
2241 w_ = x_ - 12
2242 h_ = y_ - 12
2243
2244 thisdate = (day + i * ONEDAY).date()
2245 isokey = thisdate.isocalendar()
2246 tags = []
2247 id = self.canvas.create_rectangle(xy, outline="", width=0)
2248 busy_ids.add(id)
2249 monthid2date[id] = thisdate
2250 today = (thisdate == self.current_day.date())
2251 if today:
2252 flagcolor = CURRENTFILL
2253 tags.append('current_day')
2254 if loop.occasions is not None and isokey in loop.occasions:
19512255 bt = []
19522256 for item in loop.occasions[isokey]:
19532257 it = list(item)
19612265 item = tuple(it)
19622266 bt.append(item)
19632267 occasion_lst.append(bt)
2268 if bt:
2269 if not today:
2270 flagcolor = OCCASIONFILL
2271 tags.append('occasion')
2272 self.busyHsh.setdefault(id, []).extend(["^ {0}".format(x[0]) for x in bt])
19642273 else:
19652274 occasion_lst.append([])
19662275
1967 if isokey in loop.busytimes:
2276 if loop.busytimes is not None and isokey in loop.busytimes:
19682277 bt = []
2278 overlap = False
19692279 for item in loop.busytimes[isokey]:
19702280 it = list(item)
19712281 if it[0] == it[1]:
19812291 item = tuple(it)
19822292 bt.append(item)
19832293 busy_lst.append(bt)
2294 busy_dates.append(thisdate.strftime("%a %d"))
2295 if bt:
2296 lastend = 0
2297 for pts in bt:
2298 busytimes += pts[1] - pts[0]
2299 self.busyHsh.setdefault(id, []).append("* {0}".format(pts[2]))
2300 if pts[0] < lastend:
2301 overlap = True
2302 lastend = pts[1]
2303 if overlap:
2304 flagcolor = "red"
2305 tags.append('busy')
2306
2307 busylines = [[], [], [], []]
2308 # each side 360 minutes plus 2 times bar width
2309
2310 for pts in bt:
2311 pt1 = max(0, pts[0])
2312 pt2 = min(pts[1], 1440)
2313 tmp = [[], [], [], []]
2314
2315 for ii in range(0, 4):
2316 if pt1 >= intervals[ii]:
2317 continue
2318 tmp[ii].append(pt1)
2319 for jj in range(ii, 4):
2320 if jj > ii:
2321 tmp[jj].append(intervals[jj-1])
2322 if pt2 <= intervals[jj]:
2323 tmp[jj].append(pt2)
2324 break
2325 else:
2326 tmp[jj].append(intervals[jj])
2327 break
2328 for ii in range(4):
2329 if tmp[ii]:
2330 busylines[ii].append(tmp[ii])
2331
2332
2333 if busylines:
2334 for side in range(4):
2335 lines = busylines[side]
2336 if lines:
2337 if side == 0: # left
2338 for line in lines:
2339 bx = ex = bl_x + offset
2340 by = bl_y - indent - int(Decimal((line[0])/360) * h_)
2341 ey = bl_y - indent - int(Decimal((line[1])/360) * h_)
2342 self.canvas.create_line((bx, by, ex, ey), fill=nightcolor, width=busywidth, tag="busy")
2343 elif side == 1: # top
2344 for line in lines:
2345 by = ey = tl_y + offset
2346 bx = tl_x + indent + int(Decimal((line[0]-360)/360) * w_)
2347 ex = tl_x + indent + int(Decimal((line[1]-360)/360) * w_)
2348 self.canvas.create_line((bx, by, ex, ey), fill=morningcolor, width=busywidth, tag="busy")
2349 elif side == 2: # right
2350 for line in lines:
2351 bx = ex = tr_x - offset
2352 by = tr_y + indent + int(Decimal((line[0]-720)/360) * h_)
2353 ey = tr_y + indent + int(Decimal((line[1]-720)/360) * h_)
2354 self.canvas.create_line((bx, by, ex, ey), fill=afternooncolor, width=busywidth, tag="busy")
2355 elif side == 3: # bottom
2356 for line in lines:
2357 by = ey = br_y - offset
2358 bx = br_x - indent - int(Decimal((line[0]-1080)/360) * w_)
2359 ex = br_x - indent - int(Decimal((line[1]-1080)/360) * w_)
2360 self.canvas.create_line((bx, by, ex, ey), fill=eveningcolor, width=busywidth, tag="busy")
2361
2362 bx = bl_x + offset - 1.5 * busywidth
2363 ex = bl_x + offset + .5 * busywidth
2364 by = bl_y - indent + 1.5 * busywidth
2365 ey = bl_y - indent - .5 * busywidth
2366 if flagcolor:
2367 self.canvas.create_rectangle((bx, by, ex, ey), fill=flagcolor, outline=flagcolor, tag="busy")
19842368 else:
19852369 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)
2370 busy_dates.append(thisdate.strftime("%a %d"))
2371
2372 if 'current_day' in tags:
2373 self.canvas.itemconfig(id, tag='current_day', fill=CURRENTFILL)
2374 elif 'occasion' in tags:
2375 self.canvas.itemconfig(id, tag='occasion', fill=OCCASIONFILL)
2376 elif 'busy' in tags:
2377 self.canvas.itemconfig(id, tag='busy', fill="white")
2378
2379 # if fill:
2380 self.canvas.create_text(p, text="{0}".format(weekdates[i]), fill=barcolor)
2381
2382 busy_ids = list(busy_ids)
2383
2384 self.conf_ids = conf_ids
2385
2386
2387 # border
2388 # xy = int(l), int(t), int(l + x_ * 7), int(t + y_)
2389 # self.canvas.create_rectangle(xy, tag="grid")
2390
2391 # verticals
2392 for i in range(1, 7):
2393 xy = int(l + x_ * i), int(t-18), int(l + x_ * i), int(t + y_)
2394 self.canvas.create_line(xy, fill=LINECOLOR, tag="grid")
2395
2396 for i in range(7):
2397 p = int(l + x_ / 2 + x_ * i), int(t - 10)
2398 self.canvas.create_text(p, text="{0}".format(weekdays[i]), fill=barcolor)
2399
2400 self.busy_info = (theweek, busy_dates, busy_lst, occasion_lst)
2401 self.busy_ids = busy_ids
2402 self.busy_ids.sort()
2403 for id in self.busy_ids:
2404 self.canvas.tag_bind(id, '<Any-Enter>', self.on_enter_item)
2405 # self.canvas.tag_bind(id, '<Any-Leave>', self.on_leave_item)
2406 self.canvas_ids = self.busy_ids
2407 self.monthid2date = monthid2date
2408
2409
2410 def closeMonthly(self, event=None):
2411 self.month_height = self.topwindow.panecget(self.toppane, "height")
2412 self.topwindow.forget(self.toppane)
2413 self.monthly = False
20232414 self.tree.pack(fill="both", expand=1, padx=4, pady=0)
20242415 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")
2416 for i in range(4, 6):
2417 self.toolsmenu.entryconfig(i, state="disabled")
22832418 self.bind("<Control-f>", self.setFilter)
22842419
22852420 def showMonthly(self, event=None, chosen_day=None):
22902425 tt = TimeIt(loglevel=2, label="month view")
22912426 logger.debug("chosen_day: {0}; active_date: {1}".format(chosen_day, self.active_date))
22922427 if self.monthly:
2293 # we're in weekview already
2428 # we're in month view already
22942429 return
22952430 if self.weekly:
22962431 self.closeWeekly()
22972432 self.content.delete("1.0", END)
22982433 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")
2434 for i in range(4, 6):
2435 self.toolsmenu.entryconfig(i, state="normal")
2436
2437 self.setView(DAY)
23032438
23042439 self.view = MONTH
23052440 self.currentView.set(MONTH)
23112446 else:
23122447 self.chosen_day = get_current_time()
23132448
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()
2449 self.topwindow.add(self.toppane, padx=0, pady=0, before=self.botwindow, height=self.month_height)
2450
2451 self.showMonth(event=event)
23212452 tt.stop()
23222453
23232454 def showMonth(self, event=None, month=None):
23332464 logger.debug('self.current_day: {0}, minutes: {1}'.format(self.current_day, self.current_minutes))
23342465 self.x_win = self.canvas.winfo_width()
23352466 self.y_win = self.canvas.winfo_height()
2467 month_day = 1
2468 use_chosen = False
23362469 if month in [-1, 0, 1]:
23372470 if month == 0:
23382471 self.year_month = [self.current_day.year, self.current_day.month]
23472480 self.year_month[1] += 12
23482481 self.year_month[0] -= 1
23492482 elif self.chosen_day:
2483 use_chosen = True
23502484 self.year_month = [self.chosen_day.year, self.chosen_day.month]
2485 month_day = self.chosen_day.day
23512486 else:
23522487 return
23532488 logger.debug('month active_date: {0}'.format(self.active_date))
2489 day = date(self.year_month[0], self.year_month[1], month_day)
2490 if use_chosen:
2491 scrolldate = self.chosen_day.date()
2492 self.canvas_idpos = month_day - 1
2493 # self.canvas_idpos = weekdaynum - 1
2494 else:
2495 scrolldate = day
2496 self.canvas_idpos = 0
2497 self.scrollToDate(scrolldate)
2498
23542499 weeks = self.monthly_calendar.monthdatescalendar(*self.year_month)
23552500 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]
2501 weekdays = [s2or3(x.strftime("%a")) for x in weeks[0]]
23592502 themonth = weeks[1][0].strftime("%B %Y")
23602503 self.canvas.delete("all")
2361 l = 36
2362 r = 8
2363 t = 56
2364 b = 8
2504 l = 4
2505 r = 4
2506 t = 22
2507 b = 4
23652508 if event:
23662509 logger.debug('event: {0}'.format(event))
23672510 w, h = event.width, event.height
23732516 h = self.canvas.winfo_height()
23742517 else:
23752518 w = self.canvas.winfo_width()
2376 h = self.canvas.winfo_height()
2519 # h = self.canvas.winfo_height()
2520 h = self.month_height
23772521 logger.debug("w: {0}, h: {1}, l: {2}, t: {3}".format(w, h, l, t))
23782522
23792523 self.margins = (w, h, l, r, t, b)
23852529
23862530 # month
23872531 p = l + int((w - 1 - l - r) / 2), 20
2388 self.canvas.create_text(p, text="{0}".format(themonth))
2532 self.currentView.set(themonth)
23892533 self.busyHsh = {}
23902534
23912535 # occasions
23922536 busy_ids = set([])
23932537 monthid2date = {}
23942538
2395 self.canvas.bind('<Escape>', self.on_clear_item)
2539 # self.canvas.bind('<Escape>', self.on_leave_item)
23962540
23972541 # monthdays
2398 intervals = [240, 480, 720, 960]
2542 intervals = [360, 720, 1080, 1440]
23992543 busywidth = 2
24002544 offset = 6
24012545 indent = 7
24032547 # barcolor = "SkyBlue3"
24042548 # barcolor = "SlateBlue4"
24052549 # barcolor = "SlateGray3"
2550
2551 # nightcolor = "indianred1"
2552 # morningcolor = "khaki3"
2553 # afternooncolor = "PaleGreen3"
2554 # eveningcolor = "SteelBlue3"
2555
24062556 barcolor = "SteelBlue3"
2557 nightcolor = barcolor
2558 morningcolor = barcolor
2559 afternooncolor = barcolor
2560 eveningcolor = barcolor
24072561
24082562 for j in range(num_weeks):
24092563 for i in range(7):
24102564 busytimes = 0
2565 flagcolor = "white"
24112566 start_x = l + i * x_
24122567 end_x = start_x + x_
24132568 start_y = int(t + y_ * j)
24142569 end_y = start_y + y_
24152570 xy = int(start_x), int(start_y), int(end_x), int(end_y)
24162571 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_ )
2572 # pp = int(l + x_ + x_ * i), int(t + y_ * j + y_ )
24182573
24192574 tl_x = bl_x = int(l + x_ * i)
24202575 tl_y = tr_y = int(t + y_ *j)
24262581 thisdate = weeks[j][i]
24272582 isokey = thisdate.isocalendar()
24282583 month = thisdate.month
2429 fill = None
24302584 tags = []
24312585 if (month != self.year_month[1]):
24322586 fill = "gray70"
24382592 today = (thisdate == self.current_day.date())
24392593 bt = []
24402594 if today:
2595 flagcolor = CURRENTFILL
24412596 tags.append('current_day')
2442 if isokey in loop.occasions:
2597 if loop.occasions is not None and isokey in loop.occasions:
24432598 bt = []
24442599 for item in loop.occasions[isokey]:
24452600 it = list(item)
24552610 occasion_lst.append(bt)
24562611 if bt:
24572612 if not today:
2613 flagcolor = OCCASIONFILL
24582614 tags.append('occasion')
24592615 self.busyHsh.setdefault(id, []).extend(["^ {0}".format(x[0]) for x in bt])
24602616 else:
24612617 occasion_lst.append([])
24622618
2463 if isokey in loop.busytimes:
2619 if loop.busytimes is not None and isokey in loop.busytimes:
24642620 bt = []
2621 overlap = False
24652622 for item in loop.busytimes[isokey]:
24662623 it = list(item)
24672624 if it[0] == it[1]:
24792636 busy_lst.append(bt)
24802637 busy_dates.append(thisdate.strftime("%a %d"))
24812638 if bt:
2639 lastend = 0
24822640 for pts in bt:
24832641 busytimes += pts[1] - pts[0]
24842642 self.busyHsh.setdefault(id, []).append("* {0}".format(pts[2]))
2643 if pts[0] < lastend:
2644 overlap = True
2645 lastend = pts[1]
2646 if overlap:
2647 flagcolor = "red"
24852648 tags.append('busy')
24862649
24872650 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
2651 # each side 360 minutes plus 2 times bar width
24942652 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
2653 pt1 = max(0, pts[0])
2654 pt2 = min(pts[1], 1440)
25072655 tmp = [[], [], [], []]
25082656
25092657 for ii in range(0, 4):
25232671 if tmp[ii]:
25242672 busylines[ii].append(tmp[ii])
25252673
2526
25272674 if busylines:
25282675 for side in range(4):
25292676 lines = busylines[side]
25302677 if lines:
2531 if side == 0: # top
2678 if side == 0: # left
2679 for line in lines:
2680 bx = ex = bl_x + offset
2681 by = bl_y - indent - int(Decimal((line[0])/360) * h_)
2682 ey = bl_y - indent - int(Decimal((line[1])/360) * h_)
2683 self.canvas.create_line((bx, by, ex, ey), fill=nightcolor, width=busywidth, tag="busy")
2684 elif side == 1: # top
25322685 for line in lines:
25332686 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
2687 bx = tl_x + indent + int(Decimal((line[0]-360)/360) * w_)
2688 ex = tl_x + indent + int(Decimal((line[1]-360)/360) * w_)
2689 self.canvas.create_line((bx, by, ex, ey), fill=morningcolor, width=busywidth, tag="busy")
2690 elif side == 2: # right
25382691 for line in lines:
25392692 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
2693 by = tr_y + indent + int(Decimal((line[0]-720)/360) * h_)
2694 ey = tr_y + indent + int(Decimal((line[1]-720)/360) * h_)
2695 self.canvas.create_line((bx, by, ex, ey), fill=afternooncolor, width=busywidth, tag="busy")
2696 elif side == 3: # bottom
25442697 for line in lines:
25452698 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")
2699 bx = br_x - indent - int(Decimal((line[0]-1080)/360) * w_)
2700 ex = br_x - indent - int(Decimal((line[1]-1080)/360) * w_)
2701 self.canvas.create_line((bx, by, ex, ey), fill=eveningcolor, width=busywidth, tag="busy")
2702
2703 bx = bl_x + offset - 1.5 * busywidth
2704 ex = bl_x + offset + .5 * busywidth
2705 by = bl_y - indent + 1.5 * busywidth
2706 ey = bl_y - indent - .5 * busywidth
2707 if flagcolor:
2708 self.canvas.create_rectangle((bx, by, ex, ey), fill=flagcolor, outline=flagcolor, tag="busy")
25552709 else:
25562710 busy_lst.append([])
25572711 busy_dates.append(thisdate.strftime("%a %d"))
25642718
25652719 if fill:
25662720 self.canvas.create_text(p, text="{0}".format(weeks[j][i].day), fill=fill)
2721
25672722 busy_ids = list(busy_ids)
25682723 for id in busy_ids:
25692724 self.canvas.tag_bind(id, '<Any-Enter>', self.on_enter_item)
25702725 self.canvas.tag_bind(id, '<Any-Leave>', self.on_leave_item)
25712726
25722727 # 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")
2728 # xy = int(l), int(t), int(l + x_ * 7), int(t + y_ * num_weeks + 1)
2729 # self.canvas.create_rectangle(xy, tag="grid")
25752730
25762731 # verticals
25772732 for i in range(1, 7):
2578 xy = int(l + x_ * i), int(t), int(l + x_ * i), int(t + y_ * num_weeks)
2733 xy = int(l + x_ * i), int(t-18), int(l + x_ * i), int(t + y_ * num_weeks)
25792734 self.canvas.create_line(xy, fill=LINECOLOR, tag="grid")
25802735 # horizontals
25812736 for j in range(1, num_weeks):
25822737 xy = int(l), int(t + y_ * j), int(l + x_ * 7), int(t + y_ * j)
25832738 self.canvas.create_line(xy, fill=LINECOLOR, tag="grid")
25842739
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")
25892740 # days
25902741 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]))
2742 p = int(l + x_ / 2 + x_ * i), int(t - 10)
2743 self.canvas.create_text(p, text="{0}".format(weekdays[i]),fill = "SteelBlue4")
25982744
25992745 self.busy_info = (themonth, busy_dates, busy_lst, occasion_lst)
26002746 self.busy_ids = busy_ids
26012747 self.busy_ids.sort()
26022748 self.canvas_ids = self.busy_ids
26032749 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
2750
26232751
26242752 def selectId(self, event, d=1):
26252753 ids = self.busy_ids
26442772 if d == -1:
26452773 self.canvas_idpos -= 1
26462774 if self.canvas_idpos < 0:
2775 self.priorWeekMonth(event=event)
26472776 self.canvas_idpos = len(self.canvas_ids) - 1
26482777 elif d == 1:
26492778 self.canvas_idpos += 1
26502779 if self.canvas_idpos > len(self.canvas_ids) - 1:
2780 self.nextWeekMonth(event=event)
26512781 self.canvas_idpos = 0
2652 if self.weekly:
2653 self.selectedId = id = self.canvas_ids[self.canvas_idpos]
2782
2783 if old_id is not None and old_id in self.busy_ids:
2784 tags = self.canvas.gettags(old_id)
2785 if 'current_day' in tags:
2786 self.canvas.itemconfig(old_id, fill=CURRENTFILL)
2787 elif 'occasion' in tags:
2788 self.canvas.itemconfig(old_id, fill=OCCASIONFILL)
2789 elif 'busy' in tags:
2790 self.canvas.itemconfig(old_id, fill="white")
2791 else:
2792 self.canvas.itemconfig(old_id, fill="white")
2793
2794 self.selectedId = id = self.canvas_ids[self.canvas_idpos]
2795 self.active_date = self.monthid2date[id]
2796 self.canvas_date = self.monthid2date[id]
2797 self.scrollToDate(self.active_date)
2798 self.canvas_idpos = self.canvas_ids.index(id)
2799 if id in self.busy_ids:
26542800 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:
2801 if id in self.busyHsh:
2802 txt = "\n".join(self.busyHsh[id])
2803 self.content.delete("1.0", END)
2804 self.content.insert("1.0", txt)
2805 else:
2806 self.content.delete("1.0", END)
2807 self.setFocus(e=event)
2808
2809
2810 def setFocus(self, e):
2811 self.canvas.focus()
2812 self.canvas.focus_set()
2813
2814 def on_enter_item(self, e):
2815 if self.canvas_idpos is not None:
2816 old_id = self.canvas_ids[self.canvas_idpos]
2817 if old_id in self.busy_ids:
26662818 tags = self.canvas.gettags(old_id)
26672819 if 'current_day' in tags:
26682820 self.canvas.itemconfig(old_id, fill=CURRENTFILL)
26692821 elif 'occasion' in tags:
26702822 self.canvas.itemconfig(old_id, fill=OCCASIONFILL)
2671 elif 'busy' in tags:
2672 self.canvas.itemconfig(old_id, fill="white")
26732823 else:
26742824 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
2825 self.selectedId = id = self.canvas.find_withtag(CURRENT)[0]
2826 self.active_date = self.monthid2date[id]
2827 self.canvas_date = self.monthid2date[id]
2828 self.canvas_idpos = self.canvas_ids.index(id)
2829 if id in self.busy_ids:
27102830 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)
2831 if id in self.busyHsh:
2832 txt = "\n".join(self.busyHsh[id])
2833 self.content.delete("1.0", END)
2834 self.content.insert("1.0", txt)
2835 else:
2836 self.content.delete("1.0", END)
27442837
27452838 def on_leave_item(self, e):
2746 if self.weekly or self.monthly:
2747 return
27482839 self.content.delete("1.0", END)
27492840 id = self.canvas.find_withtag(CURRENT)[0]
27502841 if id in self.busy_ids:
27582849 else:
27592850 self.canvas.itemconfig(id, fill="white")
27602851
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("")
2852 # def on_clear_item(self, e=None):
2853 # if not self.weekly or self.monthly:
2854 # return
2855 # if self.selectedId:
2856 # id = self.selectedId
2857 # if id in self.busy_ids:
2858 # tags = self.canvas.gettags(id)
2859 # if 'other' in tags:
2860 # self.canvas.itemconfig(id, fill=OTHERFILL)
2861 # else:
2862 # self.canvas.itemconfig(id, fill=DEFAULTFILL)
2863 # else:
2864 # self.canvas.itemconfig(id, fill=OCCASIONFILL)
2865 # self.canvas.tag_raise('conflict')
2866 # self.canvas.tag_raise('grid')
2867 # self.canvas.tag_lower('occasion')
2868 # self.selectedId = None
2869 # self.OnSelect()
2870 # self.canvas.focus("")
27802871
27812872 def on_select_item(self, event):
2782 if self.monthly:
2873 if self.monthly or self.weekly:
27832874 self.newItem()
27842875 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)
27922876 return "break"
27932877
27942878 def on_activate_item(self, event):
2795 if self.monthly:
2879 if self.monthly or self.weekly:
27962880 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()
28102881
28112882 def newEvent(self, event):
28122883 logger.debug("event: {0}".format(event))
28132884 self.canvas.focus_set()
2814 min_round = 15
28152885 px = event.x
28162886 py = event.y
28172887 (w, h, l, r, t, b) = self.margins
28182888 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
28292889 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)
2890 dt = (self.week_beg + rx * ONEDAY).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
28382891 dfmt = dt.strftime("%a %b %d")
2839 dtfmt = "{0} {1}".format(tfmt, dfmt)
2840 s = "* @s {0}".format(dtfmt)
2892 s = "* @s {0}".format(dfmt)
28412893 changed = SimpleEditor(parent=self, master=self.canvas, start=s, options=loop.options).changed
28422894
28432895 if changed:
28452897
28462898 self.updateAlerts()
28472899 if self.weekly:
2848 self.showWeek()
2900 self.updateDay()
2901 self.showWeek(event=event)
28492902 elif self.monthly:
2850 self.showMonth()
2903 self.updateDay()
2904 self.showMonth(event=event)
28512905 else:
28522906 self.showView()
28532907
28912945 takefocus=False)
28922946 win.bind('<Left>', (lambda e: showYear(-1)))
28932947 win.bind('<Right>', (lambda e: showYear(1)))
2894 win.bind('<space>', (lambda e: showYear()))
2948 win.bind('<Home>', (lambda e: showYear()))
28952949 showYear()
28962950 t.pack(side='left', fill=tkinter.BOTH, expand=1, padx=0, pady=0)
28972951 ysb = Scrollbar(f, orient='vertical', command=t.yview, width=8)
29883042 return "break"
29893043
29903044 def goHome(self, event=None):
3045 today = get_current_time().date()
29913046 if self.weekly:
2992 self.showWeek(week=0)
3047 self.showWeek(event=event, week=0)
3048 self.scrollToDate(today)
29933049 elif self.monthly:
2994 self.showMonth(month=0)
3050 self.showMonth(event=event, month=0)
3051 self.scrollToDate(today)
29953052 elif self.view == DAY:
2996 today = get_current_time().date()
29973053 self.scrollToDate(today)
29983054 else:
29993055 self.tree.focus_set()
30003056 self.tree.focus(1)
30013057 self.tree.selection_set(1)
30023058 self.tree.yview(0)
3059 return
30033060
30043061 def nextItem(self, e=None):
30053062 item = self.tree.selection()[0]
30063063 if item:
30073064 next = self.tree.next(item)
30083065 if next:
3009
30103066 next = int(next)
30113067 next -= 1
30123068 self.tree.focus(next)
30173073 if item:
30183074 prev = self.tree.prev(item)
30193075 if prev:
3020
30213076 prev = int(prev)
30223077 prev += 1
30233078 self.tree.focus(prev)
30293084 """
30303085 logger.debug("starting OnSelect with uuid: {0}".format(uuid))
30313086 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
3087 if uuid is None: # tree view
3088 if not self.tree.selection():
3089 return
30383090 item = self.tree.selection()[0]
30393091 self.rowSelected = int(item)
30403092 logger.debug('rowSelected: {0}'.format(self.rowSelected))
30413093 # 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]
3094 type_chr = self.tree.item(item)['text'][0]
30443095 uuid, dt, hsh = self.getInstance(item)
30453096 logger.debug('tree rowSelected: {0}; {1}; {2}'.format(self.rowSelected, self.tree.item(item)['text'], dt))
3046 if self.view in [AGENDA, DAY]:
3097 if self.view in [AGENDA]:
30473098 if self.rowSelected in self.id2date:
30483099 if dt is None:
30493100 # we have the date selected
30753126 self.itemmenu.entryconfig(1, label=self.em_opts[1])
30763127 self.itemmenu.entryconfig(2, label=self.em_opts[2])
30773128 item = _('selected')
3078 isUnfinished = (type_chr in ['-', '+', '%'] and show_chr != 'X')
3129 isUnfinished = (type_chr in ['-', '+', '%'])
30793130 hasLink = ('g' in hsh and hsh['g'])
3080 # hasUser = ('u' in hsh and hsh['u'] and hsh['u'] in loop.options['user_data'])
30813131 hasUser = ('u' in hsh and hsh['u'])
30823132 l1 = hsh['fileinfo'][1]
30833133 l2 = hsh['fileinfo'][2]
30903140 text = "{1}\n\n{2}: {3}\n\n{4}: {5}".format(item, hsh['entry'].lstrip(), _("Errors"), hsh['errors'], _("file"), filetext)
30913141 else:
30923142 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)
3143 for i in [0, 1, 2, 3, 5, 6, 7, 8]: # everything except finish (4), open link (9) and show user (10)
30943144 self.itemmenu.entryconfig(i, state='normal')
30953145 if isUnfinished:
30963146 self.itemmenu.entryconfig(4, state='normal')
30973147 else:
30983148 self.itemmenu.entryconfig(4, state='disabled')
30993149 if hasLink:
3100 self.itemmenu.entryconfig(8, state='normal')
3101 else:
3102 self.itemmenu.entryconfig(8, state='disabled')
3103 if hasUser:
31043150 self.itemmenu.entryconfig(9, state='normal')
31053151 else:
31063152 self.itemmenu.entryconfig(9, state='disabled')
3153 if hasUser:
3154 self.itemmenu.entryconfig(10, state='normal')
3155 else:
3156 self.itemmenu.entryconfig(10, state='disabled')
31073157 self.uuidSelected = uuid
31083158 self.itemSelected = hsh
31093159 logger.debug('dt selected: {0}, {1}'.format(dt, type(dt)))
31103160 self.dtSelected = dt
31113161 else:
31123162 text = ""
3113 for i in range(10):
3163 for i in range(11):
31143164 self.itemmenu.entryconfig(i, state='disabled')
31153165 self.itemSelected = None
31163166 self.uuidSelected = None
31543204 logger.debug('returning None')
31553205 return None, None, None
31563206
3207 def updateTimerStatus(self):
3208 title, status = self.actionTimer.getStatus()
3209 # print('setting title:', title, " and status:", status)
3210 self.timerTitle.set(title)
3211 self.timerStatus.set(status)
3212
31573213 def updateClock(self):
31583214 tt = TimeIt(loglevel=2, label="updateClock")
31593215 self.now = get_current_time()
31603216 self.current_minutes = self.now.hour * 60 + self.now.minute
31613217 nxt = (60 - self.now.second) * 1000 - self.now.microsecond // 1000
3218 nowfmt = "{0} {1}".format(
3219 s2or3(self.now.strftime("%a %b %d")),
3220 s2or3(self.now.strftime(loop.options['reprtimefmt']).lower()),
3221 )
31623222 logger.debug('next update in {0} milliseconds.'.format(nxt))
31633223 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")))
31673224
31683225 nowfmt = leadingzero.sub("", nowfmt)
31693226 self.currentTime.set("{0}".format(nowfmt))
3227 self.title(self.currentTime.get())
31703228 today = self.now.date()
31713229 newday = (today != self.today)
31723230 self.today = today
31773235 if newday or new or modified or deleted:
31783236 if newday:
31793237 logger.info('newday')
3238 self.actionTimer.newDay()
3239
31803240 logger.info("new: {0}; modified: {1}; deleted: {2}".format(len(new), len(modified), len(deleted)))
31813241 logger.debug('calling loadData')
31823242 loop.loadData()
31843244
31853245 if self.weekly:
31863246 logger.debug('calling showWeek')
3247 self.updateDay()
31873248 self.showWeek()
3249 if newday:
3250 self.scrollToDate(today)
31883251 elif self.monthly:
31893252 logger.debug('calling showMonth')
3253 self.updateDay()
31903254 self.showMonth()
3255 if newday:
3256 self.scrollToDate(today)
31913257 else:
31923258 logger.debug('calling showView')
31933259 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()
32003260
32013261 if self.current_minutes % loop.options['update_minutes'] == 0:
32023262 if loop.do_update:
32223282
32233283 self.updateAlerts()
32243284
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':
3285 if self.actionTimer.currentStatus != STOPPED:
3286 title, status = self.actionTimer.getStatus()
3287 self.timerTitle.set(title)
3288 self.timerStatus.set(status)
3289 if self.actionTimer.currentMinutes >= 1:
3290 if (self.options['action_interval'] and self.actionTimer.currentMinutes % loop.options['action_interval'] == 0):
3291 logger.debug('action_minutes trigger: {0} {1}'.format(self.actionTimer.currentMinutes, self.actionTimer.currentStatus))
3292 if self.actionTimer.currentStatus == 'running':
32313293 if ('running' in loop.options['action_timer'] and
32323294 loop.options['action_timer']['running']):
32333295 tcmd = loop.options['action_timer']['running']
32353297
32363298 subprocess.call(tcmd, shell=True)
32373299
3238 elif self.actionTimer.timer_status == 'paused':
3300 elif self.actionTimer.currentStatus == 'paused':
32393301 if ('paused' in loop.options['action_timer'] and
32403302 loop.options['action_timer']['paused']):
32413303 tcmd = loop.options['action_timer']['paused']
32423304
32433305 logger.debug('paused: {0}'.format(tcmd))
32443306 subprocess.call(tcmd, shell=True)
3245
32463307 tt.stop()
32473308
32483309 def updateAlerts(self):
33753436 td = alerts[0][0] - curr_minutes
33763437 if alerts and len(alerts) > 0:
33773438 self.pendingAlerts.set(len(alerts))
3378 self.pending.configure(state="normal")
3439 # self.pending.configure(state="normal")
33793440 self.activeAlerts = alerts
33803441 else:
33813442 self.pendingAlerts.set(0)
33823443 self.activeAlerts = []
3383 self.pending.configure(state="disabled")
3444 # self.pending.configure(state="disabled")
33843445
33853446 def textWindow(self, parent, title=None, prompt=None, opts=None, modal=True):
33863447 TextDialog(parent, title=title, prompt=prompt, opts=opts, modal=modal)
33873448
33883449 def goToDate(self, e=None):
3389 """
3390 :param e:
3391 :return:
3392 """
33933450 if e and e.char != "j":
33943451 return
33953452 prompt = _("""\
33963453 Return an empty string for the current date or a date to be parsed.
33973454 Relative dates and fuzzy parsing are supported.""")
3398 if self.view not in [DAY, WEEK]:
3399 self.view = DAY
3400 self.showView()
3455 if self.view not in [DAY, WEEK, MONTH]:
3456 return
34013457 d = GetDateTime(parent=self, title=_('date'), prompt=prompt)
34023458 day = d.value
34033459
34043460 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())
3461 if day is None:
3462 return
3463 self.chosen_day = day
3464
3465 if self.weekly:
3466 self.showWeek(event=e, week=None)
3467 elif self.monthly:
3468 self.showMonth(event=e, month=None)
3469 self.scrollToDate(day.date())
34143470 return
34153471
34163472 def setFilter(self, *args):
3417 if self.view in [WEEK, MONTH, CUSTOM]:
3473 if self.view in [CUSTOM]:
34183474 return
34193475 self.filter_active = True
3420 self.viewmenu.entryconfig(6, state="disabled")
3421 self.viewmenu.entryconfig(7, state="normal")
3476 # self.motionmenu.entryconfig(6, state="disabled")
3477 # self.motionmenu.entryconfig(7, state="normal")
34223478 self.fltr.configure(bg="white", state="normal")
34233479 self.fltr.focus_set()
34243480
34253481 def clearFilter(self, e=None):
3426 if self.view in [WEEK, MONTH, CUSTOM]:
3482 if self.view in [CUSTOM]:
34273483 return
34283484 self.filter_active = False
3429 self.viewmenu.entryconfig(6, state="normal")
3430 self.viewmenu.entryconfig(7, state="disabled")
3485 # self.motionmenu.entryconfig(6, state="normal")
3486 # self.motionmenu.entryconfig(7, state="disabled")
34313487 self.filterValue.set('')
34323488 self.fltr.configure(bg=BGCOLOR)
34333489 self.tree.focus_set()
34433499 self.tree.selection_set(self.rowSelected)
34443500 self.tree.see(self.rowSelected)
34453501
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):
3502
3503 def kloneTimer(self, e=None):
34713504 """
3472 Prompt for a summary and start action timer.
3473 if uuid:
3474 if ~
3475 restart timer?
3476 else:
3477 enter summary or empty
34783505 """
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
3506 # hack to avoid activating with Ctrl-k
3507 if e and e.char != "k":
3508 return
3509 if not self.uuidSelected:
3510 return
3511 hsh = loop.uuid2hash[self.uuidSelected]
3512 self.timerItem = self.uuidSelected
3513 logger.debug('item: {0}'.format(hsh))
3514 self.actionTimer.selectTimer(name=hsh['_summary'])
35393515
35403516 def finishActionTimer(self, e=None):
35413517 if e and e.char != "T":
35423518 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
3519 thsh = self.actionTimer.finishTimer(e=e)
3520 if not thsh:
3521 return
3522 self.updateTimerStatus()
3523
3524 hsh = {"itemtype": "~", "_summary": thsh['summary'], "s": thsh['start'], "e": thsh['total']}
35493525 changed = SimpleEditor(parent=self, newhsh=hsh, rephsh=None, options=loop.options, title=_("new action"), modified=True).changed
35503526 if changed:
35513527 # clear status and reload
3552 self.actionTimer.timer_clear()
3553 # self.timerStatus.set("")
3554 self.newmenu.entryconfig(3, state="disabled")
3528 self.actionTimer.deleteTimer(timer = self.actionTimer.selected)
35553529
35563530 self.updateAlerts()
35573531 if self.weekly:
3532 self.updateDay()
35583533 self.showWeek()
35593534 elif self.monthly:
3535 self.updateDay()
35603536 self.showMonth()
35613537 else:
35623538 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()
3539
3540 self.updateTimerStatus()
35813541
35823542 def gettext(self, event=None):
35833543 s = self.e.get()
36473607 self.textWindow(self, title='etm', prompt=res, opts=self.options)
36483608 return 0
36493609
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
36573610 def expand2Depth(self, e=None):
3658 self.getDepths()
3659
36603611 if e and e.char != "o":
36613612 return
36623613 prompt = _("""\
36963647 except:
36973648 logger.exception('open: {0}, {1}'.format(i, item))
36983649
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
3650 def scrollToDate(self, date=None):
3651 if not loop.prevnext or date is None:
3652 return
3653 if self.view not in [DAY, WEEK, MONTH] or date not in loop.prevnext:
3654 return
3655 # new: go to the first date on or **after**, i.e., prevnext last
37043656 active_date = loop.prevnext[date][1]
37053657 if active_date not in self.date2id:
37063658 return
3659 if self.weekly:
3660 pos = date.isocalendar()[2] - 1
3661 self.canvas_idpos = pos
3662 elif self.monthly:
3663 pos = date.day - 1
3664 self.canvas_idpos = pos
37073665 uid = self.date2id[active_date]
37083666 self.active_date = active_date
37093667 self.scrollToId(uid)
37103668
37113669 def scrollToId(self, uid):
37123670 self.update_idletasks()
3713 self.tree.focus_set()
3671 # self.tree.focus_set()
37143672 self.tree.focus(uid)
37153673 self.tree.selection_set(uid)
37163674 self.tree.yview(int(uid) - 1)
37313689
37323690 if event is None:
37333691
3734 if self.view == DAY and self.active_date:
3692 if self.view in [DAY, WEEK, MONTH] and self.active_date:
37353693 self.scrollToDate(self.active_date)
37363694 else:
3737 if self.view in [AGENDA, DAY, TAG, KEYWORD, NOTE, PATH]:
3695 if self.view in [AGENDA, TAG, KEYWORD, NOTE, PATH]:
37383696 if self.filter_active:
37393697 depth = 0
37403698 else:
37573715 self.goHome()
37583716
37593717 def popupTree(self, e=None):
3760 if self.weekly or self.monthly:
3761 return
3718 # if self.weekly or self.monthly:
3719 # return
37623720 if not self.active_tree:
37633721 return
37643722 depth = self.outline_depths[self.view]
37813739
37823740 def printTree(self, e=None):
37833741 if e and e.char != "p":
3784 return
3785 if self.weekly or self.monthly:
37863742 return
37873743 if not self.active_tree:
37883744 return
39593915 export=False)
39603916 text = "\n".join([x for x in tree2Text(tree)[0]])
39613917 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()
3918 filename = FileChoice(self, "text file", prefix=prefix, list=tuples, ext="text", new=False).returnValue()
39633919 if not filename:
39643920 return False
39653921 fo = codecs.open(filename, 'w', self.options['encoding']['file'])
39773933 self.loop.uuid2hash,
39783934 self.loop.options,
39793935 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()
3936 prefix, tuples = getFileTuples(loop.options['exportdir'], include=r'*.csv', all=True)
3937 filename = FileChoice(self, "csv file", prefix=prefix, list=tuples, ext="csv", new=True).returnValue()
39823938 if not filename:
39833939 return
39843940 import csv as CSV
3985 c = CSV.writer(open(filename, "w"), delimiter=",")
3941 c = CSV.writer(open(filename, "w"), delimiter=",", lineterminator="\n")
39863942 for line in data:
39873943 c.writerow(line)
39883944 MessageWindow(self, "etm", "Exported CSV to {0}".format(filename))
00 Metadata-Version: 1.1
11 Name: etmtk
2 Version: 3.0.43
2 Version: 3.1.18
33 Summary: event and task manager
44 Home-page: http://people.duke.edu/~dgraham/etmtk
55 Author: Daniel A Graham
22 etm
33 etmtk
44 setup.py
5 etm-tk.wiki/help/UserManual.html
65 etmTk/CHANGES
76 etmTk/__init__.py
87 etmTk/data.py
98 etmTk/dialog.py
10 etmTk/edit.py
119 etmTk/etm.1
1210 etmTk/etm.appdata.xml
1311 etmTk/etm.desktop
1917 etmTk/version.py
2018 etmTk/view.py
2119 etmTk/help/UserManual.html
20 etmTk/icons/etmlogo.gif
21 etmTk/icons/icon_check.gif
22 etmTk/icons/icon_check_green.gif
23 etmTk/icons/icon_check_red.gif
24 etmTk/icons/icon_clock.gif
25 etmTk/icons/icon_pause.gif
26 etmTk/icons/icon_play.gif
27 etmTk/icons/icon_plus.gif
28 etmTk/icons/icon_refresh.gif
29 etmTk/icons/icon_search.gif
30 etmTk/icons/icon_settings.gif
31 etmTk/icons/icon_stop.gif
2232 etmtk/data.py
2333 etmtk/v.py
2434 etmtk.egg-info/PKG-INFO
2535 etmtk.egg-info/SOURCES.txt
2636 etmtk.egg-info/dependency_links.txt
2737 etmtk.egg-info/not-zip-safe
28 etmtk.egg-info/top_level.txt
29 test/test.py
38 etmtk.egg-info/top_level.txt
00 [egg_info]
11 tag_build =
2 tag_svn_revision = 0
23 tag_date = 0
3 tag_svn_revision = 0
44
5959 # install_requires=REQUIRES,
6060 # extras_require={"icalendar": EXTRAS},
6161 # 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', ]},
62 package_data={
63 'etmTk': ['icons/*', 'etm.desktop', 'etm.appdata.xml', 'CHANGES', 'etm.1', 'etm.xpm'],
64 'etmTk/help' : ['help/UserManual.html'],
65 'etmTk/icons': ['icons/*']},
6466 data_files=[
6567 ('share/man/man1', ['etmTk/etm.1']),
6668 ('share/doc/etm', ['etmTk/CHANGES']),
6769 ('share/pixmaps', ['etmTk/etm.xpm']),
70 ('share/icons', glob.glob('etmTk/icons/*.gif')),
6871 ('share/applications', ['etmTk/etm.desktop']),
6972 ('share/appdata', ['etmTk/etm.appdata.xml']),
7073 ]
+0
-42
test/test.py less more
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()