Ruby, etc

Learn all the things

He who enjoys doing and enjoys what he has done is happy. - Fortune Cookie

Setting Up Jenkins for Robot Framework

Robot framework is a test automation framework. There are many uses for it, but I use it for selenium web testing. But no matter what you test you can use it for many things.

Jenkins a tool to manage “jobs”, segments of work that you might use “cron” for.

I use it to run robot framework tests nightly or every 6 hours, or whatever you need.

Jenkins is breeze to set up (at least on mac) and once installed search in the plugins for the robot framework plugin. The directions on the plugin page are good but. However, If you wish to view the report you have to enable Javascript on jenkins.

There have been lots of questions on this subject but it all comes down to jenkins permissions.

To view the beautiful reports by robot framework you must relax the ridged rules explained here. This new feature was added in Jenkins 1.641.

Once JS is enabled then you can view the report and drill down on areas you want to see, including screenshots.

Another jenkins plugin I’ve found useful is Naginator to restart a job after a failure.

Once the Jenkins plugin is installed, configure your robot command line:

Build Step

And then your results:

Post Build

Contributing to Open Source

You probably use open source software if you are programmer and you know you should probably at some point give back right? I’ve been programming since I was a kid and working as a professional programmer for 17 years (at the time of this post). I freaking love code but I wouldn’t say I’ve contributed a lot in the past years. :(

I want to talk about some of my experiences and what you can do to get started.

I contributed to Clojure 1.8 by adding more functions to the string namespace. I took off two weeks from consulting for “vacation” and worked on that. I found this particular task by posting on twitter asking for any ideas for a project I could complete in a short amount of time. Alex Miller saw it and said he had a ticket I could work on and he would help me. It was a great experience and I learned a lot. The Clojure Team has a system of applying patches so they don’t use the typical “github pull requests” but it was an easy enough process to learn and it was fine.

Most open source projects on Github use pull requests. I remember one of my first pull requests to a project was to organize the examples at httparty (ruby). At the time I was using a lot of httparty in my project and kept going back to the examples and always forgetting which one was which. Organizing it make it easy to see which examples did what.

My current company Condé Nast has been holding Open Source Hack Nights, where we meet as a company and work on project that helps us with our job. I have been working with Robot Framework a test automation tool. This was the push I needed. There have been a few things that have come up when I was working with it that I wished it had. I now have 3 contributions to the project which we currently use!

Now that I’ve been so active with Robot Framework, I’ve been asked to join the list of active contributors which of course I will do, and happy to help :)

Now you may wonder how you can find things you can do?

  1. Begging :) Like when I was looking for a project to work for Clojure for my “vacation”.
  2. You could ask the maintainers if there is anything you can do to help.
  3. Something you need. When working with httparty, I was constantly referring to the examples directory to find something I knew was there. With Robot Framework, there was some functionality I needed.
  4. Look through the existing issues on a project to see if you can help.
  5. Keep checking the issues that come through on a project. One of the tickets I did for Robot Framework was something someone else requested.

Now, you found something you need or you think you can help with.

Ask on the chat channel explaining your need, they might said oh do it this way or its already there or may say, go ahead and submit a PR. You should always ask because you want to make sure you at least have a chance at being accepted.

When I saw someone ask for a feature on Robot Framework, I asked if he didn’t want to make a PR, that I would be happy to help. He said he didn’t have time and that I could do it. Come to think of it, I also asked on that Clojure ticket since someone had already completed half the work but didn’t finish. I asked if I could finish and the other party said sure go ahead! … which leads me to this..

It is just polite to ask :) I witnessed someone reporting a bug with a project, the maintainers said “oh thats a bug can you submit a PR?” and before my friend could act, someone not involved in the conversation submitted a PR and “took it”. Great, but he could have asked first. Needless to say, my friend still holds a grudge. So speak up first and be polite.

Look on the contributor guidelines for the project. Style guides, tests etc all make your PR go smoother.

You can use your open source contributions as “proof you know what you are doing” at your next job interview. In the past, when applying for Clojure jobs I talked about the PR I did for Clojure 1.8. Now I can use the PRs I did for Robot Framework. I even put them on my resume in an Open Source section!

I hope I’ve inspired you to go out there and submit some open source code (or some docs, like my submission to httparty). Worst they can say is No and you may even something in the process.

Cool Things in Clojure 1.9

I was glad to see Clojure 1.9 out because of Spec and some more things. But, was sad, because I can no longer say “Hey! I have a commit in the latest version of clojure!”. Course, that just gives me some motivation to find ways to help out in .. Clojure 2.0 ?? OMG. I don’t like to brag, but it was fun when this guy in my community (mostly he didn’t do Clojure) when he heard I had a patch in 1.8, come up to me and say “BADASS!!! WAY TO GO!!!”. I did have the help of Alex Miller and others and it was partly done when I took it over. It was really fun to be a small part of 1.8

Here are the things I think are cool (besides Spec) in Clojure 1.9:

Reader Syntax for Namespaced Maps

1
2
3
4
5
user=> (def data #:person{:name "Nola", :color "purple", :state "TX"})
#'user/data

user=> (keys data)
(:person/name :person/color :person/state)

Using #:ns-name{ key value key value } gives you an easy way to write out your map without prefixing each key. COOL.

Predicate Functions

This were put in mainly (I think) to support Spec, but in short, predicate functions (ending in ?) return true or false.

Some of them added in this patch are:

  • boolean?
  • int? pos-int? nat-int?
  • double?
  • ident?
  • qualified-keyword?

For example, Using remove with a collection will remove all values for which the predicate is true. So to remove all the postive integers from a vector:

1
2
user=> (remove pos-int? [-4 -2 4 1 0 -2 5])
(-4 -2 0 -2)

Then of course, in a spec:

1
2
3
4
5
6
7
8
user=> (s/def ::age pos-int?)
:user/age

user=> (s/conform ::age -16)
:clojure.spec.alpha/invalid

user=> (s/conform ::age 13)
13

Then lets remove everything not a qualified keyword, using in an anonymous function::

1
2
user=> (remove #(not (qualified-keyword? %))  [:name/nola :color/purple 1 5 0])
(:name/nola :color/purple)

Previous you could do the samething of course, but you’d have to write more code. I don’t know if you would use them much outside of spec, but if you do there they are there. Nice.

Atoms get new functions!

Ever wanted to give an atom a new value but also get back what the value was too?

1
2
3
4
user=> (def name (atom "Nola"))

user=> (reset-vals! name "Nick")
["Nola" "Nick"]

Instead of just reset! you now have reset-vals! returning the old value followed by the new (current) value. And this funtion’s cousin swap-vals!:

1
2
3
4
5
user=> (def size (atom 10))
#'user/size

user=> (swap-vals! size inc)
[10 11]

This function, swap-vals! takes an atom and a function and returns the new and the old value.

And before you bikeshed on the names of these functions, don’t bother, it has been done. I think they are good names :)

Brew Install

Not exactly a part of the Clojure 1.9 Release Notes but I think it bears mention along with this release is that for you Mac Users (like me!), you can now brew install clojure. I wrote about it early because I was so excited. Read the post for more details which also links to more documentation on it.

And also if you are an new Clojure dev, with Atom you can setup a decent Clojure environment with Atom. I recommend looking over the docs for parinfer to learn its magics, but once you get it you are good for life.

I hope you are excited about Clojure 1.9 and are using it in your projets :) It was a long time coming, but the best things are worth waiting for, right? :)

One-Liners in Clojure and Ruby

A quick post to start off the new year.

You and your co-workers can’t decide where to go for lunch?

1
2
 ruby -e "puts [:tacos, :chicken, :bbq].sample"
chicken
1
2
 clj -e "(rand-nth [:tacos :chicken :bbq])"
:tacos

Observations:

Ruby I had to tell it what to do with the output, to puts it. It didn’t automatically display like if I had done that line in the irb repl. Oh and don’t do what I did at first and forget the , (comma) :)

Clojure, since everything returns a value, I just had to call the function.

Have fun using code to make important decisions, like .. Lunch :)

Getting Started With Clojure Is Now Easier Than Ever - on a Mac

As of Dec 8, 2017 a brew recipe has been added to install Clojure with brew install clojure and it gives you commands clj and clojure.

To try it out, I created a file, test.clj with the following:

1
2
3
4
5
6
7
#!/usr/bin/env clojure

(println "Hello World")

(println (+ 1 2 3 4 5))

(println (clojure.string/upper-case "hello world"))

After you make it executable with:

1
chmod u+x test.clj

You can execute it:

1
2
3
4
▶ ./test.clj
Hello World
15
HELLO WORLD

A popular way to play with Clojure is to use a repl. You can start a repl, by using either clj or clojure without a file name

1
2
3
4
5
6
7
8
 clojure
Clojure 1.9.0
user=> (+ 1 2 3 4)
10
user=> (apply str [1 2 3 4])
"1234"
user=> (map #(* 2 %) [1 2 3 4])
(2 4 6 8)

I was curious about commands clojure and clj and found an explaination of how it works.

You can have a deps.edn file to include dependencies. At first I was confused on :mvn/ prefix, and thought how to include a library from clojars? So I looked farther and found that :mvn includes this group of locations (which includes clojars), as shown here.

1
2
3
{:mvn/repos
 {"central" {:url "https://repo1.maven.org/maven2/"}
  "clojars" {:url "https://repo.clojars.org/"}}}

to test this out, I created a directory and put a file deps.edn file with the following:

1
2
{:deps
 {anagrams {:mvn/version "4" }}}

Anagrams is a fun little library I found browsing clojars. It takes a list of words separated with \n and finds words that are anagrams of a word you give it. My example is a little contrived, but see it working:

Start the repl in that same directory, if needed it will download and install dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
 clj
Clojure 1.9.0
user=> (require '[anagrams.core :as a])
nil

user=> (a/set-word-list! "cat\ntac\natc\ndog\ngod\ntaco")
"done"

user=> (a/anagrams-of "cat")
#{"tac" "atc" "cat"}

user=> (a/anagrams-of "dog")
#{"dog" "god"}

Cool, that’s fun :)

A new way to try out Clojure.. (on a mac at least). Pretty easy!

Data Structures in Clojure and Elixir: Structs and Records

Both Elixir and Clojure have a way to make a more “organized” entity, like a map but more structure. Elixir can easily have default values whereas with Clojure, it is not built in but you can create a helper functions to set some default values and create a record.

Elixir

A defstruct takes on the name of the defmodule it is in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
iex(1)> defmodule User do
...(1)>   defstruct name: "", state: "", color: "green", size: "M"
...(1)> end
{:module, User,
 <<70, 79, 82, 49, 0, 0, 8, 60, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 232,
   0, 0, 0, 22, 11, 69, 108, 105, 120, 105, 114, 46, 85, 115, 101, 114, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 9, 102, ...>>,
 %User{color: "green", name: "", size: "M", state: ""}}

iex(2)> %User{name: "nola", state: "TX"}
%User{color: "green", name: "nola", size: "M", state: "TX"}

iex(30)> user.name
"nola"

iex(30)> user.color
"red"

Using an attribute that doesn’t exist:

1
2
3
4
iex(32)> user["country"]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
    User.fetch(%User{color: "", name: "nola", state: "TX"}, "country")
    (elixir) lib/access.ex:304: Access.get/3

Yeah don’t do that :) Accessing a key not defined is not good.

Clojure

First define a record with defrecord and a vector for the names of the attributes:

1
2
user=> (defrecord DataRecord [name state])
user.DataRecord

Then create a record

1
2
user=> (def data (->DataRecord "Nola" "TX"))
#'user/data

Access the values just like a map:

1
2
3
4
5
user=> (:name data)
"Nola"

user=> (:state data)
"TX"

Or create using a map:

1
2
user=> (map->DataRecord {:name "bob" :state "IL"})
#user.DataRecord{:name "bob", :state "IL"}

To create a Record that may have default values, create a helper function to build it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
user=> (defrecord Person [name state color size])
user.Person

user=> (defn create-person [{:keys [name state color size] :or {color "green" size "M" }}] (->Person name state color size))
#'user/create-person

user=> (def data (create-person {:name "bob" :state "TX"}))
#user.Person{:name "bob", :state "TX", :color "green", :size "M"}

user=> (:name data)
"bob"

user=> (:state data)
"TX"

user=>

I wrote about Clojure records awhile back, See more on records.

See my previous posts on Maps, Lists, Tuples, Vectors and Sets.

Data Structures in Clojure and Elixir: Maps

Elixir

Tuples used { } .. but maps use %{ }. You can use anything for a key.. here are string keys:

1
2
3
4
5
iex(1)> data = %{"name" => "nola", "color" => "red"}
%{"color" => "red", "name" => "nola"}

iex(4)> data["name"]
"nola"

If you use a string to set the value, obviously you need to use that to get the value.

Using atoms for keys is alot nicer for maps, the syntax is shorter and you can use .name to access the value

1
2
3
4
5
iex(2)> more_data = %{city: "Austin", state: "TX"}
%{city: "Austin", state: "TX"}

iex(5)> more_data.city
"Austin"

I don’t know why you would, but you can use integers as keys too:

1
2
3
4
5
iex(3)> yet_more = %{1 => "user 1", 2 => "user 2"}
%{1 => "user 1", 2 => "user 2"}

iex(12)> yet_more[1]
"user 1"

And, even a list as a key:

1
2
3
4
5
iex(8)> weird_keys = %{ [1, 2] => "one two", [3, 4] => "three four"}
%{[1, 2] => "one two", [3, 4] => "three four"}

iex(9)> weird_keys[[1,2]]
"one two"

I can’t think of a use case for that, but if you need it, then you can do it.

Some methods for maps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
iex(6)> Map.keys(more_data)
[:city, :state]

iex(7)> Map.keys(data)
["color", "name"]

iex(13)> Map.keys(weird_keys)
[[1, 2], [3, 4]]

iex(14)> Map.values(weird_keys)
["one two", "three four"]

iex(25)> Enum.count(weird_keys)
2

Updating a value in a map

1
2
3
4
iex(15)> data = %{color: "red", name: "nola"}

(16)> updated = %{data | color: "pink" }
%{color: "pink", name: "nola"}

Using the | operator with either syntax for strings or atom keys (whichever you are using) ie `”color” => “pink”’.

But if the key does not exist, you will get an exception!

To add a new key to a map, you use Map.put_new:

1
2
3
4
iex(1)> data = %{color: "red", name: "nola"}

iex(2)> Map.put_new(data, :state, "TX")
%{color: "red", name: "nola", state: "TX"}

If the key was already there, the change is ignored.

To add a new key to a map reguardless if it was there before or not use:

1
2
3
4
5
6
7
8
iex(10)> data = %{color: "red", name: "nola"}
%{color: "red", name: "nola"}

iex(11)> more = Map.put(data, :color, "green")
%{color: "green", name: "nola"}

iex(12)> Map.put(more, :state, "TX")
%{color: "green", name: "nola", state: "TX"}

Still, the data is not actually changed but a copy is returned. The original is unchanged.

Clojure

1
2
3
4
5
user=> (hash-map :name "nola" :state "TX" :color "red")
{:color "red", :name "nola", :state "TX"}

user=> (array-map :name "nola" :state "TX" :color "red")
{:name "nola", :state "TX", :color "red"}

I was trying to figure out what the difference was and found this on stackoverflow

Array maps and hash maps have the same interface, but array maps have O(N) lookup complexity (i.e. it is implemented as an simple array of entries), while hash maps have O(1) lookup complexity.

If you notice in my code above, the hash-map has the keys in a different order than I created them in.

So, thats the difference.

Using the literal { } it produces an array-map.

1
2
user=> (class {:name "Nola" :state "tx"})
clojure.lang.PersistentArrayMap

Some functions for maps:

1
2
3
4
5
6
7
8
9
10
11
user=> (def data {:color "red", :name "nola", :state "TX"})
#'user/data

user=> (keys data)
(:color :name :state)

user=> (vals data)
("red" "nola" "TX")

user=> (count data)
3

Editing or adding new keys to a map:

1
2
3
4
5
user=> (assoc data :state "IL")
{:color "red", :name "nola", :state "IL"}

user=> (assoc data :food "tacos")
{:color "red", :name "nola", :state "TX", :food "tacos"}

Unlike Elixir, Clojure doesn’t care if this value is new or not when updating a map. It also doesn’t change the original data structure like Elixir.

Let me know if I am understanding these languages right. Next post will talk about Elixir Structs and Clojure Records.

See my post on Lists, Tuples, Vectors and Sets.

Data Structures in Clojure and Elixir: Sets

My last post talked about Lists, Tuples and Vectors comparing Elixir and Clojure … now lets cover a related topic.. sets.

When you think of set, think of Math Sets.

Elixir

There are two ways to create a Set in Elixir

Using the Pipe Operator and put:

1
2
set = MapSet.new |> MapSet.put("apple") |> MapSet.put("apple") |> MapSet.put("banana")
#MapSet<["apple", "banana"]>

Or use a List and pass that to MapSet.new

1
2
iex(1)> set = MapSet.new(["apple","orange","grape", "grape"])
#MapSet<["apple", "grape", "orange"]>

I purposely put duplicates when I created it to see what would happen. It quietly dismissed the duplicate value and made a collection of unique values.

Passing a list to new would be a good way to filter out duplicates in a collection as well as build a set more easily.

1
2
iex(3)> unique_values = MapSet.new(["apple","orange","grape", "grape"]) |> MapSet.to_list()
["apple", "grape", "orange"]

Some more functions for sets

1
2
3
4
5
iex(4)> fruit = MapSet.new(["apple","orange","grape"])
#MapSet<["apple", "grape", "orange"]>

iex(5)> MapSet.member?(fruit, "apple")
true

and subset

1
2
3
4
5
6
7
8
iex(4)> fruit = MapSet.new(["apple","orange","grape"])
#MapSet<["apple", "grape", "orange"]>

iex(6)> other_fruit = MapSet.new(["apple", "grape"])
#MapSet<["apple", "grape"]>

iex(8)> MapSet.subset?(other_fruit, fruit)
true

Read more on MapSet

Clojure

You have two ways to make a set in Clojure, using the set function and the #{} literal.

1
2
3
4
5
user=> (set [:apple :grape :orange :orange])
#{:orange :apple :grape}

user=> (set '(:apple :grape :orange :orange)
#{:orange :apple :grape}

We are converting a vector or a list to a set.

Also testing putting duplicates and as expected, the result is all unique values.

1
2
3
4
user=> (def taco-restaurants #{"torchys" "maudies" "torchys" "taco bell"})

IllegalArgumentException Duplicate key: torchys  clojure.lang.PersistentHashSet.createWithCheck (PersistentHashSet.java:68)
RuntimeException Unmatched delimiter: )  clojure.lang.Util.runtimeException (Util.java:221)

No, the error is not calling taco bell a restaurant, it is putting torchys twice. YIKES… the literal syntax does not like duplicates. If you think your values might have duplicates, put in vector first and pass to set.

Edit: Alex Miller pointed out, the reason we have an error it because it is an invalid set. Now I think it should error and since Elixir doesn’t have a literal syntax it is not exactly the same thing to compare. Thanks for pointing that out why it errors Alex :)

Some useful functions for sets:

1
2
3
4
5
6
7
8
user=> (def valid-actions #{:get :post})
#'user/valid-actions

user=> (contains? valid-actions :post)
true

user=> (contains? valid-actions :head)
false

Use a set when you need to see if a certain value is one of X. It is surprisingly not easy to do the same with checking membership in a list or vector :) Use a set, you don’t need duplicates anyways so it makes sense.

There is a Set namespace with functions for sets:

1
2
3
4
5
6
7
8
user=> (require '[clojure.set :as set])
nil

user=> (set/subset? #{:post} valid-actions)
true

user=> (set/subset? #{:head} valid-actions)
false

Next, we’ll cover Maps!

Data Structures in Clojure and Elixir: Lists, Tuples, Vectors

In my free time I’ve been learning more Elixir, and I’ve already been doing Clojure for awhile now. I am going to write this blog post to compare/contrast the data structures available in both languages as well as a few functions you can use. Just for fun and so I can keep it in my head.

List / Vector

This is aslso known as an array in some languages. This is a collection of items, in which case order is generally depended on to access items. When I am explaining this to new developers I describe it as a bookshelf, each book on the shelf is an element.

Elixir

We have two kinds of these in Elixir, a Tuple and a List. A Tuple is fast and intended to be for short (<= 4 items) and a List is stored as Linked List. It is much slower than a Tuple because it is stored as a Linked List (if you are not familiar, under the hood, each item in the list has a pointer to the next item).

Lists are defined using [] Here’s what it looks like:

1
2
iex(1)> favorites = ["bbq", "tacos", "pizza"]
["bbq", "tacos", "pizza"]

Then some methods to access items in a list:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
iex(2)> Enum.at(favorites, 0)
"bbq"

iex(4)> Enum.at(favorites, 2)
"pizza"

iex(5)> List.first(favorites)
"bbq"

iex(6)> List.last(favorites)
"pizza"

iex(7)> length(favorites)
3

Notice that length is not part of any namespace, it’s part of Kernel which doesn’t need prefix of module name (as you might expect) … it is the same place that def, defmodule, if and hd are defined.

A tuple is define with { }:

1
2
iex(7)> not_favorites = {"shrimp", "squid", "octopus"}
{"shrimp", "squid", "octopus"}

Then accessing elements is like this:

1
2
3
4
5
6
7
8
9
10
11
iex(7)> not_favorites = {"shrimp", "squid", "octopus"}
{"shrimp", "squid", "octopus"}

iex(8)> elem(not_favorites, 0)  # index of first item is 0
"shrimp"

iex(9)> elem(not_favorites, 2)  # index of third item is 2
"octopus"

iex(10)> tuple_size(not_favorites)
3

Tuples are often used as return values from functions and for pattern matching in case statements. I dont often see the elements of a tuple accessed with elem, most of the time I’ve seen it decontructed with pattern matching. But if you need it, you that is how you do it.

Clojure

Clojure has two kinds of sequential containers … Vectors and Lists. A vector is more efficient adding to the end, where as a list (think single linked lists) is more efficient adding to the front.

1
2
3
4
5
6
7
8
9
10
11
user=> (def favorites ["pizza" "tacos" "cake"])
#'user/favorites

user=> (def not-favorites '("squid" "shrimp" "octopus"))
#'user/not-favorites

user=> (first favorites)
"pizza"

user=> (rest not-favorites)
("shrimp" "octopus")

The ’ in front of a list tells clojure don’t evaluate this right now.. which you’ll see why next.

Take this code:

1
(def name "nola")

It defines a value of “nola” which you can refer to as “name”. It’s not exactly like a variable that you might thing of from other languages, but for now lets say it is close.

However notice something about a list and code:

1
2
3
4
5
6
7
8
9
10
11
user=> (count '(1 2 3))
3

user=> (count '(def name "nola"))
3

user=> (first '(def name "nola"))
def

user=> (rest '(def name "nola"))
(name "nola")

This code is defining a value called name to be nola … is itself a list. So you may hear Clojure people chant “code is data. data is code.” and this is what they are talking about. It took me awhile to get it then it seemed so simple.

Next post I will talk about Sets in Elixir and Clojure.

Managing My Tasks

Some one asked me how I manage my tasks and I showed him but I thought it would make for a good blog post. I’m not saying I am the perfect solution but this is what works for me.

For long term storage of projects, tasks and ideas I use Omnifocus. I have used it for 7 years. I initially got a free copy of the Mac app because I know someone who works there but since then I’ve paid for updates and bought it for iPhone and iPad. It is really handy for whatever device I have in my hand at the moment. They provide a sync service so all my apps are in sync. I can put ideas into the Omnifocus “inbox” via my phone then later put them in the right place when I am on my mac. Omnifocus is only available on apple platforms.

The key feature for me with Omnifocus is the Review Tab, which I often use the iPad. It steps through each project allowing you to change due dates for tasks, flag, or delete.

No system will work unless you have a way to review! You can use a spreadsheet, stack of index cards (See hipster pda) or trello. But unless you review regularly it is junk. The fact this is one of the side tabs in Omnifocus is Review is the #1 reason I have used it this long.

If you are disciplined enough to review without that, then good for you. Maybe you don’t need Omnifocus :).

For very short term (ie todo this week) I’ve used a variety of things. Trello. Google Spreadsheets. Index cards … right now I have been using org mode in emacs partly for fun to learn it. I commit the files to github in a private repo. I use org-pomadoro to set a 25 minute timer (especially when I really need to focus) and it makes a log section under the task.

This time last year, I was using index cards. One per project, with tasks written out. When I was working on a project I would put that card on top to remind me to stay focused. When I was done, I recorded the project done in OmniFocus and threw the card away.

And even longer ago, I used Google Spreadsheets. I had one tab per project with the tasks listed out. It was easy to get to on multiple devices (hard to edit on a phone but not impossible).

I get bored easy, so maybe thats why I switch my short term tasks planning, but that keeps it fun.

Anyways, hope I’ve given you some ideas on manging your own tasks, both long term and short term :)