Skip to content

Prompts

maxlandon edited this page May 29, 2023 · 8 revisions

Summary

The underlying readline shell support various prompt types, which can be used altogether should you need to:

  • PS1 (primary prompt)
  • PS2 (secondary prompt)
  • RPROMPT (right-side prompt)
  • Tooltips (prompt conditionally printing itself if input line matches a condition)
  • Transient (transient prompt refresh before executing commands)

As such, the console will perfectly accomodate itself of oh-my-posh prompt engines for each menu. This allows users to have a cross-platform, versatile and customizable prompt system. Courtesy of @JanDeDobbeleer and its amazing maintenance and code, users now can:

  • Write custom prompt segments, specific to their application logic and/or to a console menu.
  • Use those segments in a prompt configuration file loaded by default (or not) in their applicaiton.
  • Make use of all the builtin prompt segments offered by Oh-My-Posh.

Binding simple prompt handlers

In many cases a prompt engine as elaborate as oh-my-posh is not required, and developers/users will accomodate themselves with simpler prompt handlers. The following snippet shows how to bind such handlers for a given menu: each menu can have its own.

// Using the main/default menu to bind our prompts
mainMenu := app.Menu("")
prompt := mainMenu.Prompt()

// Assigning handlers to each prompt type
prompt.Primary = func() string { return "application >" }
prompt.Secondary = func() string { return ">" }
prompt.Right = func() string { return fmt.Sprintf(time.Now().Format("03:04:05.000")) }
prompt.Transient = func() string { return ">>>" }

prompt.Tooltip = func(tip string) string { 
    switch tip {
    case "git":
        return "branch something"
    case "command":
        return "status needed by command"
    default:
        return ""
    }
}

(Optional) Using oh-my-posh prompt engine

If you wish to use oh-my-posh prompt engines, you can several from high to deep in detail and customization. You can either simply load a default engine, using a specific configuration file or the user-one, and/or you can add custom prompt segment writers to the engine itself, and reference them in an application-specific configuration file.

Loading a default engine, with user-configuration.

We start with a default engine, which prompt handlers are bound to our current menu like above:

import (
	"os"

	"github.com/jandedobbeleer/oh-my-posh/src/engine"
	"github.com/jandedobbeleer/oh-my-posh/src/shell"
)

// prompt wraps an oh-my-posh prompt engine, so as to be able
// to be configured/enhanced and used the same way oh-my-posh is.
// Some methods have been added for ordering the application to
// recompute prompts, print logs in sync with them, etc.
type prompt struct {
	*engine.Engine
	app *console.Console
}

// LoadConfig loads the prompt JSON configuration at the specified path.
// It returns an error if the file is not found, or could not be read.
func (p *prompt) LoadConfig(path string) error {
	if _, err := os.Stat(path); err != nil {
		return err
	}

	flags := &platform.Flags{
		Shell:  shell.GENERIC,
		Config: path,
	}

	p.Engine = engine.New(flags)

	return nil
}

// bind binds all oh-my-posh prompt handlers to the main menu prompt.
func (p *Prompt) bind() {
	prompt := p.app.Menu("").Prompt()

	prompt.Primary(p.PrintPrimary)
	prompt.Right(p.PrintRPrompt)

	secondary := func() string {
		return p.PrintExtraPrompt(engine.Secondary)
	}
	prompt.Secondary(secondary)

	transient := func() string {
		return p.PrintExtraPrompt(engine.Transient)
	}
	prompt.Transient(transient)

	prompt.Tooltip(p.PrintTooltip)
}

Implementing the oh-my-posh SegmentWriter

A prompt segment writer is any type satisfying the following interface:

import (
	"github.com/jandedobbeleer/oh-my-posh/src/platform"
	"github.com/jandedobbeleer/oh-my-posh/src/properties"
)

type SegmentWriter interface {
    Enabled() bool
    Template() string
    Init(props properties.Properties, env platform.Environment) {
}

This interface is quite simple, but also very powerful. It allows library users to declare an arbitrary structure, and to use its various fields in a template string in order to provide a prompt segment. The following is a very simple example, where we just alias a string, and where we just return the value of it. You could as well return a full template in the Template() string method.

The following is maybe the simplest segment one can write, from the example console:

type Module struct {
	Type string
	Path string
}

func (m Module) Enabled() bool {
	return string(m.Path) != ""
}

func (m Module) Template() string {
	return string(m.Path)
}

func (m Module) Init(props properties.Properties, env platform.Environment) {
}

var module = Module{"scan", "protocol/tcp"}

Binding to oh-my-posh

You can then directly register this segment to the oh-my-posh library, without going through the console:

import (
	"github.com/jandedobbeleer/oh-my-posh/src/engine"
)

func main() {
    engine.Segments[engine.SegmentType("module")] = func() engine.SegmentWriter { return module }
}

Note: It is not mandatory to register a custom segment before loading a configuration making use of it. You could as well go the next section of this page, and add this code hereafter. The prompt engine would still work as intended.

Referencing the segment in a configuration

The following is an extracted snippet of the example application prompt configuration:

    "blocks": [
        {
            "alignment": "left",
            "newline": true,
            "segments": [
                {
                    "style": "plain",
                    "template": " using (<#df2e1c>{{ .Segment }}</>) ",
                    "type": "module"
                }
                ... other segments

Loading a prompt configuration

Now that we have our custom segment, we need to load a custom prompt configuration should we want to, for a given menu. Please see [here] for a complete documentation on how to write this configuration.

One of the big advantages of being able to use the oh-my-posh prompt engine, is that we can now use our module segment in pretty much any block of the prompt (primary/secondary/RPROMPT/tooltip, etc).

Coming back to our newly created menu:

func createMenu(c *console.Console) {
    clientMenu := c.NewMenu("client")

    prompt := clientMenu.Prompt()

    // Add custom segments to the prompt library.
    // (again, can be used by all menu engines)
    engine.Segments[engine.SegmentType("module")] = func() engine.SegmentWriter { return module }

    // Load a configuration file on the system,
    // where we actually use our new prompt segment.
    prompt.LoadConfig("prompt.omp.json")
}

References and documentation

  • Here for an introduction to the oh-my-posh prompt concepts.
  • Here for documentation on how to write configurations and segments.