Liner is a pure Go line editor with history and no cgo
Pure Go line editor with history, inspired by linenoise
At a glance
- What is it?
- Liner is a command line editor with history, inspired by linenoise, written in pure Go so that a cross platform application can embed it without cgo, and it deliberately drops the key bindings that behave differently on Unix and Windows. It also falls back to plain line reading when the terminal is too narrow to edit in.
- Who is it for?
- Adopt Liner when you are writing a command line program in Go that needs line editing, history and completion, and you want the same behaviour on every platform you ship. Do not adopt it for a full terminal multiplexer, since it is a line editor rather than a screen editor, and do not expect Unix shortcuts you are used to, because Ctrl-Z is ignored on purpose.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 15 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A line editor for applications, not a terminal
Liner is a command line editor with history, inspired by linenoise, and the framing sentence in the readme sets the expectation about the terminal. Everything Unix-like is a VT100, or is trying very hard to be, and if your terminal is not pretending to be a VT100, change it. Liner also supports Windows.
The distinction between a line editor and a terminal emulator is the one to hold on to. Liner edits one line of input at a time, in the style of the prompt you get in a shell, and it is a library you link into a program rather than a program you run. Its job is to make the line the user is typing behave like a line in a shell, with history, completion and the usual cursor movement, without the program having to implement any of it.
The implementation language is Go, and the licence is MIT per the repository metadata, with the readme saying that Liner is released under the X11 license, which it describes as similar to the new BSD license. The two are compatible in practice, and the COPYING file at the root of the repository is the authoritative text.
The last push was on 2026-10-08, the module targets Go 1.26.0, and the repository has no GitHub releases, so the version history lives in the commit log and in the module proxy.
Pure Go means the same behaviour everywhere, including the omissions
The design rationale is stated in one paragraph and it is more interesting than a build instruction.
Liner is intended for use by cross-platform applications. Therefore, the decision was made to write it in pure Go, avoiding cgo, for ease of cross compilation.
The cgo avoidance is about distribution, and it is the usual reason a Go project does this. A cgo dependency means the binary needs a C toolchain on every target and a C library at runtime, which for a library embedded in someone else's application is a serious imposition on the people who depend on it. A pure Go build cross compiles with a single command.
The second half of the paragraph is the more interesting part. Features only supported on some platforms have been intentionally omitted. The example given is Ctrl-Z, which is suspend on Unix and EOF on Windows, so in the interest of making an application behave the same way on every supported platform, Ctrl-Z is ignored by Liner.
That is a deliberate subtraction. The alternative, supporting Ctrl-Z on Unix and treating it as EOF on Windows, would give every application a platform dependent behaviour and every application author a bug report about it. Choosing to have one binding do nothing is a defensible answer, and it is the kind of decision a library makes on behalf of every application that uses it.
The same reasoning explains the platform specific source files in the repository, since the terminal handling differs but the behaviour does not.
The key bindings, and the history search that keeps what you typed
The line editing table is the core of the library, and it is worth reading for the two entries that are not what a shell user expects.
Movement is the familiar set. Ctrl-A and Home move to the beginning of the line, Ctrl-E and End to the end, Ctrl-B and Left move one character left, Ctrl-F and Right one character right, and Ctrl-Left or Alt-B and Ctrl-Right or Alt-F move by word. Deletion has the usual shapes, with Ctrl-D deleting the character under the cursor when the line is not empty and meaning end of file when it is, Ctrl-H and Backspace deleting before the cursor, Ctrl-W and Alt-Backspace deleting the word before the cursor and Alt-D the word after it, and Ctrl-K and Ctrl-U cutting to the end and to the start respectively.
History is Ctrl-P and Up for the previous match and Ctrl-N and Down for the next, and Ctrl-R opens a reverse search with Ctrl-S for forward and Ctrl-G to cancel. Completion is Tab for the next completion and Shift-Tab, after a Tab, for the previous one.
The behaviour worth knowing is in the note below the table. Previous and Next match from history retain the part of the line that the user has already typed, similar to zsh's up-line-or-beginning-search, which is the default on some systems, or to bash's history-search-backward.
That is a genuinely different behaviour from the plain up arrow in most shells, where the whole line is replaced. Here the text you have already typed is preserved and the history match is appended, which is what you want when you are typing a long command and want to change only its first word.
A narrow terminal silently changes the mode you are in
There is a paragraph about prompts that reads like a footnote and is really a behavioural contract.
Prompts should be kept short. If a prompt leaves fewer than 10 columns of working space in the terminal, where the terminal width is less than the prompt width plus 10, Liner falls back to unsupported mode, which is plain line reading without editing support. In this mode, pressing Ctrl-C raises SIGINT instead of resetting the prompt or returning ErrPromptAborted.
So there are two modes and the switch is invisible unless you know about it. In the normal mode, Ctrl-C resets the input and you get an empty prompt, and the abort case surfaces as ErrPromptAborted to the caller. In the fallback mode, Ctrl-C sends SIGINT to the process, which is the traditional Unix behaviour and probably kills your application.
The reason for the fallback is straightforward. A line editor needs to render the prompt and the line the user is typing, and if there is no room, the cursor maths breaks and the display becomes garbage. Rather than draw nonsense, the library stops editing.
The ten column margin is the interesting number, because it is the difference between a prompt that fits and one that eats the screen. A program that prints a long banner as its prompt will find itself in fallback mode in a narrow terminal, and its Ctrl-C behaviour will change with the window size, which is exactly the kind of thing that only shows up on someone else's machine.
Using it: a completer, a history file and the abort error
The getting started section is a complete program, and the shape of it is the API in miniature.
line := liner.NewLiner()
defer line.Close()
line.SetCtrlCAborts(true)
line.SetCompleter(func(line string) (c []string) {
for _, n := range names {
if strings.HasPrefix(n, strings.ToLower(line)) {
c = append(c, n)
}
}
return
})The completer is a function that takes what has been typed and returns the candidates, and the example matches on a prefix, lowercasing the input so the comparison is case insensitive.
SetCtrlCAborts(true) is the switch that changes the meaning of Ctrl-C, and it is the first thing an application should decide. With it set, an aborted prompt comes back as the error ErrPromptAborted rather than as a signal, which is what lets the example print Aborted and carry on.
The rest of the example is the history file. It opens a file in the temp directory, calls ReadHistory on it, and writes the history back at the end with WriteHistory after appending the accepted line with AppendHistory. The history is yours to store, and the library only offers the read and write primitives.
The documentation is on godoc, and the import path is github.com/peterh/liner.
One file per platform, and a deliberately small dependency list
The top level listing is a portrait of a library that has to behave differently in five places.
There is input.go as the shared interface, and then input_linux.go, input_darwin.go, input_solaris.go and input_windows.go, with bsdinput.go and fallbackinput.go alongside them. The output side is the same story, with output.go, output_unix.go, output_windows.go and output_solaris.go, and unixmode.go and unixmode_solaris.go for the terminal mode handling. width.go handles the character width calculation that a line editor needs to know where the cursor is, and common.go holds the shared pieces.
That split is what the pure Go decision buys. Each platform's terminal handling is in its own file, so the Windows implementation of the same behaviour is a separate piece of code rather than a branch through the shared one.
The tests match the layout. There is input_test.go, input_windows_test.go, line_test.go, prefix_test.go, width_test.go and race_test.go, so the concurrency behaviour is checked explicitly rather than incidentally.
The dependency list in the module file is short. go-runewidth is there for the character widths, golang.org/x/sys for the system calls, and the only indirect dependency is the Unicode text segmentation library. There is no cgo and no third party terminal library, which is the point of the whole exercise.
So for a project that wants a line editor in a binary it ships to several platforms, this is about two dependencies and a set of small files per platform.
Editorial conclusion
Adopt Liner when you are writing a command line program in Go that needs line editing, history and completion, and you want the same behaviour on every platform you ship. Do not adopt it for a full terminal multiplexer, since it is a line editor rather than a screen editor, and do not expect Unix shortcuts you are used to, because Ctrl-Z is ignored on purpose. Verify first that your terminal provides a VT100 interface, since the library falls back to unsupported mode otherwise, and that you handle ErrPromptAborted, because a user pressing Ctrl-C is a normal outcome rather than an error.
Frequently asked questions
What is the Liner library?
It is a command line editor with history, inspired by linenoise, for use by cross-platform applications. It supports the usual VT100 line editing, history search and completion, and it also supports Windows.
Why is Liner written in pure Go?
Because it is intended for use by cross-platform applications, so the decision was to avoid cgo for ease of cross compilation. The same cross-platform goal is why features that behave differently on some platforms were intentionally omitted, with Ctrl-Z ignored since it is suspend on Unix and EOF on Windows.
How does Liner handle a terminal that is too narrow?
If the prompt leaves fewer than 10 columns of working space, Liner falls back to unsupported mode, which is plain line reading without editing support. In that mode pressing Ctrl-C raises SIGINT instead of resetting the prompt or returning ErrPromptAborted.
What does the history search in Liner do differently to a shell?
Previous and next match from history retain the part of the line that the user has already typed, similar to zsh's up-line-or-beginning-search or bash's history-search-backward, rather than replacing the whole line.
How do I handle a user pressing Ctrl-C in Liner?
Call SetCtrlCAborts(true) so the abort is returned as the ErrPromptAborted error rather than as a signal, and then check for that error after the prompt call. Without it, Ctrl-C resets the input and returns an empty prompt.
What are Liner's dependencies?
Two direct ones, github.com/mattn/go-runewidth for character widths and golang.org/x/sys for system calls, plus one indirect dependency. There is no cgo and no third party terminal library.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/peterh-liner)Was this useful?