diff --git a/cmdasana.py b/cmdasana.py index cfc9bf5..b604a8e 100755 --- a/cmdasana.py +++ b/cmdasana.py @@ -3,6 +3,7 @@ import os import sys import json +from threading import Thread import urwid import asana @@ -16,6 +17,8 @@ from secrets import CLIENT_ID, CLIENT_SECRET PERSONAL = 498346170860 class CmdAsana: + loop = None + def __init__(self): try: f = open(".oauth", "r") @@ -110,30 +113,73 @@ class CmdAsana: self.client.tasks.update(task_id, completed=True) def newTask(self, task_after_id): - if self.state['view'] == 'project': - task = self.client.tasks.create_in_workspace( - self.state['workspace_id'], - projects=[self.state['id']] - ) - if task_after_id != None: - self.client.tasks.add_project(task['id'], - project=self.state['id'], - insert_after=task_after_id) - else: - task = self.client.tasks.create_in_workspace( - self.state['workspace_id'], - assignee=self.me['id'] - ) + def runInThread(): + if self.state['view'] == 'project': + task = self.client.tasks.create_in_workspace( + self.state['workspace_id'], + projects=[self.state['id']] + ) + if task_after_id != None: + self.client.tasks.add_project(task['id'], + project=self.state['id'], + insert_after=task_after_id) + else: + task = self.client.tasks.create_in_workspace( + self.state['workspace_id'], + assignee=self.me['id'] + ) - task_list,_ = self.frame.contents[1] - task_list.insertNewTask(task) + update(task) + + def update(task): + task_list,_ = self.frame.contents[1] + task_list.insertNewTask(task) + + thread = Thread(target=runInThread) + thread.start() def updateTask(self, task_id, name): - self.client.tasks.update(task_id, name=name) + def runInThread(): + self.client.tasks.update(task_id, name=name) + + thread = Thread(target=runInThread) + thread.start() + + def updateDescription(self, task_id, description): + def runInThread(): + self.client.tasks.update(task_id, notes=description) + + thread = Thread(target=runInThread) + thread.start() + + def assignTask(self, task_id, user_id): + def runInThread(): + self.client.tasks.update(task_id, assignee=user_id) + + thread = Thread(target=runInThread) + thread.start() def addComment(self, task_id, comment): - self.client.stories.create_on_task(task_id, {"text": comment}) - self.showDetails(task_id) + def runInThread(): + self.client.stories.create_on_task(task_id, {"text": comment}) + self.showDetails(task_id, show_loading=False) + + thread = Thread(target=runInThread) + thread.start() + + def userTypeAhead(self, text, callback): + def runInThread(): + users = self.client.workspaces.typeahead(self.state['workspace_id'], + { + 'type': 'user', + 'query': text, + 'count': 5 + }) + callback(users) + self.loop.draw_screen() + + thread = Thread(target=runInThread) + thread.start() def replaceBody(self, widget): old_widget,_ = self.frame.contents.pop() @@ -141,15 +187,31 @@ class CmdAsana: self.clearSignals(old_widget) self.frame.contents.append((widget, self.frame.options())) self.frame.focus_position = 0 + if self.loop != None: + self.loop.draw_screen() + + def showMainLoading(self): + text = urwid.Text(('loading', '[loading...]')) + self.replaceBody(urwid.Filler(text)) def showMyTasks(self, workspace_id): self.state['view'] = 'atm' self.state['id'] = workspace_id self.state['workspace_id'] = workspace_id - task_list = ui.TaskList(self.allMyTasks(workspace_id)) - self.connectTaskListSignals(task_list) - self.replaceBody(task_list) + self.showMainLoading() + + def runInThread(): + tasks = self.allMyTasks(workspace_id) + update(tasks) + + def update(tasks): + task_list = ui.TaskList(tasks) + self.connectTaskListSignals(task_list) + self.replaceBody(task_list) + + thread = Thread(target=runInThread) + thread.start() def showProject(self, project_id): if project_id == None: @@ -157,60 +219,110 @@ class CmdAsana: self.state['view'] = 'project' self.state['id'] = project_id - task_list = ui.TaskList(self.projectTasks(project_id)) - self.connectTaskListSignals(task_list) - self.replaceBody(task_list) + self.showMainLoading() + + def runInThread(): + tasks = self.projectTasks(project_id) + update(tasks) + + def update(tasks): + task_list = ui.TaskList(tasks) + self.connectTaskListSignals(task_list) + self.replaceBody(task_list) + + thread = Thread(target=runInThread) + thread.start() def showProjectList(self, workspace_id): self.state['view'] = 'workspace' self.state['id'] = workspace_id self.state['workspace_id'] = workspace_id - self.workspace_id = workspace_id - project_list = ui.ProjectList(self.allMyProjects()) - urwid.connect_signal(project_list, 'loadproject', self.showProject) - self.replaceBody(project_list) + + self.showMainLoading() + + def showDetails(self, task_id): + + def runInThread(): + projects = self.allMyProjects() + update(projects) + + def update(projects): + project_list = ui.ProjectList(projects) + urwid.connect_signal(project_list, 'loadproject', self.showProject) + self.replaceBody(project_list) + + thread = Thread(target=runInThread) + thread.start() def loadProjectSearch(self): #callback somehow? - def showDetails(self, task_id): + + def showDetails(self, task_id, show_loading=True): self.state['view'] = 'details' self.state['id'] = task_id - task = self.client.tasks.find_by_id(task_id) - stories = self.client.stories.find_by_task(task_id) - task_details = ui.TaskDetails(task, stories) - urwid.connect_signal(task_details, 'comment', self.addComment) - urwid.connect_signal(task_details, 'loadproject', self.showProject) - self.replaceBody(task_details) + if show_loading: + self.showMainLoading() + + def runInThread(): + task = self.client.tasks.find_by_id(task_id) + stories = self.client.stories.find_by_task(task_id) + update(task, stories) + + def update(task, stories): + task_details = ui.TaskDetails(task, stories) + self.connectDetailsSignals(task_details) + self.replaceBody(task_details) + + thread = Thread(target=runInThread) + thread.start() def registerSignals(self): urwid.register_signal(ui.TaskList, [ 'complete', 'newtask', 'updatetask', - 'details' + 'details', ]) urwid.register_signal(ui.TaskEdit, [ 'complete', 'newtask', 'updatetask', - 'details' + 'details', + ]) + + urwid.register_signal(ui.TaskDetails, [ + 'comment', + 'loadproject', + 'updatedescription', + 'updatetask', + 'usertypeahead', + 'assigntask', + ]) + + urwid.register_signal(ui.AssigneeTypeAhead, [ + 'usertypeahead', + 'assigntask', ]) - urwid.register_signal(ui.TaskDetails, ['comment', 'loadproject']) urwid.register_signal(ui.CommentEdit, ['comment']) - + urwid.register_signal(ui.DescriptionEdit, ['updatedescription']) + urwid.register_signal(ui.TaskNameEdit, 'updatetask') urwid.register_signal(ui.WorkspaceMenu, 'click') - urwid.register_signal(ui.ProjectList, 'loadproject') + def clearSignals(self, widget): urwid.disconnect_signal(widget, 'complete', self.completeTask) urwid.disconnect_signal(widget, 'newtask', self.newTask) urwid.disconnect_signal(widget, 'updatetask', self.updateTask) urwid.disconnect_signal(widget, 'details', self.showDetails) + urwid.disconnect_signal(widget, 'updatedescription', + self.updateDescription) + urwid.disconnect_signal(widget, 'updatetask', self.updateTask) + urwid.disconnect_signal(widget, 'usertypeahead', self.userTypeAhead) def connectTaskListSignals(self, task_list): urwid.connect_signal(task_list, 'complete', self.completeTask) @@ -218,6 +330,15 @@ class CmdAsana: urwid.connect_signal(task_list, 'updatetask', self.updateTask) urwid.connect_signal(task_list, 'details', self.showDetails) + def connectDetailsSignals(self, task_details): + urwid.connect_signal(task_details, 'comment', self.addComment) + urwid.connect_signal(task_details, 'loadproject', self.showProject) + urwid.connect_signal(task_details, 'updatedescription', + self.updateDescription) + urwid.connect_signal(task_details, 'updatetask', self.updateTask) + urwid.connect_signal(task_details, 'usertypeahead', self.userTypeAhead) + urwid.connect_signal(task_details, 'assigntask', self.assignTask) + def handleInput(self, key): if key in ('q', 'Q'): raise urwid.ExitMainLoop() @@ -244,11 +365,11 @@ class CmdAsana: else: raise KeyError - loop = urwid.MainLoop(self.frame, + self.loop = urwid.MainLoop(self.frame, unhandled_input=self.handleInput, palette=ui.palette ) - loop.run() + self.loop.run() def main(): cmdasana = CmdAsana() diff --git a/ui.py b/ui.py index e75961b..2d0f0b0 100644 --- a/ui.py +++ b/ui.py @@ -10,6 +10,7 @@ palette = [ ('selected', 'standout', ''), ('selected workspace', 'standout,bold', ''), ('header', 'bold,light green', ''), + ('secondary', 'light gray', ''), ('task', 'light green', ''), ('section', 'white', 'dark green'), ('workspace', 'white', 'dark blue'), @@ -48,6 +49,11 @@ class PagerButton(urwid.Button): super(PagerButton, self).__init__(('pager', 'load more')) urwid.connect_signal(self, 'click', loadPage) +class TypeAheadButton(urwid.Button): + def __init__(self, item, onClick): + super(TypeAheadButton, self).__init__(item['name']) + urwid.connect_signal(self, 'click', onClick, item) + class ProjectIcon(urwid.SelectableIcon): def __init__(self, project, onClick): self.project = project @@ -210,13 +216,71 @@ class TaskEdit(urwid.Edit): class CommentEdit(urwid.Edit): def __init__(self, task): self.task = task - super(CommentEdit, self).__init__('Add a comment:\n') + super(CommentEdit, self).__init__(('secondary', u'Add a comment:\n')) def keypress(self, size, key): if key != 'enter': return super(CommentEdit, self).keypress(size, key) urwid.emit_signal(self, 'comment', self.task['id'], self.edit_text) +class TaskNameEdit(urwid.Edit): + def __init__(self, task): + self.task = task + super(TaskNameEdit, self).__init__(('secondary', + u'#' + str(task['id']) + ' '), + task['name']) + + def keypress(self, size, key): + if key in ('enter', 'esc', 'up', 'down'): + if (self.edit_text != self.task['name']): + urwid.emit_signal(self, 'updatetask', self.task['id'], + self.edit_text) + return super(TaskNameEdit, self).keypress(size, key) + +class DescriptionEdit(urwid.Edit): + def __init__(self, task): + self.task = task + super(DescriptionEdit, self).__init__(('secondary', u'Description:\n'), + task['notes'], + multiline=True) + + def keypress(self, size, key): + if key != 'esc': + return super(DescriptionEdit, self).keypress(size, key) + urwid.emit_signal(self, 'updatedescription', self.task['id'], + self.edit_text) + +class AssigneeTypeAhead(urwid.Pile): + def __init__(self, task): + self.task = task + + if task['assignee'] != None: + assignee = task['assignee']['name'] + else: + assignee = "" + + self.edit = urwid.Edit('Assignee: ', assignee) + urwid.connect_signal(self.edit, 'change', self.typeAhead) + + body = [('pack', self.edit)] + + super(AssigneeTypeAhead, self).__init__(body) + + def typeAhead(self, widget, text): + urwid.emit_signal(self, 'usertypeahead', text, self.updateTypeAhead) + + def updateTypeAhead(self, users): + users = [(TypeAheadButton(u, self.assign), ('pack', None)) for u in users] + + users.insert(0, self.contents[0]) + + self.contents = users + + def assign(self, widget, user): + urwid.emit_signal(self, 'assigntask', self.task['id'], user['id']) + self.contents = [self.contents[0]] + self.edit.set_edit_text(user['name']) + class TaskDetails(urwid.Pile): def __init__(self, task, stories): self.task = task @@ -225,23 +289,29 @@ class TaskDetails(urwid.Pile): comment_edit = CommentEdit(task) urwid.connect_signal(comment_edit, 'comment', self.comment) + self.description_edit = DescriptionEdit(task) + urwid.connect_signal(self.description_edit, 'updatedescription', + self.updateDescription) + + task_name_edit = TaskNameEdit(task) + urwid.connect_signal(task_name_edit, 'updatetask', self.updateTask) + + assignee_type_ahead = AssigneeTypeAhead(task) + urwid.connect_signal(assignee_type_ahead, 'usertypeahead', + self.userTypeAhead) + urwid.connect_signal(assignee_type_ahead, 'assigntask', self.assignTask) + projects = [('pack', ProjectIcon(project, self.loadProject)) for project in task['projects']] - if task['assignee']: - assignee = urwid.Text('Assigned to: ' + task['assignee']['name']) - else: - assignee = urwid.Text('(not assigned)') - - body = projects + \ [ ('pack', urwid.Divider('=')), - ('pack', urwid.Text(('header', task['name'] + \ - " #" + str(task['id'])))), - ('pack', assignee), + ('pack', task_name_edit), + ('pack', assignee_type_ahead), + ('pack', urwid.Divider('-')), + ('pack', self.description_edit), ('pack', urwid.Divider('-')), - ('pack', urwid.Text(task['notes'])), ] + \ [('pack', urwid.Text('[' + story['created_by']['name'] + '] ' + \ story['text'])) for story in stories] + \ @@ -251,8 +321,30 @@ class TaskDetails(urwid.Pile): super(TaskDetails, self).__init__(body) + def keypress(self, size, key): + key = super(TaskDetails, self).keypress(size, key) + + if self.focus != self.description_edit and \ + self.description_edit.edit_text != self.task['notes']: + self.updateDescription(self.task['id'], + self.description_edit.edit_text) + + return key + def comment(self, task_id, comment): urwid.emit_signal(self, 'comment', task_id, comment) + def updateDescription(self, task_id, description): + urwid.emit_signal(self, 'updatedescription', task_id, description) + + def updateTask(self, task_id, name): + urwid.emit_signal(self, 'updatetask', task_id, name) + def loadProject(self, project_id): urwid.emit_signal(self, 'loadproject', project_id) + + def userTypeAhead(self, text, callback): + urwid.emit_signal(self, 'usertypeahead', text, callback) + + def assignTask(self, task_id, user_id): + urwid.emit_signal(self, 'assigntask', task_id, user_id)