├── .gitignore ├── LICENSE ├── README.org ├── images ├── creation_date.png ├── date_replacement-01.png ├── date_replacement-02.png ├── debugging.png ├── note-capture.png ├── note-notification.png ├── result.png ├── supported_notificaitons.png ├── todo-capture.png ├── todo-notification.png ├── user_configuration.png └── workflow.png ├── org-mode-capture.alfredworkflow └── src ├── icons ├── icon-a.png ├── icon-a.psd └── icon-b.png ├── info.plist ├── py27 └── orgmode_entry.py ├── py38 ├── org_mode_capture_run.py └── org_mode_entry.py ├── test.py └── test_inbox.org /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alexander Gogl 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 | 23 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | # An org-mode-capture workflow for Alfred 2 | 3 | Append a todo or a note to an org-mode file with a short and convenient command in [[https://www.alfredapp.com/][Alfred]]. The workflow requires Alfred's "Powerpack." 4 | 5 | * Features 6 | 7 | The command appends a second level heading to a user defined .org file and puts all what follows ~::~ into the body of the heading (see figures below). 8 | 9 | Type todo to add a todo: 10 | 11 | #+caption: Capture a todo 12 | [[file:images/todo-capture.png]] 13 | 14 | #+caption: Capture a todo 15 | [[file:images/todo-notification.png]] 16 | 17 | Type note to add a note: 18 | 19 | #+caption: Capture a note 20 | [[file:images/note-capture.png]] 21 | 22 | #+caption: Capture a note 23 | [[file:images/note-notification.png]] 24 | 25 | The added notes and todos are divided into title and content: 26 | 27 | #+caption: Capture a note 28 | [[file:images/result.png]] 29 | 30 | Relative dates (Monday, tuesday, tomorrow, morgen, freitag) in the content part of the entry are converted into orgmode specific date formats ~<2015-09-11 Fri>~. 31 | 32 | #+caption: Relative dates in Alfred 33 | [[file:images/date_replacement-01.png]] 34 | 35 | #+caption: become orgmode dates 36 | [[file:images/date_replacement-02.png]] 37 | 38 | You can also use relative dates to add a SCHEDULE or DEADLINE by using the following syntax, where ~S:~ converts the following date to a SCHEDULE date, and ~DL:~ to a DEADLINE. Note: the conversion only works if the pattern (S: or DL:) is followed by a date without a space between the pattern and the date. 39 | 40 | ~~~ 41 | todo Title of the workflow:: S:tomorrow DL:monday 42 | ~~~ 43 | 44 | By default, the date of creation is added to a property car (you can disable it inside Alfred; see Installation below): 45 | 46 | #+caption: Date of creation 47 | [[file:images/creation_date.png]] 48 | 49 | * Installation and customising variables 50 | 51 | Double klick on ~org-mode-capture.alfredworkflow~ to add it to Alfred's set of workflows. Then you need to set up the workflow by customising the workflow variables with Alfred's ~Configure Workflow...~ command (see figure below). It is obligatory to set at least the path to your inbox.org files. The non-obligatory variables have sane defaults, but can be customised by your liking: if you prefer ~--~ as a title-content separator, then you can change it as well. 52 | 53 | #+caption: Configure Workflow 54 | [[file:images/user_configuration.png]] 55 | 56 | * Reporting bugs 57 | 58 | If you encounter a bug, please enable Alfred's debugging mode and post the error message. 59 | 60 | #+caption: Alfred debugger 61 | [[file:images/debugging.png]] 62 | -------------------------------------------------------------------------------- /images/creation_date.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/creation_date.png -------------------------------------------------------------------------------- /images/date_replacement-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/date_replacement-01.png -------------------------------------------------------------------------------- /images/date_replacement-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/date_replacement-02.png -------------------------------------------------------------------------------- /images/debugging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/debugging.png -------------------------------------------------------------------------------- /images/note-capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/note-capture.png -------------------------------------------------------------------------------- /images/note-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/note-notification.png -------------------------------------------------------------------------------- /images/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/result.png -------------------------------------------------------------------------------- /images/supported_notificaitons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/supported_notificaitons.png -------------------------------------------------------------------------------- /images/todo-capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/todo-capture.png -------------------------------------------------------------------------------- /images/todo-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/todo-notification.png -------------------------------------------------------------------------------- /images/user_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/user_configuration.png -------------------------------------------------------------------------------- /images/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/images/workflow.png -------------------------------------------------------------------------------- /org-mode-capture.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/org-mode-capture.alfredworkflow -------------------------------------------------------------------------------- /src/icons/icon-a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/src/icons/icon-a.png -------------------------------------------------------------------------------- /src/icons/icon-a.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/src/icons/icon-a.psd -------------------------------------------------------------------------------- /src/icons/icon-b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandergogl/alfred-org-mode-workflow/f1f69ee1f97f3d2ad372e74e489ec092a58b0c74/src/icons/icon-b.png -------------------------------------------------------------------------------- /src/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | ago.alfred-todo-to-orgmode 7 | connections 8 | 9 | 0CBAD592-FA38-441B-977C-7CF0489F909B 10 | 11 | 12 | destinationuid 13 | ED716DC1-030F-4216-875E-A795905D6B18 14 | modifiers 15 | 0 16 | modifiersubtext 17 | 18 | 19 | 20 | 2C99F6F1-EF16-4CF1-9762-5D05A1FFAA4D 21 | 22 | 23 | destinationuid 24 | 0CBAD592-FA38-441B-977C-7CF0489F909B 25 | modifiers 26 | 0 27 | modifiersubtext 28 | 29 | 30 | 31 | BDE1908D-A596-4E64-8EC9-E0B45830589C 32 | 33 | 34 | destinationuid 35 | 13734DD2-23C2-489A-9D4B-8638EFF5CEFB 36 | modifiers 37 | 0 38 | modifiersubtext 39 | 40 | 41 | 42 | CE3708AA-1D2C-4DA9-8AE8-7AB93F64FC30 43 | 44 | 45 | destinationuid 46 | BDE1908D-A596-4E64-8EC9-E0B45830589C 47 | modifiers 48 | 0 49 | modifiersubtext 50 | 51 | 52 | 53 | 54 | createdby 55 | Alexander Gogl 56 | description 57 | Create orgmode todos or headlines from todo and note strings. 58 | disabled 59 | 60 | name 61 | org-mode-capture 62 | objects 63 | 64 | 65 | config 66 | 67 | lastpathcomponent 68 | 69 | onlyshowifquerypopulated 70 | 71 | output 72 | 1 73 | removeextension 74 | 75 | sticky 76 | 77 | text 78 | {query} 79 | title 80 | TODO 81 | 82 | type 83 | alfred.workflow.output.notification 84 | uid 85 | ED716DC1-030F-4216-875E-A795905D6B18 86 | version 87 | 0 88 | 89 | 90 | config 91 | 92 | concurrently 93 | 94 | escaping 95 | 68 96 | script 97 | from orgmode_entry import OrgmodeEntry 98 | 99 | entry = """{query}""" 100 | org = OrgmodeEntry() 101 | # Use an absolute path 102 | org.inbox_file = '/Users/Alex/Documents/Planung/Planning/Inbox.org' 103 | org.delimiter = ':: ' 104 | org.add_creation_date = True # enable to add a creation date 105 | org.replace_relative_dates = True # enable to convert relative dates into orgmode dates 106 | 107 | entry = 'TODO ' + entry 108 | 109 | message = org.add_entry(entry) 110 | 111 | print(message) 112 | type 113 | 3 114 | 115 | type 116 | alfred.workflow.action.script 117 | uid 118 | 0CBAD592-FA38-441B-977C-7CF0489F909B 119 | version 120 | 0 121 | 122 | 123 | config 124 | 125 | argumenttype 126 | 0 127 | keyword 128 | todo 129 | subtext 130 | Add a Todo to org's Inbox 131 | text 132 | Todo 133 | withspace 134 | 135 | 136 | type 137 | alfred.workflow.input.keyword 138 | uid 139 | 2C99F6F1-EF16-4CF1-9762-5D05A1FFAA4D 140 | version 141 | 0 142 | 143 | 144 | config 145 | 146 | lastpathcomponent 147 | 148 | onlyshowifquerypopulated 149 | 150 | output 151 | 1 152 | removeextension 153 | 154 | sticky 155 | 156 | text 157 | {query} 158 | title 159 | Note 160 | 161 | type 162 | alfred.workflow.output.notification 163 | uid 164 | 13734DD2-23C2-489A-9D4B-8638EFF5CEFB 165 | version 166 | 0 167 | 168 | 169 | config 170 | 171 | concurrently 172 | 173 | escaping 174 | 68 175 | script 176 | from orgmode_entry import OrgmodeEntry 177 | 178 | entry = """{query}""" 179 | org = OrgmodeEntry() 180 | # Use an absolute path 181 | org.inbox_file = '/Users/Alex/Documents/Planung/Planning/Inbox.org' 182 | org.delimiter = ':: ' 183 | org.add_creation_date = True # enable to add a creation date 184 | org.replace_relative_dates = True # enable to convert relative dates into orgmode dates 185 | 186 | message = org.add_entry(entry) 187 | 188 | print(message) 189 | 190 | type 191 | 3 192 | 193 | type 194 | alfred.workflow.action.script 195 | uid 196 | BDE1908D-A596-4E64-8EC9-E0B45830589C 197 | version 198 | 0 199 | 200 | 201 | config 202 | 203 | argumenttype 204 | 0 205 | keyword 206 | note 207 | subtext 208 | Add a Note to org's Inbox 209 | text 210 | Note 211 | withspace 212 | 213 | 214 | type 215 | alfred.workflow.input.keyword 216 | uid 217 | CE3708AA-1D2C-4DA9-8AE8-7AB93F64FC30 218 | version 219 | 0 220 | 221 | 222 | readme 223 | See https://github.com/alexandergogl/alfred-org-mode-workflow. 224 | uidata 225 | 226 | 0CBAD592-FA38-441B-977C-7CF0489F909B 227 | 228 | ypos 229 | 40 230 | 231 | 13734DD2-23C2-489A-9D4B-8638EFF5CEFB 232 | 233 | ypos 234 | 130 235 | 236 | 2C99F6F1-EF16-4CF1-9762-5D05A1FFAA4D 237 | 238 | ypos 239 | 60 240 | 241 | BDE1908D-A596-4E64-8EC9-E0B45830589C 242 | 243 | ypos 244 | 160 245 | 246 | CE3708AA-1D2C-4DA9-8AE8-7AB93F64FC30 247 | 248 | ypos 249 | 180 250 | 251 | ED716DC1-030F-4216-875E-A795905D6B18 252 | 253 | ypos 254 | 10 255 | 256 | 257 | webaddress 258 | http://issuu.com/alex.gogl 259 | 260 | 261 | -------------------------------------------------------------------------------- /src/py27/orgmode_entry.py: -------------------------------------------------------------------------------- 1 | # python version 2.7 2 | import datetime 3 | import re 4 | # UTF-8 encoding 5 | import codecs 6 | import unicodedata 7 | 8 | 9 | class OrgmodeEntry(object): 10 | """Convert a generic text into an org-mode heading with an optional body and add it to an orgmode file. 11 | 12 | Supported modes: 13 | - convert relative dates like Thuesday, tomorrow, morgen and montag into org-mode dates 14 | - add the date of creation to the heading 15 | """ 16 | def __init__(self): 17 | self.inbox_file = "/Users/Alex/Desktop/inbox.org" 18 | self.delimiter = ":: " 19 | 20 | # Depth of heading 21 | self.heading_suffix = "\n* " 22 | 23 | # Smart line breaks: add line breaks with a substitude; ie. " " (two spaces) 24 | self.smart_line_break = True 25 | self.line_break_pattern = "\s\s" 26 | self.line_break_char = "\n" 27 | 28 | # Cleanup spaces (double, leading, and trailing) 29 | self.cleanup_spaces = True 30 | 31 | # Add priority tags to entry 32 | self.use_priority_tags = True 33 | self.priority_tag = '#' # tag that marks a priority value: #B => [#B] 34 | 35 | # Add a task created date to entry 36 | self.add_creation_date = True # add a creation date to the entry 37 | self.creation_date_format = ":PROPERTIES:\n:CREATED: [%s-%s-%s %s]\n:END:" 38 | 39 | # Replace absolute dates like 01.10 15:00 => <2016-10-01 Sun 15:00> 40 | self.replace_absolute_dates = True 41 | 42 | # Replace relative dates 43 | self.replace_relative_dates = True 44 | self.date_format = "<%s-%s-%s %s>" 45 | self.date_format_regex = "<\d{4}-\d{2}-\d{2}\s[A-Z][a-z]{2}>" 46 | self.weekdays = { 47 | "montag": 0, 48 | "monday": 0, 49 | "dienstag": 1, 50 | "tuesday": 1, 51 | "mittwoch": 2, 52 | "wednesday": 2, 53 | "donnerstag": 3, 54 | "thursday": 3, 55 | "freitag": 4, 56 | "friday": 4, 57 | "samstag": 5, 58 | "saturday": 5, 59 | "sonntag": 6, 60 | "sunday": 6 61 | } 62 | self.relative_dates = { 63 | "heute": 0, 64 | "today": 0, 65 | "morgen": 1, 66 | "tomorrow": 1 67 | } 68 | self.convenience_dates = [] 69 | for month in range(1, 13): 70 | for day in range(1, 32): 71 | date = "%s.%s" % (day, month) 72 | self.convenience_dates.append(date) 73 | 74 | # Schedule and deadline keywords 75 | self.convert_deadlines = True 76 | self.deadline_pattern = "DL: " 77 | self.deadline_keyword = "DEADLINE: " 78 | 79 | self.convert_scheduled = True 80 | self.scheduled_pattern = "S: " 81 | self.scheduled_keyword = "SCHEDULED: " 82 | 83 | # Message handling 84 | self.message_format = [ 85 | "Added '%s' to %s.", # input without body 86 | "Added '%s\n%s' to %s." # input with heading and body 87 | ] 88 | 89 | def encode(self, string): 90 | """Encode the input string into unicode.""" 91 | if not isinstance(string, unicode): 92 | string = unicode(string, "utf-8") 93 | string = unicodedata.normalize('NFC', string) 94 | return string 95 | 96 | def add_entry(self, string): 97 | string = self.encode(string) 98 | entry = self.format_entry(string) 99 | self.write_to_file(entry) 100 | message = self.create_message() 101 | return message 102 | 103 | def write_to_file(self, string): 104 | with codecs.open(self.inbox_file, "a", encoding='utf-8') as myfile: 105 | myfile.write(string) 106 | pass 107 | 108 | def format_entry(self, string): 109 | items = self.split_string(string) 110 | deadline, scheduled = None, None 111 | 112 | # Format body 113 | if len(items) == 1: 114 | # String has no body 115 | body = "" 116 | self.body = None 117 | else: 118 | # String has a body 119 | body = items[1] 120 | 121 | if self.replace_absolute_dates is True: 122 | # Replace absolute dates 123 | body = self.convert_absolute_date(body) 124 | 125 | if self.replace_relative_dates is True: 126 | # Replace relative dates 127 | body = self.replace_date(body) 128 | 129 | if self.smart_line_break is True: 130 | # convert line breaks 131 | body = self.convert_line_breaks(body) 132 | 133 | if self.convert_deadlines is True: 134 | # Replace deadlines 135 | deadline, body = self.get_deadline_date(body) 136 | 137 | if self.convert_scheduled is True: 138 | # Replace deadlines 139 | scheduled, body = self.get_scheduled_date(body) 140 | 141 | if self.cleanup_spaces is True: 142 | body = self.remove_double_spaces(body) 143 | body = self.remove_leading_trailling_spaces(body) 144 | 145 | self.body = body 146 | 147 | # Format heading 148 | heading = items[0] 149 | # Priority 150 | if self.use_priority_tags is True: 151 | # Search heading string for priority tag and add an orgmode 152 | # priority tag to the heading 153 | heading = self.add_priority(heading) 154 | 155 | self.heading = heading 156 | heading = self.heading_suffix + heading 157 | 158 | # Format entry 159 | entry = heading 160 | if deadline is not None: 161 | entry += '\n%s' % deadline 162 | if scheduled is not None: 163 | if deadline is not None: 164 | entry += ' ' 165 | else: 166 | entry += '\n' 167 | entry += scheduled 168 | if self.add_creation_date is True: 169 | entry += '\n%s' % self.get_creation_date() 170 | entry += '\n%s' % body 171 | 172 | return entry 173 | 174 | def split_string(self, string): 175 | return string.split(self.delimiter) 176 | 177 | def replace_date(self, string): 178 | dict_keys = '|'.join(self.weekdays.keys()) + "|" + '|'.join(self.relative_dates.keys()) 179 | expression = r'\b(' + dict_keys + r')\b' 180 | pattern = re.compile(expression, re.IGNORECASE) 181 | string = pattern.sub(lambda x: self.convert_date(x.group()), string) 182 | return string 183 | 184 | def convert_date(self, string): 185 | today = datetime.datetime.now() 186 | delta = self.convert_relative_date(string) 187 | date = today + datetime.timedelta(days=delta) 188 | date = self.format_date(date, self.date_format) 189 | return date 190 | 191 | def convert_relative_date(self, string): 192 | # string dictionaries with delta day value 193 | string = string.lower() 194 | relative_dates = self.relative_dates 195 | weekdays = self.weekdays 196 | convenience_dates = self.convenience_dates 197 | 198 | if string in relative_dates: 199 | delta = relative_dates[string] 200 | elif string in weekdays: 201 | today = datetime.datetime.today() 202 | current = today.weekday() 203 | weekday = weekdays[string] 204 | if current == weekday: 205 | delta = 7 206 | elif weekday < current: 207 | delta = 7 - (current - weekday) 208 | else: 209 | delta = weekday - current 210 | # TODO: Extend replace date function 211 | elif string in convenience_dates: 212 | item = string.split(".") 213 | day = item[0] 214 | month = item[1] 215 | print(day, month) 216 | delta = False 217 | else: 218 | delta = False 219 | return delta 220 | 221 | def format_date(self, date, date_format): 222 | year = date.strftime("%Y") 223 | month = date.strftime("%m") 224 | day = date.strftime("%d") 225 | weekday = date.strftime("%a") 226 | 227 | date = date_format % (year, month, day, weekday) 228 | return date 229 | 230 | def convert_absolute_date(self, string): 231 | # Set up pattern 232 | date_pattern = [ 233 | '\d{1,2}\.\d{1,2}\.\d{4}', # 01.09.2016 and 1.9.2016 234 | '\d{1,2}\.\d{1,2}' 235 | ] 236 | time_pattern = '\s\d{2}\:\d{2}' # HH:MM 237 | 238 | # Search for date string 239 | date = None 240 | # Pattern 0 241 | pattern = "(%s)" % date_pattern[0] 242 | result = re.search(pattern, string) 243 | # format to orgmode timestamp 244 | if result is not None: 245 | sub_pattern = pattern 246 | date = result.group(1) 247 | date = datetime.datetime.strptime(date, '%d.%m.%Y').strftime('%Y-%m-%d %a') 248 | # format as orgmode date format 249 | # date = self.format_date(date, self.date_format) 250 | else: 251 | # Pattern 1 252 | pattern = "(%s)" % date_pattern[1] 253 | result = re.search(pattern, string) 254 | if result is not None: 255 | sub_pattern = pattern 256 | date = result.group(1) 257 | date = datetime.datetime.strptime(date, '%d.%m').strftime('%m-%d') 258 | 259 | # Add year to date 260 | # Compare with today's date 261 | today = datetime.datetime.today() 262 | date_compare = "%s-%s" % (today.year, date) 263 | date_compare = datetime.datetime.strptime(date_compare, "%Y-%m-%d") 264 | if date_compare > today: 265 | # Date is this year 266 | year = today.year 267 | else: 268 | # Date is next year 269 | year = today.year + 1 270 | # Add year to date 271 | date = "%s-%s" % (year, date) 272 | # format as orgmode date 273 | date = datetime.datetime.strptime(date, '%Y-%m-%d').strftime('%Y-%m-%d %a') 274 | 275 | # Search for time in string 276 | time = "" 277 | pattern = "(%s|%s)(%s)" % (date_pattern[0], date_pattern[1], time_pattern) 278 | result = re.search(pattern, string) 279 | if result is not None: 280 | sub_pattern = pattern 281 | time = result.group(2) 282 | 283 | if date is not None: 284 | # format as orgmode timestamp 285 | date = "<%s%s>" % (date, time) 286 | 287 | # replace date with orgmode timestamp 288 | string = re.sub(sub_pattern, date, string, count=1) 289 | 290 | return string 291 | 292 | def get_creation_date(self): 293 | today = datetime.datetime.now() 294 | date = self.format_date(today, self.creation_date_format) 295 | return date 296 | 297 | def add_priority(self, heading): 298 | # search for priority tag 299 | pattern = '(.+?|.?)%s(.?)\s' % self.priority_tag 300 | result = re.match(pattern, heading) 301 | 302 | # Add orgmode's priority tag to heading 303 | if result is not None: 304 | # remove priority tag from heading 305 | pattern = '(%s.?)\s' % self.priority_tag 306 | heading = re.sub(pattern, "", heading) 307 | 308 | # add priority to heading 309 | priority = result.group(2).upper() 310 | task_tag = "TODO" 311 | if re.match(task_tag, heading) is not None: 312 | # Heading is task: add priority after task tag 313 | task_tag_pos = len(task_tag) 314 | heading = "%s [#%s] %s" % ( 315 | heading[:task_tag_pos], 316 | priority, 317 | heading[task_tag_pos + 1:] 318 | ) 319 | else: 320 | # Heading is note 321 | heading = "[#%s] %s" % (priority, heading) 322 | return heading 323 | 324 | def convert_line_breaks(self, string): 325 | expression = r'(' + self.line_break_pattern + ')' 326 | pattern = re.compile(expression, re.IGNORECASE) 327 | string = re.sub(pattern, self.line_break_char, string) 328 | return string 329 | 330 | def remove_double_spaces(self, string): 331 | # remove double spaces (run twice) 332 | expression = r'(' + '\s\s' + ')' 333 | pattern = re.compile(expression) 334 | for i in range(2): 335 | string = re.sub(pattern, ' ', string) 336 | return string 337 | 338 | def remove_leading_trailling_spaces(self, string): 339 | # remove leading and trailing spaces 340 | expression = r'(' + '^\s|\s$' ')' 341 | pattern = re.compile(expression) 342 | string = re.sub(pattern, '', string) 343 | return string 344 | 345 | def get_deadline_date(self, string): 346 | # Get deadline 347 | expression = r'(' + self.deadline_pattern + self.date_format_regex + ')' 348 | pattern = re.compile(expression, re.IGNORECASE) 349 | deadline = re.search(pattern, string) 350 | if deadline is not None: 351 | # DL: => DEADLINE: 352 | deadline = re.sub(r'(' + self.deadline_pattern + ')', self.deadline_keyword, deadline.group(1)) 353 | 354 | # Remove deadline from string 355 | pattern = re.compile(expression, re.IGNORECASE) 356 | body = re.sub(pattern, '', string) 357 | 358 | return deadline, body 359 | 360 | def get_scheduled_date(self, string): 361 | # Get scheduled 362 | expression = r'(' + self.scheduled_pattern + self.date_format_regex + ')' 363 | pattern = re.compile(expression, re.IGNORECASE) 364 | scheduled = re.search(pattern, string) 365 | if scheduled is not None: 366 | # S: => SCHEDULED: 367 | scheduled = re.sub(r'(' + self.scheduled_pattern + ')', self.scheduled_keyword, scheduled.group(1)) 368 | 369 | # Remove scheduled from string 370 | pattern = re.compile(expression, re.IGNORECASE) 371 | body = re.sub(pattern, '', string) 372 | 373 | return scheduled, body 374 | 375 | def create_message(self): 376 | # Get inbox_file of file path 377 | filepath = self.inbox_file.split('/') 378 | filename = filepath[len(filepath) - 1] 379 | 380 | if self.body is None: 381 | message = self.message_format[0] % (self.heading, filename) 382 | else: 383 | message = self.message_format[1] % (self.heading, self.body, filename) 384 | 385 | return message 386 | -------------------------------------------------------------------------------- /src/py38/org_mode_capture_run.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from org_mode_entry import OrgmodeEntry 4 | 5 | 6 | def convert_boolean(string): 7 | if string == "1": 8 | return True 9 | else: 10 | return False 11 | 12 | 13 | def run(entry, action): 14 | org = OrgmodeEntry() 15 | 16 | if action == "note": 17 | # Entries are added to the following orgmode file (use an absolute path): 18 | org.inbox_file = os.getenv("notes_inbox") 19 | # heading level of entry 20 | org.heading_suffix = "\n%s " % ("*" * 21 | int(os.getenv("notes_heading_level"))) 22 | elif action == "inspiration": 23 | # Entries are added to the following orgmode file (use an absolute path): 24 | org.inbox_file = os.getenv("inspirations_inbox") 25 | # heading level of entry 26 | org.heading_suffix = "\n%s " % ( 27 | "*" * int(os.getenv("inspirations_heading_level"))) 28 | else: 29 | # Entry is a todo 30 | # Entries are added to the following orgmode file (use an absolute path): 31 | org.inbox_file = os.getenv("todos_inbox") 32 | # heading level of entry 33 | org.heading_suffix = "\n%s " % ("*" * 34 | int(os.getenv("todos_heading_level"))) 35 | 36 | # tag to separate the head from the body of the entry 37 | org.delimiter = str(os.getenv("delimiter")) 38 | 39 | # use priority tags: #b => [#B] 40 | org.use_priority_tags = convert_boolean(os.getenv("use_priority_tags")) 41 | # tag that marks a priority value 42 | # Default: # 43 | org.priority_tag = str(os.getenv("priority_tag")) 44 | 45 | # add a creation date 46 | org.add_creation_date = convert_boolean(os.getenv("add_creation_date")) 47 | 48 | # convert absolute dates like 01.10 15:00 into orgmode dates => <2016-10-01 Sun 15:00> 49 | org.replace_absolute_dates = convert_boolean( 50 | os.getenv("replace_absolute_dates")) 51 | 52 | # convert relative dates like monday or tomorrow into orgmode dates 53 | org.replace_relative_dates = convert_boolean( 54 | os.getenv("replace_relative_dates")) 55 | 56 | # Convert a schedule pattern into an org scheduled date 57 | # Default: "S: " 58 | org.convert_scheduled = convert_boolean(os.getenv("convert_scheduled")) 59 | org.scheduled_pattern = str(os.getenv("scheduled_pattern")) 60 | 61 | # Convert a deadline pattern into an org deadline 62 | # Default: "DL: " 63 | org.convert_deadlines = convert_boolean(os.getenv("convert_deadlines")) 64 | org.deadline_pattern = str(os.getenv("deadline_pattern")) 65 | 66 | # convert a pattern into a linebreak 67 | org.smart_line_break = convert_boolean(os.getenv("smart_line_break")) 68 | # two spaces 69 | # Default: "\s\s" 70 | org.line_break_pattern = os.getenv("line_break_pattern") 71 | 72 | # Cleanup spaces (double, leading, and trailing) 73 | org.cleanup_spaces = convert_boolean(os.getenv("cleanup_spaces")) 74 | 75 | entry = 'TODO ' + entry 76 | message = org.add_entry(entry) 77 | 78 | return message 79 | -------------------------------------------------------------------------------- /src/py38/org_mode_entry.py: -------------------------------------------------------------------------------- 1 | # python version 3.8 2 | # UTF-8 encoding 3 | import codecs 4 | import datetime 5 | import re 6 | import unicodedata 7 | 8 | 9 | class OrgmodeEntry(object): 10 | """Convert a generic text into an org-mode heading with an optional body and add it to an orgmode file. 11 | 12 | Supported modes: 13 | - convert relative dates like Thuesday, tomorrow, morgen and montag into org-mode dates 14 | - add the date of creation to the heading 15 | """ 16 | def __init__(self): 17 | self.inbox_file = "/Users/Alex/Desktop/inbox.org" 18 | self.delimiter = ":: " 19 | 20 | # Depth of heading 21 | self.heading_suffix = "\n* " 22 | 23 | # Smart line breaks: add line breaks with a substitude; ie. " " (two spaces) 24 | self.smart_line_break = True 25 | self.line_break_pattern = "\s\s" 26 | self.line_break_char = "\n" 27 | 28 | # Cleanup spaces (double, leading, and trailing) 29 | self.cleanup_spaces = True 30 | 31 | # Add priority tags to entry 32 | self.use_priority_tags = True 33 | self.priority_tag = '#' # tag that marks a priority value: #B => [#B] 34 | 35 | # Add a task created date to entry 36 | self.add_creation_date = True # add a creation date to the entry 37 | self.creation_date_format = ":PROPERTIES:\n:CREATED: [%s-%s-%s %s]\n:END:" 38 | 39 | # Replace absolute dates like 01.10 15:00 => <2016-10-01 Sun 15:00> 40 | self.replace_absolute_dates = True 41 | 42 | # Replace relative dates 43 | self.replace_relative_dates = True 44 | self.date_format = "<%s-%s-%s %s>" 45 | self.date_format_regex = "<\d{4}-\d{2}-\d{2}\s[A-Z][a-z]{2}>" 46 | self.weekdays = { 47 | "montag": 0, 48 | "monday": 0, 49 | "dienstag": 1, 50 | "tuesday": 1, 51 | "mittwoch": 2, 52 | "wednesday": 2, 53 | "donnerstag": 3, 54 | "thursday": 3, 55 | "freitag": 4, 56 | "friday": 4, 57 | "samstag": 5, 58 | "saturday": 5, 59 | "sonntag": 6, 60 | "sunday": 6 61 | } 62 | self.relative_dates = { 63 | "heute": 0, 64 | "today": 0, 65 | "morgen": 1, 66 | "tomorrow": 1 67 | } 68 | self.convenience_dates = [] 69 | for month in range(1, 13): 70 | for day in range(1, 32): 71 | date = "%s.%s" % (day, month) 72 | self.convenience_dates.append(date) 73 | 74 | # Schedule and deadline keywords 75 | self.convert_deadlines = True 76 | self.deadline_pattern = "DL: " 77 | self.deadline_keyword = "DEADLINE: " 78 | 79 | self.convert_scheduled = True 80 | self.scheduled_pattern = "S: " 81 | self.scheduled_keyword = "SCHEDULED: " 82 | 83 | # Message handling 84 | self.message_format = [ 85 | "Added '%s' to %s.", # input without body 86 | "Added '%s\n%s' to %s." # input with heading and body 87 | ] 88 | 89 | def encode(self, string): 90 | """Encode the input string into unicode.""" 91 | # if not isinstance(string, unicode): 92 | # string = unicode(string, "utf-8") 93 | string = unicodedata.normalize('NFC', string) 94 | return string 95 | 96 | def add_entry(self, string): 97 | string = self.encode(string) 98 | entry = self.format_entry(string) 99 | self.write_to_file(entry) 100 | message = self.create_message() 101 | return message 102 | 103 | def write_to_file(self, string): 104 | with codecs.open(self.inbox_file, "a", encoding='utf-8') as myfile: 105 | myfile.write(string) 106 | pass 107 | 108 | def format_entry(self, string): 109 | items = self.split_string(string) 110 | deadline, scheduled = None, None 111 | 112 | # Format body 113 | if len(items) == 1: 114 | # String has no body 115 | body = "" 116 | self.body = None 117 | else: 118 | # String has a body 119 | body = items[1] 120 | 121 | if self.replace_absolute_dates is True: 122 | # Replace absolute dates 123 | body = self.convert_absolute_date(body) 124 | 125 | if self.replace_relative_dates is True: 126 | # Replace relative dates 127 | body = self.replace_date(body) 128 | 129 | if self.smart_line_break is True: 130 | # convert line breaks 131 | body = self.convert_line_breaks(body) 132 | 133 | if self.convert_deadlines is True: 134 | # Replace deadlines 135 | deadline, body = self.get_deadline_date(body) 136 | 137 | if self.convert_scheduled is True: 138 | # Replace deadlines 139 | scheduled, body = self.get_scheduled_date(body) 140 | 141 | if self.cleanup_spaces is True: 142 | body = self.remove_double_spaces(body) 143 | body = self.remove_leading_trailling_spaces(body) 144 | 145 | self.body = body 146 | 147 | # Format heading 148 | heading = items[0] 149 | # Priority 150 | if self.use_priority_tags is True: 151 | # Search heading string for priority tag and add an orgmode 152 | # priority tag to the heading 153 | heading = self.add_priority(heading) 154 | 155 | self.heading = heading 156 | heading = self.heading_suffix + heading 157 | 158 | # Format entry 159 | entry = heading 160 | if deadline is not None: 161 | entry += '\n%s' % deadline 162 | if scheduled is not None: 163 | if deadline is not None: 164 | entry += ' ' 165 | else: 166 | entry += '\n' 167 | entry += scheduled 168 | if self.add_creation_date is True: 169 | entry += '\n%s' % self.get_creation_date() 170 | entry += '\n%s' % body 171 | 172 | return entry 173 | 174 | def split_string(self, string): 175 | return string.split(self.delimiter) 176 | 177 | def replace_date(self, string): 178 | dict_keys = '|'.join(self.weekdays.keys()) + "|" + '|'.join( 179 | self.relative_dates.keys()) 180 | expression = r'\b(' + dict_keys + r')\b' 181 | pattern = re.compile(expression, re.IGNORECASE) 182 | string = pattern.sub(lambda x: self.convert_date(x.group()), string) 183 | return string 184 | 185 | def convert_date(self, string): 186 | today = datetime.datetime.now() 187 | delta = self.convert_relative_date(string) 188 | date = today + datetime.timedelta(days=delta) 189 | date = self.format_date(date, self.date_format) 190 | return date 191 | 192 | def convert_relative_date(self, string): 193 | # string dictionaries with delta day value 194 | string = string.lower() 195 | relative_dates = self.relative_dates 196 | weekdays = self.weekdays 197 | convenience_dates = self.convenience_dates 198 | 199 | if string in relative_dates: 200 | delta = relative_dates[string] 201 | elif string in weekdays: 202 | today = datetime.datetime.today() 203 | current = today.weekday() 204 | weekday = weekdays[string] 205 | if current == weekday: 206 | delta = 7 207 | elif weekday < current: 208 | delta = 7 - (current - weekday) 209 | else: 210 | delta = weekday - current 211 | # TODO: Extend replace date function 212 | elif string in convenience_dates: 213 | item = string.split(".") 214 | day = item[0] 215 | month = item[1] 216 | print(day, month) 217 | delta = False 218 | else: 219 | delta = False 220 | return delta 221 | 222 | def format_date(self, date, date_format): 223 | year = date.strftime("%Y") 224 | month = date.strftime("%m") 225 | day = date.strftime("%d") 226 | weekday = date.strftime("%a") 227 | 228 | date = date_format % (year, month, day, weekday) 229 | return date 230 | 231 | def convert_absolute_date(self, string): 232 | # Set up pattern 233 | date_pattern = [ 234 | '\d{1,2}\.\d{1,2}\.\d{4}', # 01.09.2016 and 1.9.2016 235 | '\d{1,2}\.\d{1,2}' 236 | ] 237 | time_pattern = '\s\d{2}\:\d{2}' # HH:MM 238 | 239 | # Search for date string 240 | date = None 241 | # Pattern 0 242 | pattern = "(%s)" % date_pattern[0] 243 | result = re.search(pattern, string) 244 | # format to orgmode timestamp 245 | if result is not None: 246 | sub_pattern = pattern 247 | date = result.group(1) 248 | date = datetime.datetime.strptime( 249 | date, '%d.%m.%Y').strftime('%Y-%m-%d %a') 250 | # format as orgmode date format 251 | # date = self.format_date(date, self.date_format) 252 | else: 253 | # Pattern 1 254 | pattern = "(%s)" % date_pattern[1] 255 | result = re.search(pattern, string) 256 | if result is not None: 257 | sub_pattern = pattern 258 | date = result.group(1) 259 | date = datetime.datetime.strptime(date, 260 | '%d.%m').strftime('%m-%d') 261 | 262 | # Add year to date 263 | # Compare with today's date 264 | today = datetime.datetime.today() 265 | date_compare = "%s-%s" % (today.year, date) 266 | date_compare = datetime.datetime.strptime( 267 | date_compare, "%Y-%m-%d") 268 | if date_compare > today: 269 | # Date is this year 270 | year = today.year 271 | else: 272 | # Date is next year 273 | year = today.year + 1 274 | # Add year to date 275 | date = "%s-%s" % (year, date) 276 | # format as orgmode date 277 | date = datetime.datetime.strptime( 278 | date, '%Y-%m-%d').strftime('%Y-%m-%d %a') 279 | 280 | # Search for time in string 281 | time = "" 282 | pattern = "(%s|%s)(%s)" % (date_pattern[0], date_pattern[1], 283 | time_pattern) 284 | result = re.search(pattern, string) 285 | if result is not None: 286 | sub_pattern = pattern 287 | time = result.group(2) 288 | 289 | if date is not None: 290 | # format as orgmode timestamp 291 | date = "<%s%s>" % (date, time) 292 | 293 | # replace date with orgmode timestamp 294 | string = re.sub(sub_pattern, date, string, count=1) 295 | 296 | return string 297 | 298 | def get_creation_date(self): 299 | today = datetime.datetime.now() 300 | date = self.format_date(today, self.creation_date_format) 301 | return date 302 | 303 | def add_priority(self, heading): 304 | # search for priority tag 305 | pattern = '(.+?|.?)%s(.?)\s' % self.priority_tag 306 | result = re.match(pattern, heading) 307 | 308 | # Add orgmode's priority tag to heading 309 | if result is not None: 310 | # remove priority tag from heading 311 | pattern = '(%s.?)\s' % self.priority_tag 312 | heading = re.sub(pattern, "", heading) 313 | 314 | # add priority to heading 315 | priority = result.group(2).upper() 316 | task_tag = "TODO" 317 | if re.match(task_tag, heading) is not None: 318 | # Heading is task: add priority after task tag 319 | task_tag_pos = len(task_tag) 320 | heading = "%s [#%s] %s" % (heading[:task_tag_pos], priority, 321 | heading[task_tag_pos + 1:]) 322 | else: 323 | # Heading is note 324 | heading = "[#%s] %s" % (priority, heading) 325 | return heading 326 | 327 | def convert_line_breaks(self, string): 328 | expression = r'(' + self.line_break_pattern + ')' 329 | pattern = re.compile(expression, re.IGNORECASE) 330 | string = re.sub(pattern, self.line_break_char, string) 331 | return string 332 | 333 | def remove_double_spaces(self, string): 334 | # remove double spaces (run twice) 335 | expression = r'(' + '\s\s' + ')' 336 | pattern = re.compile(expression) 337 | for i in range(2): 338 | string = re.sub(pattern, ' ', string) 339 | return string 340 | 341 | def remove_leading_trailling_spaces(self, string): 342 | # remove leading and trailing spaces 343 | expression = r'(' + '^\s|\s$' ')' 344 | pattern = re.compile(expression) 345 | string = re.sub(pattern, '', string) 346 | return string 347 | 348 | def get_deadline_date(self, string): 349 | # Get deadline 350 | expression = r'(' + self.deadline_pattern + self.date_format_regex + ')' 351 | pattern = re.compile(expression, re.IGNORECASE) 352 | deadline = re.search(pattern, string) 353 | if deadline is not None: 354 | # DL: => DEADLINE: 355 | deadline = re.sub(r'(' + self.deadline_pattern + ')', 356 | self.deadline_keyword, deadline.group(1)) 357 | 358 | # Remove deadline from string 359 | pattern = re.compile(expression, re.IGNORECASE) 360 | body = re.sub(pattern, '', string) 361 | 362 | return deadline, body 363 | 364 | def get_scheduled_date(self, string): 365 | # Get scheduled 366 | expression = r'(' + self.scheduled_pattern + self.date_format_regex + ')' 367 | pattern = re.compile(expression, re.IGNORECASE) 368 | scheduled = re.search(pattern, string) 369 | if scheduled is not None: 370 | # S: => SCHEDULED: 371 | scheduled = re.sub(r'(' + self.scheduled_pattern + ')', 372 | self.scheduled_keyword, scheduled.group(1)) 373 | 374 | # Remove scheduled from string 375 | pattern = re.compile(expression, re.IGNORECASE) 376 | body = re.sub(pattern, '', string) 377 | 378 | return scheduled, body 379 | 380 | def create_message(self): 381 | # Get inbox_file of file path 382 | filepath = self.inbox_file.split('/') 383 | filename = filepath[len(filepath) - 1] 384 | 385 | if self.body is None: 386 | message = self.message_format[0] % (self.heading, filename) 387 | else: 388 | message = self.message_format[1] % (self.heading, self.body, 389 | filename) 390 | 391 | return message 392 | -------------------------------------------------------------------------------- /src/test.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from py38.orgmode_entry import OrgmodeEntry 4 | 5 | entry = u'#A Etwas machen:: DL: Morgen S: Heute Ausstellung am 23.09.2014 12:00 oder am Montag bzw. am 22.10 13:00 sollte man anschauen. ' 6 | 7 | org = OrgmodeEntry() 8 | # Use an absolute path 9 | org.inbox_file = 'test_inbox.org' 10 | 11 | org.delimiter = ':: ' # tag to separate the head from the body of the entry 12 | org.heading_suffix = "\n* " # depth of entry 13 | 14 | org.use_priority_tags = True # use priority tags: #b => [#B] 15 | org.priority_tag = '#' # tag that marks a priority value 16 | 17 | org.add_creation_date = True # add a creation date 18 | 19 | org.replace_absolute_dates = True # convert absolute dates like 01.10 15:00 into orgmode dates => <2016-10-01 Sun 15:00> 20 | org.replace_relative_dates = True # convert relative dates like monday or tomorrow into orgmode dates 21 | 22 | # Convert a schedule pattern into an org scheduled date 23 | org.convert_scheduled = True # convert sche 24 | org.scheduled_pattern = "S: " 25 | 26 | # Convert a deadline pattern into an org deadline 27 | org.convert_deadlines = True 28 | org.deadline_pattern = "DL: " 29 | 30 | org.smart_line_break = True # convert a pattern into a linebreak 31 | org.line_break_pattern = "\s\s" # two spaces 32 | 33 | # Cleanup spaces (double, leading, and trailing) 34 | org.cleanup_spaces = True 35 | 36 | entry = 'TODO ' + entry 37 | 38 | message = org.add_entry(entry).encode('utf-8') 39 | 40 | print(message) 41 | -------------------------------------------------------------------------------- /src/test_inbox.org: -------------------------------------------------------------------------------- 1 | 2 | * TODO [#A] Etwas machen 3 | DEADLINE: <2023-01-20 Fri> SCHEDULED: <2023-01-19 Thu> 4 | :PROPERTIES: 5 | :CREATED: [2023-01-19 Thu] 6 | :END: 7 | Ausstellung am <2014-09-23 Tue 12:00> oder am <2023-01-23 Mon> bzw. am 22.10 13:00 sollte man anschauen. 8 | * TODO [#A] Etwas machen 9 | DEADLINE: <2023-01-20 Fri> SCHEDULED: <2023-01-19 Thu> 10 | :PROPERTIES: 11 | :CREATED: [2023-01-19 Thu] 12 | :END: 13 | Ausstellung am <2014-09-23 Tue 12:00> oder am <2023-01-23 Mon> bzw. am 22.10 13:00 sollte man anschauen. --------------------------------------------------------------------------------