You open a terminal many times every day, and most of the time, you follow the same five-minute sequence: get to your current project, run something inside the project, check the terminal prompt, open the file you had in mind. That sequence means some frictions, and there are tools that help solve them. Let me present such tools, each helping for a step of the sequence.

1. zoxide: get to the project

Once you start using zoxide, you don't cd to projects anymore. Instead, you type z followed by a fragment of the name of the destination folder.

As an example, let's say you have a directory path ~/projects/newsletter-manager. From any directory (/tmp, your home directory, or wherever the previous session left you), typing z news will get you in ~/projects/newsletter-manager. It's like jumping to the right place when you need.

The fragment is enough because zoxide ranks the directories you actually visit. You will see the evolution after a few weeks of normal work. The first week, the ranking database is thin, you use z to jump sparingly and you continue using cd as normal; the tool is learning about the paths you visit more often. By the second week, the jumps using z are your new habit.

Note that if two paths share a fragment, the one you visit more wins. When you want the other one, cd to it a few times so the database re-ranks, or type a longer fragment.

2. mise: run the project

Now, you're in the project. For the example, let's say it is a Node.js project that includes the following mise.toml file:

[tools] node = "20" [tasks.dev] run = "node dev-server.js" [tasks.build] run = "npm run build" 

The project's tasks are defined next to its tools and the pinned toolchain they run on. On a machine with no Node installed at all, when you cd into the project, mise will read the mise.toml file, see the node = "20" tool dependency pin, and install the missing Node. The next time you run mise run there, it will run the target task on the pinned toolchain.

To run the build task:

mise run build 

As you can see, no need to scan the README for "how do we start this", no trying npm run for a project that isn't managed using npm, no remembering whether this project's commands use npm or Make or a specific shell script. On every project, you can use mise tasks ls to see the tasks that are part of the project, and mise run <task> to run a given task.

Note that mise run <task> is the one-shot form. The task definition also has a "watch" form, for situations where you are editing code and need to rerun it as you go. For example:

mise watch -r dev 

When used, as in the above example, mise watch runs a task (the one defined via the [tasks.dev] from the mise.toml config above) and reruns it when files change. With -r, each file save kills the old process and starts the new one. The watching itself is done by watchexec, which must be installed; use mise use -g watchexec@latest if it isn't.

In addition to handling task definition, you can use the mise.toml file to set project environment variables. You can use the CLI; for example, run mise set PORT=3001 and mise writes the following block into the mise.toml:

[env] PORT = '3001' 

Now the project's dev server reads PORT without an export line in ~/.zshrc or ~/.bashrc and without a .env-sourcing step in the task.

3. starship: see where you are

While you work, the prompt carries the context. Inside the project, the prompt shows the git branch, so no more need for the occasional git status to know which branch you are on. And when the project has a pinned toolchain, the prompt shows the version, so a wrong version surprise is catched at the prompt, not a few commands later.

You don't configure this per project. Starship renders what's true in the current directory, and that is the point: the context is always on, and can be checked without an action or your memory check.

4. fzf: pick the file you need to work on

Do a Ctrl-T, type a search term for a file in the project, for example "app", and then Enter. The command line now holds the path you picked, pasted from a fuzzy search over the project's files. You don't need to remember if the file is src/app.js or app/index.js; you just type the fragment, and then you can pick your target file from the list of results.

This works also if you are in the middle of a command. For example, type vim, press Ctrl-T, and the picker helps you add the target path in place.

One important thing: The widget working here is wired by sourcing fzf's shell init (for zsh, source ~/.fzf.zsh). If the widget isn't bound on your shell, the picker won't fire on Ctrl-T.

Also, fzf can still be used in a scriptable way; for example via the command:

fzf --filter "app" < <(printf 'src/app.js\nsrc/cli.js\ntests/app.test.js\nREADME.md\n') # → src/app.js # → tests/app.test.js 

That's the same fuzzy match, without the TUI. This is useful for piping paths into other commands, and as a fallback when the widget isn't available.

What the composition of the tools brings

None of these four steps is impressive alone. But each one removes a small friction from the loop you open the terminal for:

  • zoxide to get to where the project lives on disk (using one fragment instead of a path)
  • mise for what the project's tasks and environment are, and how to run them
  • starship for what state you're in right now (branch, toolchain) without a check
  • fzf for which file you meant, when you don't quite remember the path

Each tool fires at a different moment in the loop, and the loop runs every time you open a terminal for a work session.

The wiring

To get the work session described above, those tools need to be enabled with one line in your ~/.zshrc (eval "$(mise activate zsh)", eval "$(starship init zsh)", eval "$(zoxide init zsh)") or ~/.bashrc, and fzf's keybindings need to be sourced.

This article expands issue #11 of my newsletter, Shellcraft — one piece of terminal craft per week.