├── .DS_Store ├── .gitattributes ├── .github └── FUNDING.yml ├── LICENSE ├── OmniFocus Plug-Ins ├── .DS_Store ├── Change Defer Date │ ├── +1 day (Defer).omnifocusjs │ ├── +1 month (Defer).omnifocusjs │ ├── +1 week (Defer).omnifocusjs │ ├── +30 mins (Defer).omnifocusjs │ ├── -1 day (Defer).omnifocusjs │ ├── -30 mins (Defer).omnifocusjs │ ├── This Weekend (Defer).omnifocusjs │ ├── Today (Defer).omnifocusjs │ └── Tomorrow (Defer).omnifocusjs ├── Organize │ ├── Convert Tasks to Projects (Here).omnifocusjs │ ├── Move To Bottom.omnifocusjs │ ├── Move To Top.omnifocusjs │ └── Move Up.omnifocusjs ├── Postpone Due Date │ ├── +1 day (Due).omnifocusjs │ ├── +1 month (Due).omnifocusjs │ ├── +1 week (Due).omnifocusjs │ ├── This Weekend (Due).omnifocusjs │ ├── Today (Due).omnifocusjs │ └── Tomorrow (Due).omnifocusjs ├── Sharing │ └── Share Tasks With Specific Tag.omnijs └── Statistics │ ├── OF Statistics.omnijs │ └── Total of estimated minutes.omnifocusjs └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unlocked2412/Omni-Automation/83bca76008d7f3e090aa792cbf886939f2c2fe54/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.omnifocusjs linguist-language=JavaScript 2 | *.omnioutlinerjs linguist-language=JavaScript 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: unlocked2412 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 unlocked2412 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unlocked2412/Omni-Automation/83bca76008d7f3e090aa792cbf886939f2c2fe54/OmniFocus Plug-Ins/.DS_Store -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Change Defer Date/+1 day (Defer).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultStartTime'), 18 | ts = ["projects", "tasks"].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | tomorrow = addDays(1)( 22 | setTime( 23 | splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ), 28 | p = ( 29 | null === ts[0].deferDate || 30 | any(x => !eqDateTimeS(ts[0].deferDate)(x.deferDate))(ts) 31 | ); 32 | 33 | return p ? ( 34 | ts.map(updateDeferDate(tomorrow)) 35 | ) : ts.map(task => 36 | updateDeferDate(addDays(1)(task.deferDate))( 37 | task 38 | ) 39 | ) 40 | }; 41 | 42 | // updateDeferDate :: Date -> OFTask -> IO OFTask 43 | const updateDeferDate = dte => task => { 44 | return ( 45 | task.deferDate = dte, 46 | task 47 | ) 48 | }; 49 | 50 | 51 | // FUNCTIONS -- 52 | // https://github.com/RobTrew/prelude-jxa 53 | // JS Basics --------------------------------------------------- 54 | // addHours :: Int -> Date -> Date 55 | const addHours = n => dte => { 56 | const dte2 = new Date(dte); 57 | return ( 58 | dte2.setHours(n + dte.getHours()), 59 | dte2 60 | ); 61 | }; 62 | 63 | // addDays :: Int -> Date -> Date 64 | const addDays = n => dte => { 65 | const dte2 = new Date(dte); 66 | return ( 67 | dte2.setDate(n + dte.getDate()), 68 | dte2 69 | ); 70 | }; 71 | 72 | // addWeeks :: Int -> Date -> Date 73 | const addWeeks = n => addDays(7 * n); 74 | 75 | // addMonths :: Int -> Date -> Date 76 | const addMonths = n => dte => { 77 | const dte2 = new Date(dte); 78 | return ( 79 | dte2.setMonth(n + dte.getMonth()), 80 | dte2 81 | ); 82 | }; 83 | 84 | // eqDateTimeS :: Int -> Date -> Date -> Bool 85 | const eqDateTimeS = n => 86 | // Equivalence of two JS Date values 87 | // at a granularity of n seconds. 88 | // e.g. 89 | // Same second: eqDateTime(1)(a)(b) 90 | // Same minute: eqDateTime(60)(a)(b) 91 | // Same hour: eqDateTime(3600)(a)(b) 92 | on(a => b => a === b)( 93 | flip(div)(1E3 * n) 94 | ); 95 | 96 | // setTime :: [Int] -> Date -> Date 97 | const setTime = xs => dte => { 98 | const dte2 = new Date(dte); 99 | return ( 100 | dte2.setHours(0, 0, 0, 0), 101 | dte2.setHours(...xs), 102 | dte2 103 | ); 104 | }; 105 | 106 | // JS Prelude -------------------------------------------------- 107 | // Ratio :: Integral a => a -> a -> Ratio a 108 | const Ratio = a => b => { 109 | const go = (x, y) => 110 | 0 !== y ? (() => { 111 | const d = gcd(x)(y); 112 | 113 | return { 114 | type: "Ratio", 115 | // numerator 116 | "n": Math.trunc(x / d), 117 | // denominator 118 | "d": Math.trunc(y / d) 119 | }; 120 | })() : undefined; 121 | 122 | return go(a * signum(b), abs(b)); 123 | }; 124 | 125 | // Tuple (,) :: a -> b -> (a, b) 126 | const Tuple = a => 127 | b => ({ 128 | type: "Tuple", 129 | "0": a, 130 | "1": b, 131 | length: 2 132 | }); 133 | 134 | // abs :: Num -> Num 135 | const abs = 136 | // Absolute value of a given number - without the sign. 137 | x => 0 > x ? ( 138 | -x 139 | ) : x; 140 | 141 | // any :: (a -> Bool) -> [a] -> Bool 142 | const any = p => 143 | // True if p(x) holds for at least 144 | // one item in xs. 145 | xs => [...xs].some(p); 146 | 147 | // concat :: [[a]] -> [a] 148 | // concat :: [String] -> String 149 | const concat = xs => 150 | 0 < xs.length ? ( 151 | ( 152 | xs.every(x => "string" === typeof x) ? ( 153 | "" 154 | ) : [] 155 | ).concat(...xs) 156 | ) : xs; 157 | 158 | // div :: Int -> Int -> Int 159 | const div = x => 160 | y => Math.floor(x / y); 161 | 162 | // eq (==) :: Eq a => a -> a -> Bool 163 | const eq = a => 164 | // True when a and b are equivalent in the terms 165 | // defined below for their shared data type. 166 | b => { 167 | const t = typeof a; 168 | 169 | return t !== typeof b ? ( 170 | false 171 | ) : "object" !== t ? ( 172 | "function" !== t ? ( 173 | a === b 174 | ) : a.toString() === b.toString() 175 | ) : (() => { 176 | const kvs = Object.entries(a); 177 | 178 | return kvs.length !== Object.keys(b).length ? ( 179 | false 180 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 181 | })(); 182 | }; 183 | 184 | // findIndices :: (a -> Bool) -> [a] -> [Int] 185 | // findIndices :: (String -> Bool) -> String -> [Int] 186 | const findIndices = p => 187 | xs => { 188 | const ys = [...xs]; 189 | 190 | return ys.flatMap( 191 | (y, i) => p(y, i, ys) ? ( 192 | [i] 193 | ) : [] 194 | ); 195 | }; 196 | 197 | // flip :: (a -> b -> c) -> b -> a -> c 198 | const flip = op => 199 | // The binary function op with 200 | // its arguments reversed. 201 | 1 < op.length ? ( 202 | (a, b) => op(b, a) 203 | ) : (x => y => op(y)(x)); 204 | 205 | // floor :: Num -> Int 206 | const floor = x => { 207 | const 208 | nr = ( 209 | "Ratio" !== x.type ? ( 210 | properFraction 211 | ) : properFracRatio 212 | )(x), 213 | n = nr[0]; 214 | 215 | return 0 > nr[1] ? n - 1 : n; 216 | }; 217 | 218 | // fst :: (a, b) -> a 219 | const fst = tpl => 220 | // First member of a pair. 221 | tpl[0]; 222 | 223 | // gcd :: Integral a => a -> a -> a 224 | const gcd = x => 225 | y => { 226 | const zero = x.constructor(0); 227 | const go = (a, b) => 228 | zero === b ? ( 229 | a 230 | ) : go(b, a % b); 231 | 232 | return go(abs(x), abs(y)); 233 | }; 234 | 235 | // keys :: Dict -> [String] 236 | const keys = Object.keys; 237 | 238 | // length :: [a] -> Int 239 | const length = xs => 240 | // Returns Infinity over objects without finite 241 | // length. This enables zip and zipWith to choose 242 | // the shorter argument when one is non-finite, 243 | // like cycle, repeat etc 244 | "GeneratorFunction" !== xs.constructor 245 | .constructor.name ? ( 246 | xs.length 247 | ) : Infinity; 248 | 249 | // map :: (a -> b) -> [a] -> [b] 250 | const map = f => 251 | // The list obtained by applying f 252 | // to each element of xs. 253 | // (The image of xs under f). 254 | xs => [...xs].map(f); 255 | 256 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 257 | const matching = pat => { 258 | // A sequence-matching function for findIndices etc 259 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 260 | // -> [1, 4] 261 | const 262 | lng = pat.length, 263 | bln = 0 < lng, 264 | h = bln ? pat[0] : undefined; 265 | 266 | return x => i => src => 267 | bln && h === x && eq(pat)( 268 | src.slice(i, lng + i) 269 | ); 270 | }; 271 | 272 | // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c 273 | const on = f => 274 | // e.g. groupBy(on(eq)(length)) 275 | g => a => b => f(g(a))(g(b)); 276 | 277 | // properFracRatio :: Ratio -> (Int, Ratio) 278 | const properFracRatio = nd => { 279 | const [q, r] = Array.from(quotRem(nd.n, nd.d)); 280 | 281 | return Tuple(q, ratio(r, nd.d)); 282 | }; 283 | 284 | // properFraction :: Real -> (Int, Real) 285 | const properFraction = n => { 286 | const i = Math.floor(n) + (n < 0 ? 1 : 0); 287 | 288 | return Tuple(i)(n - i); 289 | }; 290 | 291 | // quotRem :: Integral a => a -> a -> (a, a) 292 | const quotRem = m => 293 | // The quotient, tupled with the remainder. 294 | n => Tuple( 295 | Math.trunc(m / n) 296 | )( 297 | m % n 298 | ); 299 | 300 | // signum :: Num -> Num 301 | const signum = n => 302 | // | Sign of a number. 303 | n.constructor( 304 | 0 > n ? ( 305 | -1 306 | ) : ( 307 | 0 < n ? 1 : 0 308 | ) 309 | ); 310 | 311 | // snd :: (a, b) -> b 312 | const snd = tpl => 313 | // Second member of a pair. 314 | tpl[1]; 315 | 316 | // splitOn :: [a] -> [a] -> [[a]] 317 | // splitOn :: String -> String -> [String] 318 | const splitOn = pat => src => 319 | // A list of the strings delimited by 320 | // instances of a given pattern in s. 321 | ("string" === typeof src) ? ( 322 | src.split(pat) 323 | ) : (() => { 324 | const 325 | lng = pat.length, 326 | tpl = findIndices(matching(pat))(src).reduce( 327 | (a, i) => Tuple( 328 | fst(a).concat([src.slice(snd(a), i)]) 329 | )(lng + i), 330 | Tuple([])(0) 331 | ); 332 | 333 | return fst(tpl).concat([src.slice(snd(tpl))]); 334 | })(); 335 | 336 | return main() 337 | }; 338 | 339 | return omniJSContext() 340 | }), { 341 | validate: selection => ["tasks", "projects"].some( 342 | k => selection[k].length > 0 343 | ) 344 | }) 345 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Change Defer Date/+1 month (Defer).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultStartTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | tomorrow = addMonths(1)( 22 | setTime( 23 | splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ), 28 | p = ( 29 | null === ts[0].deferDate || 30 | any(x => !eqDateTimeS(ts[0].deferDate)(x.deferDate))(ts) 31 | ); 32 | 33 | return p ? ( 34 | ts.map(updateDeferDate(tomorrow)) 35 | ) : ts.map(task => 36 | updateDeferDate(addMonths(1)(task.deferDate))( 37 | task 38 | ) 39 | ) 40 | }; 41 | 42 | // updateDeferDate :: Date -> OFTask -> IO OFTask 43 | const updateDeferDate = dte => task => { 44 | return ( 45 | task.deferDate = dte, 46 | task 47 | ) 48 | } 49 | 50 | 51 | // FUNCTIONS -- 52 | // https://github.com/RobTrew/prelude-jxa 53 | // JS Basics --------------------------------------------------- 54 | // addHours :: Int -> Date -> Date 55 | const addHours = n => dte => { 56 | const dte2 = new Date(dte); 57 | return ( 58 | dte2.setHours(n + dte.getHours()), 59 | dte2 60 | ); 61 | }; 62 | 63 | // addDays :: Int -> Date -> Date 64 | const addDays = n => dte => { 65 | const dte2 = new Date(dte); 66 | return ( 67 | dte2.setDate(n + dte.getDate()), 68 | dte2 69 | ); 70 | }; 71 | 72 | // addWeeks :: Int -> Date -> Date 73 | const addWeeks = n => addDays(7 * n); 74 | 75 | // addMonths :: Int -> Date -> Date 76 | const addMonths = n => dte => { 77 | const dte2 = new Date(dte); 78 | return ( 79 | dte2.setMonth(n + dte.getMonth()), 80 | dte2 81 | ); 82 | }; 83 | 84 | // eqDateTimeS :: Int -> Date -> Date -> Bool 85 | const eqDateTimeS = n => 86 | // Equivalence of two JS Date values 87 | // at a granularity of n seconds. 88 | // e.g. 89 | // Same second: eqDateTime(1)(a)(b) 90 | // Same minute: eqDateTime(60)(a)(b) 91 | // Same hour: eqDateTime(3600)(a)(b) 92 | on(a => b => a === b)( 93 | flip(div)(1E3 * n) 94 | ); 95 | 96 | // setTime :: [Int] -> Date -> Date 97 | const setTime = xs => dte => { 98 | const dte2 = new Date(dte); 99 | return ( 100 | dte2.setHours(0, 0, 0, 0), 101 | dte2.setHours(...xs), 102 | dte2 103 | ); 104 | }; 105 | 106 | // JS Prelude -------------------------------------------------- 107 | // Ratio :: Integral a => a -> a -> Ratio a 108 | const Ratio = a => b => { 109 | const go = (x, y) => 110 | 0 !== y ? (() => { 111 | const d = gcd(x)(y); 112 | 113 | return { 114 | type: "Ratio", 115 | // numerator 116 | "n": Math.trunc(x / d), 117 | // denominator 118 | "d": Math.trunc(y / d) 119 | }; 120 | })() : undefined; 121 | 122 | return go(a * signum(b), abs(b)); 123 | }; 124 | 125 | // Tuple (,) :: a -> b -> (a, b) 126 | const Tuple = a => 127 | b => ({ 128 | type: "Tuple", 129 | "0": a, 130 | "1": b, 131 | length: 2 132 | }); 133 | 134 | // abs :: Num -> Num 135 | const abs = 136 | // Absolute value of a given number - without the sign. 137 | x => 0 > x ? ( 138 | -x 139 | ) : x; 140 | 141 | // any :: (a -> Bool) -> [a] -> Bool 142 | const any = p => 143 | // True if p(x) holds for at least 144 | // one item in xs. 145 | xs => [...xs].some(p); 146 | 147 | // concat :: [[a]] -> [a] 148 | // concat :: [String] -> String 149 | const concat = xs => 150 | 0 < xs.length ? ( 151 | ( 152 | xs.every(x => "string" === typeof x) ? ( 153 | "" 154 | ) : [] 155 | ).concat(...xs) 156 | ) : xs; 157 | 158 | // div :: Int -> Int -> Int 159 | const div = x => 160 | y => Math.floor(x / y); 161 | 162 | // eq (==) :: Eq a => a -> a -> Bool 163 | const eq = a => 164 | // True when a and b are equivalent in the terms 165 | // defined below for their shared data type. 166 | b => { 167 | const t = typeof a; 168 | 169 | return t !== typeof b ? ( 170 | false 171 | ) : "object" !== t ? ( 172 | "function" !== t ? ( 173 | a === b 174 | ) : a.toString() === b.toString() 175 | ) : (() => { 176 | const kvs = Object.entries(a); 177 | 178 | return kvs.length !== Object.keys(b).length ? ( 179 | false 180 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 181 | })(); 182 | }; 183 | 184 | // findIndices :: (a -> Bool) -> [a] -> [Int] 185 | // findIndices :: (String -> Bool) -> String -> [Int] 186 | const findIndices = p => 187 | xs => { 188 | const ys = [...xs]; 189 | 190 | return ys.flatMap( 191 | (y, i) => p(y, i, ys) ? ( 192 | [i] 193 | ) : [] 194 | ); 195 | }; 196 | 197 | // flip :: (a -> b -> c) -> b -> a -> c 198 | const flip = op => 199 | // The binary function op with 200 | // its arguments reversed. 201 | 1 < op.length ? ( 202 | (a, b) => op(b, a) 203 | ) : (x => y => op(y)(x)); 204 | 205 | // floor :: Num -> Int 206 | const floor = x => { 207 | const 208 | nr = ( 209 | "Ratio" !== x.type ? ( 210 | properFraction 211 | ) : properFracRatio 212 | )(x), 213 | n = nr[0]; 214 | 215 | return 0 > nr[1] ? n - 1 : n; 216 | }; 217 | 218 | // fst :: (a, b) -> a 219 | const fst = tpl => 220 | // First member of a pair. 221 | tpl[0]; 222 | 223 | // gcd :: Integral a => a -> a -> a 224 | const gcd = x => 225 | y => { 226 | const zero = x.constructor(0); 227 | const go = (a, b) => 228 | zero === b ? ( 229 | a 230 | ) : go(b, a % b); 231 | 232 | return go(abs(x), abs(y)); 233 | }; 234 | 235 | // keys :: Dict -> [String] 236 | const keys = Object.keys; 237 | 238 | // length :: [a] -> Int 239 | const length = xs => 240 | // Returns Infinity over objects without finite 241 | // length. This enables zip and zipWith to choose 242 | // the shorter argument when one is non-finite, 243 | // like cycle, repeat etc 244 | "GeneratorFunction" !== xs.constructor 245 | .constructor.name ? ( 246 | xs.length 247 | ) : Infinity; 248 | 249 | // map :: (a -> b) -> [a] -> [b] 250 | const map = f => 251 | // The list obtained by applying f 252 | // to each element of xs. 253 | // (The image of xs under f). 254 | xs => [...xs].map(f); 255 | 256 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 257 | const matching = pat => { 258 | // A sequence-matching function for findIndices etc 259 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 260 | // -> [1, 4] 261 | const 262 | lng = pat.length, 263 | bln = 0 < lng, 264 | h = bln ? pat[0] : undefined; 265 | 266 | return x => i => src => 267 | bln && h === x && eq(pat)( 268 | src.slice(i, lng + i) 269 | ); 270 | }; 271 | 272 | // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c 273 | const on = f => 274 | // e.g. groupBy(on(eq)(length)) 275 | g => a => b => f(g(a))(g(b)); 276 | 277 | // properFracRatio :: Ratio -> (Int, Ratio) 278 | const properFracRatio = nd => { 279 | const [q, r] = Array.from(quotRem(nd.n, nd.d)); 280 | 281 | return Tuple(q, ratio(r, nd.d)); 282 | }; 283 | 284 | // properFraction :: Real -> (Int, Real) 285 | const properFraction = n => { 286 | const i = Math.floor(n) + (n < 0 ? 1 : 0); 287 | 288 | return Tuple(i)(n - i); 289 | }; 290 | 291 | // quotRem :: Integral a => a -> a -> (a, a) 292 | const quotRem = m => 293 | // The quotient, tupled with the remainder. 294 | n => Tuple( 295 | Math.trunc(m / n) 296 | )( 297 | m % n 298 | ); 299 | 300 | // signum :: Num -> Num 301 | const signum = n => 302 | // | Sign of a number. 303 | n.constructor( 304 | 0 > n ? ( 305 | -1 306 | ) : ( 307 | 0 < n ? 1 : 0 308 | ) 309 | ); 310 | 311 | // snd :: (a, b) -> b 312 | const snd = tpl => 313 | // Second member of a pair. 314 | tpl[1]; 315 | 316 | // splitOn :: [a] -> [a] -> [[a]] 317 | // splitOn :: String -> String -> [String] 318 | const splitOn = pat => src => 319 | // A list of the strings delimited by 320 | // instances of a given pattern in s. 321 | ("string" === typeof src) ? ( 322 | src.split(pat) 323 | ) : (() => { 324 | const 325 | lng = pat.length, 326 | tpl = findIndices(matching(pat))(src).reduce( 327 | (a, i) => Tuple( 328 | fst(a).concat([src.slice(snd(a), i)]) 329 | )(lng + i), 330 | Tuple([])(0) 331 | ); 332 | 333 | return fst(tpl).concat([src.slice(snd(tpl))]); 334 | })(); 335 | 336 | return main() 337 | }; 338 | 339 | return omniJSContext() 340 | }), { 341 | validate: selection => ["tasks", "projects"].some( 342 | k => selection[k].length > 0 343 | ) 344 | }) 345 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Change Defer Date/+1 week (Defer).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultStartTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | tomorrow = addWeeks(1)( 22 | setTime( 23 | splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ), 28 | p = ( 29 | null === ts[0].deferDate || 30 | any(x => !eqDateTimeS(ts[0].deferDate)(x.deferDate))(ts) 31 | ); 32 | 33 | return p ? ( 34 | ts.map(updateDeferDate(tomorrow)) 35 | ) : ts.map(task => 36 | updateDeferDate(addWeeks(1)(task.deferDate))( 37 | task 38 | ) 39 | ) 40 | }; 41 | 42 | // updateDeferDate :: Date -> OFTask -> IO OFTask 43 | const updateDeferDate = dte => task => { 44 | return ( 45 | task.deferDate = dte, 46 | task 47 | ) 48 | } 49 | 50 | 51 | // FUNCTIONS -- 52 | // https://github.com/RobTrew/prelude-jxa 53 | // JS Basics --------------------------------------------------- 54 | // addHours :: Int -> Date -> Date 55 | const addHours = n => dte => { 56 | const dte2 = new Date(dte); 57 | return ( 58 | dte2.setHours(n + dte.getHours()), 59 | dte2 60 | ); 61 | }; 62 | 63 | // addDays :: Int -> Date -> Date 64 | const addDays = n => dte => { 65 | const dte2 = new Date(dte); 66 | return ( 67 | dte2.setDate(n + dte.getDate()), 68 | dte2 69 | ); 70 | }; 71 | 72 | // addWeeks :: Int -> Date -> Date 73 | const addWeeks = n => addDays(7 * n); 74 | 75 | // addMonths :: Int -> Date -> Date 76 | const addMonths = n => dte => { 77 | const dte2 = new Date(dte); 78 | return ( 79 | dte2.setMonth(n + dte.getMonth()), 80 | dte2 81 | ); 82 | }; 83 | 84 | // eqDateTimeS :: Int -> Date -> Date -> Bool 85 | const eqDateTimeS = n => 86 | // Equivalence of two JS Date values 87 | // at a granularity of n seconds. 88 | // e.g. 89 | // Same second: eqDateTime(1)(a)(b) 90 | // Same minute: eqDateTime(60)(a)(b) 91 | // Same hour: eqDateTime(3600)(a)(b) 92 | on(a => b => a === b)( 93 | flip(div)(1E3 * n) 94 | ); 95 | 96 | // setTime :: [Int] -> Date -> Date 97 | const setTime = xs => dte => { 98 | const dte2 = new Date(dte); 99 | return ( 100 | dte2.setHours(0, 0, 0, 0), 101 | dte2.setHours(...xs), 102 | dte2 103 | ); 104 | }; 105 | 106 | // JS Prelude -------------------------------------------------- 107 | // Ratio :: Integral a => a -> a -> Ratio a 108 | const Ratio = a => b => { 109 | const go = (x, y) => 110 | 0 !== y ? (() => { 111 | const d = gcd(x)(y); 112 | 113 | return { 114 | type: "Ratio", 115 | // numerator 116 | "n": Math.trunc(x / d), 117 | // denominator 118 | "d": Math.trunc(y / d) 119 | }; 120 | })() : undefined; 121 | 122 | return go(a * signum(b), abs(b)); 123 | }; 124 | 125 | // Tuple (,) :: a -> b -> (a, b) 126 | const Tuple = a => 127 | b => ({ 128 | type: "Tuple", 129 | "0": a, 130 | "1": b, 131 | length: 2 132 | }); 133 | 134 | // abs :: Num -> Num 135 | const abs = 136 | // Absolute value of a given number - without the sign. 137 | x => 0 > x ? ( 138 | -x 139 | ) : x; 140 | 141 | // any :: (a -> Bool) -> [a] -> Bool 142 | const any = p => 143 | // True if p(x) holds for at least 144 | // one item in xs. 145 | xs => [...xs].some(p); 146 | 147 | // concat :: [[a]] -> [a] 148 | // concat :: [String] -> String 149 | const concat = xs => 150 | 0 < xs.length ? ( 151 | ( 152 | xs.every(x => "string" === typeof x) ? ( 153 | "" 154 | ) : [] 155 | ).concat(...xs) 156 | ) : xs; 157 | 158 | // div :: Int -> Int -> Int 159 | const div = x => 160 | y => Math.floor(x / y); 161 | 162 | // eq (==) :: Eq a => a -> a -> Bool 163 | const eq = a => 164 | // True when a and b are equivalent in the terms 165 | // defined below for their shared data type. 166 | b => { 167 | const t = typeof a; 168 | 169 | return t !== typeof b ? ( 170 | false 171 | ) : "object" !== t ? ( 172 | "function" !== t ? ( 173 | a === b 174 | ) : a.toString() === b.toString() 175 | ) : (() => { 176 | const kvs = Object.entries(a); 177 | 178 | return kvs.length !== Object.keys(b).length ? ( 179 | false 180 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 181 | })(); 182 | }; 183 | 184 | // findIndices :: (a -> Bool) -> [a] -> [Int] 185 | // findIndices :: (String -> Bool) -> String -> [Int] 186 | const findIndices = p => 187 | xs => { 188 | const ys = [...xs]; 189 | 190 | return ys.flatMap( 191 | (y, i) => p(y, i, ys) ? ( 192 | [i] 193 | ) : [] 194 | ); 195 | }; 196 | 197 | // flip :: (a -> b -> c) -> b -> a -> c 198 | const flip = op => 199 | // The binary function op with 200 | // its arguments reversed. 201 | 1 < op.length ? ( 202 | (a, b) => op(b, a) 203 | ) : (x => y => op(y)(x)); 204 | 205 | // floor :: Num -> Int 206 | const floor = x => { 207 | const 208 | nr = ( 209 | "Ratio" !== x.type ? ( 210 | properFraction 211 | ) : properFracRatio 212 | )(x), 213 | n = nr[0]; 214 | 215 | return 0 > nr[1] ? n - 1 : n; 216 | }; 217 | 218 | // fst :: (a, b) -> a 219 | const fst = tpl => 220 | // First member of a pair. 221 | tpl[0]; 222 | 223 | // gcd :: Integral a => a -> a -> a 224 | const gcd = x => 225 | y => { 226 | const zero = x.constructor(0); 227 | const go = (a, b) => 228 | zero === b ? ( 229 | a 230 | ) : go(b, a % b); 231 | 232 | return go(abs(x), abs(y)); 233 | }; 234 | 235 | // keys :: Dict -> [String] 236 | const keys = Object.keys; 237 | 238 | // length :: [a] -> Int 239 | const length = xs => 240 | // Returns Infinity over objects without finite 241 | // length. This enables zip and zipWith to choose 242 | // the shorter argument when one is non-finite, 243 | // like cycle, repeat etc 244 | "GeneratorFunction" !== xs.constructor 245 | .constructor.name ? ( 246 | xs.length 247 | ) : Infinity; 248 | 249 | // map :: (a -> b) -> [a] -> [b] 250 | const map = f => 251 | // The list obtained by applying f 252 | // to each element of xs. 253 | // (The image of xs under f). 254 | xs => [...xs].map(f); 255 | 256 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 257 | const matching = pat => { 258 | // A sequence-matching function for findIndices etc 259 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 260 | // -> [1, 4] 261 | const 262 | lng = pat.length, 263 | bln = 0 < lng, 264 | h = bln ? pat[0] : undefined; 265 | 266 | return x => i => src => 267 | bln && h === x && eq(pat)( 268 | src.slice(i, lng + i) 269 | ); 270 | }; 271 | 272 | // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c 273 | const on = f => 274 | // e.g. groupBy(on(eq)(length)) 275 | g => a => b => f(g(a))(g(b)); 276 | 277 | // properFracRatio :: Ratio -> (Int, Ratio) 278 | const properFracRatio = nd => { 279 | const [q, r] = Array.from(quotRem(nd.n, nd.d)); 280 | 281 | return Tuple(q, ratio(r, nd.d)); 282 | }; 283 | 284 | // properFraction :: Real -> (Int, Real) 285 | const properFraction = n => { 286 | const i = Math.floor(n) + (n < 0 ? 1 : 0); 287 | 288 | return Tuple(i)(n - i); 289 | }; 290 | 291 | // quotRem :: Integral a => a -> a -> (a, a) 292 | const quotRem = m => 293 | // The quotient, tupled with the remainder. 294 | n => Tuple( 295 | Math.trunc(m / n) 296 | )( 297 | m % n 298 | ); 299 | 300 | // signum :: Num -> Num 301 | const signum = n => 302 | // | Sign of a number. 303 | n.constructor( 304 | 0 > n ? ( 305 | -1 306 | ) : ( 307 | 0 < n ? 1 : 0 308 | ) 309 | ); 310 | 311 | // snd :: (a, b) -> b 312 | const snd = tpl => 313 | // Second member of a pair. 314 | tpl[1]; 315 | 316 | // splitOn :: [a] -> [a] -> [[a]] 317 | // splitOn :: String -> String -> [String] 318 | const splitOn = pat => src => 319 | // A list of the strings delimited by 320 | // instances of a given pattern in s. 321 | ("string" === typeof src) ? ( 322 | src.split(pat) 323 | ) : (() => { 324 | const 325 | lng = pat.length, 326 | tpl = findIndices(matching(pat))(src).reduce( 327 | (a, i) => Tuple( 328 | fst(a).concat([src.slice(snd(a), i)]) 329 | )(lng + i), 330 | Tuple([])(0) 331 | ); 332 | 333 | return fst(tpl).concat([src.slice(snd(tpl))]); 334 | })(); 335 | 336 | return main() 337 | }; 338 | 339 | return omniJSContext() 340 | }), { 341 | validate: selection => ["tasks", "projects"].some( 342 | k => selection[k].length > 0 343 | ) 344 | }) 345 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Change Defer Date/+30 mins (Defer).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultStartTime'), 18 | ts = ["projects", "tasks"].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | tomorrow = addMinutes(30)( 22 | setTime( 23 | splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ), 28 | p = ( 29 | null === ts[0].deferDate || 30 | any(x => !eqDateTimeS(ts[0].deferDate)(x.deferDate))(ts) 31 | ); 32 | 33 | return p ? ( 34 | ts.map(updateDeferDate(tomorrow)) 35 | ) : ts.map(task => 36 | updateDeferDate(addMinutes(30)(task.deferDate))( 37 | task 38 | ) 39 | ) 40 | }; 41 | 42 | // updateDeferDate :: Date -> OFTask -> IO OFTask 43 | const updateDeferDate = dte => task => { 44 | return ( 45 | task.deferDate = dte, 46 | task 47 | ) 48 | }; 49 | 50 | 51 | // FUNCTIONS -- 52 | // https://github.com/RobTrew/prelude-jxa 53 | // JS Basics --------------------------------------------------- 54 | // addMinutes :: Int -> Date -> Date 55 | const addMinutes = n => dte => { 56 | const dte2 = new Date(dte); 57 | return ( 58 | dte2.setMinutes(n + dte.getMinutes()), 59 | dte2 60 | ); 61 | }; 62 | 63 | // addHours :: Int -> Date -> Date 64 | const addHours = n => dte => { 65 | const dte2 = new Date(dte); 66 | return ( 67 | dte2.setHours(n + dte.getHours()), 68 | dte2 69 | ); 70 | }; 71 | 72 | // addDays :: Int -> Date -> Date 73 | const addDays = n => dte => { 74 | const dte2 = new Date(dte); 75 | return ( 76 | dte2.setDate(n + dte.getDate()), 77 | dte2 78 | ); 79 | }; 80 | 81 | // addWeeks :: Int -> Date -> Date 82 | const addWeeks = n => addDays(7 * n); 83 | 84 | // addMonths :: Int -> Date -> Date 85 | const addMonths = n => dte => { 86 | const dte2 = new Date(dte); 87 | return ( 88 | dte2.setMonth(n + dte.getMonth()), 89 | dte2 90 | ); 91 | }; 92 | 93 | // eqDateTimeS :: Int -> Date -> Date -> Bool 94 | const eqDateTimeS = n => 95 | // Equivalence of two JS Date values 96 | // at a granularity of n seconds. 97 | // e.g. 98 | // Same second: eqDateTime(1)(a)(b) 99 | // Same minute: eqDateTime(60)(a)(b) 100 | // Same hour: eqDateTime(3600)(a)(b) 101 | on(a => b => a === b)( 102 | flip(div)(1E3 * n) 103 | ); 104 | 105 | // setTime :: [Int] -> Date -> Date 106 | const setTime = xs => dte => { 107 | const dte2 = new Date(dte); 108 | return ( 109 | dte2.setHours(0, 0, 0, 0), 110 | dte2.setHours(...xs), 111 | dte2 112 | ); 113 | }; 114 | 115 | // JS Prelude -------------------------------------------------- 116 | // Ratio :: Integral a => a -> a -> Ratio a 117 | const Ratio = a => b => { 118 | const go = (x, y) => 119 | 0 !== y ? (() => { 120 | const d = gcd(x)(y); 121 | 122 | return { 123 | type: "Ratio", 124 | // numerator 125 | "n": Math.trunc(x / d), 126 | // denominator 127 | "d": Math.trunc(y / d) 128 | }; 129 | })() : undefined; 130 | 131 | return go(a * signum(b), abs(b)); 132 | }; 133 | 134 | // Tuple (,) :: a -> b -> (a, b) 135 | const Tuple = a => 136 | b => ({ 137 | type: "Tuple", 138 | "0": a, 139 | "1": b, 140 | length: 2 141 | }); 142 | 143 | // abs :: Num -> Num 144 | const abs = 145 | // Absolute value of a given number - without the sign. 146 | x => 0 > x ? ( 147 | -x 148 | ) : x; 149 | 150 | // any :: (a -> Bool) -> [a] -> Bool 151 | const any = p => 152 | // True if p(x) holds for at least 153 | // one item in xs. 154 | xs => [...xs].some(p); 155 | 156 | // concat :: [[a]] -> [a] 157 | // concat :: [String] -> String 158 | const concat = xs => 159 | 0 < xs.length ? ( 160 | ( 161 | xs.every(x => "string" === typeof x) ? ( 162 | "" 163 | ) : [] 164 | ).concat(...xs) 165 | ) : xs; 166 | 167 | // div :: Int -> Int -> Int 168 | const div = x => 169 | y => Math.floor(x / y); 170 | 171 | // eq (==) :: Eq a => a -> a -> Bool 172 | const eq = a => 173 | // True when a and b are equivalent in the terms 174 | // defined below for their shared data type. 175 | b => { 176 | const t = typeof a; 177 | 178 | return t !== typeof b ? ( 179 | false 180 | ) : "object" !== t ? ( 181 | "function" !== t ? ( 182 | a === b 183 | ) : a.toString() === b.toString() 184 | ) : (() => { 185 | const kvs = Object.entries(a); 186 | 187 | return kvs.length !== Object.keys(b).length ? ( 188 | false 189 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 190 | })(); 191 | }; 192 | 193 | // findIndices :: (a -> Bool) -> [a] -> [Int] 194 | // findIndices :: (String -> Bool) -> String -> [Int] 195 | const findIndices = p => 196 | xs => { 197 | const ys = [...xs]; 198 | 199 | return ys.flatMap( 200 | (y, i) => p(y, i, ys) ? ( 201 | [i] 202 | ) : [] 203 | ); 204 | }; 205 | 206 | // flip :: (a -> b -> c) -> b -> a -> c 207 | const flip = op => 208 | // The binary function op with 209 | // its arguments reversed. 210 | 1 < op.length ? ( 211 | (a, b) => op(b, a) 212 | ) : (x => y => op(y)(x)); 213 | 214 | // floor :: Num -> Int 215 | const floor = x => { 216 | const 217 | nr = ( 218 | "Ratio" !== x.type ? ( 219 | properFraction 220 | ) : properFracRatio 221 | )(x), 222 | n = nr[0]; 223 | 224 | return 0 > nr[1] ? n - 1 : n; 225 | }; 226 | 227 | // fst :: (a, b) -> a 228 | const fst = tpl => 229 | // First member of a pair. 230 | tpl[0]; 231 | 232 | // gcd :: Integral a => a -> a -> a 233 | const gcd = x => 234 | y => { 235 | const zero = x.constructor(0); 236 | const go = (a, b) => 237 | zero === b ? ( 238 | a 239 | ) : go(b, a % b); 240 | 241 | return go(abs(x), abs(y)); 242 | }; 243 | 244 | // keys :: Dict -> [String] 245 | const keys = Object.keys; 246 | 247 | // length :: [a] -> Int 248 | const length = xs => 249 | // Returns Infinity over objects without finite 250 | // length. This enables zip and zipWith to choose 251 | // the shorter argument when one is non-finite, 252 | // like cycle, repeat etc 253 | "GeneratorFunction" !== xs.constructor 254 | .constructor.name ? ( 255 | xs.length 256 | ) : Infinity; 257 | 258 | // map :: (a -> b) -> [a] -> [b] 259 | const map = f => 260 | // The list obtained by applying f 261 | // to each element of xs. 262 | // (The image of xs under f). 263 | xs => [...xs].map(f); 264 | 265 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 266 | const matching = pat => { 267 | // A sequence-matching function for findIndices etc 268 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 269 | // -> [1, 4] 270 | const 271 | lng = pat.length, 272 | bln = 0 < lng, 273 | h = bln ? pat[0] : undefined; 274 | 275 | return x => i => src => 276 | bln && h === x && eq(pat)( 277 | src.slice(i, lng + i) 278 | ); 279 | }; 280 | 281 | // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c 282 | const on = f => 283 | // e.g. groupBy(on(eq)(length)) 284 | g => a => b => f(g(a))(g(b)); 285 | 286 | // properFracRatio :: Ratio -> (Int, Ratio) 287 | const properFracRatio = nd => { 288 | const [q, r] = Array.from(quotRem(nd.n, nd.d)); 289 | 290 | return Tuple(q, ratio(r, nd.d)); 291 | }; 292 | 293 | // properFraction :: Real -> (Int, Real) 294 | const properFraction = n => { 295 | const i = Math.floor(n) + (n < 0 ? 1 : 0); 296 | 297 | return Tuple(i)(n - i); 298 | }; 299 | 300 | // quotRem :: Integral a => a -> a -> (a, a) 301 | const quotRem = m => 302 | // The quotient, tupled with the remainder. 303 | n => Tuple( 304 | Math.trunc(m / n) 305 | )( 306 | m % n 307 | ); 308 | 309 | // signum :: Num -> Num 310 | const signum = n => 311 | // | Sign of a number. 312 | n.constructor( 313 | 0 > n ? ( 314 | -1 315 | ) : ( 316 | 0 < n ? 1 : 0 317 | ) 318 | ); 319 | 320 | // snd :: (a, b) -> b 321 | const snd = tpl => 322 | // Second member of a pair. 323 | tpl[1]; 324 | 325 | // splitOn :: [a] -> [a] -> [[a]] 326 | // splitOn :: String -> String -> [String] 327 | const splitOn = pat => src => 328 | // A list of the strings delimited by 329 | // instances of a given pattern in s. 330 | ("string" === typeof src) ? ( 331 | src.split(pat) 332 | ) : (() => { 333 | const 334 | lng = pat.length, 335 | tpl = findIndices(matching(pat))(src).reduce( 336 | (a, i) => Tuple( 337 | fst(a).concat([src.slice(snd(a), i)]) 338 | )(lng + i), 339 | Tuple([])(0) 340 | ); 341 | 342 | return fst(tpl).concat([src.slice(snd(tpl))]); 343 | })(); 344 | 345 | return main() 346 | }; 347 | 348 | return omniJSContext() 349 | }), { 350 | validate: selection => ["tasks", "projects"].some( 351 | k => selection[k].length > 0 352 | ) 353 | }) 354 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Change Defer Date/-1 day (Defer).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultStartTime'), 18 | ts = ["projects", "tasks"].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | tomorrow = addDays(1)( 22 | setTime( 23 | splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ), 28 | p = ( 29 | null === ts[0].deferDate || 30 | any(x => !eqDateTimeS(ts[0].deferDate)(x.deferDate))(ts) 31 | ); 32 | 33 | return p ? ( 34 | ts.map(updateDeferDate(tomorrow)) 35 | ) : ts.map(task => 36 | updateDeferDate(addDays(-1)(task.deferDate))( 37 | task 38 | ) 39 | ) 40 | }; 41 | 42 | // updateDeferDate :: Date -> OFTask -> IO OFTask 43 | const updateDeferDate = dte => task => { 44 | return ( 45 | task.deferDate = dte, 46 | task 47 | ) 48 | }; 49 | 50 | 51 | // FUNCTIONS -- 52 | // https://github.com/RobTrew/prelude-jxa 53 | // JS Basics --------------------------------------------------- 54 | // addHours :: Int -> Date -> Date 55 | const addHours = n => dte => { 56 | const dte2 = new Date(dte); 57 | return ( 58 | dte2.setHours(n + dte.getHours()), 59 | dte2 60 | ); 61 | }; 62 | 63 | // addDays :: Int -> Date -> Date 64 | const addDays = n => dte => { 65 | const dte2 = new Date(dte); 66 | return ( 67 | dte2.setDate(n + dte.getDate()), 68 | dte2 69 | ); 70 | }; 71 | 72 | // addWeeks :: Int -> Date -> Date 73 | const addWeeks = n => addDays(7 * n); 74 | 75 | // addMonths :: Int -> Date -> Date 76 | const addMonths = n => dte => { 77 | const dte2 = new Date(dte); 78 | return ( 79 | dte2.setMonth(n + dte.getMonth()), 80 | dte2 81 | ); 82 | }; 83 | 84 | // eqDateTimeS :: Int -> Date -> Date -> Bool 85 | const eqDateTimeS = n => 86 | // Equivalence of two JS Date values 87 | // at a granularity of n seconds. 88 | // e.g. 89 | // Same second: eqDateTime(1)(a)(b) 90 | // Same minute: eqDateTime(60)(a)(b) 91 | // Same hour: eqDateTime(3600)(a)(b) 92 | on(a => b => a === b)( 93 | flip(div)(1E3 * n) 94 | ); 95 | 96 | // setTime :: [Int] -> Date -> Date 97 | const setTime = xs => dte => { 98 | const dte2 = new Date(dte); 99 | return ( 100 | dte2.setHours(0, 0, 0, 0), 101 | dte2.setHours(...xs), 102 | dte2 103 | ); 104 | }; 105 | 106 | // JS Prelude -------------------------------------------------- 107 | // Ratio :: Integral a => a -> a -> Ratio a 108 | const Ratio = a => b => { 109 | const go = (x, y) => 110 | 0 !== y ? (() => { 111 | const d = gcd(x)(y); 112 | 113 | return { 114 | type: "Ratio", 115 | // numerator 116 | "n": Math.trunc(x / d), 117 | // denominator 118 | "d": Math.trunc(y / d) 119 | }; 120 | })() : undefined; 121 | 122 | return go(a * signum(b), abs(b)); 123 | }; 124 | 125 | // Tuple (,) :: a -> b -> (a, b) 126 | const Tuple = a => 127 | b => ({ 128 | type: "Tuple", 129 | "0": a, 130 | "1": b, 131 | length: 2 132 | }); 133 | 134 | // abs :: Num -> Num 135 | const abs = 136 | // Absolute value of a given number - without the sign. 137 | x => 0 > x ? ( 138 | -x 139 | ) : x; 140 | 141 | // any :: (a -> Bool) -> [a] -> Bool 142 | const any = p => 143 | // True if p(x) holds for at least 144 | // one item in xs. 145 | xs => [...xs].some(p); 146 | 147 | // concat :: [[a]] -> [a] 148 | // concat :: [String] -> String 149 | const concat = xs => 150 | 0 < xs.length ? ( 151 | ( 152 | xs.every(x => "string" === typeof x) ? ( 153 | "" 154 | ) : [] 155 | ).concat(...xs) 156 | ) : xs; 157 | 158 | // div :: Int -> Int -> Int 159 | const div = x => 160 | y => Math.floor(x / y); 161 | 162 | // eq (==) :: Eq a => a -> a -> Bool 163 | const eq = a => 164 | // True when a and b are equivalent in the terms 165 | // defined below for their shared data type. 166 | b => { 167 | const t = typeof a; 168 | 169 | return t !== typeof b ? ( 170 | false 171 | ) : "object" !== t ? ( 172 | "function" !== t ? ( 173 | a === b 174 | ) : a.toString() === b.toString() 175 | ) : (() => { 176 | const kvs = Object.entries(a); 177 | 178 | return kvs.length !== Object.keys(b).length ? ( 179 | false 180 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 181 | })(); 182 | }; 183 | 184 | // findIndices :: (a -> Bool) -> [a] -> [Int] 185 | // findIndices :: (String -> Bool) -> String -> [Int] 186 | const findIndices = p => 187 | xs => { 188 | const ys = [...xs]; 189 | 190 | return ys.flatMap( 191 | (y, i) => p(y, i, ys) ? ( 192 | [i] 193 | ) : [] 194 | ); 195 | }; 196 | 197 | // flip :: (a -> b -> c) -> b -> a -> c 198 | const flip = op => 199 | // The binary function op with 200 | // its arguments reversed. 201 | 1 < op.length ? ( 202 | (a, b) => op(b, a) 203 | ) : (x => y => op(y)(x)); 204 | 205 | // floor :: Num -> Int 206 | const floor = x => { 207 | const 208 | nr = ( 209 | "Ratio" !== x.type ? ( 210 | properFraction 211 | ) : properFracRatio 212 | )(x), 213 | n = nr[0]; 214 | 215 | return 0 > nr[1] ? n - 1 : n; 216 | }; 217 | 218 | // fst :: (a, b) -> a 219 | const fst = tpl => 220 | // First member of a pair. 221 | tpl[0]; 222 | 223 | // gcd :: Integral a => a -> a -> a 224 | const gcd = x => 225 | y => { 226 | const zero = x.constructor(0); 227 | const go = (a, b) => 228 | zero === b ? ( 229 | a 230 | ) : go(b, a % b); 231 | 232 | return go(abs(x), abs(y)); 233 | }; 234 | 235 | // keys :: Dict -> [String] 236 | const keys = Object.keys; 237 | 238 | // length :: [a] -> Int 239 | const length = xs => 240 | // Returns Infinity over objects without finite 241 | // length. This enables zip and zipWith to choose 242 | // the shorter argument when one is non-finite, 243 | // like cycle, repeat etc 244 | "GeneratorFunction" !== xs.constructor 245 | .constructor.name ? ( 246 | xs.length 247 | ) : Infinity; 248 | 249 | // map :: (a -> b) -> [a] -> [b] 250 | const map = f => 251 | // The list obtained by applying f 252 | // to each element of xs. 253 | // (The image of xs under f). 254 | xs => [...xs].map(f); 255 | 256 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 257 | const matching = pat => { 258 | // A sequence-matching function for findIndices etc 259 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 260 | // -> [1, 4] 261 | const 262 | lng = pat.length, 263 | bln = 0 < lng, 264 | h = bln ? pat[0] : undefined; 265 | 266 | return x => i => src => 267 | bln && h === x && eq(pat)( 268 | src.slice(i, lng + i) 269 | ); 270 | }; 271 | 272 | // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c 273 | const on = f => 274 | // e.g. groupBy(on(eq)(length)) 275 | g => a => b => f(g(a))(g(b)); 276 | 277 | // properFracRatio :: Ratio -> (Int, Ratio) 278 | const properFracRatio = nd => { 279 | const [q, r] = Array.from(quotRem(nd.n, nd.d)); 280 | 281 | return Tuple(q, ratio(r, nd.d)); 282 | }; 283 | 284 | // properFraction :: Real -> (Int, Real) 285 | const properFraction = n => { 286 | const i = Math.floor(n) + (n < 0 ? 1 : 0); 287 | 288 | return Tuple(i)(n - i); 289 | }; 290 | 291 | // quotRem :: Integral a => a -> a -> (a, a) 292 | const quotRem = m => 293 | // The quotient, tupled with the remainder. 294 | n => Tuple( 295 | Math.trunc(m / n) 296 | )( 297 | m % n 298 | ); 299 | 300 | // signum :: Num -> Num 301 | const signum = n => 302 | // | Sign of a number. 303 | n.constructor( 304 | 0 > n ? ( 305 | -1 306 | ) : ( 307 | 0 < n ? 1 : 0 308 | ) 309 | ); 310 | 311 | // snd :: (a, b) -> b 312 | const snd = tpl => 313 | // Second member of a pair. 314 | tpl[1]; 315 | 316 | // splitOn :: [a] -> [a] -> [[a]] 317 | // splitOn :: String -> String -> [String] 318 | const splitOn = pat => src => 319 | // A list of the strings delimited by 320 | // instances of a given pattern in s. 321 | ("string" === typeof src) ? ( 322 | src.split(pat) 323 | ) : (() => { 324 | const 325 | lng = pat.length, 326 | tpl = findIndices(matching(pat))(src).reduce( 327 | (a, i) => Tuple( 328 | fst(a).concat([src.slice(snd(a), i)]) 329 | )(lng + i), 330 | Tuple([])(0) 331 | ); 332 | 333 | return fst(tpl).concat([src.slice(snd(tpl))]); 334 | })(); 335 | 336 | return main() 337 | }; 338 | 339 | return omniJSContext() 340 | }), { 341 | validate: selection => ["tasks", "projects"].some( 342 | k => selection[k].length > 0 343 | ) 344 | }) 345 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Change Defer Date/-30 mins (Defer).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultStartTime'), 18 | ts = ["projects", "tasks"].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | tomorrow = addMinutes(-30)( 22 | setTime( 23 | splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ), 28 | p = ( 29 | null === ts[0].deferDate || 30 | any(x => !eqDateTimeS(ts[0].deferDate)(x.deferDate))(ts) 31 | ); 32 | 33 | return p ? ( 34 | ts.map(updateDeferDate(tomorrow)) 35 | ) : ts.map(task => 36 | updateDeferDate(addMinutes(-30)(task.deferDate))( 37 | task 38 | ) 39 | ) 40 | }; 41 | 42 | // updateDeferDate :: Date -> OFTask -> IO OFTask 43 | const updateDeferDate = dte => task => { 44 | return ( 45 | task.deferDate = dte, 46 | task 47 | ) 48 | }; 49 | 50 | 51 | // FUNCTIONS -- 52 | // https://github.com/RobTrew/prelude-jxa 53 | // JS Basics --------------------------------------------------- 54 | // addMinutes :: Int -> Date -> Date 55 | const addMinutes = n => dte => { 56 | const dte2 = new Date(dte); 57 | return ( 58 | dte2.setMinutes(n + dte.getMinutes()), 59 | dte2 60 | ); 61 | }; 62 | 63 | // addHours :: Int -> Date -> Date 64 | const addHours = n => dte => { 65 | const dte2 = new Date(dte); 66 | return ( 67 | dte2.setHours(n + dte.getHours()), 68 | dte2 69 | ); 70 | }; 71 | 72 | // addDays :: Int -> Date -> Date 73 | const addDays = n => dte => { 74 | const dte2 = new Date(dte); 75 | return ( 76 | dte2.setDate(n + dte.getDate()), 77 | dte2 78 | ); 79 | }; 80 | 81 | // addWeeks :: Int -> Date -> Date 82 | const addWeeks = n => addDays(7 * n); 83 | 84 | // addMonths :: Int -> Date -> Date 85 | const addMonths = n => dte => { 86 | const dte2 = new Date(dte); 87 | return ( 88 | dte2.setMonth(n + dte.getMonth()), 89 | dte2 90 | ); 91 | }; 92 | 93 | // eqDateTimeS :: Int -> Date -> Date -> Bool 94 | const eqDateTimeS = n => 95 | // Equivalence of two JS Date values 96 | // at a granularity of n seconds. 97 | // e.g. 98 | // Same second: eqDateTime(1)(a)(b) 99 | // Same minute: eqDateTime(60)(a)(b) 100 | // Same hour: eqDateTime(3600)(a)(b) 101 | on(a => b => a === b)( 102 | flip(div)(1E3 * n) 103 | ); 104 | 105 | // setTime :: [Int] -> Date -> Date 106 | const setTime = xs => dte => { 107 | const dte2 = new Date(dte); 108 | return ( 109 | dte2.setHours(0, 0, 0, 0), 110 | dte2.setHours(...xs), 111 | dte2 112 | ); 113 | }; 114 | 115 | // JS Prelude -------------------------------------------------- 116 | // Ratio :: Integral a => a -> a -> Ratio a 117 | const Ratio = a => b => { 118 | const go = (x, y) => 119 | 0 !== y ? (() => { 120 | const d = gcd(x)(y); 121 | 122 | return { 123 | type: "Ratio", 124 | // numerator 125 | "n": Math.trunc(x / d), 126 | // denominator 127 | "d": Math.trunc(y / d) 128 | }; 129 | })() : undefined; 130 | 131 | return go(a * signum(b), abs(b)); 132 | }; 133 | 134 | // Tuple (,) :: a -> b -> (a, b) 135 | const Tuple = a => 136 | b => ({ 137 | type: "Tuple", 138 | "0": a, 139 | "1": b, 140 | length: 2 141 | }); 142 | 143 | // abs :: Num -> Num 144 | const abs = 145 | // Absolute value of a given number - without the sign. 146 | x => 0 > x ? ( 147 | -x 148 | ) : x; 149 | 150 | // any :: (a -> Bool) -> [a] -> Bool 151 | const any = p => 152 | // True if p(x) holds for at least 153 | // one item in xs. 154 | xs => [...xs].some(p); 155 | 156 | // concat :: [[a]] -> [a] 157 | // concat :: [String] -> String 158 | const concat = xs => 159 | 0 < xs.length ? ( 160 | ( 161 | xs.every(x => "string" === typeof x) ? ( 162 | "" 163 | ) : [] 164 | ).concat(...xs) 165 | ) : xs; 166 | 167 | // div :: Int -> Int -> Int 168 | const div = x => 169 | y => Math.floor(x / y); 170 | 171 | // eq (==) :: Eq a => a -> a -> Bool 172 | const eq = a => 173 | // True when a and b are equivalent in the terms 174 | // defined below for their shared data type. 175 | b => { 176 | const t = typeof a; 177 | 178 | return t !== typeof b ? ( 179 | false 180 | ) : "object" !== t ? ( 181 | "function" !== t ? ( 182 | a === b 183 | ) : a.toString() === b.toString() 184 | ) : (() => { 185 | const kvs = Object.entries(a); 186 | 187 | return kvs.length !== Object.keys(b).length ? ( 188 | false 189 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 190 | })(); 191 | }; 192 | 193 | // findIndices :: (a -> Bool) -> [a] -> [Int] 194 | // findIndices :: (String -> Bool) -> String -> [Int] 195 | const findIndices = p => 196 | xs => { 197 | const ys = [...xs]; 198 | 199 | return ys.flatMap( 200 | (y, i) => p(y, i, ys) ? ( 201 | [i] 202 | ) : [] 203 | ); 204 | }; 205 | 206 | // flip :: (a -> b -> c) -> b -> a -> c 207 | const flip = op => 208 | // The binary function op with 209 | // its arguments reversed. 210 | 1 < op.length ? ( 211 | (a, b) => op(b, a) 212 | ) : (x => y => op(y)(x)); 213 | 214 | // floor :: Num -> Int 215 | const floor = x => { 216 | const 217 | nr = ( 218 | "Ratio" !== x.type ? ( 219 | properFraction 220 | ) : properFracRatio 221 | )(x), 222 | n = nr[0]; 223 | 224 | return 0 > nr[1] ? n - 1 : n; 225 | }; 226 | 227 | // fst :: (a, b) -> a 228 | const fst = tpl => 229 | // First member of a pair. 230 | tpl[0]; 231 | 232 | // gcd :: Integral a => a -> a -> a 233 | const gcd = x => 234 | y => { 235 | const zero = x.constructor(0); 236 | const go = (a, b) => 237 | zero === b ? ( 238 | a 239 | ) : go(b, a % b); 240 | 241 | return go(abs(x), abs(y)); 242 | }; 243 | 244 | // keys :: Dict -> [String] 245 | const keys = Object.keys; 246 | 247 | // length :: [a] -> Int 248 | const length = xs => 249 | // Returns Infinity over objects without finite 250 | // length. This enables zip and zipWith to choose 251 | // the shorter argument when one is non-finite, 252 | // like cycle, repeat etc 253 | "GeneratorFunction" !== xs.constructor 254 | .constructor.name ? ( 255 | xs.length 256 | ) : Infinity; 257 | 258 | // map :: (a -> b) -> [a] -> [b] 259 | const map = f => 260 | // The list obtained by applying f 261 | // to each element of xs. 262 | // (The image of xs under f). 263 | xs => [...xs].map(f); 264 | 265 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 266 | const matching = pat => { 267 | // A sequence-matching function for findIndices etc 268 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 269 | // -> [1, 4] 270 | const 271 | lng = pat.length, 272 | bln = 0 < lng, 273 | h = bln ? pat[0] : undefined; 274 | 275 | return x => i => src => 276 | bln && h === x && eq(pat)( 277 | src.slice(i, lng + i) 278 | ); 279 | }; 280 | 281 | // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c 282 | const on = f => 283 | // e.g. groupBy(on(eq)(length)) 284 | g => a => b => f(g(a))(g(b)); 285 | 286 | // properFracRatio :: Ratio -> (Int, Ratio) 287 | const properFracRatio = nd => { 288 | const [q, r] = Array.from(quotRem(nd.n, nd.d)); 289 | 290 | return Tuple(q, ratio(r, nd.d)); 291 | }; 292 | 293 | // properFraction :: Real -> (Int, Real) 294 | const properFraction = n => { 295 | const i = Math.floor(n) + (n < 0 ? 1 : 0); 296 | 297 | return Tuple(i)(n - i); 298 | }; 299 | 300 | // quotRem :: Integral a => a -> a -> (a, a) 301 | const quotRem = m => 302 | // The quotient, tupled with the remainder. 303 | n => Tuple( 304 | Math.trunc(m / n) 305 | )( 306 | m % n 307 | ); 308 | 309 | // signum :: Num -> Num 310 | const signum = n => 311 | // | Sign of a number. 312 | n.constructor( 313 | 0 > n ? ( 314 | -1 315 | ) : ( 316 | 0 < n ? 1 : 0 317 | ) 318 | ); 319 | 320 | // snd :: (a, b) -> b 321 | const snd = tpl => 322 | // Second member of a pair. 323 | tpl[1]; 324 | 325 | // splitOn :: [a] -> [a] -> [[a]] 326 | // splitOn :: String -> String -> [String] 327 | const splitOn = pat => src => 328 | // A list of the strings delimited by 329 | // instances of a given pattern in s. 330 | ("string" === typeof src) ? ( 331 | src.split(pat) 332 | ) : (() => { 333 | const 334 | lng = pat.length, 335 | tpl = findIndices(matching(pat))(src).reduce( 336 | (a, i) => Tuple( 337 | fst(a).concat([src.slice(snd(a), i)]) 338 | )(lng + i), 339 | Tuple([])(0) 340 | ); 341 | 342 | return fst(tpl).concat([src.slice(snd(tpl))]); 343 | })(); 344 | 345 | return main() 346 | }; 347 | 348 | return omniJSContext() 349 | }), { 350 | validate: selection => ["tasks", "projects"].some( 351 | k => selection[k].length > 0 352 | ) 353 | }) 354 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Change Defer Date/This Weekend (Defer).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultStartTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | nextWeekendDay = setTime( 22 | ...splitOn(":")(defaultStartTime) 23 | )(until(isWeekend)( 24 | addDays(1) 25 | )(new Date())); 26 | 27 | return ts.map(task => { 28 | return ( 29 | task.deferDate = nextWeekendDay, 30 | task 31 | ) 32 | }) 33 | }; 34 | 35 | 36 | // FUNCTIONS -- 37 | // JS Basics --------------------------------------------------- 38 | // addDays :: Int -> Date -> Date 39 | const addDays = n => dte => { 40 | const dte2 = new Date(dte); 41 | return ( 42 | dte2.setDate(n + dte.getDate()), 43 | dte2 44 | ); 45 | }; 46 | 47 | // setTime :: (Int, Int, Int) -> Date -> Date 48 | const setTime = (h, m, s) => dte => { 49 | const dte2 = new Date(dte); 50 | return ( 51 | dte2.setHours(h, m, s, 0), 52 | dte2 53 | ); 54 | }; 55 | 56 | // isWeekend :: Date -> Bool 57 | const isWeekend = dte => 58 | any( 59 | eq(dte.getDay()) 60 | )([0, 6]) 61 | 62 | // JS Prelude -------------------------------------------------- 63 | // https://github.com/RobTrew/prelude-jxa 64 | // Tuple (,) :: a -> b -> (a, b) 65 | const Tuple = a => 66 | b => ({ 67 | type: "Tuple", 68 | "0": a, 69 | "1": b, 70 | length: 2 71 | }); 72 | 73 | // any :: (a -> Bool) -> [a] -> Bool 74 | const any = p => 75 | // True if p(x) holds for at least 76 | // one item in xs. 77 | xs => [...xs].some(p); 78 | 79 | // concat :: [[a]] -> [a] 80 | // concat :: [String] -> String 81 | const concat = xs => 82 | 0 < xs.length ? ( 83 | ( 84 | xs.every(x => "string" === typeof x) ? ( 85 | "" 86 | ) : [] 87 | ).concat(...xs) 88 | ) : xs; 89 | 90 | // eq (==) :: Eq a => a -> a -> Bool 91 | const eq = a => 92 | // True when a and b are equivalent in the terms 93 | // defined below for their shared data type. 94 | b => { 95 | const t = typeof a; 96 | 97 | return t !== typeof b ? ( 98 | false 99 | ) : "object" !== t ? ( 100 | "function" !== t ? ( 101 | a === b 102 | ) : a.toString() === b.toString() 103 | ) : (() => { 104 | const kvs = Object.entries(a); 105 | 106 | return kvs.length !== Object.keys(b).length ? ( 107 | false 108 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 109 | })(); 110 | }; 111 | 112 | // findIndices :: (a -> Bool) -> [a] -> [Int] 113 | // findIndices :: (String -> Bool) -> String -> [Int] 114 | const findIndices = p => 115 | xs => { 116 | const ys = [...xs]; 117 | 118 | return ys.flatMap( 119 | (y, i) => p(y, i, ys) ? ( 120 | [i] 121 | ) : [] 122 | ); 123 | }; 124 | 125 | // fst :: (a, b) -> a 126 | const fst = tpl => 127 | // First member of a pair. 128 | tpl[0]; 129 | 130 | // keys :: Dict -> [String] 131 | const keys = Object.keys; 132 | 133 | // length :: [a] -> Int 134 | const length = xs => 135 | // Returns Infinity over objects without finite 136 | // length. This enables zip and zipWith to choose 137 | // the shorter argument when one is non-finite, 138 | // like cycle, repeat etc 139 | "GeneratorFunction" !== xs.constructor 140 | .constructor.name ? ( 141 | xs.length 142 | ) : Infinity; 143 | 144 | // map :: (a -> b) -> [a] -> [b] 145 | const map = f => 146 | // The list obtained by applying f 147 | // to each element of xs. 148 | // (The image of xs under f). 149 | xs => [...xs].map(f); 150 | 151 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 152 | const matching = pat => { 153 | // A sequence-matching function for findIndices etc 154 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 155 | // -> [1, 4] 156 | const 157 | lng = pat.length, 158 | bln = 0 < lng, 159 | h = bln ? pat[0] : undefined; 160 | 161 | return x => i => src => 162 | bln && h === x && eq(pat)( 163 | src.slice(i, lng + i) 164 | ); 165 | }; 166 | 167 | // snd :: (a, b) -> b 168 | const snd = tpl => 169 | // Second member of a pair. 170 | tpl[1]; 171 | 172 | // splitOn :: [a] -> [a] -> [[a]] 173 | // splitOn :: String -> String -> [String] 174 | const splitOn = pat => src => 175 | // A list of the strings delimited by 176 | // instances of a given pattern in s. 177 | ("string" === typeof src) ? ( 178 | src.split(pat) 179 | ) : (() => { 180 | const 181 | lng = pat.length, 182 | tpl = findIndices(matching(pat))(src).reduce( 183 | (a, i) => Tuple( 184 | fst(a).concat([src.slice(snd(a), i)]) 185 | )(lng + i), 186 | Tuple([])(0) 187 | ); 188 | 189 | return fst(tpl).concat([src.slice(snd(tpl))]); 190 | })(); 191 | 192 | // until :: (a -> Bool) -> (a -> a) -> a -> a 193 | const until = p => 194 | // The value resulting from repeated applications 195 | // of f to the seed value x, terminating when 196 | // that result returns true for the predicate p. 197 | f => x => { 198 | let v = x; 199 | 200 | while (!p(v)) { 201 | v = f(v); 202 | } 203 | 204 | return v; 205 | }; 206 | 207 | return main() 208 | }; 209 | 210 | return omniJSContext() 211 | }), { 212 | validate: selection => ['projects', 'tasks'].flatMap( 213 | k => selection[k] 214 | ).length > 0 215 | }) 216 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Change Defer Date/Today (Defer).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file OmniFocus plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultStartTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | today = addDays(0)( 22 | setTime( 23 | ...splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ); 28 | 29 | return ts.map(updateDeferDate(today)) 30 | }; 31 | 32 | // updateDeferDate :: Date -> OFTask -> IO OFTask 33 | const updateDeferDate = dte => task => { 34 | return ( 35 | task.deferDate = dte, 36 | task 37 | ) 38 | } 39 | 40 | // FUNCTIONS -- 41 | // https://github.com/RobTrew/prelude-jxa 42 | // JS Basics --------------------------------------------------- 43 | // addDays :: Int -> Date -> Date 44 | const addDays = n => dte => { 45 | const dte2 = new Date(dte); 46 | return ( 47 | dte2.setDate(n + dte.getDate()), 48 | dte2 49 | ); 50 | }; 51 | 52 | // setTime :: (Int, Int, Int) -> Date -> Date 53 | const setTime = (h, m, s) => dte => { 54 | const dte2 = new Date(dte); 55 | return ( 56 | dte2.setHours(h, m, s, 0), 57 | dte2 58 | ); 59 | }; 60 | 61 | // addHours :: Int -> Date -> Date 62 | const addHours = n => dte => { 63 | const dte2 = new Date(dte); 64 | return ( 65 | dte2.setHours(n + dte.getHours()), 66 | dte2 67 | ); 68 | }; 69 | 70 | // addWeeks :: Int -> Date -> Date 71 | const addWeeks = n => addDays(7 * n); 72 | 73 | // addMonths :: Int -> Date -> Date 74 | const addMonths = n => dte => { 75 | const dte2 = new Date(dte); 76 | return ( 77 | dte2.setMonth(n + dte.getMonth()), 78 | dte2 79 | ); 80 | }; 81 | 82 | // JS Prelude -------------------------------------------------- 83 | // Tuple (,) :: a -> b -> (a, b) 84 | const Tuple = a => 85 | b => ({ 86 | type: "Tuple", 87 | "0": a, 88 | "1": b, 89 | length: 2 90 | }); 91 | 92 | // add (+) :: Num a => a -> a -> a 93 | const add = a => 94 | // Curried addition. 95 | b => a + b; 96 | 97 | // all :: (a -> Bool) -> [a] -> Bool 98 | const all = p => 99 | // True if p(x) holds for every x in xs. 100 | xs => [...xs].every(p); 101 | 102 | // any :: (a -> Bool) -> [a] -> Bool 103 | const any = p => 104 | // True if p(x) holds for at least 105 | // one item in xs. 106 | xs => [...xs].some(p); 107 | 108 | 109 | // concat :: [[a]] -> [a] 110 | // concat :: [String] -> String 111 | const concat = xs => 112 | 0 < xs.length ? ( 113 | ( 114 | xs.every(x => "string" === typeof x) ? ( 115 | "" 116 | ) : [] 117 | ).concat(...xs) 118 | ) : xs; 119 | 120 | // eq (==) :: Eq a => a -> a -> Bool 121 | const eq1 = a => 122 | // True when a and b are equivalent in the terms 123 | // defined below for their shared data type. 124 | b => { 125 | const t = typeName(a); 126 | 127 | return t !== typeName(b) ? ( 128 | false 129 | ) : "Dict" !== t ? ( 130 | "Date" !== t ? ( 131 | "function" !== t ? ( 132 | a === b 133 | ) : a.toString() === b.toString() 134 | ) : 0 === (a - b) // Date equality 135 | ) : (() => { 136 | const kvs = Object.entries(a); 137 | 138 | return kvs.length !== Object.keys(b).length ? ( 139 | false 140 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 141 | })(); 142 | }; 143 | 144 | // findIndices :: (a -> Bool) -> [a] -> [Int] 145 | // findIndices :: (String -> Bool) -> String -> [Int] 146 | const findIndices = p => 147 | xs => { 148 | const ys = [...xs]; 149 | 150 | return ys.flatMap( 151 | (y, i) => p(y, i, ys) ? ( 152 | [i] 153 | ) : [] 154 | ); 155 | }; 156 | 157 | // fst :: (a, b) -> a 158 | const fst = tpl => 159 | // First member of a pair. 160 | tpl[0]; 161 | 162 | // keys :: Dict -> [String] 163 | const keys = Object.keys; 164 | 165 | // length :: [a] -> Int 166 | const length = xs => 167 | // Returns Infinity over objects without finite 168 | // length. This enables zip and zipWith to choose 169 | // the shorter argument when one is non-finite, 170 | // like cycle, repeat etc 171 | "GeneratorFunction" !== xs.constructor 172 | .constructor.name ? ( 173 | xs.length 174 | ) : Infinity; 175 | 176 | // map :: (a -> b) -> [a] -> [b] 177 | const map = f => 178 | // The list obtained by applying f 179 | // to each element of xs. 180 | // (The image of xs under f). 181 | xs => [...xs].map(f); 182 | 183 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 184 | const matching = pat => { 185 | // A sequence-matching function for findIndices etc 186 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 187 | // -> [1, 4] 188 | const 189 | lng = pat.length, 190 | bln = 0 < lng, 191 | h = bln ? pat[0] : undefined; 192 | 193 | return x => i => src => 194 | bln && h === x && eq(pat)( 195 | src.slice(i, lng + i) 196 | ); 197 | }; 198 | 199 | // snd :: (a, b) -> b 200 | const snd = tpl => 201 | // Second member of a pair. 202 | tpl[1]; 203 | 204 | // splitOn :: [a] -> [a] -> [[a]] 205 | // splitOn :: String -> String -> [String] 206 | const splitOn = pat => src => 207 | // A list of the strings delimited by 208 | // instances of a given pattern in s. 209 | ("string" === typeof src) ? ( 210 | src.split(pat) 211 | ) : (() => { 212 | const 213 | lng = pat.length, 214 | tpl = findIndices(matching(pat))(src).reduce( 215 | (a, i) => Tuple( 216 | fst(a).concat([src.slice(snd(a), i)]) 217 | )(lng + i), 218 | Tuple([])(0) 219 | ); 220 | 221 | return fst(tpl).concat([src.slice(snd(tpl))]); 222 | })(); 223 | 224 | // typeName :: a -> String 225 | const typeName = v => { 226 | const t = typeof v; 227 | 228 | return null !== v ? ( 229 | "object" === t ? ( 230 | Array.isArray(v) ? ( 231 | "List" 232 | ) : "Date" === v.constructor.name ? ( 233 | "Date" 234 | ) : (() => { 235 | const ct = v.type; 236 | 237 | return Boolean(ct) ? ( 238 | (/Tuple\d+/u).test(ct) ? ( 239 | "TupleN" 240 | ) : ct 241 | ) : "Dict"; 242 | })() 243 | ) : { 244 | "boolean": "Bool", 245 | "date": "Date", 246 | "number": "Num", 247 | "string": "String", 248 | "function": "(a -> b)" 249 | } [t] || "Bottom" 250 | ) : "Bottom"; 251 | }; 252 | 253 | return main() 254 | }; 255 | 256 | return omniJSContext() 257 | }), { 258 | validate: selection => ['projects', 'tasks'].flatMap( 259 | k => Array.from(selection[k]) 260 | ).length > 0 261 | }) 262 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Change Defer Date/Tomorrow (Defer).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file OmniFocus plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultStartTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | tomorrow = addDays(1)( 22 | setTime( 23 | ...splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ); 28 | 29 | return ts.map(updateDeferDate(tomorrow)) 30 | }; 31 | 32 | // updateDeferDate :: Date -> OFTask -> IO OFTask 33 | const updateDeferDate = dte => task => { 34 | return ( 35 | task.deferDate = dte, 36 | task 37 | ) 38 | } 39 | 40 | // FUNCTIONS -- 41 | // https://github.com/RobTrew/prelude-jxa 42 | // JS Basics --------------------------------------------------- 43 | // addDays :: Int -> Date -> Date 44 | const addDays = n => dte => { 45 | const dte2 = new Date(dte); 46 | return ( 47 | dte2.setDate(n + dte.getDate()), 48 | dte2 49 | ); 50 | }; 51 | 52 | // setTime :: (Int, Int, Int) -> Date -> Date 53 | const setTime = (h, m, s) => dte => { 54 | const dte2 = new Date(dte); 55 | return ( 56 | dte2.setHours(h, m, s, 0), 57 | dte2 58 | ); 59 | }; 60 | 61 | // addHours :: Int -> Date -> Date 62 | const addHours = n => dte => { 63 | const dte2 = new Date(dte); 64 | return ( 65 | dte2.setHours(n + dte.getHours()), 66 | dte2 67 | ); 68 | }; 69 | 70 | // addWeeks :: Int -> Date -> Date 71 | const addWeeks = n => addDays(7 * n); 72 | 73 | // addMonths :: Int -> Date -> Date 74 | const addMonths = n => dte => { 75 | const dte2 = new Date(dte); 76 | return ( 77 | dte2.setMonth(n + dte.getMonth()), 78 | dte2 79 | ); 80 | }; 81 | 82 | // JS Prelude -------------------------------------------------- 83 | // Tuple (,) :: a -> b -> (a, b) 84 | const Tuple = a => 85 | b => ({ 86 | type: "Tuple", 87 | "0": a, 88 | "1": b, 89 | length: 2 90 | }); 91 | 92 | // add (+) :: Num a => a -> a -> a 93 | const add = a => 94 | // Curried addition. 95 | b => a + b; 96 | 97 | // all :: (a -> Bool) -> [a] -> Bool 98 | const all = p => 99 | // True if p(x) holds for every x in xs. 100 | xs => [...xs].every(p); 101 | 102 | // any :: (a -> Bool) -> [a] -> Bool 103 | const any = p => 104 | // True if p(x) holds for at least 105 | // one item in xs. 106 | xs => [...xs].some(p); 107 | 108 | 109 | // concat :: [[a]] -> [a] 110 | // concat :: [String] -> String 111 | const concat = xs => 112 | 0 < xs.length ? ( 113 | ( 114 | xs.every(x => "string" === typeof x) ? ( 115 | "" 116 | ) : [] 117 | ).concat(...xs) 118 | ) : xs; 119 | 120 | // eq (==) :: Eq a => a -> a -> Bool 121 | const eq1 = a => 122 | // True when a and b are equivalent in the terms 123 | // defined below for their shared data type. 124 | b => { 125 | const t = typeName(a); 126 | 127 | return t !== typeName(b) ? ( 128 | false 129 | ) : "Dict" !== t ? ( 130 | "Date" !== t ? ( 131 | "function" !== t ? ( 132 | a === b 133 | ) : a.toString() === b.toString() 134 | ) : 0 === (a - b) // Date equality 135 | ) : (() => { 136 | const kvs = Object.entries(a); 137 | 138 | return kvs.length !== Object.keys(b).length ? ( 139 | false 140 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 141 | })(); 142 | }; 143 | 144 | // findIndices :: (a -> Bool) -> [a] -> [Int] 145 | // findIndices :: (String -> Bool) -> String -> [Int] 146 | const findIndices = p => 147 | xs => { 148 | const ys = [...xs]; 149 | 150 | return ys.flatMap( 151 | (y, i) => p(y, i, ys) ? ( 152 | [i] 153 | ) : [] 154 | ); 155 | }; 156 | 157 | // fst :: (a, b) -> a 158 | const fst = tpl => 159 | // First member of a pair. 160 | tpl[0]; 161 | 162 | // keys :: Dict -> [String] 163 | const keys = Object.keys; 164 | 165 | // length :: [a] -> Int 166 | const length = xs => 167 | // Returns Infinity over objects without finite 168 | // length. This enables zip and zipWith to choose 169 | // the shorter argument when one is non-finite, 170 | // like cycle, repeat etc 171 | "GeneratorFunction" !== xs.constructor 172 | .constructor.name ? ( 173 | xs.length 174 | ) : Infinity; 175 | 176 | // map :: (a -> b) -> [a] -> [b] 177 | const map = f => 178 | // The list obtained by applying f 179 | // to each element of xs. 180 | // (The image of xs under f). 181 | xs => [...xs].map(f); 182 | 183 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 184 | const matching = pat => { 185 | // A sequence-matching function for findIndices etc 186 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 187 | // -> [1, 4] 188 | const 189 | lng = pat.length, 190 | bln = 0 < lng, 191 | h = bln ? pat[0] : undefined; 192 | 193 | return x => i => src => 194 | bln && h === x && eq(pat)( 195 | src.slice(i, lng + i) 196 | ); 197 | }; 198 | 199 | // snd :: (a, b) -> b 200 | const snd = tpl => 201 | // Second member of a pair. 202 | tpl[1]; 203 | 204 | // splitOn :: [a] -> [a] -> [[a]] 205 | // splitOn :: String -> String -> [String] 206 | const splitOn = pat => src => 207 | // A list of the strings delimited by 208 | // instances of a given pattern in s. 209 | ("string" === typeof src) ? ( 210 | src.split(pat) 211 | ) : (() => { 212 | const 213 | lng = pat.length, 214 | tpl = findIndices(matching(pat))(src).reduce( 215 | (a, i) => Tuple( 216 | fst(a).concat([src.slice(snd(a), i)]) 217 | )(lng + i), 218 | Tuple([])(0) 219 | ); 220 | 221 | return fst(tpl).concat([src.slice(snd(tpl))]); 222 | })(); 223 | 224 | // typeName :: a -> String 225 | const typeName = v => { 226 | const t = typeof v; 227 | 228 | return null !== v ? ( 229 | "object" === t ? ( 230 | Array.isArray(v) ? ( 231 | "List" 232 | ) : "Date" === v.constructor.name ? ( 233 | "Date" 234 | ) : (() => { 235 | const ct = v.type; 236 | 237 | return Boolean(ct) ? ( 238 | (/Tuple\d+/u).test(ct) ? ( 239 | "TupleN" 240 | ) : ct 241 | ) : "Dict"; 242 | })() 243 | ) : { 244 | "boolean": "Bool", 245 | "date": "Date", 246 | "number": "Num", 247 | "string": "String", 248 | "function": "(a -> b)" 249 | } [t] || "Bottom" 250 | ) : "Bottom"; 251 | }; 252 | 253 | return main() 254 | }; 255 | 256 | return omniJSContext() 257 | }), { 258 | validate: selection => ['projects', 'tasks'].flatMap( 259 | k => Array.from(selection[k]) 260 | ).length > 0 261 | }) 262 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Organize/Convert Tasks to Projects (Here).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action" 3 | }*/ 4 | // Twitter: @unlocked2412 5 | (() => { 6 | // ---------------------- PLUGIN ----------------------- 7 | return Object.assign( 8 | new PlugIn.Action(selection => { 9 | 'use strict'; 10 | 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | proj = selection 17 | .tasks[0] 18 | .containingProject; 19 | 20 | return convertTasksToProjects( 21 | selection.tasks, 22 | null === proj ? ( 23 | // Tasks are in the inbox 24 | library.ending 25 | ) : null === proj.parentFolder ? ( 26 | proj.after 27 | ) : proj.parentFolder.ending 28 | ) 29 | }; 30 | 31 | return main(); 32 | }; 33 | 34 | return omniJSContext() 35 | 36 | }), { 37 | validate: selection => 0 < selection.tasks.length 38 | }); 39 | })(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Organize/Move To Bottom.omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | // Test 9 | (() => 10 | Object.assign( 11 | new PlugIn.Action(selection => { 12 | // OMNI JS CODE --------------------------------------- 13 | const omniJSContext = () => { 14 | // main :: IO () 15 | const main = () => { 16 | const 17 | win = document.windows[0], 18 | selection = win.selection, 19 | contentTree = win.content, 20 | parentNode = contentTree.nodeForObject( 21 | selection.tasks[0] 22 | ).parent; 23 | 24 | return parentNode.isRootNode ? ( 25 | ( 26 | new Alert( 27 | "Feature isn't available", 28 | "Parent node is root node." 29 | ).show(), 30 | [] 31 | ) 32 | ) : parentNode.object.constructor.name === "Task" || parentNode.object.constructor.name === "Project" ? ( 33 | ( 34 | moveTasks(selection.tasks, parentNode.object.ending), 35 | contentTree.select([]) 36 | ) 37 | ) : parentNode.object.constructor.name === "Tag" ? ( 38 | ( 39 | parentNode.object.moveTasks(selection.tasks, parentNode.object.endingOfTasks), 40 | contentTree.select([]) 41 | ) 42 | ) : ( 43 | new Alert( 44 | "Feature isn't available", 45 | "Feature is not available in current view." 46 | ).show(), 47 | [] 48 | ) 49 | }; 50 | 51 | return main() 52 | }; 53 | 54 | return omniJSContext() 55 | }), { 56 | validate: selection => 0 < selection.tasks.length 57 | }) 58 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Organize/Move To Top.omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | // Test 9 | (() => 10 | Object.assign( 11 | new PlugIn.Action(selection => { 12 | // OMNI JS CODE --------------------------------------- 13 | const omniJSContext = () => { 14 | // main :: IO () 15 | const main = () => { 16 | const 17 | win = document.windows[0], 18 | selection = win.selection, 19 | contentTree = win.content, 20 | parentNode = contentTree.nodeForObject( 21 | selection.tasks[0] 22 | ).parent; 23 | 24 | return parentNode.isRootNode ? ( 25 | ( 26 | new Alert( 27 | "Feature isn't available", 28 | "Parent node is root node." 29 | ).show(), 30 | [] 31 | ) 32 | ) : parentNode.object.constructor.name === "Task" || parentNode.object.constructor.name === "Project" ? ( 33 | ( 34 | moveTasks(selection.tasks, parentNode.object.beginning), 35 | contentTree.select([]) 36 | ) 37 | ) : parentNode.object.constructor.name === "Tag" ? ( 38 | ( 39 | parentNode.object.moveTasks(selection.tasks, parentNode.object.beginningOfTasks), 40 | contentTree.select([]) 41 | ) 42 | ) : ( 43 | new Alert( 44 | "Feature isn't available", 45 | "Feature is not available in current view." 46 | ).show(), 47 | [] 48 | ) 49 | }; 50 | 51 | return main() 52 | }; 53 | 54 | return omniJSContext() 55 | }), { 56 | validate: selection => 0 < selection.tasks.length 57 | }) 58 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Organize/Move Up.omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | win = selection.window, 17 | node = win.content.nodeForObject( 18 | selection.tasks[0] 19 | ), 20 | p = node.parent; 21 | 22 | return bindMay( 23 | previousSibling(node) 24 | )( 25 | nd => ( 26 | isTag(p.object) ? ( 27 | p.object.moveTasks(selection.tasks, p.object.beforeTask(nd.object)) 28 | ) : moveTasks(selection.tasks, nd.object.before), 29 | Just(selection.tasks) 30 | ) 31 | ) 32 | }; 33 | // GENERICS ---------------------------------------------------------------- 34 | // https://github.com/RobTrew/prelude-jxa 35 | // JS Prelude -------------------------------------------------- 36 | // Just :: a -> Maybe a 37 | const Just = x => ({ 38 | type: "Maybe", 39 | Nothing: false, 40 | Just: x 41 | }); 42 | 43 | // Nothing :: Maybe a 44 | const Nothing = () => ({ 45 | type: "Maybe", 46 | Nothing: true 47 | }); 48 | 49 | // bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b 50 | const bindMay = mb => 51 | // Nothing if mb is Nothing, or the application of the 52 | // (a -> Maybe b) function mf to the contents of mb. 53 | mf => mb.Nothing ? ( 54 | mb 55 | ) : mf(mb.Just); 56 | 57 | // or :: [Bool] -> Bool 58 | const or = xs => 59 | xs.some(Boolean); 60 | 61 | // OmniJS OmniFocus -------------------------------------------- 62 | // isTag :: DatabaseObject -> Bool 63 | const isTag = dbObject => { 64 | return dbObject.constructor.name === "Tag" 65 | } 66 | 67 | // previousSibling :: TreeNode -> Maybe TreeNode 68 | const previousSibling = node => { 69 | const 70 | parent = node.parent, 71 | children = parent.children, 72 | i = children.findIndex( 73 | x => x === node 74 | ); 75 | return i < 1 ? ( 76 | Nothing() 77 | ) : Just(children[i - 1]) 78 | }; 79 | 80 | return main() 81 | }; 82 | 83 | return omniJSContext() 84 | }), { 85 | validate: selection => selection.tasks.length > 0 86 | }) 87 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Postpone Due Date/+1 day (Due).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file OmniFocus plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultDueTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | tomorrow = addDays(1)( 22 | setTime( 23 | ...splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ), 28 | p = ( 29 | null === ts[0].dueDate || 30 | any(x => !eq1(ts[0].dueDate)(x.dueDate))(ts) 31 | ); 32 | 33 | return p ? ( 34 | ts.map(updatedueDate(tomorrow)) 35 | ) : ts.map(task => 36 | updatedueDate(addDays(1)(task.dueDate))( 37 | task 38 | ) 39 | ) 40 | }; 41 | 42 | // updatedueDate :: Date -> OFTask -> IO OFTask 43 | const updatedueDate = dte => task => { 44 | return ( 45 | task.dueDate = dte, 46 | task 47 | ) 48 | } 49 | 50 | // FUNCTIONS -- 51 | // https://github.com/RobTrew/prelude-jxa 52 | // JS Basics --------------------------------------------------- 53 | // addDays :: Int -> Date -> Date 54 | const addDays = n => dte => { 55 | const dte2 = new Date(dte); 56 | return ( 57 | dte2.setDate(n + dte.getDate()), 58 | dte2 59 | ); 60 | }; 61 | 62 | // setTime :: (Int, Int, Int) -> Date -> Date 63 | const setTime = (h, m, s) => dte => { 64 | const dte2 = new Date(dte); 65 | return ( 66 | dte2.setHours(h, m, s, 0), 67 | dte2 68 | ); 69 | }; 70 | 71 | // addHours :: Int -> Date -> Date 72 | const addHours = n => dte => { 73 | const dte2 = new Date(dte); 74 | return ( 75 | dte2.setHours(n + dte.getHours()), 76 | dte2 77 | ); 78 | }; 79 | 80 | // addWeeks :: Int -> Date -> Date 81 | const addWeeks = n => addDays(7 * n); 82 | 83 | // addMonths :: Int -> Date -> Date 84 | const addMonths = n => dte => { 85 | const dte2 = new Date(dte); 86 | return ( 87 | dte2.setMonth(n + dte.getMonth()), 88 | dte2 89 | ); 90 | }; 91 | 92 | // JS Prelude -------------------------------------------------- 93 | // Tuple (,) :: a -> b -> (a, b) 94 | const Tuple = a => 95 | b => ({ 96 | type: "Tuple", 97 | "0": a, 98 | "1": b, 99 | length: 2 100 | }); 101 | 102 | // add (+) :: Num a => a -> a -> a 103 | const add = a => 104 | // Curried addition. 105 | b => a + b; 106 | 107 | // all :: (a -> Bool) -> [a] -> Bool 108 | const all = p => 109 | // True if p(x) holds for every x in xs. 110 | xs => [...xs].every(p); 111 | 112 | // any :: (a -> Bool) -> [a] -> Bool 113 | const any = p => 114 | // True if p(x) holds for at least 115 | // one item in xs. 116 | xs => [...xs].some(p); 117 | 118 | 119 | // concat :: [[a]] -> [a] 120 | // concat :: [String] -> String 121 | const concat = xs => 122 | 0 < xs.length ? ( 123 | ( 124 | xs.every(x => "string" === typeof x) ? ( 125 | "" 126 | ) : [] 127 | ).concat(...xs) 128 | ) : xs; 129 | 130 | // eq (==) :: Eq a => a -> a -> Bool 131 | const eq1 = a => 132 | // True when a and b are equivalent in the terms 133 | // defined below for their shared data type. 134 | b => { 135 | const t = typeName(a); 136 | 137 | return t !== typeName(b) ? ( 138 | false 139 | ) : "Dict" !== t ? ( 140 | "Date" !== t ? ( 141 | "function" !== t ? ( 142 | a === b 143 | ) : a.toString() === b.toString() 144 | ) : 0 === (a - b) // Date equality 145 | ) : (() => { 146 | const kvs = Object.entries(a); 147 | 148 | return kvs.length !== Object.keys(b).length ? ( 149 | false 150 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 151 | })(); 152 | }; 153 | 154 | // findIndices :: (a -> Bool) -> [a] -> [Int] 155 | // findIndices :: (String -> Bool) -> String -> [Int] 156 | const findIndices = p => 157 | xs => { 158 | const ys = [...xs]; 159 | 160 | return ys.flatMap( 161 | (y, i) => p(y, i, ys) ? ( 162 | [i] 163 | ) : [] 164 | ); 165 | }; 166 | 167 | // fst :: (a, b) -> a 168 | const fst = tpl => 169 | // First member of a pair. 170 | tpl[0]; 171 | 172 | // keys :: Dict -> [String] 173 | const keys = Object.keys; 174 | 175 | // length :: [a] -> Int 176 | const length = xs => 177 | // Returns Infinity over objects without finite 178 | // length. This enables zip and zipWith to choose 179 | // the shorter argument when one is non-finite, 180 | // like cycle, repeat etc 181 | "GeneratorFunction" !== xs.constructor 182 | .constructor.name ? ( 183 | xs.length 184 | ) : Infinity; 185 | 186 | // map :: (a -> b) -> [a] -> [b] 187 | const map = f => 188 | // The list obtained by applying f 189 | // to each element of xs. 190 | // (The image of xs under f). 191 | xs => [...xs].map(f); 192 | 193 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 194 | const matching = pat => { 195 | // A sequence-matching function for findIndices etc 196 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 197 | // -> [1, 4] 198 | const 199 | lng = pat.length, 200 | bln = 0 < lng, 201 | h = bln ? pat[0] : undefined; 202 | 203 | return x => i => src => 204 | bln && h === x && eq(pat)( 205 | src.slice(i, lng + i) 206 | ); 207 | }; 208 | 209 | // snd :: (a, b) -> b 210 | const snd = tpl => 211 | // Second member of a pair. 212 | tpl[1]; 213 | 214 | // splitOn :: [a] -> [a] -> [[a]] 215 | // splitOn :: String -> String -> [String] 216 | const splitOn = pat => src => 217 | // A list of the strings delimited by 218 | // instances of a given pattern in s. 219 | ("string" === typeof src) ? ( 220 | src.split(pat) 221 | ) : (() => { 222 | const 223 | lng = pat.length, 224 | tpl = findIndices(matching(pat))(src).reduce( 225 | (a, i) => Tuple( 226 | fst(a).concat([src.slice(snd(a), i)]) 227 | )(lng + i), 228 | Tuple([])(0) 229 | ); 230 | 231 | return fst(tpl).concat([src.slice(snd(tpl))]); 232 | })(); 233 | 234 | // typeName :: a -> String 235 | const typeName = v => { 236 | const t = typeof v; 237 | 238 | return null !== v ? ( 239 | "object" === t ? ( 240 | Array.isArray(v) ? ( 241 | "List" 242 | ) : "Date" === v.constructor.name ? ( 243 | "Date" 244 | ) : (() => { 245 | const ct = v.type; 246 | 247 | return Boolean(ct) ? ( 248 | (/Tuple\d+/u).test(ct) ? ( 249 | "TupleN" 250 | ) : ct 251 | ) : "Dict"; 252 | })() 253 | ) : { 254 | "boolean": "Bool", 255 | "date": "Date", 256 | "number": "Num", 257 | "string": "String", 258 | "function": "(a -> b)" 259 | } [t] || "Bottom" 260 | ) : "Bottom"; 261 | }; 262 | 263 | return main() 264 | }; 265 | 266 | return omniJSContext() 267 | }), { 268 | validate: selection => ['projects', 'tasks'].flatMap( 269 | k => Array.from(selection[k]) 270 | ).length > 0 271 | }) 272 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Postpone Due Date/+1 month (Due).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file OmniFocus plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultDueTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | nextMonth = addMonths(1)( 22 | setTime( 23 | ...splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ), 28 | p = ( 29 | null === ts[0].dueDate || 30 | any(x => !eq1(ts[0].dueDate)(x.dueDate))(ts) 31 | ); 32 | 33 | return p ? ( 34 | ts.map(updatedueDate(nextMonth)) 35 | ) : ts.map(task => 36 | updatedueDate(addMonths(1)(task.dueDate))( 37 | task 38 | ) 39 | ) 40 | }; 41 | 42 | // updatedueDate :: Date -> OFTask -> IO OFTask 43 | const updatedueDate = dte => task => { 44 | return ( 45 | task.dueDate = dte, 46 | task 47 | ) 48 | } 49 | 50 | // FUNCTIONS -- 51 | // https://github.com/RobTrew/prelude-jxa 52 | // JS Basics --------------------------------------------------- 53 | // addDays :: Int -> Date -> Date 54 | const addDays = n => dte => { 55 | const dte2 = new Date(dte); 56 | return ( 57 | dte2.setDate(n + dte.getDate()), 58 | dte2 59 | ); 60 | }; 61 | 62 | // setTime :: (Int, Int, Int) -> Date -> Date 63 | const setTime = (h, m, s) => dte => { 64 | const dte2 = new Date(dte); 65 | return ( 66 | dte2.setHours(h, m, s, 0), 67 | dte2 68 | ); 69 | }; 70 | 71 | // addHours :: Int -> Date -> Date 72 | const addHours = n => dte => { 73 | const dte2 = new Date(dte); 74 | return ( 75 | dte2.setHours(n + dte.getHours()), 76 | dte2 77 | ); 78 | }; 79 | 80 | // addWeeks :: Int -> Date -> Date 81 | const addWeeks = n => addDays(7 * n); 82 | 83 | // addMonths :: Int -> Date -> Date 84 | const addMonths = n => dte => { 85 | const dte2 = new Date(dte); 86 | return ( 87 | dte2.setMonth(n + dte.getMonth()), 88 | dte2 89 | ); 90 | }; 91 | 92 | // JS Prelude -------------------------------------------------- 93 | // Tuple (,) :: a -> b -> (a, b) 94 | const Tuple = a => 95 | b => ({ 96 | type: "Tuple", 97 | "0": a, 98 | "1": b, 99 | length: 2 100 | }); 101 | 102 | // add (+) :: Num a => a -> a -> a 103 | const add = a => 104 | // Curried addition. 105 | b => a + b; 106 | 107 | // all :: (a -> Bool) -> [a] -> Bool 108 | const all = p => 109 | // True if p(x) holds for every x in xs. 110 | xs => [...xs].every(p); 111 | 112 | // any :: (a -> Bool) -> [a] -> Bool 113 | const any = p => 114 | // True if p(x) holds for at least 115 | // one item in xs. 116 | xs => [...xs].some(p); 117 | 118 | 119 | // concat :: [[a]] -> [a] 120 | // concat :: [String] -> String 121 | const concat = xs => 122 | 0 < xs.length ? ( 123 | ( 124 | xs.every(x => "string" === typeof x) ? ( 125 | "" 126 | ) : [] 127 | ).concat(...xs) 128 | ) : xs; 129 | 130 | // eq (==) :: Eq a => a -> a -> Bool 131 | const eq1 = a => 132 | // True when a and b are equivalent in the terms 133 | // defined below for their shared data type. 134 | b => { 135 | const t = typeName(a); 136 | 137 | return t !== typeName(b) ? ( 138 | false 139 | ) : "Dict" !== t ? ( 140 | "Date" !== t ? ( 141 | "function" !== t ? ( 142 | a === b 143 | ) : a.toString() === b.toString() 144 | ) : 0 === (a - b) // Date equality 145 | ) : (() => { 146 | const kvs = Object.entries(a); 147 | 148 | return kvs.length !== Object.keys(b).length ? ( 149 | false 150 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 151 | })(); 152 | }; 153 | 154 | // findIndices :: (a -> Bool) -> [a] -> [Int] 155 | // findIndices :: (String -> Bool) -> String -> [Int] 156 | const findIndices = p => 157 | xs => { 158 | const ys = [...xs]; 159 | 160 | return ys.flatMap( 161 | (y, i) => p(y, i, ys) ? ( 162 | [i] 163 | ) : [] 164 | ); 165 | }; 166 | 167 | // fst :: (a, b) -> a 168 | const fst = tpl => 169 | // First member of a pair. 170 | tpl[0]; 171 | 172 | // keys :: Dict -> [String] 173 | const keys = Object.keys; 174 | 175 | // length :: [a] -> Int 176 | const length = xs => 177 | // Returns Infinity over objects without finite 178 | // length. This enables zip and zipWith to choose 179 | // the shorter argument when one is non-finite, 180 | // like cycle, repeat etc 181 | "GeneratorFunction" !== xs.constructor 182 | .constructor.name ? ( 183 | xs.length 184 | ) : Infinity; 185 | 186 | // map :: (a -> b) -> [a] -> [b] 187 | const map = f => 188 | // The list obtained by applying f 189 | // to each element of xs. 190 | // (The image of xs under f). 191 | xs => [...xs].map(f); 192 | 193 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 194 | const matching = pat => { 195 | // A sequence-matching function for findIndices etc 196 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 197 | // -> [1, 4] 198 | const 199 | lng = pat.length, 200 | bln = 0 < lng, 201 | h = bln ? pat[0] : undefined; 202 | 203 | return x => i => src => 204 | bln && h === x && eq(pat)( 205 | src.slice(i, lng + i) 206 | ); 207 | }; 208 | 209 | // snd :: (a, b) -> b 210 | const snd = tpl => 211 | // Second member of a pair. 212 | tpl[1]; 213 | 214 | // splitOn :: [a] -> [a] -> [[a]] 215 | // splitOn :: String -> String -> [String] 216 | const splitOn = pat => src => 217 | // A list of the strings delimited by 218 | // instances of a given pattern in s. 219 | ("string" === typeof src) ? ( 220 | src.split(pat) 221 | ) : (() => { 222 | const 223 | lng = pat.length, 224 | tpl = findIndices(matching(pat))(src).reduce( 225 | (a, i) => Tuple( 226 | fst(a).concat([src.slice(snd(a), i)]) 227 | )(lng + i), 228 | Tuple([])(0) 229 | ); 230 | 231 | return fst(tpl).concat([src.slice(snd(tpl))]); 232 | })(); 233 | 234 | // typeName :: a -> String 235 | const typeName = v => { 236 | const t = typeof v; 237 | 238 | return null !== v ? ( 239 | "object" === t ? ( 240 | Array.isArray(v) ? ( 241 | "List" 242 | ) : "Date" === v.constructor.name ? ( 243 | "Date" 244 | ) : (() => { 245 | const ct = v.type; 246 | 247 | return Boolean(ct) ? ( 248 | (/Tuple\d+/u).test(ct) ? ( 249 | "TupleN" 250 | ) : ct 251 | ) : "Dict"; 252 | })() 253 | ) : { 254 | "boolean": "Bool", 255 | "date": "Date", 256 | "number": "Num", 257 | "string": "String", 258 | "function": "(a -> b)" 259 | } [t] || "Bottom" 260 | ) : "Bottom"; 261 | }; 262 | 263 | return main() 264 | }; 265 | 266 | return omniJSContext() 267 | }), { 268 | validate: selection => ['projects', 'tasks'].flatMap( 269 | k => Array.from(selection[k]) 270 | ).length > 0 271 | }) 272 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Postpone Due Date/+1 week (Due).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file OmniFocus plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultDueTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | nextWeek = addWeeks(1)( 22 | setTime( 23 | ...splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ), 28 | p = ( 29 | null === ts[0].dueDate || 30 | any(x => !eq1(ts[0].dueDate)(x.dueDate))(ts) 31 | ); 32 | 33 | return p ? ( 34 | ts.map(updatedueDate(nextWeek)) 35 | ) : ts.map(task => 36 | updatedueDate(addWeeks(1)(task.dueDate))( 37 | task 38 | ) 39 | ) 40 | }; 41 | 42 | // updatedueDate :: Date -> OFTask -> IO OFTask 43 | const updatedueDate = dte => task => { 44 | return ( 45 | task.dueDate = dte, 46 | task 47 | ) 48 | } 49 | 50 | // FUNCTIONS -- 51 | // https://github.com/RobTrew/prelude-jxa 52 | // JS Basics --------------------------------------------------- 53 | // addDays :: Int -> Date -> Date 54 | const addDays = n => dte => { 55 | const dte2 = new Date(dte); 56 | return ( 57 | dte2.setDate(n + dte.getDate()), 58 | dte2 59 | ); 60 | }; 61 | 62 | // setTime :: (Int, Int, Int) -> Date -> Date 63 | const setTime = (h, m, s) => dte => { 64 | const dte2 = new Date(dte); 65 | return ( 66 | dte2.setHours(h, m, s, 0), 67 | dte2 68 | ); 69 | }; 70 | 71 | // addHours :: Int -> Date -> Date 72 | const addHours = n => dte => { 73 | const dte2 = new Date(dte); 74 | return ( 75 | dte2.setHours(n + dte.getHours()), 76 | dte2 77 | ); 78 | }; 79 | 80 | // addWeeks :: Int -> Date -> Date 81 | const addWeeks = n => addDays(7 * n); 82 | 83 | // addMonths :: Int -> Date -> Date 84 | const addMonths = n => dte => { 85 | const dte2 = new Date(dte); 86 | return ( 87 | dte2.setMonth(n + dte.getMonth()), 88 | dte2 89 | ); 90 | }; 91 | 92 | // JS Prelude -------------------------------------------------- 93 | // Tuple (,) :: a -> b -> (a, b) 94 | const Tuple = a => 95 | b => ({ 96 | type: "Tuple", 97 | "0": a, 98 | "1": b, 99 | length: 2 100 | }); 101 | 102 | // add (+) :: Num a => a -> a -> a 103 | const add = a => 104 | // Curried addition. 105 | b => a + b; 106 | 107 | // all :: (a -> Bool) -> [a] -> Bool 108 | const all = p => 109 | // True if p(x) holds for every x in xs. 110 | xs => [...xs].every(p); 111 | 112 | // any :: (a -> Bool) -> [a] -> Bool 113 | const any = p => 114 | // True if p(x) holds for at least 115 | // one item in xs. 116 | xs => [...xs].some(p); 117 | 118 | 119 | // concat :: [[a]] -> [a] 120 | // concat :: [String] -> String 121 | const concat = xs => 122 | 0 < xs.length ? ( 123 | ( 124 | xs.every(x => "string" === typeof x) ? ( 125 | "" 126 | ) : [] 127 | ).concat(...xs) 128 | ) : xs; 129 | 130 | // eq (==) :: Eq a => a -> a -> Bool 131 | const eq1 = a => 132 | // True when a and b are equivalent in the terms 133 | // defined below for their shared data type. 134 | b => { 135 | const t = typeName(a); 136 | 137 | return t !== typeName(b) ? ( 138 | false 139 | ) : "Dict" !== t ? ( 140 | "Date" !== t ? ( 141 | "function" !== t ? ( 142 | a === b 143 | ) : a.toString() === b.toString() 144 | ) : 0 === (a - b) // Date equality 145 | ) : (() => { 146 | const kvs = Object.entries(a); 147 | 148 | return kvs.length !== Object.keys(b).length ? ( 149 | false 150 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 151 | })(); 152 | }; 153 | 154 | // findIndices :: (a -> Bool) -> [a] -> [Int] 155 | // findIndices :: (String -> Bool) -> String -> [Int] 156 | const findIndices = p => 157 | xs => { 158 | const ys = [...xs]; 159 | 160 | return ys.flatMap( 161 | (y, i) => p(y, i, ys) ? ( 162 | [i] 163 | ) : [] 164 | ); 165 | }; 166 | 167 | // fst :: (a, b) -> a 168 | const fst = tpl => 169 | // First member of a pair. 170 | tpl[0]; 171 | 172 | // keys :: Dict -> [String] 173 | const keys = Object.keys; 174 | 175 | // length :: [a] -> Int 176 | const length = xs => 177 | // Returns Infinity over objects without finite 178 | // length. This enables zip and zipWith to choose 179 | // the shorter argument when one is non-finite, 180 | // like cycle, repeat etc 181 | "GeneratorFunction" !== xs.constructor 182 | .constructor.name ? ( 183 | xs.length 184 | ) : Infinity; 185 | 186 | // map :: (a -> b) -> [a] -> [b] 187 | const map = f => 188 | // The list obtained by applying f 189 | // to each element of xs. 190 | // (The image of xs under f). 191 | xs => [...xs].map(f); 192 | 193 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 194 | const matching = pat => { 195 | // A sequence-matching function for findIndices etc 196 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 197 | // -> [1, 4] 198 | const 199 | lng = pat.length, 200 | bln = 0 < lng, 201 | h = bln ? pat[0] : undefined; 202 | 203 | return x => i => src => 204 | bln && h === x && eq(pat)( 205 | src.slice(i, lng + i) 206 | ); 207 | }; 208 | 209 | // snd :: (a, b) -> b 210 | const snd = tpl => 211 | // Second member of a pair. 212 | tpl[1]; 213 | 214 | // splitOn :: [a] -> [a] -> [[a]] 215 | // splitOn :: String -> String -> [String] 216 | const splitOn = pat => src => 217 | // A list of the strings delimited by 218 | // instances of a given pattern in s. 219 | ("string" === typeof src) ? ( 220 | src.split(pat) 221 | ) : (() => { 222 | const 223 | lng = pat.length, 224 | tpl = findIndices(matching(pat))(src).reduce( 225 | (a, i) => Tuple( 226 | fst(a).concat([src.slice(snd(a), i)]) 227 | )(lng + i), 228 | Tuple([])(0) 229 | ); 230 | 231 | return fst(tpl).concat([src.slice(snd(tpl))]); 232 | })(); 233 | 234 | // typeName :: a -> String 235 | const typeName = v => { 236 | const t = typeof v; 237 | 238 | return null !== v ? ( 239 | "object" === t ? ( 240 | Array.isArray(v) ? ( 241 | "List" 242 | ) : "Date" === v.constructor.name ? ( 243 | "Date" 244 | ) : (() => { 245 | const ct = v.type; 246 | 247 | return Boolean(ct) ? ( 248 | (/Tuple\d+/u).test(ct) ? ( 249 | "TupleN" 250 | ) : ct 251 | ) : "Dict"; 252 | })() 253 | ) : { 254 | "boolean": "Bool", 255 | "date": "Date", 256 | "number": "Num", 257 | "string": "String", 258 | "function": "(a -> b)" 259 | } [t] || "Bottom" 260 | ) : "Bottom"; 261 | }; 262 | 263 | return main() 264 | }; 265 | 266 | return omniJSContext() 267 | }), { 268 | validate: selection => ['projects', 'tasks'].flatMap( 269 | k => Array.from(selection[k]) 270 | ).length > 0 271 | }) 272 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Postpone Due Date/This Weekend (Due).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultDueTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | nextWeekendDay = setTime( 22 | ...splitOn(":")(defaultStartTime) 23 | )(until(isWeekend)( 24 | addDays(1) 25 | )(new Date())); 26 | 27 | return ts.map(task => { 28 | return ( 29 | task.dueDate = nextWeekendDay, 30 | task 31 | ) 32 | }) 33 | }; 34 | 35 | 36 | // FUNCTIONS -- 37 | // JS Basics --------------------------------------------------- 38 | // addDays :: Int -> Date -> Date 39 | const addDays = n => dte => { 40 | const dte2 = new Date(dte); 41 | return ( 42 | dte2.setDate(n + dte.getDate()), 43 | dte2 44 | ); 45 | }; 46 | 47 | // setTime :: (Int, Int, Int) -> Date -> Date 48 | const setTime = (h, m, s) => dte => { 49 | const dte2 = new Date(dte); 50 | return ( 51 | dte2.setHours(h, m, s, 0), 52 | dte2 53 | ); 54 | }; 55 | 56 | // isWeekend :: Date -> Bool 57 | const isWeekend = dte => 58 | any( 59 | eq(dte.getDay()) 60 | )([0, 6]) 61 | 62 | // JS Prelude -------------------------------------------------- 63 | // https://github.com/RobTrew/prelude-jxa 64 | // Tuple (,) :: a -> b -> (a, b) 65 | const Tuple = a => 66 | b => ({ 67 | type: "Tuple", 68 | "0": a, 69 | "1": b, 70 | length: 2 71 | }); 72 | 73 | // any :: (a -> Bool) -> [a] -> Bool 74 | const any = p => 75 | // True if p(x) holds for at least 76 | // one item in xs. 77 | xs => [...xs].some(p); 78 | 79 | // concat :: [[a]] -> [a] 80 | // concat :: [String] -> String 81 | const concat = xs => 82 | 0 < xs.length ? ( 83 | ( 84 | xs.every(x => "string" === typeof x) ? ( 85 | "" 86 | ) : [] 87 | ).concat(...xs) 88 | ) : xs; 89 | 90 | // eq (==) :: Eq a => a -> a -> Bool 91 | const eq = a => 92 | // True when a and b are equivalent in the terms 93 | // defined below for their shared data type. 94 | b => { 95 | const t = typeof a; 96 | 97 | return t !== typeof b ? ( 98 | false 99 | ) : "object" !== t ? ( 100 | "function" !== t ? ( 101 | a === b 102 | ) : a.toString() === b.toString() 103 | ) : (() => { 104 | const kvs = Object.entries(a); 105 | 106 | return kvs.length !== Object.keys(b).length ? ( 107 | false 108 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 109 | })(); 110 | }; 111 | 112 | // findIndices :: (a -> Bool) -> [a] -> [Int] 113 | // findIndices :: (String -> Bool) -> String -> [Int] 114 | const findIndices = p => 115 | xs => { 116 | const ys = [...xs]; 117 | 118 | return ys.flatMap( 119 | (y, i) => p(y, i, ys) ? ( 120 | [i] 121 | ) : [] 122 | ); 123 | }; 124 | 125 | // fst :: (a, b) -> a 126 | const fst = tpl => 127 | // First member of a pair. 128 | tpl[0]; 129 | 130 | // keys :: Dict -> [String] 131 | const keys = Object.keys; 132 | 133 | // length :: [a] -> Int 134 | const length = xs => 135 | // Returns Infinity over objects without finite 136 | // length. This enables zip and zipWith to choose 137 | // the shorter argument when one is non-finite, 138 | // like cycle, repeat etc 139 | "GeneratorFunction" !== xs.constructor 140 | .constructor.name ? ( 141 | xs.length 142 | ) : Infinity; 143 | 144 | // map :: (a -> b) -> [a] -> [b] 145 | const map = f => 146 | // The list obtained by applying f 147 | // to each element of xs. 148 | // (The image of xs under f). 149 | xs => [...xs].map(f); 150 | 151 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 152 | const matching = pat => { 153 | // A sequence-matching function for findIndices etc 154 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 155 | // -> [1, 4] 156 | const 157 | lng = pat.length, 158 | bln = 0 < lng, 159 | h = bln ? pat[0] : undefined; 160 | 161 | return x => i => src => 162 | bln && h === x && eq(pat)( 163 | src.slice(i, lng + i) 164 | ); 165 | }; 166 | 167 | // snd :: (a, b) -> b 168 | const snd = tpl => 169 | // Second member of a pair. 170 | tpl[1]; 171 | 172 | // splitOn :: [a] -> [a] -> [[a]] 173 | // splitOn :: String -> String -> [String] 174 | const splitOn = pat => src => 175 | // A list of the strings delimited by 176 | // instances of a given pattern in s. 177 | ("string" === typeof src) ? ( 178 | src.split(pat) 179 | ) : (() => { 180 | const 181 | lng = pat.length, 182 | tpl = findIndices(matching(pat))(src).reduce( 183 | (a, i) => Tuple( 184 | fst(a).concat([src.slice(snd(a), i)]) 185 | )(lng + i), 186 | Tuple([])(0) 187 | ); 188 | 189 | return fst(tpl).concat([src.slice(snd(tpl))]); 190 | })(); 191 | 192 | // until :: (a -> Bool) -> (a -> a) -> a -> a 193 | const until = p => 194 | // The value resulting from repeated applications 195 | // of f to the seed value x, terminating when 196 | // that result returns true for the predicate p. 197 | f => x => { 198 | let v = x; 199 | 200 | while (!p(v)) { 201 | v = f(v); 202 | } 203 | 204 | return v; 205 | }; 206 | 207 | return main() 208 | }; 209 | 210 | return omniJSContext() 211 | }), { 212 | validate: selection => ['projects', 'tasks'].flatMap( 213 | k => selection[k] 214 | ).length > 0 215 | }) 216 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Postpone Due Date/Today (Due).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file OmniFocus plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultDueTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | today = addDays(0)( 22 | setTime( 23 | ...splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ); 28 | 29 | return ts.map(updatedueDate(today)) 30 | }; 31 | 32 | // updatedueDate :: Date -> OFTask -> IO OFTask 33 | const updatedueDate = dte => task => { 34 | return ( 35 | task.dueDate = dte, 36 | task 37 | ) 38 | } 39 | 40 | // FUNCTIONS -- 41 | // https://github.com/RobTrew/prelude-jxa 42 | // JS Basics --------------------------------------------------- 43 | // addDays :: Int -> Date -> Date 44 | const addDays = n => dte => { 45 | const dte2 = new Date(dte); 46 | return ( 47 | dte2.setDate(n + dte.getDate()), 48 | dte2 49 | ); 50 | }; 51 | 52 | // setTime :: (Int, Int, Int) -> Date -> Date 53 | const setTime = (h, m, s) => dte => { 54 | const dte2 = new Date(dte); 55 | return ( 56 | dte2.setHours(h, m, s, 0), 57 | dte2 58 | ); 59 | }; 60 | 61 | // addHours :: Int -> Date -> Date 62 | const addHours = n => dte => { 63 | const dte2 = new Date(dte); 64 | return ( 65 | dte2.setHours(n + dte.getHours()), 66 | dte2 67 | ); 68 | }; 69 | 70 | // addWeeks :: Int -> Date -> Date 71 | const addWeeks = n => addDays(7 * n); 72 | 73 | // addMonths :: Int -> Date -> Date 74 | const addMonths = n => dte => { 75 | const dte2 = new Date(dte); 76 | return ( 77 | dte2.setMonth(n + dte.getMonth()), 78 | dte2 79 | ); 80 | }; 81 | 82 | // JS Prelude -------------------------------------------------- 83 | // Tuple (,) :: a -> b -> (a, b) 84 | const Tuple = a => 85 | b => ({ 86 | type: "Tuple", 87 | "0": a, 88 | "1": b, 89 | length: 2 90 | }); 91 | 92 | // add (+) :: Num a => a -> a -> a 93 | const add = a => 94 | // Curried addition. 95 | b => a + b; 96 | 97 | // all :: (a -> Bool) -> [a] -> Bool 98 | const all = p => 99 | // True if p(x) holds for every x in xs. 100 | xs => [...xs].every(p); 101 | 102 | // any :: (a -> Bool) -> [a] -> Bool 103 | const any = p => 104 | // True if p(x) holds for at least 105 | // one item in xs. 106 | xs => [...xs].some(p); 107 | 108 | 109 | // concat :: [[a]] -> [a] 110 | // concat :: [String] -> String 111 | const concat = xs => 112 | 0 < xs.length ? ( 113 | ( 114 | xs.every(x => "string" === typeof x) ? ( 115 | "" 116 | ) : [] 117 | ).concat(...xs) 118 | ) : xs; 119 | 120 | // eq (==) :: Eq a => a -> a -> Bool 121 | const eq1 = a => 122 | // True when a and b are equivalent in the terms 123 | // defined below for their shared data type. 124 | b => { 125 | const t = typeName(a); 126 | 127 | return t !== typeName(b) ? ( 128 | false 129 | ) : "Dict" !== t ? ( 130 | "Date" !== t ? ( 131 | "function" !== t ? ( 132 | a === b 133 | ) : a.toString() === b.toString() 134 | ) : 0 === (a - b) // Date equality 135 | ) : (() => { 136 | const kvs = Object.entries(a); 137 | 138 | return kvs.length !== Object.keys(b).length ? ( 139 | false 140 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 141 | })(); 142 | }; 143 | 144 | // findIndices :: (a -> Bool) -> [a] -> [Int] 145 | // findIndices :: (String -> Bool) -> String -> [Int] 146 | const findIndices = p => 147 | xs => { 148 | const ys = [...xs]; 149 | 150 | return ys.flatMap( 151 | (y, i) => p(y, i, ys) ? ( 152 | [i] 153 | ) : [] 154 | ); 155 | }; 156 | 157 | // fst :: (a, b) -> a 158 | const fst = tpl => 159 | // First member of a pair. 160 | tpl[0]; 161 | 162 | // keys :: Dict -> [String] 163 | const keys = Object.keys; 164 | 165 | // length :: [a] -> Int 166 | const length = xs => 167 | // Returns Infinity over objects without finite 168 | // length. This enables zip and zipWith to choose 169 | // the shorter argument when one is non-finite, 170 | // like cycle, repeat etc 171 | "GeneratorFunction" !== xs.constructor 172 | .constructor.name ? ( 173 | xs.length 174 | ) : Infinity; 175 | 176 | // map :: (a -> b) -> [a] -> [b] 177 | const map = f => 178 | // The list obtained by applying f 179 | // to each element of xs. 180 | // (The image of xs under f). 181 | xs => [...xs].map(f); 182 | 183 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 184 | const matching = pat => { 185 | // A sequence-matching function for findIndices etc 186 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 187 | // -> [1, 4] 188 | const 189 | lng = pat.length, 190 | bln = 0 < lng, 191 | h = bln ? pat[0] : undefined; 192 | 193 | return x => i => src => 194 | bln && h === x && eq(pat)( 195 | src.slice(i, lng + i) 196 | ); 197 | }; 198 | 199 | // snd :: (a, b) -> b 200 | const snd = tpl => 201 | // Second member of a pair. 202 | tpl[1]; 203 | 204 | // splitOn :: [a] -> [a] -> [[a]] 205 | // splitOn :: String -> String -> [String] 206 | const splitOn = pat => src => 207 | // A list of the strings delimited by 208 | // instances of a given pattern in s. 209 | ("string" === typeof src) ? ( 210 | src.split(pat) 211 | ) : (() => { 212 | const 213 | lng = pat.length, 214 | tpl = findIndices(matching(pat))(src).reduce( 215 | (a, i) => Tuple( 216 | fst(a).concat([src.slice(snd(a), i)]) 217 | )(lng + i), 218 | Tuple([])(0) 219 | ); 220 | 221 | return fst(tpl).concat([src.slice(snd(tpl))]); 222 | })(); 223 | 224 | // typeName :: a -> String 225 | const typeName = v => { 226 | const t = typeof v; 227 | 228 | return null !== v ? ( 229 | "object" === t ? ( 230 | Array.isArray(v) ? ( 231 | "List" 232 | ) : "Date" === v.constructor.name ? ( 233 | "Date" 234 | ) : (() => { 235 | const ct = v.type; 236 | 237 | return Boolean(ct) ? ( 238 | (/Tuple\d+/u).test(ct) ? ( 239 | "TupleN" 240 | ) : ct 241 | ) : "Dict"; 242 | })() 243 | ) : { 244 | "boolean": "Bool", 245 | "date": "Date", 246 | "number": "Num", 247 | "string": "String", 248 | "function": "(a -> b)" 249 | } [t] || "Bottom" 250 | ) : "Bottom"; 251 | }; 252 | 253 | return main() 254 | }; 255 | 256 | return omniJSContext() 257 | }), { 258 | validate: selection => ['projects', 'tasks'].flatMap( 259 | k => Array.from(selection[k]) 260 | ).length > 0 261 | }) 262 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Postpone Due Date/Tomorrow (Due).omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action", 3 | "author": "unlocked2412", 4 | "version": "0.1", 5 | "description": "A single-file OmniFocus plug-in.", 6 | }*/ 7 | // Twitter: @unlocked2412 8 | (() => 9 | Object.assign( 10 | new PlugIn.Action(selection => { 11 | // OMNI JS CODE --------------------------------------- 12 | const omniJSContext = () => { 13 | // main :: IO () 14 | const main = () => { 15 | const 16 | defaultStartTime = settings 17 | .objectForKey('DefaultDueTime'), 18 | ts = ['projects', 'tasks'].flatMap( 19 | k => Array.from(selection[k]) 20 | ), 21 | tomorrow = addDays(1)( 22 | setTime( 23 | ...splitOn(":")(defaultStartTime) 24 | )( 25 | new Date() 26 | ) 27 | ); 28 | 29 | return ts.map(updatedueDate(tomorrow)) 30 | }; 31 | 32 | // updatedueDate :: Date -> OFTask -> IO OFTask 33 | const updatedueDate = dte => task => { 34 | return ( 35 | task.dueDate = dte, 36 | task 37 | ) 38 | } 39 | 40 | // FUNCTIONS -- 41 | // https://github.com/RobTrew/prelude-jxa 42 | // JS Basics --------------------------------------------------- 43 | // addDays :: Int -> Date -> Date 44 | const addDays = n => dte => { 45 | const dte2 = new Date(dte); 46 | return ( 47 | dte2.setDate(n + dte.getDate()), 48 | dte2 49 | ); 50 | }; 51 | 52 | // setTime :: (Int, Int, Int) -> Date -> Date 53 | const setTime = (h, m, s) => dte => { 54 | const dte2 = new Date(dte); 55 | return ( 56 | dte2.setHours(h, m, s, 0), 57 | dte2 58 | ); 59 | }; 60 | 61 | // addHours :: Int -> Date -> Date 62 | const addHours = n => dte => { 63 | const dte2 = new Date(dte); 64 | return ( 65 | dte2.setHours(n + dte.getHours()), 66 | dte2 67 | ); 68 | }; 69 | 70 | // addWeeks :: Int -> Date -> Date 71 | const addWeeks = n => addDays(7 * n); 72 | 73 | // addMonths :: Int -> Date -> Date 74 | const addMonths = n => dte => { 75 | const dte2 = new Date(dte); 76 | return ( 77 | dte2.setMonth(n + dte.getMonth()), 78 | dte2 79 | ); 80 | }; 81 | 82 | // JS Prelude -------------------------------------------------- 83 | // Tuple (,) :: a -> b -> (a, b) 84 | const Tuple = a => 85 | b => ({ 86 | type: "Tuple", 87 | "0": a, 88 | "1": b, 89 | length: 2 90 | }); 91 | 92 | // add (+) :: Num a => a -> a -> a 93 | const add = a => 94 | // Curried addition. 95 | b => a + b; 96 | 97 | // all :: (a -> Bool) -> [a] -> Bool 98 | const all = p => 99 | // True if p(x) holds for every x in xs. 100 | xs => [...xs].every(p); 101 | 102 | // any :: (a -> Bool) -> [a] -> Bool 103 | const any = p => 104 | // True if p(x) holds for at least 105 | // one item in xs. 106 | xs => [...xs].some(p); 107 | 108 | 109 | // concat :: [[a]] -> [a] 110 | // concat :: [String] -> String 111 | const concat = xs => 112 | 0 < xs.length ? ( 113 | ( 114 | xs.every(x => "string" === typeof x) ? ( 115 | "" 116 | ) : [] 117 | ).concat(...xs) 118 | ) : xs; 119 | 120 | // eq (==) :: Eq a => a -> a -> Bool 121 | const eq1 = a => 122 | // True when a and b are equivalent in the terms 123 | // defined below for their shared data type. 124 | b => { 125 | const t = typeName(a); 126 | 127 | return t !== typeName(b) ? ( 128 | false 129 | ) : "Dict" !== t ? ( 130 | "Date" !== t ? ( 131 | "function" !== t ? ( 132 | a === b 133 | ) : a.toString() === b.toString() 134 | ) : 0 === (a - b) // Date equality 135 | ) : (() => { 136 | const kvs = Object.entries(a); 137 | 138 | return kvs.length !== Object.keys(b).length ? ( 139 | false 140 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 141 | })(); 142 | }; 143 | 144 | // findIndices :: (a -> Bool) -> [a] -> [Int] 145 | // findIndices :: (String -> Bool) -> String -> [Int] 146 | const findIndices = p => 147 | xs => { 148 | const ys = [...xs]; 149 | 150 | return ys.flatMap( 151 | (y, i) => p(y, i, ys) ? ( 152 | [i] 153 | ) : [] 154 | ); 155 | }; 156 | 157 | // fst :: (a, b) -> a 158 | const fst = tpl => 159 | // First member of a pair. 160 | tpl[0]; 161 | 162 | // keys :: Dict -> [String] 163 | const keys = Object.keys; 164 | 165 | // length :: [a] -> Int 166 | const length = xs => 167 | // Returns Infinity over objects without finite 168 | // length. This enables zip and zipWith to choose 169 | // the shorter argument when one is non-finite, 170 | // like cycle, repeat etc 171 | "GeneratorFunction" !== xs.constructor 172 | .constructor.name ? ( 173 | xs.length 174 | ) : Infinity; 175 | 176 | // map :: (a -> b) -> [a] -> [b] 177 | const map = f => 178 | // The list obtained by applying f 179 | // to each element of xs. 180 | // (The image of xs under f). 181 | xs => [...xs].map(f); 182 | 183 | // matching :: [a] -> (a -> Int -> [a] -> Bool) 184 | const matching = pat => { 185 | // A sequence-matching function for findIndices etc 186 | // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) 187 | // -> [1, 4] 188 | const 189 | lng = pat.length, 190 | bln = 0 < lng, 191 | h = bln ? pat[0] : undefined; 192 | 193 | return x => i => src => 194 | bln && h === x && eq(pat)( 195 | src.slice(i, lng + i) 196 | ); 197 | }; 198 | 199 | // snd :: (a, b) -> b 200 | const snd = tpl => 201 | // Second member of a pair. 202 | tpl[1]; 203 | 204 | // splitOn :: [a] -> [a] -> [[a]] 205 | // splitOn :: String -> String -> [String] 206 | const splitOn = pat => src => 207 | // A list of the strings delimited by 208 | // instances of a given pattern in s. 209 | ("string" === typeof src) ? ( 210 | src.split(pat) 211 | ) : (() => { 212 | const 213 | lng = pat.length, 214 | tpl = findIndices(matching(pat))(src).reduce( 215 | (a, i) => Tuple( 216 | fst(a).concat([src.slice(snd(a), i)]) 217 | )(lng + i), 218 | Tuple([])(0) 219 | ); 220 | 221 | return fst(tpl).concat([src.slice(snd(tpl))]); 222 | })(); 223 | 224 | // typeName :: a -> String 225 | const typeName = v => { 226 | const t = typeof v; 227 | 228 | return null !== v ? ( 229 | "object" === t ? ( 230 | Array.isArray(v) ? ( 231 | "List" 232 | ) : "Date" === v.constructor.name ? ( 233 | "Date" 234 | ) : (() => { 235 | const ct = v.type; 236 | 237 | return Boolean(ct) ? ( 238 | (/Tuple\d+/u).test(ct) ? ( 239 | "TupleN" 240 | ) : ct 241 | ) : "Dict"; 242 | })() 243 | ) : { 244 | "boolean": "Bool", 245 | "date": "Date", 246 | "number": "Num", 247 | "string": "String", 248 | "function": "(a -> b)" 249 | } [t] || "Bottom" 250 | ) : "Bottom"; 251 | }; 252 | 253 | return main() 254 | }; 255 | 256 | return omniJSContext() 257 | }), { 258 | validate: selection => ['projects', 'tasks'].flatMap( 259 | k => Array.from(selection[k]) 260 | ).length > 0 261 | }) 262 | )(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Sharing/Share Tasks With Specific Tag.omnijs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action" 3 | }*/ 4 | // Twitter: @unlocked2412 5 | (() => Object.assign( 6 | new PlugIn.Action(selection => { 7 | 8 | // USER OPTIONS -------------------------------------- 9 | const options = { 10 | tag: 'Communications' 11 | }; 12 | 13 | // OMNI JS CODE --------------------------------------- 14 | const omniJSContext = () => { 15 | // main :: IO () 16 | const main = () => { 17 | const 18 | tagName = options.tag, 19 | tpText = ofTaskPaperFromTree( 20 | fmapTree( 21 | either(dictFromLeft)( 22 | dictFromRight 23 | ) 24 | )( 25 | filteredTree( 26 | either(constant(true))( 27 | x => elem(tagName)( 28 | x.tags.map(x => x.name) 29 | ) 30 | ) 31 | )( 32 | pureOFdbLR(library) 33 | ) 34 | ) 35 | ) 36 | return new SharePanel([tpText]).show() 37 | }; 38 | 39 | // OMNIFOCUS FUNCTIONS ------------------------------ 40 | const dictFromLeft = x => ({ 41 | text: x 42 | }) 43 | 44 | const dictFromRight = x => { 45 | const v = 'Project' !== x.constructor.name ? ( 46 | x 47 | ) : x.task 48 | return ({ 49 | text: v.name, 50 | note: v.note, 51 | tags: v.tags.map(x => x.name) 52 | }) 53 | } 54 | 55 | // pureOFdbLR :: OF Item -> Tree Either String OF Item 56 | const pureOFdbLR = item => { 57 | const go = x => { 58 | const k = x.constructor.name; 59 | return ['Project', 'Task'].includes(k) ? ( 60 | fmapPureOF(Right)(x) 61 | ) : 'Folder' !== k ? ( 62 | Node(Left(k))(( 63 | 'Database' !== k ? ( 64 | x 65 | ) : [x.inbox, x.library] 66 | ).map(go)) 67 | ) : Node(Left('Folder: ' + x.name))( 68 | x.children.map(go) 69 | ); 70 | }; 71 | return go(item); 72 | }; 73 | 74 | // fmapPureOF :: (OF Item -> a) -> OF Item -> Tree a 75 | const fmapPureOF = f => item => { 76 | const go = x => Node(f(x))(( 77 | 'Project' !== x.constructor.name ? ( 78 | x 79 | ) : x.task 80 | ).children.map(go)); 81 | return go(item); 82 | }; 83 | 84 | // ofTaskPaperFromTree :: Tree -> String 85 | const ofTaskPaperFromTree = x => { 86 | const 87 | rgxSpace = /\s+/g, 88 | go = strIndent => x => { 89 | const 90 | nest = x.nest, 91 | root = x.root, 92 | txt = root.text || '', 93 | tags = root.tags, 94 | ks = Boolean(tags) ? ( 95 | Object.keys(tags) 96 | ) : [], 97 | note = root.note, 98 | blnNotes = Boolean(note), 99 | blnTags = ks.length > 0, 100 | blnNest = nest.length > 0, 101 | strNext = '\t' + strIndent; 102 | 103 | return or([Boolean(txt), blnTags, blnNotes, blnNest]) ? ( 104 | strIndent + (root.type !== 'project' ? ( 105 | '- ' + txt 106 | ) : txt + ':') + 107 | ( 108 | blnTags ? ` @tags(${ 109 | intercalateS(',')(tags) 110 | })` : '' 111 | ) + 112 | (blnNotes ? ( 113 | '\n' + unlines(map( 114 | s => strNext + s 115 | )(lines(note))) 116 | ) : '') + (blnNest ? ( 117 | '\n' + unlines(map(go(strNext))(nest)) 118 | ) : '') 119 | ) : ''; 120 | }; 121 | return go('')(x); 122 | }; 123 | 124 | // GENERIC FUNCTIONS -------------------------------------------- 125 | // https://github.com/RobTrew/prelude-jxa 126 | // Left :: a -> Either a b 127 | const Left = x => ({ 128 | type: 'Either', 129 | Left: x 130 | }); 131 | 132 | // Node :: a -> [Tree a] -> Tree a 133 | const Node = v => 134 | // Constructor for a Tree node which connects a 135 | // value of some kind to a list of zero or 136 | // more child trees. 137 | xs => ({ 138 | type: 'Node', 139 | root: v, 140 | nest: xs || [] 141 | }); 142 | 143 | // Right :: b -> Either a b 144 | const Right = x => ({ 145 | type: 'Either', 146 | Right: x 147 | }); 148 | 149 | // constant :: a -> b -> a 150 | const constant = k => 151 | _ => k; 152 | 153 | // either :: (a -> c) -> (b -> c) -> Either a b -> c 154 | const either = fl => 155 | fr => e => 'Either' === e.type ? ( 156 | undefined !== e.Left ? ( 157 | fl(e.Left) 158 | ) : fr(e.Right) 159 | ) : undefined; 160 | 161 | // elem :: Eq a => a -> [a] -> Bool 162 | // elem :: Char -> String -> Bool 163 | const elem = x => 164 | xs => { 165 | const t = xs.constructor.name; 166 | return 'Array' !== t ? ( 167 | xs['Set' !== t ? 'includes' : 'has'](x) 168 | ) : xs.some(eq(x)); 169 | }; 170 | 171 | // eq (==) :: Eq a => a -> a -> Bool 172 | const eq = a => 173 | // True when a and b are equivalent in the terms 174 | // defined below for their shared data type. 175 | b => { 176 | const t = typeof a; 177 | return t !== typeof b ? ( 178 | false 179 | ) : 'object' !== t ? ( 180 | 'function' !== t ? ( 181 | a === b 182 | ) : a.toString() === b.toString() 183 | ) : (() => { 184 | const kvs = Object.entries(a); 185 | return kvs.length !== Object.keys(b).length ? ( 186 | false 187 | ) : kvs.every(([k, v]) => eq(v)(b[k])); 188 | })(); 189 | }; 190 | 191 | // filteredTree (a -> Bool) -> Tree a -> Tree a 192 | const filteredTree = p => 193 | // A tree including only those children 194 | // which either match the predicate p, or have 195 | // descendants which match the predicate p. 196 | foldTree(x => xs => 197 | Node(x)(xs.filter( 198 | tree => (0 < tree.nest.length) || ( 199 | p(tree.root) 200 | ) 201 | )) 202 | ); 203 | 204 | // fmapTree :: (a -> b) -> Tree a -> Tree b 205 | const fmapTree = f => { 206 | // A new tree. The result of a structure-preserving 207 | // application of f to each root in the existing tree. 208 | const go = tree => Node(f(tree.root))( 209 | tree.nest.map(go) 210 | ); 211 | return go; 212 | }; 213 | 214 | // foldTree :: (a -> [b] -> b) -> Tree a -> b 215 | const foldTree = f => { 216 | // The catamorphism on trees. A summary 217 | // value obtained by a depth-first fold. 218 | const go = tree => f(tree.root)( 219 | tree.nest.map(go) 220 | ); 221 | return go; 222 | }; 223 | 224 | // intercalateS :: String -> [String] -> String 225 | const intercalateS = s => 226 | // The concatenation of xs 227 | // interspersed with copies of s. 228 | xs => xs.join(s); 229 | 230 | // lines :: String -> [String] 231 | const lines = s => 232 | // A list of strings derived from a single 233 | // newline-delimited string. 234 | 0 < s.length ? ( 235 | s.split(/[\r\n]/) 236 | ) : []; 237 | 238 | // map :: (a -> b) -> [a] -> [b] 239 | const map = f => 240 | // The list obtained by applying f 241 | // to each element of xs. 242 | // (The image of xs under f). 243 | xs => ( 244 | Array.isArray(xs) ? ( 245 | xs 246 | ) : xs.split('') 247 | ).map(f); 248 | 249 | // or :: [Bool] -> Bool 250 | const or = xs => 251 | xs.some(Boolean); 252 | 253 | // unlines :: [String] -> String 254 | const unlines = xs => 255 | // A single string formed by the intercalation 256 | // of a list of strings with the newline character. 257 | xs.join('\n'); 258 | 259 | // MAIN ----------------------------------------- 260 | return main() 261 | }; 262 | 263 | return omniJSContext() 264 | 265 | }), { 266 | validate: selection => true 267 | } 268 | ))(); -------------------------------------------------------------------------------- /OmniFocus Plug-Ins/Statistics/Total of estimated minutes.omnifocusjs: -------------------------------------------------------------------------------- 1 | /*{ 2 | "type": "action" 3 | }*/ 4 | // Twitter: @unlocked2412 5 | (() => 6 | Object.assign( 7 | new PlugIn.Action(selection => { 8 | // OMNI JS CODE --------------------------------------- 9 | const omniJSContext = () => { 10 | // main :: IO () 11 | const main = () => { 12 | const 13 | //selection = document.windows[0].selection, 14 | selectedObjects = ["projects", "tasks"].flatMap( 15 | k => Array.from(selection[k]) 16 | ), 17 | tree = Node(0)( 18 | map( 19 | fmapPureOF( 20 | x => x.estimatedMinutes || 0 21 | ) 22 | )(commonAncestors(selectedObjects)) 23 | ), 24 | totalEstimatedMinutes = foldTree( 25 | x => xs => x + sum(xs) 26 | )(tree) 27 | return ( 28 | new Alert( 29 | 'Total of estimated minutes', 30 | `${minutesAsHHMM(totalEstimatedMinutes)}\n\n` + ( 31 | 'in descendants of selected project(s) or task(s).' 32 | ) 33 | ) 34 | ).show(); 35 | }; 36 | 37 | 38 | // FUNCTIONS -- 39 | // https://github.com/RobTrew/prelude-jxa 40 | // JS Prelude -------------------------------------------------- 41 | // Node :: a -> [Tree a] -> Tree a 42 | const Node = v => 43 | // Constructor for a Tree node which connects a 44 | // value of some kind to a list of zero or 45 | // more child trees. 46 | xs => ({ 47 | type: 'Node', 48 | root: v, 49 | nest: xs || [] 50 | }); 51 | 52 | // Tuple (,) :: a -> b -> (a, b) 53 | const Tuple = a => 54 | // A pair of values, possibly of 55 | // different types. 56 | b => ({ 57 | type: "Tuple", 58 | "0": a, 59 | "1": b, 60 | length: 2, 61 | *[Symbol.iterator]() { 62 | for (const k in this) { 63 | if (!isNaN(k)) { 64 | yield this[k]; 65 | } 66 | } 67 | } 68 | }); 69 | 70 | // concat :: [[a]] -> [a] 71 | // concat :: [String] -> String 72 | const concat = xs => ( 73 | ys => 0 < ys.length ? ( 74 | ys.every(Array.isArray) ? ( 75 | [] 76 | ) : '' 77 | ).concat(...ys) : ys 78 | )(list(xs)); 79 | 80 | // divMod :: Int -> Int -> (Int, Int) 81 | const divMod = n => d => { 82 | // Integer division, truncated toward negative infinity, 83 | // and integer modulus such that: 84 | // (x `div` y)*y + (x `mod` y) == x 85 | const [q, r] = [Math.trunc(n / d), n % d]; 86 | 87 | return signum(n) === signum(-d) 88 | ? Tuple(q - 1)(r + d) 89 | : Tuple(q)(r); 90 | }; 91 | 92 | 93 | // filter :: (a -> Bool) -> [a] -> [a] 94 | const filter = p => 95 | // The elements of xs which match 96 | // the predicate p. 97 | xs => [...xs].filter(p); 98 | 99 | // foldTree :: (a -> [b] -> b) -> Tree a -> b 100 | const foldTree = f => { 101 | // The catamorphism on trees. A summary 102 | // value obtained by a depth-first fold. 103 | const go = tree => f(tree.root)( 104 | tree.nest.map(go) 105 | ); 106 | return go; 107 | }; 108 | 109 | // length :: [a] -> Int 110 | const length = xs => 111 | // Returns Infinity over objects without finite 112 | // length. This enables zip and zipWith to choose 113 | // the shorter argument when one is non-finite, 114 | // like cycle, repeat etc 115 | 'GeneratorFunction' !== xs.constructor 116 | .constructor.name ? ( 117 | xs.length 118 | ) : Infinity; 119 | 120 | // list :: StringOrArrayLike b => b -> [a] 121 | const list = xs => 122 | // xs itself, if it is an Array, 123 | // or an Array derived from xs. 124 | Array.isArray(xs) ? ( 125 | xs 126 | ) : Array.from(xs || []); 127 | 128 | // map :: (a -> b) -> [a] -> [b] 129 | const map = f => 130 | // The list obtained by applying f 131 | // to each element of xs. 132 | // (The image of xs under f). 133 | xs => [...xs].map(f); 134 | 135 | // nest :: Tree a -> [a] 136 | const nest = tree => { 137 | // Allowing for lazy (on-demand) evaluation. 138 | // If the nest turns out to be a function – 139 | // rather than a list – that function is applied 140 | // here to the root, and returns a list. 141 | const xs = tree.nest; 142 | return 'function' !== typeof xs ? ( 143 | xs 144 | ) : xs(root(x)); 145 | }; 146 | 147 | // root :: Tree a -> a 148 | const root = tree => 149 | tree.root; 150 | 151 | // signum :: Num -> Num 152 | const signum = n => 153 | // Sign of a number. 154 | n.constructor( 155 | 0 > n 156 | ? -1 157 | : 0 < n 158 | ? 1 159 | : 0 160 | ); 161 | 162 | // sum :: [Num] -> Num 163 | const sum = xs => 164 | // The numeric sum of all values in xs. 165 | xs.reduce((a, x) => a + x, 0); 166 | 167 | // until :: (a -> Bool) -> (a -> a) -> a -> a 168 | const until = p => 169 | f => x => { 170 | let v = x; 171 | while (!p(v)) v = f(v); 172 | return v; 173 | }; 174 | 175 | // OmniJS OmniFocus -------------------------------------------- 176 | // commonAncestors :: [OFItem] -> [OFItem] 177 | const commonAncestors = items => ( 178 | // Only items which do not descend 179 | // from other items in the list. 180 | itemSet => items.filter(x => 181 | !until( 182 | p => !p || itemSet.has(p) 183 | )( 184 | v => v.parent 185 | )(x.parent) 186 | ) 187 | )(new Set(items)) 188 | 189 | // fmapPureOF :: (OF Item -> a) -> OF Item -> Tree a 190 | const fmapPureOF = f => item => { 191 | const go = x => { 192 | const v = 'Project' !== x.constructor.name ? ( 193 | x 194 | ) : x.task; 195 | return Node(f(v))(( 196 | v 197 | ).children.map(go)); 198 | } 199 | return go(item); 200 | }; 201 | 202 | // minutesAsHHMM :: Int -> String 203 | const minutesAsHHMM = mins => { 204 | const 205 | pair = divMod(mins)(60), 206 | hours = pair[0], 207 | minutes = pair[1]; 208 | return 0 === hours ? ( 209 | `${minutes}m` 210 | ) : 0 === minutes ? ( 211 | `${hours}h` 212 | ) : `${hours}h${minutes}m` 213 | }; 214 | 215 | return main() 216 | }; 217 | 218 | return omniJSContext() 219 | }), { 220 | validate: selection => ["projects", "tasks"].some( 221 | k => selection[k].length > 0 222 | ) 223 | }) 224 | )(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Omni-Automation 2 | A repository with plug-ins and scripts for OmniFocus and OmniOutliner using the new Omni Automation scripting API. 3 | 4 | ## Installation of Plug-Ins 5 | 6 | - Click on the green `Code` button and then `Download ZIP`. 7 | - Double click the desired **.omnifocusjs** or **.omnioutlinerjs** file. 8 | - Select (drop-down menu) `OmniFocus/OmniOutliner in iCloud Drive` location. 9 | 10 | ## Donations 11 | 12 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/U7U74O49E) 13 | 14 | Please, consider making a donation if you find these scripts useful. 15 | 16 | ### Notes: 17 | 18 | Plug-Ins for OmniOutliner can have `.omnijs` or `.omnioutlinerjs` extension; the ones for OmniFocus, `.omnijs` or `.omnifocusjs` extension. 19 | 20 | #### If you do not want to store your Plug-Ins in OmniFocus iCloud Drive folder: 21 | 22 | - Click `Automation > Configure...` menu item. 23 | - Click `Add Linked Folder...` in the recently opened dialog. 24 | - After double clicking Plug-In file, choose the recently linked folder. 25 | --------------------------------------------------------------------------------