aboutsummaryrefslogtreecommitdiff
path: root/users/ryan/modules/taskwarrior-ubersicht/taskwarrior-widget-source/taskwarrior.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'users/ryan/modules/taskwarrior-ubersicht/taskwarrior-widget-source/taskwarrior.jsx')
-rwxr-xr-xusers/ryan/modules/taskwarrior-ubersicht/taskwarrior-widget-source/taskwarrior.jsx203
1 files changed, 203 insertions, 0 deletions
diff --git a/users/ryan/modules/taskwarrior-ubersicht/taskwarrior-widget-source/taskwarrior.jsx b/users/ryan/modules/taskwarrior-ubersicht/taskwarrior-widget-source/taskwarrior.jsx
new file mode 100755
index 0000000..14e23a5
--- /dev/null
+++ b/users/ryan/modules/taskwarrior-ubersicht/taskwarrior-widget-source/taskwarrior.jsx
@@ -0,0 +1,203 @@
1// Originally inspired by Taskwarrior Widget by Kevin Bockelandt:
2// https://github.com/KevinBockelandt/TaskwarriorWidget
3// Rewritten from scratch using the new React + JSX framework.
4
5'use strict';
6
7export const command = "task +READY -PARENT export";
8
9export const refreshFrequency = 10000;
10
11// Ridiculously high number of days (in ms), used to indicate there is no due date
12export const maxDue = 100000 * 60 * 60 * 24 * 1000;
13
14export const className = `
15 left: 30px;
16 top: 45px;
17 font-family: Helvetica Neue;
18 font-size: 10pt;
19 font-weight: 400;
20 background: rgba(0, 0, 0, 0.5);
21 border-radius: 7px;
22 padding: 5px;
23`;
24
25// Configuration
26// Note date differences are in ms
27const millisecondsInDay = 24 * 60 * 60 * 1000;
28const millisecondsInWeek = millisecondsInDay * 7;
29const millisecondsInMonth = millisecondsInDay * 30.42; // on average
30const millisecondsInYear = millisecondsInDay * 365.25; // appproximately
31// Maximum number of entries to display
32const maxEntries = 20;
33// Indicator for tasks that are started
34const startedIndicator = "🟊";
35
36// Colours
37const headerColour = Object.freeze({ r: 255, g: 255, b: 255, a: 1 });
38// The opacity is determined dynamically for all the following
39const overDueColour = Object.freeze({ r: 255, g: 100, b: 100 });
40const dueColour = Object.freeze({ r: 255, g: 200, b: 0 });
41const normalColour = Object.freeze({ r: 255, g: 255, b: 255 });
42const tagsColour = Object.freeze({ r: 50, g: 225, b: 50 });
43
44// Tasks fade out towards the bottom of the table in steps of 1 / maxEntries
45const opacities = Object.freeze((() => {
46 let values = [];
47 for (let i = 0; i < maxEntries; i++) {
48 values.push(((i / maxEntries * -1) + 1).toFixed(2));
49 }
50 return values;
51})());
52
53// Reformat and compute task values ready for output
54const processTask = (task, index) => {
55 // Compute number of days to due date
56 if (task.due) {
57 let dueDateOffset = maxDue;
58 // Taskwarrior date strings aren't quite in ISO 8601 format
59 // e.g.: 20191007T110000Z, which should be 2019-10-07T11:00:00Z
60 let dateParts = [task.due.slice(0, 4), task.due.slice(4, 6), task.due.slice(6, 8)].join("-");
61 let timeParts = [task.due.slice(8, 11), task.due.slice(11, 13), task.due.slice(13)].join(":");
62 let dueDate = new Date(dateParts + timeParts);
63 let today = new Date();
64 // Truncate the offset between the due date and today to the nearest day
65 today.setHours(0);
66 today.setMinutes(0);
67 today.setSeconds(0);
68 today.setMilliseconds(0);
69 dueDate.setMinutes(0);
70 dueDate.setSeconds(0);
71 dueDate.setMilliseconds(0);
72
73 let difference = dueDate.getTime() - today.getTime();
74 if (Math.abs(difference) > millisecondsInYear) {
75 dueDateOffset = Math.floor(difference / millisecondsInYear);
76 task.units = dueDateOffset + "y";
77 } else if (Math.abs(difference) > millisecondsInMonth) {
78 dueDateOffset = Math.floor(difference / millisecondsInMonth);
79 task.units = dueDateOffset + "m";
80 } else if (Math.abs(difference) > millisecondsInWeek) {
81 dueDateOffset = Math.floor(difference / millisecondsInWeek);
82 task.units = dueDateOffset + "w";
83 } else {
84 dueDateOffset = Math.floor(difference / millisecondsInDay);
85 task.units = dueDateOffset + "d";
86 }
87 task.due = difference;
88 }
89
90 // If the task has tags, merge them into a single string
91 if (task.tags) {
92 task.tags = task.tags.map((tag) => { return "+" + tag; }).join(" ");
93 }
94
95 // Mark started tasks
96 if (task.start) {
97 task.start = startedIndicator;
98 }
99
100 // don't need to check for 0 here as it'll get set to 0.0 anyway
101 task.urgency = task.urgency ? parseFloat(task.urgency).toFixed(2) : 0.0;
102
103 return task;
104};
105
106// Colourise the tasks in the output list
107const colourTask = (task, index) => {
108 // Colour row text according to due date
109 if (task.due < 0) {
110 task.colour = Object.assign({}, overDueColour);
111 }
112 else if (task.due == 0) {
113 task.colour = Object.assign({}, dueColour);
114 }
115 else {
116 task.colour = Object.assign({}, normalColour);
117 }
118 task.colour.a = opacities[index];
119
120 return task;
121}
122
123const rgbaString = (colour, a) => {
124 if (colour.a) {
125 return "rgba(" + colour.r + ", " + colour.g + ", " + colour.b + ", " + colour.a + ")";
126 }
127 else {
128 return "rgba(" + colour.r + ", " + colour.g + ", " + colour.b + ", " + a + ")";
129 }
130};
131
132export const render = ({ output, error }) => {
133 // What if error is already set?
134 // Will the following give an exception anyway?
135 try {
136 // Get the JSON object containing all the tasks and process them.
137 let taskList = JSON.parse(output).map(processTask);
138 // Sort the tasks by due date ascending, then by urgency descending.
139 taskList.sort((a, b) => {
140 // Adjust for no due date
141 // CAUTION: 0 is "falsy" but valid, so needs to be explicitly checked for
142 let aDue = (a.due || a.due === 0) ? a.due : maxDue;
143 let bDue = (b.due || b.due === 0) ? b.due : maxDue;
144 // Need urgency as a fraction. Maximum possible urgency is 60.7
145 // with default settings, so let’s assume it’s less than 1000.
146 return (aDue - a.urgency/1000) - (bDue - b.urgency/1000);
147 });
148 // We have to colourise after the sort.
149 // Only display the first maxEntries tasks.
150 taskList = taskList.slice(0, maxEntries).map(colourTask);
151
152 return (
153 <div id="taskwarrior-widget-container">
154 <link rel="stylesheet" type="text/css" href="taskwarrior.widget/style.css" />
155 <table>
156 <thead>
157 <tr className="header" style={{ color: rgbaString(headerColour) }}>
158 <th className="star">{startedIndicator}</th>
159 <th className="num">ID</th>
160 <th className="num">DUE</th>
161 <th>DESCRIPTION</th>
162 <th>PROJECT</th>
163 <th style={{ color: rgbaString(tagsColour, 1) }}>TAGS</th>
164 <th className="num">URG</th>
165 </tr>
166 </thead>
167 <tbody>
168 {taskList.map((task, index) => {
169 return (
170 <tr style={{ color: rgbaString(task.colour) }} key={index}>
171 <td className="star">{task.start}</td>
172 <td className="num">{task.id}</td>
173 <td className="num">{task.units}</td>
174 <td>{task.description}</td>
175 <td>{task.project}</td>
176 <td style={{ color: rgbaString(tagsColour, task.colour.a) }}>{task.tags}</td>
177 <td className="num">{task.urgency}</td>
178 </tr>
179 );
180 })}
181 </tbody>
182 </table>
183 </div>
184 )
185 }
186 catch (e) {
187 if (!output) {
188 return (
189 <div id="taskwarrior-widget-container">
190 <p><strong>No tasks found.</strong></p>
191 </div>
192 )
193 }
194 else {
195 console.error(e)
196 return (
197 <div id="taskwarrior-widget-container">
198 <p><strong class="error">Error: {e}.</strong></p>
199 </div>
200 )
201 }
202 }
203};