diff options
Diffstat (limited to 'users/ryan/modules/taskwarrior-ubersicht/taskwarrior-widget-source/taskwarrior.jsx')
| -rwxr-xr-x | users/ryan/modules/taskwarrior-ubersicht/taskwarrior-widget-source/taskwarrior.jsx | 203 |
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 | |||
| 7 | export const command = "task +READY -PARENT export"; | ||
| 8 | |||
| 9 | export const refreshFrequency = 10000; | ||
| 10 | |||
| 11 | // Ridiculously high number of days (in ms), used to indicate there is no due date | ||
| 12 | export const maxDue = 100000 * 60 * 60 * 24 * 1000; | ||
| 13 | |||
| 14 | export 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 | ||
| 27 | const millisecondsInDay = 24 * 60 * 60 * 1000; | ||
| 28 | const millisecondsInWeek = millisecondsInDay * 7; | ||
| 29 | const millisecondsInMonth = millisecondsInDay * 30.42; // on average | ||
| 30 | const millisecondsInYear = millisecondsInDay * 365.25; // appproximately | ||
| 31 | // Maximum number of entries to display | ||
| 32 | const maxEntries = 20; | ||
| 33 | // Indicator for tasks that are started | ||
| 34 | const startedIndicator = "🟊"; | ||
| 35 | |||
| 36 | // Colours | ||
| 37 | const headerColour = Object.freeze({ r: 255, g: 255, b: 255, a: 1 }); | ||
| 38 | // The opacity is determined dynamically for all the following | ||
| 39 | const overDueColour = Object.freeze({ r: 255, g: 100, b: 100 }); | ||
| 40 | const dueColour = Object.freeze({ r: 255, g: 200, b: 0 }); | ||
| 41 | const normalColour = Object.freeze({ r: 255, g: 255, b: 255 }); | ||
| 42 | const 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 | ||
| 45 | const 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 | ||
| 54 | const 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 | ||
| 107 | const 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 | |||
| 123 | const 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 | |||
| 132 | export 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 | }; | ||
