Repository: fniessink/next-action

Issue:
Allow for repeating --file
Show which file the next action was selected from somehow, e.g.:

```console
$ next-action --file work.txt home.txt
(A) Fix leaking roof [home.txt]
```

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9bbe07a..270239d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 ## [Unreleased]
 
+### Added
+
+- Next-action can now read multiple todo.txt files to select the next action from. For example: `next-action --file todo.txt --file big-project-todo.txt`. Closes #35.
+
 ### Changed
 
 - Ignore tasks that have a start date in the future. Closes #34.
diff --git a/README.md b/README.md
index 3dfda58..4bc5ef1 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ $ next-action
 
 The next action is determined using priority. Creation date is considered after priority, with older tasks getting precedence over newer tasks.
 
-Completed tasks (<del>`x This is a completed task`</del>) and tasks with a creation date in the future (`9999-01-01 Start preparing for five-digit years`) are not considered when determining the next action.
+Completed tasks (~~`x This is a completed task`~~) and tasks with a creation date in the future (`9999-01-01 Start preparing for five-digit years`) are not considered when determining the next action.
 
 ### Limit next actions
 
diff --git a/next_action/arguments.py b/next_action/arguments.py
index ec87cb1..adc90f4 100644
--- a/next_action/arguments.py
+++ b/next_action/arguments.py
@@ -48,7 +48,9 @@ def parse_arguments() -> argparse.Namespace:
     parser = argparse.ArgumentParser(description="Show the next action in your todo.txt",
                                      formatter_class=argparse.ArgumentDefaultsHelpFormatter)
     parser.add_argument("--version", action="version", version="%(prog)s {0}".format(next_action.__version__))
-    parser.add_argument("-f", "--file", help="filename of the todo.txt file to read", type=str, default="todo.txt")
+    default_filenames = ["todo.txt"]
+    parser.add_argument("-f", "--file", help="todo.txt file to read; argument can be repeated", type=str,
+                        action="append", dest="filenames", metavar="FILE", default=default_filenames)
     group = parser.add_mutually_exclusive_group()
     group.add_argument("-n", "--number", metavar="N", help="number of next actions to show", type=int, default=1)
     group.add_argument("-a", "--all", help="show all next actions", action="store_true")
@@ -57,6 +59,14 @@ def parse_arguments() -> argparse.Namespace:
     parser.add_argument("projects", metavar="+PROJECT", help="show the next action for the specified projects",
                         nargs="*", type=str, default=None, action=ContextProjectAction)
     namespace = parser.parse_args()
+    # Work around the issue that the "append" action doesn't overwrite defaults.
+    # See https://bugs.python.org/issue16399.
+    if default_filenames != namespace.filenames:
+        for default_filename in default_filenames:
+            namespace.filenames.remove(default_filename)
+    # Remove duplicate filenames while maintaining order.
+    namespace.filenames = list(dict.fromkeys(namespace.filenames))
+    # If the user wants to see all next actions, set the number to something big.
     if namespace.all:
         namespace.number = sys.maxsize
     return namespace
diff --git a/next_action/cli.py b/next_action/cli.py
index 93a7d57..e0c7f41 100644
--- a/next_action/cli.py
+++ b/next_action/cli.py
@@ -10,17 +10,18 @@ def next_action() -> None:
 
         Basic recipe:
         1) parse command-line arguments,
-        2) read todo.txt file,
+        2) read todo.txt file(s),
         3) determine the next action(s) and display them.
     """
     arguments = parse_arguments()
-    filename: str = arguments.file
-    try:
-        todotxt_file = open(filename, "r")
-    except FileNotFoundError:
-        print("Can't find {0}".format(filename))
-        return
-    with todotxt_file:
-        tasks = [Task(line.strip()) for line in todotxt_file.readlines() if line.strip()]
+    tasks = []
+    for filename in arguments.filenames:
+        try:
+            todotxt_file = open(filename, "r")
+        except FileNotFoundError:
+            print("Can't find {0}".format(filename))
+            return
+        with todotxt_file:
+            tasks.extend([Task(line.strip()) for line in todotxt_file.readlines() if line.strip()])
     actions = next_actions(tasks, set(arguments.contexts), set(arguments.projects))
     print("\n".join(action.text for action in actions[:arguments.number]) if actions else "Nothing to do!")


Test patch:
diff --git a/tests/unittests/test_arguments.py b/tests/unittests/test_arguments.py
index 6fa5f60..ac314cd 100644
--- a/tests/unittests/test_arguments.py
+++ b/tests/unittests/test_arguments.py
@@ -17,17 +17,32 @@ class ArgumentParserTest(unittest.TestCase):
     @patch.object(sys, "argv", ["next-action"])
     def test_default_filename(self):
         """ Test that the argument parser has a default filename. """
-        self.assertEqual("todo.txt", parse_arguments().file)
+        self.assertEqual(["todo.txt"], parse_arguments().filenames)
 
     @patch.object(sys, "argv", ["next-action", "-f", "my_todo.txt"])
     def test_filename_argument(self):
         """ Test that the argument parser accepts a filename. """
-        self.assertEqual("my_todo.txt", parse_arguments().file)
+        self.assertEqual(["my_todo.txt"], parse_arguments().filenames)
 
     @patch.object(sys, "argv", ["next-action", "--file", "my_other_todo.txt"])
     def test_long_filename_argument(self):
         """ Test that the argument parser accepts a filename. """
-        self.assertEqual("my_other_todo.txt", parse_arguments().file)
+        self.assertEqual(["my_other_todo.txt"], parse_arguments().filenames)
+
+    @patch.object(sys, "argv", ["next-action", "-f", "todo.txt"])
+    def test_add_default_filename(self):
+        """ Test that adding the default filename doesn't get it included twice. """
+        self.assertEqual(["todo.txt"], parse_arguments().filenames)
+
+    @patch.object(sys, "argv", ["next-action", "-f", "todo.txt", "-f", "other.txt"])
+    def test_default_and_non_default(self):
+        """ Test that adding the default filename and another filename gets both included. """
+        self.assertEqual(["todo.txt", "other.txt"], parse_arguments().filenames)
+
+    @patch.object(sys, "argv", ["next-action", "-f", "other.txt", "-f", "other.txt"])
+    def test__add_filename_twice(self):
+        """ Test that adding the same filename twice includes it only once. """
+        self.assertEqual(["other.txt"], parse_arguments().filenames)
 
     @patch.object(sys, "argv", ["next-action"])
     def test_no_context(self):
diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py
index 1b80920..7c7d92a 100644
--- a/tests/unittests/test_cli.py
+++ b/tests/unittests/test_cli.py
@@ -71,7 +71,7 @@ positional arguments:
 optional arguments:
   -h, --help            show this help message and exit
   --version             show program's version number and exit
-  -f FILE, --file FILE  filename of the todo.txt file to read (default: todo.txt)
+  -f FILE, --file FILE  todo.txt file to read; argument can be repeated (default: ['todo.txt'])
   -n N, --number N      number of next actions to show (default: 1)
   -a, --all             show all next actions (default: False)
 """),