def main(argv): """Display a schedule report for taskwarrior.""" parser = argparse.ArgumentParser( description="""Display a schedule report for taskwarrior.""") parser.add_argument('-r', '--refresh', help="refresh every n seconds", type=int, default=1) parser.add_argument('--from', help="scheduled from date: ex. 'today', 'tomorrow'", type=str, dest='after') parser.add_argument('--until', help="scheduled until date: ex. 'today', 'tomorrow'", type=str, dest='before') parser.add_argument('-s', '--scheduled', help="""scheduled date: ex. 'today', 'tomorrow' (overrides --from and --until)""", type=str) parser.add_argument('-a', '--all', help="show all hours, even if empty", action='store_true', default=False) parser.add_argument('-c', '--completed', help="hide completed tasks", action='store_false', default=True) parser.add_argument('-p', '--project', help="hide project column", action='store_true', default=False) args = parser.parse_args(argv) hide_empty = not args.all if args.scheduled: if args.before or args.after: print('Error: The --scheduled option can not be used together ' 'with --until and/or --from.') sys.exit(1) else: if args.before and not args.after or not args.before and args.after: print('Error: Either both --until and --from or neither options ' 'must be used.') sys.exit(1) if not args.before and not args.after: args.scheduled = 'today' elif not args.before: args.before = 'tomorrow' elif not args.after: args.after = 'yesterday' screen = Screen(hide_empty=hide_empty, scheduled_before=args.before, scheduled_after=args.after, scheduled=args.scheduled, completed=args.completed, hide_projects=args.project) last_refresh_time = 0 try: while True: key = screen.stdscr.getch() if key == 113: break if (key == KEY_RESIZE or time.time() > last_refresh_time + args.refresh): last_refresh_time = time.time() screen.refresh_buffer() screen.draw() napms(50) except KeyboardInterrupt: pass finally: screen.close()
class ScreenTest(unittest.TestCase): def setUp(self): self.taskrc_path = "tests/test_data/.taskrc" self.task_dir_path = "tests/test_data/.task" self.assertEqual(os.path.isdir(self.taskrc_path), False) self.assertEqual(os.path.isdir(self.task_dir_path), False) # Create a sample .taskrc with open(self.taskrc_path, "w+") as file: file.write("# User Defined Attributes\n") file.write("uda.estimate.type=duration\n") file.write("uda.estimate.label=Est\n") file.write("# User Defined Attributes\n") file.write("uda.tb_estimate.type=numeric\n") file.write("uda.tb_estimate.label=Est\n") file.write("uda.tb_real.type=numeric\n") file.write("uda.tb_real.label=Real\n") # Create a sample empty .task directory os.makedirs(self.task_dir_path) self.screen = Screen( tw_data_dir=self.task_dir_path, taskrc_location=self.taskrc_path, scheduled="today", ) def tearDown(self): try: os.remove(self.taskrc_path) except FileNotFoundError: pass try: shutil.rmtree(self.task_dir_path) except FileNotFoundError: pass # Attempt to gracefully quit curses mode if it is active # to prevent messing up terminal try: self.screen.close() except: try: curses.endwin() except: pass # TODO Move to /tests/functional/ # def test_screen_refresh_buffer_hide_empty_lines(self): # self.assertEqual(self.screen.buffer, []) # taskwarrior = TaskWarrior( # data_location=self.task_dir_path, # create=True, # taskrc_location=self.taskrc_path) # Task(taskwarrior, description='test_yesterday', # schedule='yesterday', estimate='20min').save() # Task(taskwarrior, description='test_9:00_to_10:11', # schedule='today+9hr', estimate='71min', project='test').save() # self.screen.hide_empty = False # self.screen.refresh_buffer() # self.assertEqual(len(self.screen.buffer), 61) # TODO Move to /tests/functional/ # def test_screen_refresh_buffer_first_time_fills_buffer(self): # self.assertEqual(self.screen.buffer, []) # taskwarrior = TaskWarrior( # data_location=self.task_dir_path, # create=True, # taskrc_location=self.taskrc_path) # Task(taskwarrior, description='test_yesterday', # schedule='yesterday', estimate='20min').save() # Task(taskwarrior, description='test_9:00_to_10:11', # schedule='today+9hr', estimate='71min', project='test').save() # self.screen.refresh_buffer() # self.assertEqual(len(self.screen.buffer), 15) # TODO Move to /tests/functional/ # def test_screen_refresh_buffer_no_tasks(self): # self.assertEqual(self.screen.buffer, []) # self.screen.refresh_buffer() # self.assertEqual(self.screen.buffer, []) def test_screen_draw_no_tasks_to_display(self): self.screen.draw() def test_screen_draw(self): taskwarrior = TaskWarrior( data_location=self.task_dir_path, create=True, taskrc_location=self.taskrc_path, ) Task( taskwarrior, description="test_yesterday", schedule="yesterday", estimate="20min", ).save() Task( taskwarrior, description="test_9:00_to_10:11", schedule="today+9hr", estimate="71min", project="test", ).save() self.screen.draw() self.screen.refresh_buffer() Task( taskwarrior, description="test_14:00_to_16:00", schedule="today+14hr", estimate="2hr", ).save() time.sleep(0.1) self.screen.draw() self.screen.refresh_buffer() Task( taskwarrior, description="test_tomorrow", schedule="tomorrow", estimate="24min", ).save() time.sleep(0.1) self.screen.draw() self.screen.refresh_buffer() def test_screen_scroll_up_at_top_is_blocked(self): current_scroll_level = self.screen.scroll_level self.screen.scroll(-1) self.assertEqual(current_scroll_level, self.screen.scroll_level) def test_screen_scroll_down_and_up(self): current_scroll_level = self.screen.scroll_level self.screen.scroll(1) self.assertEqual(current_scroll_level + 1, self.screen.scroll_level) current_scroll_level = self.screen.scroll_level self.screen.scroll(-1) self.assertEqual(current_scroll_level - 1, self.screen.scroll_level)
class Main: def __init__(self, argv): self.home_dir = os.path.expanduser("~") self.parse_args(argv) self.check_files() task_command_args = ["task", "status.not:deleted"] task_command_args.append(f"scheduled.after:{self.scheduled_after}") task_command_args.append(f"scheduled.before:{self.scheduled_before}") if not self.show_completed: task_command_args.append(f"status.not:{self.show_completed}") self.backend = PatchedTaskWarrior( data_location=self.data_location, create=False, taskrc_location=self.taskrc_location, task_command=" ".join(task_command_args), ) self.schedule = Schedule( self.backend, scheduled_after=self.scheduled_after, scheduled_before=self.scheduled_before, ) def check_files(self): """Check if the required files, directories and settings are present.""" # Create a temporary taskwarrior instance to read the config taskwarrior = TaskWarrior( data_location=self.data_location, create=False, taskrc_location=self.taskrc_location, ) # Disable _forcecolor because it breaks tw config output taskwarrior.overrides.update({"_forcecolor": "off"}) # Check taskwarrior directory and taskrc if os.path.isdir(self.data_location) is False: raise TaskDirDoesNotExistError(".task directory not found") if os.path.isfile(self.taskrc_location) is False: raise TaskrcDoesNotExistError(".taskrc not found") # Check if required UDAs exist if taskwarrior.config.get("uda.estimate.type") is None: raise UDADoesNotExistError( ("uda.estimate.type does not exist " "in .taskrc") ) if taskwarrior.config.get("uda.estimate.label") is None: raise UDADoesNotExistError( ("uda.estimate.label does not exist " "in .taskrc") ) # Check sound file sound_file = self.home_dir + "/.taskschedule/hooks/drip.wav" if self.show_notifications and os.path.isfile(sound_file) is False: raise SoundDoesNotExistError( f"The specified sound file does not exist: {sound_file}" ) # Create user directory if it does not exist taskschedule_dir = self.home_dir + "/.taskschedule" hooks_directory = self.home_dir + "/.taskschedule/hooks" if not os.path.isdir(taskschedule_dir): os.mkdir(taskschedule_dir) if not os.path.isdir(hooks_directory): os.mkdir(hooks_directory) def parse_args(self, argv): parser = argparse.ArgumentParser( description="""Display a schedule report for taskwarrior.""" ) parser.add_argument( "-r", "--refresh", help="refresh every n seconds", type=int, default=1 ) parser.add_argument( "--from", help="scheduled from date: ex. 'today', 'tomorrow'", type=str, dest="after", default="today-1s", ) parser.add_argument( "--to", "--until", help="scheduled until date: ex. 'today', 'tomorrow'", type=str, dest="before", default="tomorrow", ) parser.add_argument( "-d", "--data-location", help="""data location (e.g. ~/.task)""", type=str, dest="data_location", default=f"{self.home_dir}/.task", ) parser.add_argument( "-t", "--taskrc-location", help="""taskrc location (e.g. ~/.taskrc)""", type=str, dest="taskrc_location", default=f"{self.home_dir}/.taskrc", ) parser.add_argument( "-a", "--all", help="show all hours, even if empty", action="store_true", default=False, ) parser.add_argument( "-c", "--completed", help="hide completed tasks", action="store_false", default=True, ) parser.add_argument( "-p", "--project", help="hide project column", action="store_true", default=False, ) parser.add_argument( "--no-notifications", help="disable notifications", action="store_false", default=True, dest="notifications", ) args = parser.parse_args(argv) if args.before and not args.after or not args.before and args.after: print( "Error: Either both --until and --from or neither options must be used." ) sys.exit(1) self.data_location = args.data_location self.taskrc_location = args.taskrc_location # Parse schedule date range self.scheduled_after: datetime = calculate_datetime(args.after) self.scheduled_before: datetime = calculate_datetime(args.before) self.show_completed = args.completed self.hide_empty = not args.all self.hide_projects = args.project self.refresh_rate = args.refresh self.show_notifications = args.notifications def main(self): """Initialize the screen and notifier, and start the main loop of the interface.""" if self.show_notifications: self.notifier = Notifier(self.backend) else: self.notifier = None self.screen = Screen( self.schedule, scheduled_after=self.scheduled_after, scheduled_before=self.scheduled_before, hide_empty=self.hide_empty, hide_projects=self.hide_projects, ) try: self.run() except TaskDirDoesNotExistError as err: print("Error: {}".format(err)) sys.exit(1) except TaskrcDoesNotExistError as err: print("Error: {}".format(err)) sys.exit(1) except KeyboardInterrupt: self.screen.close() except ValueError as err: self.screen.close() print("Error: {}".format(err)) sys.exit(1) except UDADoesNotExistError as err: self.screen.close() print("Error: {}".format(err)) sys.exit(1) except SoundDoesNotExistError as err: self.screen.close() print("Error: {}".format(err)) sys.exit(1) else: try: self.screen.close() except curses_error as err: print(err.with_traceback) def run(self): """The main loop of the interface.""" filename = f"{self.data_location}/pending.data" cached_stamp = 0.0 last_refresh_time = 0.0 while True: key = self.screen.stdscr.getch() if key == 113: # q break elif key == 65 or key == 107: # Up / k self.screen.scroll(-1) last_refresh_time = time.time() elif key == 66 or key == 106: # Down / j self.screen.scroll(1) last_refresh_time = time.time() elif key == 54: # Page down max_y, max_x = self.screen.get_maxyx() self.screen.scroll(max_y - 4) last_refresh_time = time.time() elif key == 53: # Page up max_y, max_x = self.screen.get_maxyx() self.screen.scroll(-(max_y - 4)) last_refresh_time = time.time() elif key == KEY_RESIZE: last_refresh_time = time.time() self.screen.refresh_buffer() self.screen.draw() elif time.time() > last_refresh_time + self.refresh_rate: if self.notifier: self.notifier.send_notifications() # Redraw if task data has changed stamp = os.stat(filename).st_mtime if stamp != cached_stamp: cached_stamp = stamp self.schedule.clear_cache() self.screen.refresh_buffer() self.screen.draw() last_refresh_time = time.time() napms(1) if self.refresh_rate < 0: break