A big part of our day as engineers is spent on the terminal. Yet, how many times have you used a terminal command only to find out that you don’t have a clue about what’s really happening behind the scenes or what to make of dozens of lines of output? Even worse, what if this command is going to change the state of the system in a major way and you don’t really have much control over what will happen?
I would argue that UX is rarely taken seriously in console tools. Yet it is as important for end-users (who, in this case, are engineers) as in any other UI out there.
A recent project we worked on allowed us to spend significant time coming up with a few best practices for UX on the terminal. The main principles that will be described here are Clarity and Control.
UX Principles in the Terminal
Clarity is about being transparent towards users; letting them know of everything important that takes place and making it easy for them to process this information. It includes elements like good information architecture, the proper use of colors, symbols, and icons, as well as being consistent throughout the experience. It’s about displaying all the necessary information (but not more than that) in a way that is readable and easy to digest.
Also, it’s about creating trust. When the user feels like they really know what is happening, good or bad, they can really trust the software.
Users of any UI need to feel that they are in control. For automated scripts this can be achieved in various ways. For example, it has to do with providing configuration options that alter the behavior of the script. A dry-run option is often very helpful in allowing users to only see what would happen, without actually having anything changed in their system.
It can also include the element of interactivity: allowing users to execute the script in steps and possibly making decisions along the way.
It also means giving them the appropriate amount of control, which results in a good automation/control ratio. For example, including options that give away part of the control in favor of automation, as well as having sane default options that won’t allow less experienced or eager users to mess something up by mistake.
We recently had to work on building a terminal command, as part of a new product we’re launching in Transifex, called Transifex Native. This command currently includes an SDK with Python and Django support (more programming languages and frameworks to come). With Transifex Native and this SDK, developers can have a totally new way of localizing their application, instead of using Django i18n or gettext.
We wanted to make the transition from such frameworks to Transifex Native as frictionless as possible, so we decided to create a Django migration command that automates an otherwise cumbersome, time-consuming, and uncertain process.
Here at Transifex we develop software using agile methodologies and we like to follow an iterative process, in which we deliver small and frequent increments. So the idea was to initially create the necessary functionality of the script, and later see how it could be improved further.
The Turning Point
As we started implementing the script and testing it on the Transifex codebase itself (we use Django for our main app), we realized that the changes made by the script seemed okay, but we weren’t 100% sure that was actually the case.
We were already displaying some important information like the file that was currently being migrated, how many changes had occurred, and so on, which was definitely helpful, but it didn’t feel like it was enough.
Moreover, for some reason git diff wasn’t showing the changes properly line by line, but instead it kind of mixed code chunks, making it even harder for us to spot any issues in the migration algorithm. The turning point was when we actually saw a couple of bugs in the output that were in front of us the whole time, but we just couldn’t see them.
So, right then and there, we realized that a barebones script is not going to cut it; if we were having trouble trusting the script that we had spent a good amount of time building ourselves, how could an engineer outside of Transifex feel confident that the script was changing thousands of lines in their codebase properly?
Bringing Better UX Into the Mix
The first thing we did was take a step back and think about what was important for us in the migration script, from the viewpoint of the end-user. There were mainly two things:
- We wanted to feel confident that the changes the script was making were correct and were not messing up the localized strings or the overall code in any way.
- We wanted to have control over what was happening.
This helped us come up with various concepts and features that would allow us to meet these two goals. We started building this functionality into the migration script and at the same time making it smarter in terms of what transformations it could apply in the code.
Implemented UX Concepts & Features
All in all, the following UX concepts and features were implemented during the development of the Django migration script:
- Confidence level
- Save modes
- Review modes
- Mark modes
- Visual feedback & information
- Safe & sane defaults
A migration script like this has complex work to perform. Some use cases will be straightforward for the script to handle, while others will be much trickier. For the latter, there is a risk that the transformed code is not flawless and it is important for users to know which these cases are.
Confidence level allows users to know if a transformation is considered risky or not
For these reasons we created the concept of confidence level. The script can smartly identify the code transformations that have increased risk, and thus allow users to only review those, instead of having to go through all the changes one by one with the same scrutiny.
Users can select among the following modes: none, backup, new, and replace. This is all about giving control over the most important aspect of the script: the changes it performs on the system.
Even though most codebases are under version control, having the ability to define how changes are saved can be important for some users.
The first option, none, results in a dry-run, which means that it won’t change anything on the user’s system. It is ideal for checking out the migration script initially, to get a feeling of how well it works and how much you can trust it with your code.
“New” save mode saves the migrated code in a new file
The next two, backup and new, correspond to an intermediate level of trust towards the script, since they will create backups of the original files or save the migrated code into new files, respectively.
Last but not least, replace, can be used when a user is ready to go through with applying the changes to the actual files of the project.
As a different aspect of control, review modes introduce a high degree of interactivity to the script. The available options are: none, string, file, string-low, file-low.
Available options when prompted to review a whole modified file
Setting aside none, which results in full automation, the string and file options give users the opportunity to see the migration algorithm in action and review each change. This can happen either on a string level, i.e. review the smallest chunks of code changed, one by one, or on a file level, i.e. review all changes that were done in a whole file at once.
Available options when prompted to review an individual modified string
The last two variants, string-low and file-low, are the same, except they only prompt the user in case of changes that have low confidence.
When the users are prompted for an action, there are many different options to choose from. The script allows them to print additional information like the diff, the original code, or the new code, to help them decide. It allows them to accept or reject the changes, or even accept or reject the rest of the changes, essentially doing a fast-forward and thus saving time.
This is another option that enhances automation and improves clarity. With none, file-low, and string-low values, it defines what the script will do when a risky migration is detected.
Special PROOFREAD_STRING comments are added to the final migrated file
The last two options will add special comments inside the migrated files, which will designate that a transformation is not to be 100% trusted , so users should do some proofreading afterward. This is another element that helps create trust, because it shows that the script knows its limitations and is transparent about them.
Visual Feedback & Information
This was achieved in 3 different ways: information, color-coding, and consistency. The script provides as much feedback to the user as possible while avoiding clutter.
Throughout the script, colors are used extensively to provide context and readability. File paths are always cyan, prompt messages are yellow, options, keys, and numbers are pink, string values are bold, and errors are red. When displaying code diffs, the previous code is shown in red, while the new code is shown in green.
Also, whitespace is utilized for readability and context, as it helps to visually categorize the information into sections in the absence of font variation. Finally, emoji icons are used wherever possible, to enhance the readability of the conveyed message.
Anything the user should know is displayed in the console. This includes explanations about what is going to take place next, feedback about what has happened, progress indications, and available actions.
Here are a few examples where all these are applied:
Every time the migration script is executed it shows a clear picture of what is going to happen
This appears at the beginning of the script execution. Since the script can potentially alter thousands of files, it is important to let the user know what will happen beforehand, in order to avoid any surprises. First of all, it is stated that the script is idempotent, which means that they can safely run it multiple times on the same project and the results will not change from run to run.
Also, users can see that the current config will result in replacing the source files in place (save policy), users will have the chance to review each string separately before accepting its changes (review policy), and all low-confidence changes will be automatically marked as such inside the file (mark policy).
When reviewing a transformation, users get all the information they need to make an educated decision
This is a richer example of what is shown to users when they are asked to review a change in the code. They can see the currently migrated file and its index, the number of strings that were modified, and the index of the change that is being reviewed. The options and the diff are clear, and there is visual feedback that shows what happened after the user’s action.
A full report of what happened during the migration
This report is printed every time the migration script ends. It shows aggregated statistics about the most important aspects of the script, so that users know what happened at a glance.
Safe & Sane Defaults
This is the default configuration of the command:
Default configuration is safe to run, even for large codebases
By default the script will not replace existing files but rather create new ones and it will allow users to review each file before writing the changes. Also, when asked if they want to go ahead with the actual migration, the default answer is “No”, to avoid changing multiple files by mistake.
Default action for a transformation is adjusted for maximizing user throughput
Finally, when users are presented with a change and are asked to review it, the default action is to accept the changes [A]. This was considered a sane choice so that users can easily go through most of the migration process with their hand on the Enter button.
The list shown above is not exhaustive; there are definitely more things you can do to improve the UX of console scripts. However, the impact these simple items can have is immense.
As a final note, I’d like to point out how satisfying it was to work on this project. The actual migration script was a great challenge on its own, of course, but in retrospect I believe that I got as much gratification from making a super usable UI as I did by coming up with the migration algorithms and architecture.
The techniques shown here are really easy to apply, so I hope we’ll see engineers give more love to console scripts in the future.