State of the Apps 2016

I recently found a post by CGP Grey from 2014 where he shows the apps he was using at the time. I've always liked hearing about the software and workflows other people use so I thought I would contribute with a list of the Mac and iOS apps I use today.

Mac

Terminal

The default Terminal app is my app of choice for doing terminaling. Combined with Tmux it has all the features I need. I've played with iTerm in the past but found no compelling reason to use it.

Vim

I'm a programmer by day and Vim is where I do all of my coding. I've been using Vim full time for over 5 years and cannot imagine ever using another editor. Vim is also what I'm using to write this post you're reading right now.

I've always used Vim straight from the terminal because I like being close to shell. I find it makes running scripts and interacting with Git much easier.

About a year ago I switched to Neovim and haven't looked back since. Asynchronous job control is what got me hooked.

Ruby

Ruby is my favorite programing language and I feel very lucky that there are people willing to pay to write Ruby every day. Of course Ruby is not an application in the traditional sense but I have written so many Ruby scripts over the years to automate all kinds of things that I feel like it deserves a spot on this list.

OmniFocus (Mac and iOS)

OmniFocus is the app I use to run my entire life. I like to think of OmniFocus as my brain's secretary. I tell it things and trust that I'll be reminded about them at the right time, in the right place. Its like magic once you get it.

Firefox

For years I used Safari as my browser of choice but I've recently switched to Firefox. The primary reason I switched is a plugin called Vimperator. It brings Vim into the browser and lets you navigate the web without using the mouse. Whether I'll stick with this setup I don't know but at the moment I'm liking it.

Mailplane

I use Gmail because of its great keyboard cuts and Mailplane lets me use Gmail outside the browser. I was never a fan of always having a Gmail tab open. You can also set Mailplane as your default email app so when you click a mailto: link it'll do the right thing.

Dash

I use Dash exclusively for looking up Haskell documentation. Nothing beats its fast search and Vim integration.

Skim

Skim is a PDF viewer with one killer feature: Auto reloading. This is very useful when writing reports in Latex so I don't have to toggle back and forth between the previewer and the terminal.

Fantastical

I use Fantastical primarily because of its natural language parsing.

Alfred

Alfred is the best app launcher/workflow/helper app thing I've found. It strikes a great balance between power and ease of use.

1Password

The more I learn about IT security the more I value 1Password. I use it to generate and store all my passwords.

nvAlt

I write most of my notes in Vim but when I have to find something I wrote I use nvAlt.

Keyboard Maestro

Keyboard Maestro lets me program my Mac without having to learn AppleScript, which is something I really don't wanna do.

Tweetbot (Mac and iOS)

I've used Tweetbot on iOS and the Mac every since it was released and I have always been happy with it.

iOS (iPhone)

Workflow

This app is so cool! It is basically Automator for iOS. I use this mostly to track what I spend my time on, as well as order pizza once in a while.

Drafts

Used to make new OmniFocus tasks, append to various lists, and kick off workflows.

Launch Center Pro

Used to launch various workflows.

Overcast

Two words: Smart speed. I've been gradually increasing the speed and some months ago I reached the maximum of 2.5x. I love podcasts and without this app I wouldn't be able to listen to as many as I do.

Editorial

I have three basic requirements for a notes app:

  • Have a nice icon.
  • Sync that just works.
  • Search that just works.

Editorial was the only app I found that met all three. I use none of it's super advanced workflow like features. I mostly use it to search my notes.

Spark

I had some issues with the search in the stock mail app and after trying several others I landed on Spark. My favorite feature, besides search, is being able to archive all the newsletters I get with one tap.

Learning Vim will break you

One of the reasons Vim is a bit difficult to learn is that it takes over the entire writing experience. You simply cannot write the way you're used to. You have to slow down and learn the "Vim language". Doing that comes at a loss of productivity for the first few weeks, but once you've gotten over the initial hump, you won't want to use any other editor ever again. The fact that learning Vim will make you never want to leave has been on my mind a lot lately.

While it might seem like a fair trade off, consider all the places you write text and how few of them support the Vim language. For me this includes: Taking notes in nvAlt and TextEdit, writing emails, browsing the web, and instant messaging in Slack and Messages.app. Believe me when I tell you that I have spent hours making Keyboard Maestro macros and Automator applications that will let me jump back and forth between these apps and Vim.

It also means that I shouldn't even bother trying new shiny writing apps like Ulysses, Byword, and IA writer, because while those apps might be pretty and have neat "distraction free environments", they all fall down at the essential part: Writing and editing text.

One can argue that the bottleneck will never be the speed at which you can move the cursor around and swap paragraphs, it'll be the speed at which our brains operate. While that might be true, the primary reason I love Vim is not the speed, it's the comfort with which I can make edits.

If Ulysses really wanted to be "The Ultimate App for Anyone Who Writes", I think they should take a step back and think about how to improve the core of the app: Getting text into it and editing the text once you have.

Dynamic dispatch in Haskell

Dynamic dispatch sounds like a scary term, but it really isn't. It just means that the exact function to be called will be determined at run time rather than at compile time.

Here is a Swift example of something that isn't dynamic:

class Person {
    func message() -> String {
        return "Hello, I am a person"
    }
}

class Cat {
    func message() -> String {
        return "Meow meow"
    }
}

func personSpeak(object: Person) {
    print(object.message())
}

func catSpeak(object: Cat) {
    print(object.message())
}

let person = Person()
personSpeak(person) // => "Hello, I am a person"

let cat = Cat()
catSpeak(cat) // => "Meow meow"

Here we define two classes Person and Cat. They each have a message method that just returns some constant string. We also define two top level functions: personSpeak and catSpeak. When we call personSpeak and pass in an object of type Person the function will call message from the Person class. Thus the location of the message method is known at compile time.

While this works it is a bit annoying that we have to write separate *Speak functions for each type. Lets make that function polymorphic using a protocol:

func speak(object: Verbal) {
  print(object.message())
}

protocol Verbal {
  func message() -> String
}

class Person : Verbal {
  func message() -> String {
      return "Hello, I am a person"
  }
}

class Cat : Verbal {
  func message() -> String {
      return "Meow meow"
  }
}

speak(Person())
speak(Cat())

So now we have one method speak which takes an object that implements Verbal. Notice that this time which message method we are calling is not known at compile time. We can pass whatever we want to speak as long as it implements Verbal. Thus the runtime has to find correct the message method while the program is running.

Typically we think of dynamic dispatch in examples like these, when we have polymorphic methods or functions in object oriented languages. But Haskell also has lots of polymorphism and it also has one great feature related to dynamic dispatch that I haven't seen elsewhere.

Dispatching on the input type

Before when figuring out which message method to call we looked at the type of the receiver object and used that to determine which method to call. Doing that looks like this in Haskell:

class Verbal a where
    message :: a -> String

data Person = Person

instance Verbal Person where
    message _ = "Hello, I am a person"

data Cat = Cat

instance Verbal Cat where
    message _ = "Meow meow"

speak :: Verbal a => a -> IO ()
speak x = putStrLn (message x)

This works exactly the same as the Swift example. We define a type class for verbal things, Person and Cat types which each are instances of the type class, and lastly a function that prints the message from verbal values to the screen.

Dispatching on the return type

In Haskell we are not just limited to dispatching on the input types of functions, but also the return types. Here is an example:

class FromString a where
    fromString :: String -> Maybe a

data Person = Person deriving (Show)

instance FromString Person where
    fromString "Person" = Just Person
    fromString _ = Nothing

data Cat = Cat deriving (Show)

instance FromString Cat where
    fromString "Meow" = Just Cat
    fromString _ = Nothing

This time we define a type class for types that can be constructed from strings. Notice that the type of message is String -> Maybe a. So this function always takes a string as input, but it will figure out which fromString function to call based on the return type we need.

Here is an example of using fromString in the repl:

λ > fromString "Person" :: Maybe Person
Just Person
λ > fromString "nope" :: Maybe Person
Nothing
λ > fromString "Meow" :: Maybe Cat
Just Cat
λ > fromString "Cat" :: Maybe Cat
Nothing

We have to use :: to tell the repl what the type of our expressions are. Normally this wouldn't be required because it'll be obvious from the context. But as you can see we're able to call different versions of fromString based on the type we're telling Haskell we would like back.

Something that makes great use of this is the string-conversions package. It provides a type class that looks like this:

class ConvertString a b where
    convertString :: a -> b

Notice that this type class doesn't say anything about strings. Its basically just provides a convenient way to convert between different types using dynamic dispatch.

What that allows the author to do is add instances for converting between all the different string like types. Thus all you need to convert between different types is convertString and you never need to worry about which type you have and which type you need.

Getting more familiar with types through hole driven development

I've been writing a lot of Haskell lately and while it sure is a language with a steep learning curve, I believe I found a nice way to get better at it: Getting used to reading types by doing hole driven development.

To illustrate what I mean by hole driven development lets walk through implementing our own Maybe type and making it a Functor instance. Then we'll take it a bit further by making our down State type and also adding a Functor instance for it.

Option

Since Maybe is already taken we'll call our type Option.

First we have to declare our data type:

data Option a = Some a
              | None
              deriving (Show)

This type represents a value with type a and the fact that it might be there, or it might not.

To make this type a Functor we start with an empty instance:

instance Functor Option where
    fmap f x = _

The important thing to note here is the "hole": _. This tells the compiler that you're not quite sure what goes here and would like a hint. You should get an error like so:

Option.hs:7:16: error:
  • Found hole: _ :: Option b
    Where: ‘b’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> Option a -> Option b
             at Option.hs:7:5
  • In the expression: _
    In an equation for ‘fmap’: fmap f x = _
    In the instance declaration for ‘Functor Option’
  • Relevant bindings include
      x :: Option a (bound at Option.hs:7:12)
      f :: a -> b (bound at Option.hs:7:10)
      fmap :: (a -> b) -> Option a -> Option b (bound at Option.hs:7:5)
  Failed, modules loaded: none.

This might look overwhelming but if you read it carefully you'll see that its just the compiler telling us which type goes into the hole, and the types of variables we have available. Specifically we need a type of Option b so lets change our to:

instance Functor Option where
    fmap f x = Some _

This gives us a new error:

Option.hs:7:21: error:
  • Found hole: _ :: b
    Where: ‘b’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> Option a -> Option b
             at Option.hs:7:5
  • In the first argument of ‘Some’, namely ‘_’
    In the expression: Some _
    In an equation for ‘fmap’: fmap f x = Some _
  • Relevant bindings include
      x :: Option a (bound at Option.hs:7:12)
      f :: a -> b (bound at Option.hs:7:10)
      fmap :: (a -> b) -> Option a -> Option b (bound at Option.hs:7:5)
  Failed, modules loaded: none.

Now we're missing a value of type b. Looking at the types of the "relevant bindings" we notice that the only way to get a b is to call f with a value of type a. So we need to do something with f, we can now update our code to:

instance Functor Option where
    fmap f x = Some $ f _
Option.hs:7:24: error:
  • Found hole: _ :: a
    Where: ‘a’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> Option a -> Option b
             at Option.hs:7:5
  • In the first argument of ‘f’, namely ‘_’
    In the first argument of ‘Some’, namely ‘(f _)’
    In the expression: Some (f _)
  • Relevant bindings include
      x :: Option a (bound at Option.hs:7:12)
      f :: a -> b (bound at Option.hs:7:10)
      fmap :: (a -> b) -> Option a -> Option b (bound at Option.hs:7:5)
Failed, modules loaded: none.

To get a thing with type a our only option is to pattern match on x:

instance Functor Option where
    fmap f x = Some $ f $ case x of
                            Some y -> _
                            None -> undefined

This time it is important to notice how only one of the patterns in our case has a hole. While we could have put holes in both, I find it easier to focus on one hole a time.

The error we get looks like so:

Option.hs:8:39: error:
  • Found hole: _ :: a
    Where: ‘a’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> Option a -> Option b
             at Option.hs:7:5
  • In the expression: _
    In a case alternative: Some y -> _
    In the second argument of ‘($)’, namely
      ‘case x of {
         Some y -> _
         None -> undefined }’
  • Relevant bindings include
      y :: a (bound at Option.hs:8:34)
      x :: Option a (bound at Option.hs:7:12)
      f :: a -> b (bound at Option.hs:7:10)
      fmap :: (a -> b) -> Option a -> Option b (bound at Option.hs:7:5)
Failed, modules loaded: none.

So we need a value of type a and lucky for us y has type a. So we can fix the hole and add a new one:

instance Functor Option where
    fmap f x = Some $ f $ case x of
                            Some y -> y
                            None -> _
Option.hs:9:37: error:
  • Found hole: _ :: a
    Where: ‘a’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> Option a -> Option b
             at Option.hs:7:5
  • In the expression: _
    In a case alternative: None -> _
    In the second argument of ‘($)’, namely
      ‘case x of {
         Some y -> y
         None -> _ }’
  • Relevant bindings include
      x :: Option a (bound at Option.hs:7:12)
      f :: a -> b (bound at Option.hs:7:10)
      fmap :: (a -> b) -> Option a -> Option b (bound at Option.hs:7:5)
Failed, modules loaded: none.

However this time we're in trouble. Because we need a value of type a but we don't have any... We could of course pattern match on x again but that wouldn't help. The issue is of course that we should only call f if out Option is a Some and not when its a None. So we change our code to reflect that:

instance Functor Option where
    fmap f None = _
    fmap f (Some x) = Some $ f x
Option.hs:7:19: error:
  • Found hole: _ :: Option b
    Where: ‘b’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> Option a -> Option b
             at Option.hs:7:5
  • In the expression: _
    In an equation for ‘fmap’: fmap f None = _
    In the instance declaration for ‘Functor Option’
  • Relevant bindings include
      f :: a -> b (bound at Option.hs:7:10)
      fmap :: (a -> b) -> Option a -> Option b (bound at Option.hs:7:5)
Failed, modules loaded: none.

So we need a value of type Option b, but there is no way to get a b, because the only way to do that is to call f with a value of type a, which we don't have. Again we only have one choice: Return None.

instance Functor Option where
    fmap f None = None
    fmap f (Some x) = Some $ f x

Finally the compiler is happy:

[1 of 1] Compiling Option           ( Option.hs, interpreted )
Ok, modules loaded: Option.

Lets test our code in ghci:

$ ghci Option.hs
GHCi, version 8.0.1: http://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /Users/davidpdrsn/dotfiles/haskell/ghci.symlink
[1 of 1] Compiling Option           ( Option.hs, interpreted )
Ok, modules loaded: Option.
λ > fmap (+ 10) (Some 1)
Some 11
λ > fmap (+ 10) None
None

Seems to work!

State

While you might think this example was a bit simple, the technique I used scales very well. Lets look at reimplementing State and adding our own Functor instance to it. Again we begin with a data type:

module State where

newtype State s a = State { runState :: s -> (a, s) }

I like to think of State as a special kind of function that when composed with other States automatically pass along a value of type s behind the scenes. Its fine if you're not completely familiar with State, the important bit is to read and understand the type errors we get along the way.

Lets start by adding an empty Functor instance:

instance Functor (State s) where
    fmap f x = _

Which gives us the following error:

State.hs:6:16: error:
  • Found hole: _ :: State s b
    Where: ‘s’ is a rigid type variable bound by
             the instance declaration at State.hs:5:10
           ‘b’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> State s a -> State s b
             at State.hs:6:5
  • In the expression: _
    In an equation for ‘fmap’: fmap f x = _
    In the instance declaration for ‘Functor (State s)’
  • Relevant bindings include
      x :: State s a (bound at State.hs:6:12)
      f :: a -> b (bound at State.hs:6:10)
      fmap :: (a -> b) -> State s a -> State s b (bound at State.hs:6:5)
Failed, modules loaded: none.

So we need a value of type State s b. Lets start by using our State constructor:

instance Functor (State s) where
    fmap f x = State $ _
State.hs:6:24: error:
    • Found hole: _ :: s -> (b, s)
      Where: ‘b’ is a rigid type variable bound by
               the type signature for:
                 fmap :: forall a b. (a -> b) -> State s a -> State s b
               at State.hs:6:5
             ‘s’ is a rigid type variable bound by
               the instance declaration at State.hs:5:10
    • In the second argument of ‘($)’, namely ‘_’
      In the expression: State $ _
      In an equation for ‘fmap’: fmap f x = State $ _
    • Relevant bindings include
        x :: State s a (bound at State.hs:6:12)
        f :: a -> b (bound at State.hs:6:10)
        fmap :: (a -> b) -> State s a -> State s b (bound at State.hs:6:5)
Failed, modules loaded: none.

Now we're missing a function from s to (b, s) so we add an empty lambda:

instance Functor (State s) where
    fmap f x = State $ \s -> _
State.hs:6:30: error:
  • Found hole: _ :: (b, s)
    Where: ‘b’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> State s a -> State s b
             at State.hs:6:5
           ‘s’ is a rigid type variable bound by
             the instance declaration at State.hs:5:10
  • In the expression: _
    In the second argument of ‘($)’, namely ‘\ s -> _’
    In the expression: State $ \ s -> _
  • Relevant bindings include
      s :: s (bound at State.hs:6:25)
      x :: State s a (bound at State.hs:6:12)
      f :: a -> b (bound at State.hs:6:10)
      fmap :: (a -> b) -> State s a -> State s b (bound at State.hs:6:5)
Failed, modules loaded: none.

And then we add an empty tuple (hopefully you're getting a feel for the process):

instance Functor (State s) where
    fmap f x = State $ \s -> (_, undefined)
State.hs:6:31: error:
  • Found hole: _ :: b
    Where: ‘b’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> State s a -> State s b
             at State.hs:6:5
  • In the expression: _
    In the expression: (_, undefined)
    In the second argument of ‘($)’, namely ‘\ s -> (_, undefined)’
  • Relevant bindings include
      s :: s (bound at State.hs:6:25)
      x :: State s a (bound at State.hs:6:12)
      f :: a -> b (bound at State.hs:6:10)
      fmap :: (a -> b) -> State s a -> State s b (bound at State.hs:6:5)
Failed, modules loaded: none.

Finally an error we need to think more about. We need a value of type b. The only way to get one is to apply f to a value of type a. So we update our code:

instance Functor (State s) where
    fmap f x = State $ \s -> (f _, undefined)
State.hs:6:33: error:
  • Found hole: _ :: a
    Where: ‘a’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> State s a -> State s b
             at State.hs:6:5
  • In the first argument of ‘f’, namely ‘_’
    In the expression: f _
    In the expression: (f _, undefined)
  • Relevant bindings include
      s :: s (bound at State.hs:6:25)
      x :: State s a (bound at State.hs:6:12)
      f :: a -> b (bound at State.hs:6:10)
      fmap :: (a -> b) -> State s a -> State s b (bound at State.hs:6:5)
Failed, modules loaded: none.

Now we're missing a value of type a and the only way to get that is to extract the function inside x ::State s a. We can pattern match on x by using a let binding:

instance Functor (State s) where
    fmap f x = State $ \s -> let State g = x
                             in (f _, undefined)
State.hs:7:36: error:
  • Found hole: _ :: a
    Where: ‘a’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> State s a -> State s b
             at State.hs:6:5
  • In the first argument of ‘f’, namely ‘_’
    In the expression: f _
    In the expression: (f _, undefined)
  • Relevant bindings include
      g :: s -> (a, s) (bound at State.hs:6:40)
      s :: s (bound at State.hs:6:25)
      x :: State s a (bound at State.hs:6:12)
      f :: a -> b (bound at State.hs:6:10)
      fmap :: (a -> b) -> State s a -> State s b (bound at State.hs:6:5)
Failed, modules loaded: none.

So now we have a function g with type s -> (a, s) so we're able to get the a we need from g, but first we have to give it a value of type s. We have such a value: s.

instance Functor (State s) where
    fmap f x = State $ \s -> let State g = x
                                 (a, s') = g s
                             in (f _, undefined)
State.hs:8:36: error:
  • Found hole: _ :: a
    Where: ‘a’ is a rigid type variable bound by
             the type signature for:
               fmap :: forall a b. (a -> b) -> State s a -> State s b
             at State.hs:6:5
  • In the first argument of ‘f’, namely ‘_’
    In the expression: f _
    In the expression: (f _, undefined)
  • Relevant bindings include
      a :: a (bound at State.hs:7:35)
      s' :: s (bound at State.hs:7:38)
      g :: s -> (a, s) (bound at State.hs:6:40)
      s :: s (bound at State.hs:6:25)
      x :: State s a (bound at State.hs:6:12)
      f :: a -> b (bound at State.hs:6:10)
      (Some bindings suppressed; use -fmax-relevant-binds=N or -fno-max-relevant-binds)
Failed, modules loaded: none.

So now we can call f and replace undefined with a new hole:

instance Functor (State s) where
    fmap f x = State $ \s -> let State g = x
                                 (a, s') = g s
                             in (f a, _)
State.hs:8:39: error:
  • Found hole: _ :: s
    Where: ‘s’ is a rigid type variable bound by
             the instance declaration at State.hs:5:10
  • In the expression: _
    In the expression: (f a, _)
    In the expression:
      let
        State g = x
        (a, s') = g s
      in (f a, _)
  • Relevant bindings include
      a :: a (bound at State.hs:7:35)
      s' :: s (bound at State.hs:7:38)
      g :: s -> (a, s) (bound at State.hs:6:40)
      s :: s (bound at State.hs:6:25)
      x :: State s a (bound at State.hs:6:12)
      f :: a -> b (bound at State.hs:6:10)
      (Some bindings suppressed; use -fmax-relevant-binds=N or -fno-max-relevant-binds)
Failed, modules loaded: none.

And finally we're missing a value of type s. We have two choices s and s'. The right one is probably s' because we have already used s once and it would be weird if there are some variables that aren't used. Our Functor instance ends up looking like:

instance Functor (State s) where
    fmap f x = State $ \s -> let State g = x
                                 (a, s') = g s
                             in (f a, s')

Testing that this implementation is actually correct I'll leave as an exercise because it'll be easier once we have also added Applicative and Monad instances, which is a out of scope of this post. However both of those can be made using the same mechanical approach we've been using throughout.

So whenever you're trying to build something in Haskell and it wont quite work, try to step back and look carefully at the type errors. Also try to use _ and undefined to only focus on one error at a time. I have found that doing this will quickly get you more comfortable with reading complicated types and finding ways they fit together.

If only Overcast had achievements

2x speed on Overcast

Since I started using Overcast I've been slowly increasing the playback speed and now I've arrived at the sacred 2x. I've gotten used to people talking quite fast so it almost makes me fall a sleep to listen to stuff at 1x.

I feel like Overcast should give users a special achievement when they've listened to enough stuff at 2x.

Cortex

I discovered the podcast Cortex last week and I've been listening to it pretty much non stop. The basic concept is that Myke Hurley and CGP Grey talk about how they get work done.

What really captivates me is how Grey has carefully evaluated and thought through everything he does (even as far as putting sleep on his calendar). Even though not everything he says would work for me, I found in extremely interesting to listen to people who care a lot about what they're talking about.

If you're a geek who likes "personal productivity" I highly recommend you listen to this.

Jurassic Park Theme (1000% Slower)

I head this mentioned on an episode of Cortex and I found it oddly satisfying.

I listened to it through part of my work day today and I did a good at keeping me focused on the task at hand.

Making a magical inbox folder

This is almost magic. Hook a folder on your desktop up to your OmniFocus inbox through Hazel. That way anything you put in there will appear in your inbox. I use this to capture everything from documents, to YouTube videos, and links from Twitter.

Dotfiles are meant to be stolen from

I wouldn't say dotfiles are meant to be forked but rather you're meant to steal stuff from other's. So in the interest of making your job a bit easier I thought I would talk about some of the things from my dotfiles that I think are worth stealing.

First things is the following aliases:

  • ee: Edit environment file. This is where my $PATH is configured as well as other shell variables.
  • ea: Edit aliases. I keep all my aliases in one file. This command opens that file.
  • ef: Edit functions. As for aliases, I keep all my shell functions in one file. This commands opens that file.

I find these mighty useful. If you're gonna fiddle, might as well make yourself more efficient at it.

Previously

← Archive