Adrian Schneider

Thoughts & Rants on Software Development

Jul 2015The Multiple Monitor Vim IDE

I program in Vim, using a terminal application in fullscreen. I love immersing myself in an environment and not switching contexts. I spent a lot of time researching how people can use vim/linux as an IDE and span across multiple monitors effectively. Sadly, the search came back empty-handed.

I don't want to have multiple vim instances talking to each other, and I don't want to be forced into any particular layouts. I like remaining flexible. I switch between a 13" laptop and three 27" displays at the office.

I just wanted to document my general set-up without going into too many implementation details. Those will likely come in future posts.

Since I already use tmux to manage multiple windows, I wanted something that would fall in line with my current workflow. I've identified two main contexts of work:

  1. Programming: I'd like to stay in Vim, and ideally just run commands from the command mode. (ex: :!chmod +x % to make the current file executable). Compiling, running tests, and other programming-related tasks are secondary to simply editing.

  2. System Administration: For everything else, like working on remote servers, managing local services, and every other random task, I don't really care as much since this context eats up less than 25% of my time.

It became evident that I want to optimize for programming inside of vim, while still enabling other tasks to be done effectively without being an expensive context switch. The big question then is: if you had more space, what could you use it for?

Within my programming context, I came up with a few different types of tasks I want to perform, which compete for screen focus & real-estate:

  • Text editing
  • Running tests
  • Viewing logs
  • Interacting with JIRA
  • Interacting with an API manually

I tried my best to split those into two groups: interactive and non-interactive. Interactive tasks I either need to trigger, or I need to constantly interact with.

Under non-interactive, it's pretty much just watching log files. However, if I get creative and just trigger tests from Vim, I can treat them as non-interactive as well. Or, minimally interactive.

Under interactive, that leaves my CLI JIRA app, and then the random curl calls or similar things. However, as I iterate through a feature, I'm mostly just re-running a past task anyway. Let's chalk that with running tests. Or, better put: running commands.

Having 1000 things on the screen is easy... the real trick is to deal with focus. How do you get to the window you want to without too much effort?

My solution to this is limit the tasks that require any focus. If something can be completely non-interactive, it reduces the effort required to benefit from it, and to maintain it.

The Layout

Let's assume my current triple-monitor setup:

Monitors

The regions are as follows:

  • Left: 2 regions for different log streams l1, and l2
  • Center: Huge Vim, and then the other tmux windows (zsh, git, running app, etc)
  • Right: 2 interactive regions: one large that receives commands r1, and one focused r2.

I found that trying to manage all of the different logs files I'm interested in is too much work, so I abstracted that away. Similarly, I never (or rarely) need to go anywhere except for Vim or the other focused window. This lets me switch focus between Vim and Focused Region with Alt+Tab or similar.

Each non-interactive region is named and tails a log file: tail -f /var/log/stream/[name] &. This lets me easily redirect any stream to that location via tee. some_process | tee -a /var/log/stream/l2 which I alias as t: some_process | t r1. This lets me easily aggregate logs to those areas that are related. I can manage those logs externally without having to nagivate tmux panes which ruins the focus.

If you don't want a historical copy of your data, can further optimize this by using named pipes (aka FIFOs) instead of writing all of that data to disk.

# for each region:
rm /var/log/stream/$region
mkfifo /var/log/stream/$region

For sending commands, I won't go into too much detail in this post, but the short answer is configuring Vim to be able to run arbitrary commands via tmux send-keys. Among other more complex cases, I added a :Run command and plenty of hot-keys per project:

  • :Run ls will run ls on the configured target session
  • T in normal mode will run my test suite
  • K in normal mode to send Ctrl+C to stop something bad
  • ! in normal mode will run the last command (sends !! in target session, hint, "retry")
  • and just saving will trigger any relevant tests to run

The target session is easily changable at run-time, and points to a tmux pane, which can be on another session. The beauty of this is that session can be active on a remote computer over SSH. In my primary case, another monitor, but it leaves it flexible. You can use this to gain extra monitors your computer may not natively support, or to pair program.

Hopefully this helps give you some ideas or a template to base your own madness on. I'll be following up with more specific tools on how to set this up.

May 2014Slowing Down the Train

One of the hardest things to do when delivering software is slowing down. Deadlines are tight, and you only ever have time to write more code on top of the existing mess that is there. The train is running at full speed, but it's not sustainable; not at this pace.

OK, so you're responsible, and you queue cleanup tasks with each shortcut you take. Documenting technical debt is a great first step, but it's nearly impossible to pay back with minimum payments only. Aside from the inevitable bankruptcy, there is another problem.

Design is emergent. Unless you're brilliant or your requirements truly never changed since day one, you are going to make inadequate design decisions early on. It's just a reality. By continuing to pile on cdoe onto a poor design, you are continuing to write legacy code. Things will never get better.

It's not iterative development if you completely omit the refactor cycle in your loop. Eventually you'll run out of track. The real question is: can the train be slowed down, or does it need to run its course?

May 2014Designing for Testability

I've been following along with the TDD is Dead series, and it has made me appreciate how far along I've come these past few years. If you haven't tuned in already, it's a very good listen full of insights at many different levels. It's a battle between ideals and convention. On on side, you have Kent Beck and Martin Fowler: pioneers of TDD and unit testing. On the other, you have DHH, the creator of the Ruby on Rails framework. Each side has a wealth of experience, but look at programming from nearly polar opposite viewpoints.

The main argument DHH (and many others) have against testing is the insane lengths you have to go to isolate code to allow testing. In some cases, you are creating abstractions and layers for the sake of testing. In this sense, the code you are writing is actually worse off without testing.

I want to ignore all of the benefits of testing for a moment here, and focus on design, because I think that's what the discussion really needs to be about. The underlying quesiton is: can you have well-tested code and well-designed code that concisely conveys its intent.

Of course you can, but it's not easy. There is a natural progression of hitting extremes that is part of the learning process. With more experience, you find a good balance. What I see happen, and what turns people off of TDD, is hitting that extreme and stopping. This misses the refactor step of the TDD cycle, and with more experience, the refactorings are more valuable.

Wrapping things in artificial layers and introducing more intermediate results may make testing easier, but it doesn't necessarily make your application any better. More often than not, you can't fix your design issues without fixing things at a higher level. And even more dangerously, if you already think your design is perfect, you have no chance.

As you write new code, you can't be afraid of improving the foundation it is built upon. When you are stuck, sometimes taking only a few steps back isn't enough to find your way. You need a fresh mind and willingness to introduce real change. You need to change code at a higher level and improve how things communicate. Continuing to pass the same messages through a new system might not be enough. You need to improve the message as well. And to me, this is where most people fall short when trying to refactor their application.

If the message remains the same between each layer of your application, the boundaries aren't adding any value. They may help with code isolation, but they are just artificial layers. Without boundaries, you are not gaining trust, and your layers will have cracks.