def deauthorise(): try: workflow().delete_password(config.KC_OAUTH_TOKEN) workflow().delete_password(config.KC_REFRESH_TOKEN) log.debug('Deauthorising') except PasswordNotFound: pass
def sync(cls, background=False): from mstodo.api import taskfolders start = time.time() taskfolders_data = taskfolders.taskFolders() instances = [] log.info('Retrieved all %d task folders in %s', len(taskfolders_data), time.time() - start) start = time.time() # Hacky translation of mstodo data model to wunderlist data model to avoid changing naming in rest of the files for taskfolder in taskfolders_data: for (k,v) in taskfolder.copy().iteritems(): if k == "name": taskfolder['title'] = v workflow().store_data('taskfolders', taskfolders_data) try: instances = cls.select(cls.id, cls.changeKey, cls.title) except PeeweeException: pass log.info('Loaded all %d task folders from the database in %s', len(instances), time.time() - start) return cls._perform_updates(instances, taskfolders_data)
def filter(args): util.workflow().add_item( 'Are you sure?', 'You will need to log in to a Microsoft account to continue using the workflow', arg=' '.join(args), valid=True, icon=icons.CHECKMARK) util.workflow().add_item('Nevermind', autocomplete='', icon=icons.CANCEL)
def _set(self, key, value): if value is None and key in self._data: del self._data[key] elif self._data.get(key) != value: self._data[key] = value else: return workflow().store_data('prefs', self._data)
def filter(args): taskfolder_name = _taskfolder_name(args) subtitle = taskfolder_name if taskfolder_name else 'Type the name of the task folder' util.workflow().add_item('New folder...', subtitle, arg='--stored-query', valid=taskfolder_name != '', icon=icons.LIST_NEW) util.workflow().add_item( 'Main menu', autocomplete='', icon=icons.BACK )
def __init__(self, data): self._data = data or {} # Clean up old prerelease preference if 'prerelease_channel' in self._data: # Migrate to the alfred-workflow preference self.prerelease_channel = self._data['prerelease_channel'] del self._data['prerelease_channel'] workflow().store_data('prefs', self._data)
def new_oauth_state(): log.debug('Creating new OAuth state') import random import string state_length = 20 state = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(state_length)) workflow().save_password(config.KC_OAUTH_STATE, state) return state
def is_authorised(): if oauth_token() is None: log.debug('Not authorised') return False else: if workflow().cached_data('query_event', max_age=3600) is None: log.debug('No auth in last 3600s, refreshing token') return resolve_oauth_token(refresh_token=workflow().get_password(config.KC_REFRESH_TOKEN)) else: log.debug('Using cached OAuth token') return True
def alfred_is_dark(): # Formatted rgba(255,255,255,0.90) background_rgba = workflow().alfred_env['theme_background'] if background_rgba: rgb = [int(x) for x in background_rgba[5:-6].split(',')] return (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255 < 0.5 return False
def background_sync(): from workflow.background import run_in_background task_id = 'sync' # Only runs if another sync is not already in progress run_in_background(task_id, [ '/usr/bin/env', 'python', workflow().workflowfile('alfred-mstodo-workflow.py'), 'pref sync background', '--commit' ])
def authorise(): from multiprocessing import Process import webbrowser workflow().store_data('auth', 'started') # construct auth request url. Replace quote_plus with parse.quote for Py3 state = new_oauth_state() auth_url = (config.MS_TODO_AUTH_ROOT + "/authorize" "?client_id=" + config.MS_TODO_CLIENT_ID + "&response_type=code&response_mode=query" + "&redirect_uri=" + quote_plus(config.MS_TODO_REDIRECT_URL) + "&scope=" + quote_plus((' '.join(config.MS_TODO_SCOPE)).lower()) + "&state=" + state) log.debug(auth_url) # start server to handle response server = Process(target=await_token) server.start() # open browser to auth webbrowser.open(auth_url)
def filter(args): task_id = args[1] wf = workflow() task = None try: task = Task.get(Task.id == task_id) except Task.DoesNotExist: pass if not task: wf.add_item('Unknown task', 'The ID does not match a task', autocomplete='', icon=icons.BACK) else: subtitle = task.subtitle() if task.status == 'completed': wf.add_item('Mark task not completed', subtitle, modifier_subtitles={}, arg=' '.join(args + ['toggle-completion']), valid=True, icon=icons.TASK_COMPLETED) else: wf.add_item('Complete this task', subtitle, modifier_subtitles={ 'alt': u'…and set due today %s' % subtitle }, arg=' '.join(args + ['toggle-completion']), valid=True, icon=icons.TASK) wf.add_item('View in ToDo', 'View and edit this task in the ToDo app', arg=' '.join(args + ['view']), valid=True, icon=icons.OPEN) if task.recurrence_type and not task.status == 'completed': wf.add_item('Delete', 'Delete this task and cancel recurrence', arg=' '.join(args + ['delete']), valid=True, icon=icons.TRASH) else: wf.add_item('Delete', 'Delete this task', arg=' '.join(args + ['delete']), valid=True, icon=icons.TRASH) wf.add_item('Main menu', autocomplete='', icon=icons.BACK)
def resolve_oauth_token(code=None, refresh_token=None): token_url = config.MS_TODO_AUTH_ROOT + "/token" scope = config.MS_TODO_SCOPE data = { "client_id": config.MS_TODO_CLIENT_ID, "redirect_uri": config.MS_TODO_REDIRECT_URL, "scope": ' '.join(scope) } if code is not None: log.debug('first grant') data['grant_type'] = "authorization_code" data['code'] = code elif refresh_token is not None: log.debug('refeshing token') data['grant_type'] = "refresh_token" data['refresh_token'] = refresh_token if 'grant_type' in data: log.debug('Getting token from: ' + token_url) result = requests.post(token_url, data=data) log.debug('Auth response status: ' + str(result.status_code)) if 'access_token' in result.text: log.debug('Saving access token in keychain') workflow().save_password(config.KC_OAUTH_TOKEN, result.json()['access_token']) workflow().save_password(config.KC_REFRESH_TOKEN, result.json()['refresh_token']) workflow().cache_data('query_event', True) return True return False
def commit(args, modifier=None): if 'update' in args: if workflow().start_update(): print('The workflow is being updated') else: print('You already have the latest workflow version') else: import webbrowser if 'changelog' in args: webbrowser.open( 'https://github.com/johandebeurs/alfred-mstodo-workflow/releases/tag/0.1.2' ) elif 'mstodo' in args: webbrowser.open('https://www.todo.microsoft.com/') elif 'issues' in args: webbrowser.open( 'https://github.com/johandebeurs/alfred-mstodo-workflow/issues' )
def filter(args): workflow().add_item( 'New in this version', 'Installed: 0.1.2 See the changes from the previous version', arg='-about changelog', valid=True, icon=icons.INFO) workflow().add_item( 'Questions or concerns?', 'See outstanding issues and report your own bugs or feedback', arg='-about issues', valid=True, icon=icons.HELP) workflow().add_item( 'Update workflow', 'Check for updates to the workflow (automatically checked periodically)', arg='-about update', valid=True, icon=icons.DOWNLOAD) workflow().add_item('Main menu', autocomplete='', icon=icons.BACK)
def handle_authorisation_url(url): # Parse query data & params to find out what was passed # parsed_url = parse.urlparse(url) # params = parse.parse_qs(parsed_url.query) params = parse_qs(urlparse(url).query) if 'code' in params and validate_oauth_state(params['state'][0]): log.debug('Valid OAuth response and state matches') # Request a token based on the code resolve_oauth_token(code=params['code'][0]) workflow().store_data('auth', None) workflow().delete_password(config.KC_OAUTH_STATE) print('You are now logged in') return True elif 'error' in params: workflow().store_data('auth', 'Error: %s' % params['error']) print('Please try again later') return params['error'] # Not a valid URL return False
def filter(args): wf = workflow() prefs = Preferences.current_prefs() command = args[1] if len(args) > 1 else None duration_info = _duration_info(prefs.completed_duration) if command == 'duration': selected_duration = prefs.completed_duration # Apply selected duration option if len(args) > 2: try: selected_duration = int(args[2]) except: pass duration_info = _duration_info(selected_duration) if 'custom' in duration_info: wf.add_item(duration_info['label'], duration_info['subtitle'], arg='-completed duration %d' % (duration_info['days']), valid=True, icon=icons.RADIO_SELECTED if duration_info['days'] == selected_duration else icons.RADIO) for duration_info in _durations: wf.add_item(duration_info['label'], duration_info['subtitle'], arg='-completed duration %d' % (duration_info['days']), valid=True, icon=icons.RADIO_SELECTED if duration_info['days'] == selected_duration else icons.RADIO) wf.add_item('Back', autocomplete='-completed ', icon=icons.BACK) return # Force a sync if not done recently or join if already running if not prefs.last_sync or \ datetime.utcnow() - prefs.last_sync > timedelta(seconds=30) or \ is_running('sync'): sync() wf.add_item(duration_info['label'], subtitle='Change the duration for completed tasks', autocomplete='-completed duration ', icon=icons.YESTERDAY) conditions = True # Build task title query based on the args for arg in args[1:]: if len(arg) > 1: conditions = conditions & (Task.title.contains(arg) | TaskFolder.title.contains(arg)) if conditions is None: conditions = True tasks = Task.select().join(TaskFolder).where( (Task.completedDateTime > date.today() - timedelta(days=duration_info['days'])) & Task.list.is_null(False) & conditions )\ .order_by(Task.completedDateTime.desc(), Task.reminderDateTime.asc(), Task.changeKey.asc()) try: for t in tasks: wf.add_item(u'%s – %s' % (t.list_title, t.title), t.subtitle(), autocomplete='-task %s ' % t.id, icon=icons.TASK_COMPLETED if t.status == 'completed' else icons.TASK) except OperationalError: background_sync() wf.add_item('Main menu', autocomplete='', icon=icons.BACK) # Make sure tasks stay up-to-date background_sync_if_necessary(seconds=2)
def filter(args): query = ' '.join(args[1:]) wf = workflow() prefs = Preferences.current_prefs() matching_hashtags = [] if not query: wf.add_item('Begin typing to search tasks', '', icon=icons.SEARCH) hashtag_match = re.search(_hashtag_prompt_pattern, query) if hashtag_match: from mstodo.models.hashtag import Hashtag hashtag_prompt = hashtag_match.group().lower() hashtags = Hashtag.select().where( Hashtag.id.contains(hashtag_prompt)).order_by( fn.Lower(Hashtag.tag).asc()) for hashtag in hashtags: # If there is an exact match, do not show hashtags if hashtag.id == hashtag_prompt: matching_hashtags = [] break matching_hashtags.append(hashtag) # Show hashtag prompt if there is more than one matching hashtag or the # hashtag being typed does not exactly match the single matching hashtag if len(matching_hashtags) > 0: for hashtag in matching_hashtags: wf.add_item(hashtag.tag[1:], '', autocomplete=u'-search %s%s ' % (query[:hashtag_match.start()], hashtag.tag), icon=icons.HASHTAG) else: conditions = True taskfolders = workflow().stored_data('taskfolders') matching_taskfolders = None query = ' '.join(args[1:]).strip() taskfolder_query = None # Show all task folders on the main search screen if not query: matching_taskfolders = taskfolders # Filter task folders when colon is used if ':' in query: matching_taskfolders = taskfolders components = re.split(r':\s*', query, 1) taskfolder_query = components[0] if taskfolder_query: matching_taskfolders = workflow().filter( taskfolder_query, taskfolders if taskfolders else [], lambda f: f['title'], # Ignore MATCH_ALLCHARS which is expensive and inaccurate match_on=MATCH_ALL ^ MATCH_ALLCHARS) # If no matching task folder search against all tasks if matching_taskfolders: query = components[1] if len(components) > 1 else '' # If there is a task folder exactly matching the query ignore # anything else. This takes care of taskfolders that are substrings # of other taskfolders if len(matching_taskfolders) > 1: for f in matching_taskfolders: if f['title'].lower() == taskfolder_query.lower(): matching_taskfolders = [f] break if matching_taskfolders: if not taskfolder_query: wf.add_item('Browse by hashtag', autocomplete='-search #', icon=icons.HASHTAG) if len(matching_taskfolders) > 1: for f in matching_taskfolders: icon = icons.INBOX if f['isDefaultFolder'] else icons.LIST wf.add_item(f['title'], autocomplete='-search %s: ' % f['title'], icon=icon) else: conditions = conditions & (Task.list == matching_taskfolders[0]['id']) if not matching_taskfolders or len(matching_taskfolders) <= 1: for arg in query.split(' '): if len(arg) > 1: conditions = conditions & (Task.title.contains(arg) | TaskFolder.title.contains(arg)) if conditions: if not prefs.show_completed_tasks: conditions = (Task.status != 'completed') & conditions tasks = Task.select().where( Task.list.is_null(False) & conditions) tasks = tasks.join(TaskFolder).order_by( Task.lastModifiedDateTime.desc(), TaskFolder.changeKey.asc()) # Avoid excessive results tasks = tasks.limit(50) try: for t in tasks: wf.add_item(u'%s – %s' % (t.list_title, t.title), t.subtitle(), autocomplete='-task %s ' % t.id, icon=icons.TASK_COMPLETED if t.status == 'completed' else icons.TASK) except OperationalError: background_sync() if prefs.show_completed_tasks: wf.add_item('Hide completed tasks', arg='-pref show_completed_tasks --alfred %s' % ' '.join(args), valid=True, icon=icons.HIDDEN) else: wf.add_item('Show completed tasks', arg='-pref show_completed_tasks --alfred %s' % ' '.join(args), valid=True, icon=icons.VISIBLE) wf.add_item('New search', autocomplete='-search ', icon=icons.CANCEL) wf.add_item('Main menu', autocomplete='', icon=icons.BACK) # Make sure tasks are up-to-date while searching background_sync()
def route(args): handler = None command = [] command_string = '' action = 'none' logged_in = is_authorised() # Read the stored query, which will correspond to the user's alfred query # as of the very latest keystroke. This may be different than the query # when this script was launched due to the startup latency. if args[0] == '--stored-query': query_file = workflow().workflowfile('.query') with open(query_file, 'r') as f: command_string = workflow().decode(f.read()) os.remove(query_file) # Otherwise take the command from the first command line argument elif args: command_string = args[0] command_string = re.sub(COMMAND_PATTERN, '', command_string) command = re.split(r' +', command_string) if command: action = re.sub(ACTION_PATTERN, '', command[0]) or 'none' if 'about'.find(action) == 0: from mstodo.handlers import about handler = about elif not logged_in: from mstodo.handlers import login handler = login elif 'folder'.find(action) == 0: from mstodo.handlers import taskfolder handler = taskfolder elif 'task'.find(action) == 0: from mstodo.handlers import task handler = task elif 'search'.find(action) == 0: from mstodo.handlers import search handler = search elif 'due'.find(action) == 0: from mstodo.handlers import due handler = due elif 'upcoming'.find(action) == 0: from mstodo.handlers import upcoming handler = upcoming elif 'completed'.find(action) == 0: from mstodo.handlers import completed handler = completed elif 'logout'.find(action) == 0: from mstodo.handlers import logout handler = logout elif 'pref'.find(action) == 0: from mstodo.handlers import preferences handler = preferences # If the command starts with a space (no special keywords), the workflow # creates a new task elif not command_string: from mstodo.handlers import welcome handler = welcome else: from mstodo.handlers import new_task handler = new_task if handler: if '--commit' in args: modifier = re.search(r'--(alt|cmd|ctrl|fn)\b', ' '.join(args)) if modifier: modifier = modifier.group(1) handler.commit(command, modifier) else: handler.filter(command) if workflow().update_available: update_data = workflow().cached_data('__workflow_update_status', max_age=0) if update_data.get('version') != '0.1.2': workflow().add_item('An update is available!', 'Update the ToDo workflow from version 0.1.2 to %s' % update_data.get('version'), arg='-about update', valid=True, icon=icons.DOWNLOAD) workflow().send_feedback() if logged_in: background_sync_if_necessary()
def filter(args): getting_help = False if len(args) > 0: action = re.sub(ACTION_PATTERN, '', args[0]) getting_help = action and 'help'.find(action) == 0 if not getting_help: workflow().add_item( 'Please log in', 'Authorise Alfred ToDo Workflow to use your Microsoft account', valid=True, icon=icons.ACCOUNT) # If the auth process has started, allow user to paste a key manually if getting_help: workflow().add_item( 'A "localhost" page appeared in my web browser', u'Paste the full link from your browser above then press return, td:help http://localhost:6200/…', arg=' '.join(args), valid=True, icon=icons.LINK) workflow().add_item( 'I need to log in to a different account', 'Go to microsoft.com in your browser and sign out of your account first', arg='-about mstodo', valid=True, icon=icons.ACCOUNT) workflow().add_item( 'Other issues?', 'See outstanding issues and report your own bugs or feedback', arg='-about issues', valid=True, icon=icons.HELP) else: workflow().add_item('Having trouble?', autocomplete='-help ', valid=False, icon=icons.HELP) if not getting_help: workflow().add_item('About', 'Learn about the workflow and get support', autocomplete='-about ', icon=icons.INFO)
def sync(background=False): log.info('running mstodo/sync') from mstodo.models import base, task, user, taskfolder, hashtag from peewee import OperationalError # If a sync is already running, wait for it to finish. Otherwise, store # the current pid in alfred-workflow's pid cache file if not background: if is_running('sync'): wait_count = 0 while is_running('sync'): time.sleep(.25) wait_count += 1 if wait_count == 2: notify( 'Please wait...', 'The workflow is making sure your tasks are up-to-date' ) return False pidfile = workflow().cachefile('sync.pid') with open(pidfile, 'wb') as file_obj: file_obj.write('{0}'.format(os.getpid())) base.BaseModel._meta.database.create_tables( [taskfolder.TaskFolder, task.Task, user.User, hashtag.Hashtag], safe=True) # Perform a query that requires the latest schema; if it fails due to a # mismatched scheme, delete the old database and re-sync try: task.Task.select().where(task.Task.recurrence_count > 0).count() hashtag.Hashtag.select().where(hashtag.Hashtag.tag == '').count() except OperationalError: base.BaseModel._meta.database.close() workflow().clear_data(lambda f: 'mstodo.db' in f) # Make sure that this sync does not try to wait until its own process # finishes sync(background=True) return first_sync = False try: # get root item from DB. If it doesn't exist then make this the first sync. user.User.get() except user.User.DoesNotExist: first_sync = True Preferences.current_prefs().last_sync = datetime.utcnow() notify('Please wait...', 'The workflow is syncing tasks for the first time') user.User.sync(background=background) taskfolder.TaskFolder.sync(background=background) if first_sync: task.Task.sync_all_tasks(background=background) else: task.Task.sync_modified_tasks(background=background) if background: if first_sync: notify('Initial sync has completed', 'All of your tasks are now available for browsing') # If executed manually, this will pass on to the post notification action print('Sync completed successfully') log.debug('First sync: ' + str(first_sync)) log.debug('Last sync time: ' + str(Preferences.current_prefs().last_sync)) Preferences.current_prefs().last_sync = datetime.utcnow() log.debug('This sync time: ' + str(Preferences.current_prefs().last_sync)) return True
def filter(args): wf = workflow() prefs = Preferences.current_prefs() command = args[1] if len(args) > 1 else None # Show sort options if command == 'sort': for i, order_info in enumerate(_due_orders): wf.add_item(order_info['title'], order_info['subtitle'], arg='-due sort %d' % (i + 1), valid=True, icon=icons.RADIO_SELECTED if order_info['due_order'] == prefs.due_order else icons.RADIO) wf.add_item( 'Highlight skipped recurring tasks', 'Hoists recurring tasks that have been missed multiple times over to the top', arg='-due sort toggle-skipped', valid=True, icon=icons.CHECKBOX_SELECTED if prefs.hoist_skipped_tasks else icons.CHECKBOX) wf.add_item('Back', autocomplete='-due ', icon=icons.BACK) return # Force a sync if not done recently or wait on the current sync if not prefs.last_sync or \ datetime.utcnow() - prefs.last_sync > timedelta(seconds=30) or \ is_running('sync'): sync() conditions = True # Build task title query based on the args for arg in args[1:]: if len(arg) > 1: conditions = conditions & (Task.title.contains(arg) | TaskFolder.title.contains(arg)) if conditions is None: conditions = True tasks = Task.select().join(TaskFolder).where( (Task.status != 'completed') & (Task.dueDateTime < datetime.now() + timedelta(days=1)) & Task.list_id.is_null(False) & conditions) # Sort the tasks according to user preference for key in prefs.due_order: order = 'asc' field = None if key[0] == '-': order = 'desc' key = key[1:] if key == 'due_date': field = Task.dueDateTime elif key == 'taskfolder.id': field = TaskFolder.id elif key == 'order': field = Task.lastModifiedDateTime if field: if order == 'asc': tasks = tasks.order_by(field.asc()) else: tasks = tasks.order_by(field.desc()) try: if prefs.hoist_skipped_tasks: log.debug('hoisting skipped tasks') tasks = sorted(tasks, key=lambda t: -t.overdue_times) for t in tasks: wf.add_item(u'%s – %s' % (t.list_title, t.title), t.subtitle(), autocomplete='-task %s ' % t.id, icon=icons.TASK_COMPLETED if t.status == 'completed' else icons.TASK) except OperationalError: background_sync() wf.add_item(u'Sort order', 'Change the display order of due tasks', autocomplete='-due sort', icon=icons.SORT) wf.add_item('Main menu', autocomplete='', icon=icons.BACK) # Make sure tasks stay up-to-date background_sync_if_necessary(seconds=2)
def commit(args, modifier=None): auth.deauthorise() util.workflow().clear_data() util.workflow().clear_cache() print('You are now logged out')
def commit(args, modifier=None): prefs = Preferences.current_prefs() relaunch_command = '-pref' if '--alfred' in args: relaunch_command = ' '.join(args[args.index('--alfred') + 1:]) if 'sync' in args: from mstodo.sync import sync sync('background' in args) relaunch_command = None elif 'show_completed_tasks' in args: prefs.show_completed_tasks = not prefs.show_completed_tasks if prefs.show_completed_tasks: print('Completed tasks are now visible in the workflow') else: print('Completed tasks will not be visible in the workflow') elif 'default_folder' in args: default_taskfolder_id = None taskfolders = workflow().stored_data('taskfolders') if len(args) > 2: default_taskfolder_id = args[2] prefs.default_taskfolder_id = default_taskfolder_id if default_taskfolder_id: default_folder_name = next( (f['title'] for f in taskfolders if f['id'] == default_taskfolder_id), 'most recent') print('Tasks will be added to your %s folder by default' % default_folder_name) else: print('Tasks will be added to the Tasks folder by default') elif 'explicit_keywords' in args: prefs.explicit_keywords = not prefs.explicit_keywords if prefs.explicit_keywords: print('Remember to use the "due" keyword') else: print('Implicit due dates enabled (e.g. "Recycling tomorrow")') elif 'reminder' in args: reminder_time = _parse_time(' '.join(args)) if reminder_time is not None: prefs.reminder_time = reminder_time print('Reminders will now default to %s' % format_time(reminder_time, 'short')) elif 'reminder_today' in args: reminder_today_offset = None if not 'disabled' in args: reminder_today_offset = _parse_time(' '.join(args)) prefs.reminder_today_offset = reminder_today_offset print('The offset for current-day reminders is now %s' % _format_time_offset(reminder_today_offset)) elif 'automatic_reminders' in args: prefs.automatic_reminders = not prefs.automatic_reminders if prefs.automatic_reminders: print('A reminder will automatically be set for due tasks') else: print('A reminder will not be added automatically') elif 'retheme' in args: prefs.icon_theme = 'light' if icons.icon_theme() == 'dark' else 'dark' print('The workflow is now using the %s icon theme' % (prefs.icon_theme)) elif 'prerelease_channel' in args: prefs.prerelease_channel = not prefs.prerelease_channel # Update the workflow settings and reverify the update data workflow().check_update(True) if prefs.prerelease_channel: print( 'The workflow will prompt you to update to experimental pre-releases' ) else: print( 'The workflow will only prompt you to update to final releases' ) elif 'force_en_US' in args: if prefs.date_locale: prefs.date_locale = None print( 'The workflow will expect your local language and date format') else: prefs.date_locale = 'en_US' print('The workflow will expect dates in US English') if relaunch_command: relaunch_alfred('td%s' % relaunch_command)
def filter(args): prefs = Preferences.current_prefs() if 'reminder' in args: reminder_time = _parse_time(' '.join(args)) if reminder_time is not None: workflow().add_item('Change default reminder time', u'⏰ %s' % format_time(reminder_time, 'short'), arg=' '.join(args), valid=True, icon=icons.REMINDER) else: workflow().add_item( 'Type a new reminder time', 'Date offsets like the morning before the due date are not supported yet', valid=False, icon=icons.REMINDER) workflow().add_item('Cancel', autocomplete='-pref', icon=icons.BACK) elif 'reminder_today' in args: reminder_today_offset = _parse_time(' '.join(args)) if reminder_today_offset is not None: workflow().add_item('Set a custom reminder offset', u'⏰ now + %s' % _format_time_offset(reminder_today_offset), arg=' '.join(args), valid=True, icon=icons.REMINDER) else: workflow().add_item('Type a custom reminder offset', 'Use the formats hh:mm or 2h 5m', valid=False, icon=icons.REMINDER) workflow().add_item('30 minutes', arg='-pref reminder_today 30m', valid=True, icon=icons.REMINDER) workflow().add_item('1 hour', '(default)', arg='-pref reminder_today 1h', valid=True, icon=icons.REMINDER) workflow().add_item('90 minutes', arg='-pref reminder_today 90m', valid=True, icon=icons.REMINDER) workflow().add_item( 'Always use the default reminder time', 'Avoids adjusting the reminder based on the current date', arg='-pref reminder_today disabled', valid=True, icon=icons.CANCEL) workflow().add_item('Cancel', autocomplete='-pref', icon=icons.BACK) elif 'default_folder' in args: taskfolders = workflow().stored_data('taskfolders') matching_taskfolders = taskfolders if len(args) > 2: taskfolder_query = ' '.join(args[2:]) if taskfolder_query: matching_taskfolders = workflow().filter( taskfolder_query, taskfolders, lambda f: f['title'], # Ignore MATCH_ALLCHARS which is expensive and inaccurate match_on=MATCH_ALL ^ MATCH_ALLCHARS) for i, f in enumerate(matching_taskfolders): if i == 1: workflow().add_item( 'Most recently used folder', 'Default to the last folder to which a task was added', arg='-pref default_folder %s' % DEFAULT_TASKFOLDER_MOST_RECENT, valid=True, icon=icons.RECURRENCE) icon = icons.INBOX if f['isDefaultFolder'] else icons.LIST workflow().add_item(f['title'], arg='-pref default_folder %s' % f['id'], valid=True, icon=icon) workflow().add_item('Cancel', autocomplete='-pref', icon=icons.BACK) else: current_user = None taskfolders = workflow().stored_data('taskfolders') loc = user_locale() default_folder_name = 'Tasks' try: current_user = User.get() except User.DoesNotExist: pass except OperationalError: from mstodo.sync import background_sync background_sync() if prefs.default_taskfolder_id == DEFAULT_TASKFOLDER_MOST_RECENT: default_folder_name = 'Most recent folder' else: default_taskfolder_id = prefs.default_taskfolder_id default_folder_name = next( (f['title'] for f in taskfolders if f['id'] == default_taskfolder_id), 'Tasks') if current_user and current_user.name: workflow().add_item('Sign out', 'You are logged in as ' + current_user.name, autocomplete='-logout', icon=icons.CANCEL) workflow().add_item('Show completed tasks', 'Includes completed tasks in search results', arg='-pref show_completed_tasks', valid=True, icon=icons.TASK_COMPLETED if prefs.show_completed_tasks else icons.TASK) workflow().add_item( 'Default reminder time', u'⏰ %s Reminders without a specific time will be set to this time' % format_time(prefs.reminder_time, 'short'), autocomplete='-pref reminder ', icon=icons.REMINDER) workflow().add_item( 'Default reminder when due today', u'⏰ %s Default reminder time for tasks due today is %s' % (_format_time_offset(prefs.reminder_today_offset), 'relative to the current time' if prefs.reminder_today_offset else 'always %s' % format_time(prefs.reminder_time, 'short')), autocomplete='-pref reminder_today ', icon=icons.REMINDER) workflow().add_item( 'Default folder', u'%s Change the default folder when creating new tasks' % default_folder_name, autocomplete='-pref default_folder ', icon=icons.LIST) workflow().add_item( 'Automatically set a reminder on the due date', u'Sets a default reminder for tasks with a due date.', arg='-pref automatic_reminders', valid=True, icon=icons.TASK_COMPLETED if prefs.automatic_reminders else icons.TASK) if loc != 'en_US' or prefs.date_locale: workflow().add_item('Force US English for dates', 'Rather than the current locale (%s)' % loc, arg='-pref force_en_US', valid=True, icon=icons.TASK_COMPLETED if prefs.date_locale == 'en_US' else icons.TASK) workflow().add_item( 'Require explicit due keyword', 'Requires the due keyword to avoid accidental due date extraction', arg='-pref explicit_keywords', valid=True, icon=icons.TASK_COMPLETED if prefs.explicit_keywords else icons.TASK) workflow().add_item( 'Check for experimental updates to this workflow', 'The workflow automatically checks for updates; enable this to include pre-releases', arg=':pref prerelease_channel', valid=True, icon=icons.TASK_COMPLETED if prefs.prerelease_channel else icons.TASK) workflow().add_item( 'Force sync', 'The workflow syncs automatically, but feel free to be forcible.', arg='-pref sync', valid=True, icon=icons.SYNC) workflow().add_item('Switch theme', 'Toggle between light and dark icons', arg='-pref retheme', valid=True, icon=icons.PAINTBRUSH) workflow().add_item('Main menu', autocomplete='', icon=icons.BACK)
def prerelease_channel(self): return workflow().settings.get(PRERELEASES_KEY, False)
def filter(args): task = _task(args) subtitle = task_subtitle(task) wf = workflow() matching_hashtags = [] if not task.title: subtitle = 'Begin typing to add a new task' # Preload matching hashtags into a list so that we can get the length if task.has_hashtag_prompt: from mstodo.models.hashtag import Hashtag hashtags = Hashtag.select().where( Hashtag.id.contains(task.hashtag_prompt.lower())).order_by( fn.Lower(Hashtag.tag).asc()) for hashtag in hashtags: matching_hashtags.append(hashtag) # Show hashtag prompt if there is more than one matching hashtag or the # hashtag being typed does not exactly match the single matching hashtag if task.has_hashtag_prompt and len(matching_hashtags) > 0 and ( len(matching_hashtags) > 1 or task.hashtag_prompt != matching_hashtags[0].tag): for hashtag in matching_hashtags: wf.add_item(hashtag.tag[1:], '', autocomplete=' ' + task.phrase_with(hashtag=hashtag.tag) + ' ', icon=icons.HASHTAG) elif task.has_list_prompt: taskfolders = wf.stored_data('taskfolders') if taskfolders: for taskfolder in taskfolders: # Show some full list names and some concatenated in command # suggestions sample_command = taskfolder['title'] if random() > 0.5: sample_command = sample_command[:int( len(sample_command) * .75)] icon = icons.INBOX if taskfolder[ 'isDefaultFolder'] else icons.LIST wf.add_item(taskfolder['title'], 'Assign task to this folder, e.g. %s: %s' % (sample_command.lower(), task.title), autocomplete=' ' + task.phrase_with(list_title=taskfolder['title']), icon=icon) wf.add_item('Remove folder', 'Tasks without a folder are added to the Inbox', autocomplete=' ' + task.phrase_with(list_title=False), icon=icons.CANCEL) elif is_running('sync'): wf.add_item('Your folders are being synchronized', 'Please try again in a few moments', autocomplete=' ' + task.phrase_with(list_title=False), icon=icons.BACK) # Task has an unfinished recurrence phrase elif task.has_recurrence_prompt: wf.add_item('Every month', 'Same day every month, e.g. every mo', uid="recurrence_1m", autocomplete=' %s ' % task.phrase_with(recurrence='every month'), icon=icons.RECURRENCE) wf.add_item('Every week', 'Same day every week, e.g. every week, every Tuesday', uid="recurrence_1w", autocomplete=' %s ' % task.phrase_with(recurrence='every week'), icon=icons.RECURRENCE) wf.add_item('Every year', 'Same date every year, e.g. every 1 y, every April 15', uid="recurrence_1y", autocomplete=' %s ' % task.phrase_with(recurrence='every year'), icon=icons.RECURRENCE) wf.add_item('Every 3 months', 'Same day every 3 months, e.g. every 3 months', uid="recurrence_3m", autocomplete=' %s ' % task.phrase_with(recurrence='every 3 months'), icon=icons.RECURRENCE) wf.add_item('Remove recurrence', autocomplete=' ' + task.phrase_with(recurrence=False), icon=icons.CANCEL) # Task has an unfinished due date phrase elif task.has_due_date_prompt: wf.add_item('Today', 'e.g. due today', autocomplete=' %s ' % task.phrase_with(due_date='due today'), icon=icons.TODAY) wf.add_item('Tomorrow', 'e.g. due tomorrow', autocomplete=' %s ' % task.phrase_with(due_date='due tomorrow'), icon=icons.TOMORROW) wf.add_item('Next Week', 'e.g. due next week', autocomplete=' %s ' % task.phrase_with(due_date='due next week'), icon=icons.NEXT_WEEK) wf.add_item('Next Month', 'e.g. due next month', autocomplete=' %s ' % task.phrase_with(due_date='due next month'), icon=icons.CALENDAR) wf.add_item('Next Year', 'e.g. due next year, due April 15', autocomplete=' %s ' % task.phrase_with(due_date='due next year'), icon=icons.CALENDAR) wf.add_item('Remove due date', 'Add "not due" to fix accidental dates, or see td-pref', autocomplete=' ' + task.phrase_with(due_date=False), icon=icons.CANCEL) # Task has an unfinished reminder phrase elif task.has_reminder_prompt: prefs = Preferences.current_prefs() default_reminder_time = format_time(prefs.reminder_time, 'short') due_date_hint = ' on the due date' if task.due_date else '' wf.add_item( 'Reminder at %s%s' % (default_reminder_time, due_date_hint), 'e.g. r %s' % default_reminder_time, autocomplete=' %s ' % task.phrase_with(reminder_date='remind me at %s' % format_time(prefs.reminder_time, 'short')), icon=icons.REMINDER) wf.add_item('At noon%s' % due_date_hint, 'e.g. reminder noon', autocomplete=' %s ' % task.phrase_with(reminder_date='remind me at noon'), icon=icons.REMINDER) wf.add_item('At 8:00 PM%s' % due_date_hint, 'e.g. remind at 8:00 PM', autocomplete=' %s ' % task.phrase_with(reminder_date='remind me at 8:00pm'), icon=icons.REMINDER) wf.add_item('At dinner%s' % due_date_hint, 'e.g. alarm at dinner', autocomplete=' %s ' % task.phrase_with(reminder_date='remind me at dinner'), icon=icons.REMINDER) wf.add_item( 'Today at 6:00 PM', 'e.g. remind me today at 6pm', autocomplete=' %s ' % task.phrase_with(reminder_date='remind me today at 6:00pm'), icon=icons.REMINDER) wf.add_item('Remove reminder', autocomplete=' ' + task.phrase_with(reminder_date=False), icon=icons.CANCEL) # Main menu for tasks else: wf.add_item(task.list_title + u' – create a new task...', subtitle, modifier_subtitles={ 'alt': u'…then edit it in the ToDo app %s' % subtitle }, arg='--stored-query', valid=task.title != '', icon=icons.TASK) title = 'Change folder' if task.list_title else 'Select a folder' wf.add_item(title, 'Prefix the task, e.g. Automotive: ' + task.title, autocomplete=' ' + task.phrase_with(list_title=True), icon=icons.LIST) title = 'Change the due date' if task.due_date else 'Set a due date' wf.add_item( title, '"due" followed by any date-related phrase, e.g. due next Tuesday; due May 4', autocomplete=' ' + task.phrase_with(due_date=True), icon=icons.CALENDAR) title = 'Change the recurrence' if task.recurrence_type else 'Make it a recurring task' wf.add_item( title, '"every" followed by a unit of time, e.g. every 2 months; every year; every 4w', autocomplete=' ' + task.phrase_with(recurrence=True), icon=icons.RECURRENCE) title = 'Change the reminder' if task.reminder_date else 'Set a reminder' wf.add_item( title, '"remind me" followed by a time and/or date, e.g. remind me at noon; r 10am; alarm 8:45p', autocomplete=' ' + task.phrase_with(reminder_date=True), icon=icons.REMINDER) if task.starred: wf.add_item('Remove star', 'Remove * from the task', autocomplete=' ' + task.phrase_with(starred=False), icon=icons.STAR_REMOVE) else: wf.add_item('Star', 'End the task with * (asterisk)', autocomplete=' ' + task.phrase_with(starred=True), icon=icons.STAR) wf.add_item('Main menu', autocomplete='', icon=icons.BACK)
def prerelease_channel(self, prerelease_channel): workflow().settings[PRERELEASES_KEY] = prerelease_channel
#!/usr/bin/python # encoding: utf-8 import logging from logging.config import fileConfig import sys # fileConfig('/Users/johan/Documents/Programming/Alfred workflows/alfred-mstodo-workflow/src/logging_config.ini') #@TODO switch this before pushing to Github fileConfig('logging_config.ini') from mstodo.handlers.route import route from mstodo.util import workflow log = logging.getLogger('mstodo') def main(wf): route(wf.args) log.info('Workflow response complete') if __name__ == '__main__': wf = workflow() sys.exit(wf.run(main, text_errors='--commit' in wf.args))
def current_prefs(cls): if not cls._current_prefs: cls._current_prefs = Preferences(workflow().stored_data('prefs')) if not cls._current_prefs: cls._current_prefs = Preferences({}) return cls._current_prefs