diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..a7903fe --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.13.2-otp-24 +erlang 24.0.5 diff --git a/README.md b/README.md index d0a951f..4e6a4fe 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,4 @@ File.write!("done.txt", Enum.join(Enum.map(done, &Todo.to_string/1), "\n")) ## Roadmap - Add a File Watcher for Local todo.txt - Add a File Watcher for Google Drive todo.txt +- Add Vapor for starting application with config file or environment variable diff --git a/lib/todo.ex b/lib/todo.ex index fe6b56e..927ab7f 100644 --- a/lib/todo.ex +++ b/lib/todo.ex @@ -147,14 +147,24 @@ defmodule Todo do iex> Todo.parse("task meta:data meta1:data1") %Todo{description: "task", additional_fields: %{"meta" => "data", "meta1" => "data1"}} + iex> Todo.parse("task meta:data meta1:data1 @context") + %Todo{description: "task @context", contexts: [:context], additional_fields: %{"meta" => "data", "meta1" => "data1"}} + iex> Todo.parse("task due:2021-09-13 meta:data meta1:data1") %Todo{description: "task", additional_fields: %{"meta" => "data", "meta1" => "data1"}, due_date: ~D[2021-09-13]} """ def parse(str) do case parser(str) do - {:ok, parsed, "", _, _, _} -> Enum.reduce(parsed, %Todo{}, &set_from_parsed/2) - {:error, message, _, _, _, _} -> message + {:ok, parsed, "", _, _, _} -> + Enum.reduce(parsed, %Todo{}, &set_from_parsed/2) + + {:ok, parsed, additional_description, _, _, _} -> + todo = Enum.reduce(parsed, %Todo{}, &set_from_parsed/2) + set_from_parsed({:description, additional_description}, todo) + + {:error, message, _, _, _, _} -> + message end end @@ -294,6 +304,8 @@ defmodule Todo do end defp set_from_parsed({:description, description}, todo) do + %Todo{description: d, contexts: c, projects: p} = todo + contexts = case context_parser(description) do {:ok, contexts, _, _, _, _} -> contexts @@ -306,9 +318,11 @@ defmodule Todo do {:error, message, _, _, _, _} -> {:error, message} end - Map.put(todo, :description, description) - |> Map.put(:contexts, contexts) - |> Map.put(:projects, projects) + new_descriptions = String.trim(d <> description) + + Map.put(todo, :description, new_descriptions) + |> Map.put(:contexts, c ++ contexts) + |> Map.put(:projects, p ++ projects) end defp set_from_parsed({:done, done}, todo) do diff --git a/lib/todo_txt/application.ex b/lib/todo_txt/application.ex new file mode 100644 index 0000000..53e037b --- /dev/null +++ b/lib/todo_txt/application.ex @@ -0,0 +1,21 @@ +defmodule TodoTxt.Application do + @moduledoc false + + use Application + + def start(_type, args) do + # TODO: figure this out + # Set default paths if none are passed + todo_txt_file_path = get_in(args, [Access.key(:todo_txt_file_path, "$TODO_DIR/todo.txt")]) + done_txt_file_path = get_in(args, [Access.key(:done_txt_file_path, "$TODO_DIR/done.txt")]) + + children = [ + {TodoTxt.Server, + todo_txt_file_path: todo_txt_file_path, done_txt_file_path: done_txt_file_path} + # TODO: Add file_system watcher here + ] + + opts = [strategy: :one_for_one, name: VaporExample.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/lib/todo_txt/server.ex b/lib/todo_txt/server.ex new file mode 100644 index 0000000..9f56832 --- /dev/null +++ b/lib/todo_txt/server.ex @@ -0,0 +1,25 @@ +defmodule TodoTxt.Server do + @moduledoc """ + GenServer that allows interaction with a Todo.txt and optionally a Done.txt file + """ + + use GenServer + + alias TodoTxt.State + + def start_link(args), do: GenServer.start_link(__MODULE__, State.new(args), name: __MODULE__) + + def todos do + GenServer.call(__MODULE__, :todos) + end + + @impl true + def init(state), do: {:ok, state} + # TODO: Add file watcher + + @impl true + def handle_call(:todos, _from, %State{} = state) do + reloaded_state = State.load_todos(state) + {:reply, Map.get(reloaded_state, :todos), reloaded_state} + end +end diff --git a/lib/todo_txt/state.ex b/lib/todo_txt/state.ex new file mode 100644 index 0000000..36bb534 --- /dev/null +++ b/lib/todo_txt/state.ex @@ -0,0 +1,51 @@ +defmodule TodoTxt.State do + alias TodoTxt.State + + defstruct todo_txt_file_path: "#{System.get_env("TODO_DIR")}/todo.txt", + done_txt_file_path: "#{System.get_env("TODO_DIR")}/done.txt", + file_location: :local, + options: [], + todos: [], + history: :none + + def new(state = %State{}) do + load_todos(state) + end + + defp validate(state) do + %State{ + todo_txt_file_path: todo_txt_file_path, + done_txt_file_path: done_txt_file_path, + file_location: file_location + } = state + + case file_location do + :local -> + cond do + !File.exists?(todo_txt_file_path) -> + {:error, "File #{todo_txt_file_path} does not exist"} + + done_txt_file_path != :none && !File.exists?(done_txt_file_path) -> + {:error, "File #{done_txt_file_path} does not exist"} + + true -> + {:ok, state} + end + + invalid_location -> + {:error, "#{invalid_location} is not a valid file_location"} + end + end + + def load_todos(state) do + {:ok, %State{todo_txt_file_path: todo_txt_file_path}} = validate(state) + + todos = + todo_txt_file_path + |> File.read!() + |> String.split("\n", trim: true) + |> Enum.map(&Todo.parse/1) + + Map.put(state, :todos, todos) + end +end diff --git a/mix.exs b/mix.exs index 8106495..6f50516 100644 --- a/mix.exs +++ b/mix.exs @@ -61,6 +61,7 @@ defmodule TodoTxt.MixProject do {:dialyxir, "~> 1.0.0", only: :dev, runtime: false}, {:ex_doc, "~> 0.23.0", only: :dev, runtime: false}, {:excoveralls, "~> 0.13.0", only: :test}, + {:file_system, "~> 0.2"}, {:git_hooks, "~> 0.5.2", only: :dev, runtime: false}, {:mix_test_watch, "~> 1.0.2", only: :dev, runtime: false}, {:nimble_parsec, "~> 1.1.0"}, diff --git a/test/fixtures/todo_txts/done.txt b/test/fixtures/todo_txts/done.txt new file mode 100644 index 0000000..b7d5a54 --- /dev/null +++ b/test/fixtures/todo_txts/done.txt @@ -0,0 +1,4 @@ +x 2021-05-11 Print out National Park Permit +SanJacintoTrip +x 2022-07-20 2022-07-19 Flush water heater @home pri:A +x 2022-06-27 2022-06-27 go through @work email +x 2022-06-27 2022-04-15 Organize ideas section of Proposals @work diff --git a/test/fixtures/todo_txts/todo.txt b/test/fixtures/todo_txts/todo.txt new file mode 100644 index 0000000..c6f4b46 --- /dev/null +++ b/test/fixtures/todo_txts/todo.txt @@ -0,0 +1,8 @@ +(A) Call Mom @Phone +Family due:2022-02-01 +2022-03-14 (A) Schedule annual checkup +Health +(B) Outline chapter 5 +Novel @Computer (#pomo: 4/20) +(C) Add cover sheets @Office +TPSReports +Plan backyard herb garden @Home +Pick up milk @GroceryStore +Research self-publishing services +Novel @Computer url:https://www.selfpublishingservices.com +x Download Todo.txt mobile app @Phone diff --git a/test/todo_txt/server_test.exs b/test/todo_txt/server_test.exs new file mode 100644 index 0000000..92f8d52 --- /dev/null +++ b/test/todo_txt/server_test.exs @@ -0,0 +1,10 @@ +defmodule TodoTxt.ServerTest do + use ExUnit.Case + doctest TodoTxt.Server + + describe "start_link/1" do + test "accepts a TodoTxt.State on start" do + assert {:ok, _pid} = TodoTxt.Server.start_link(%TodoTxt.State{}) + end + end +end