Starship: RFC: Rethinking the approach to sections/segments

Created on 23 Apr 2019  ·  4Comments  ·  Source: starship/starship

Motivation

Thinking over the implementation of spaceship, the "section" architecture has made sense for a long time, but as the complexity of sections grew with feature requests, I think we should rethink the approach to how sections of the prompt are divided.

The kubectl sections is a good example of where the "section" has been outgrown:

[kubectl prefix] [kubectl symbol] [kubectl context] ([kubectl namespace])
      at                ☸️               aks1            kube-system

image

This model only allows for prefix, value, and suffix. The only way to compose the above section is by giving individual components to a section multiple jobs, which will make configuration more difficult down the line.

Design

What I have in mind is the following breakdown:

└── Module
    ├── Prefix
    ├── Suffix
    └── Segment
        └── Value

Prompt - The whole visible prompt

Module - A collection of segments. In the above example, kubectl would be the module, with a prefix of at. The module could contain the default styling for each segment within it. A module could have one or many segments.

Segment - A segment is an individually configurable component within a module. In the above example, kubectl namespace would be a segment, which would inherit the styling from its module, but could have its styling overwritten through configuration or could be disabled altogether.

Drawbacks

  • Clarity when it comes to documenting the project structure to future contributors
  • Overhead involved with creating simpler modules (e.g. char)

Questions

  • Would we want prefixes/suffixes for segments as well as for modules?

Taking a look at kubectl namespace in the above example, would the parentheses around the namespace be simply a part of the segment value? It would be a shame to not support configurability with regard to the parentheses being there, but maybe that level of configuration is too granular? 🤔

I have done _very little_ OOP, so I am very open to suggestions to feedback!

Most helpful comment

We discussed this in person, but for posterity sake and for everyone else to see, @matchai and I had a small design session and were talking about a structure like this (note that this is not a circular architecture):

└── Module (implements a Segment struct)
    ├── prefix // Module prefix, applies to the entire module
    ├── suffix // Module Suffix, applies to the entire module
    ├── color // Module color, applies to the entire module
    ├── segments // Vector of segments
        └── segment_1
            └── prefix // Segment prefix, applies only to segment_1
            └── suffix
            └── color
            └── value
        └── segment_2 // We could also have a vector of segments
            └── ...
└── ...

So, each module implements a trait which contains a prefix, suffix, color, segments and format method (note: a module does not have its own value, rather it is formed by many segments in between its prefix and suffix). format should return the addition of all the segments surrounded by the prefix and suffix.

Using Matan's kubernetes example above, with the suggested architecture, the module would look more like this:

[module prefix] [segment_1]       [segment_2]      [segment_3]       [module suffix]
      "at "         "☸️"             "aks1"       "(kube-system)"            ""

If we delve deeper into each section, you can see that they have their own prefixes and suffixes (as well as a color, but same principle).

segment_1

[prefix]       [value]       [suffix]
  ""             "☸️"            ""

segment_3

[prefix]       [value]       [suffix]
  "("       "kube_system"       ")"

This is as deep as it goes, and should be a pretty familiar pattern and hopefully easy to grasp.

Rust-wise, it would look something like the below (note that I'm not at all knowledgeable at rust, so this is just pseudocode :P)

pub struct Module impl ModuleTrait {
    let prefix = PrefixSuffix {}; // white
    let suffix = PrefixSuffix {}; // white

    let color = "red"
    let segment_1 = Segment {prefix = PrefixSuffix {color = white}}
    let segment_2 = Segment {} // red
    let segment_3 = Segment {} // red

    fn format() {
        module_prefix + segment_1.format() + segment_2.format() + ... + module_suffix
    }
}

pub struct Segment {
    let prefix: PrefixSuffix;
    let suffix: PrefixSuffix;
    let value: String;
    let color: String;

    fn format() { 
        prefix + value + suffix // + color
    }
}

pub trait ModuleTrait {
    let prefix: PrefixSuffix;
    let suffix: PrefixSuffix;
    let segments: Vector<Segment>;

    fn format() { 
        // returns module prefix + all segments + module suffix
        prefix + segments.iter() + suffix // + color
    }
}

pub struct PrefixSuffix {
    let color = DEFAULT.WHITE;
    let text = DEFAULT.SINGLE_SPACE;

    fn format() {
        // return colored text
    }
}

All 4 comments

We discussed this in person, but for posterity sake and for everyone else to see, @matchai and I had a small design session and were talking about a structure like this (note that this is not a circular architecture):

└── Module (implements a Segment struct)
    ├── prefix // Module prefix, applies to the entire module
    ├── suffix // Module Suffix, applies to the entire module
    ├── color // Module color, applies to the entire module
    ├── segments // Vector of segments
        └── segment_1
            └── prefix // Segment prefix, applies only to segment_1
            └── suffix
            └── color
            └── value
        └── segment_2 // We could also have a vector of segments
            └── ...
└── ...

So, each module implements a trait which contains a prefix, suffix, color, segments and format method (note: a module does not have its own value, rather it is formed by many segments in between its prefix and suffix). format should return the addition of all the segments surrounded by the prefix and suffix.

Using Matan's kubernetes example above, with the suggested architecture, the module would look more like this:

[module prefix] [segment_1]       [segment_2]      [segment_3]       [module suffix]
      "at "         "☸️"             "aks1"       "(kube-system)"            ""

If we delve deeper into each section, you can see that they have their own prefixes and suffixes (as well as a color, but same principle).

segment_1

[prefix]       [value]       [suffix]
  ""             "☸️"            ""

segment_3

[prefix]       [value]       [suffix]
  "("       "kube_system"       ")"

This is as deep as it goes, and should be a pretty familiar pattern and hopefully easy to grasp.

Rust-wise, it would look something like the below (note that I'm not at all knowledgeable at rust, so this is just pseudocode :P)

pub struct Module impl ModuleTrait {
    let prefix = PrefixSuffix {}; // white
    let suffix = PrefixSuffix {}; // white

    let color = "red"
    let segment_1 = Segment {prefix = PrefixSuffix {color = white}}
    let segment_2 = Segment {} // red
    let segment_3 = Segment {} // red

    fn format() {
        module_prefix + segment_1.format() + segment_2.format() + ... + module_suffix
    }
}

pub struct Segment {
    let prefix: PrefixSuffix;
    let suffix: PrefixSuffix;
    let value: String;
    let color: String;

    fn format() { 
        prefix + value + suffix // + color
    }
}

pub trait ModuleTrait {
    let prefix: PrefixSuffix;
    let suffix: PrefixSuffix;
    let segments: Vector<Segment>;

    fn format() { 
        // returns module prefix + all segments + module suffix
        prefix + segments.iter() + suffix // + color
    }
}

pub struct PrefixSuffix {
    let color = DEFAULT.WHITE;
    let text = DEFAULT.SINGLE_SPACE;

    fn format() {
        // return colored text
    }
}

playing the devils advocate:

What do you guys think of the approach of removing all language specfic (rust python kubectl) specific code from the project and have all of it inside of config files? If the control is inverted like this it could be very trivial to add a language not currently supported by the project Golang, Elm or what ever.

Formatting the version returned from the command for the language is the only thing i can think of blocking this sort of feature without passing some bash?

thinking outside in JSON but could be whatever:

{
    "prompts": [{
        "name": "rust",
        "segment_1":  "🦀",
        "segment_2": "",
        "suffix": "",
        "criteria": {
            "files": ["Cargo.toml"],
            "extension": ["rs"],
            "folder": null,
        },
        "command": ["rustc", "-V"]
  }]
}

I like it! I think having a list of languages + giving a list of commands that are required to check versions would make things a lot easier since a lot of the steps are the same.

However, I remember you often would have more language-specific and complex logic that wouldn't work with this. For example, making sure you're using jq rather than something else when parsing a package.json or stitching together parts of a response to make the segments... So that's why this structure wouldn't work for a lot of our modules I think. We can always also allow the devs to give handler files that will take care of the logic for more complex commands, but 🤷‍♀?

This sounds like a really cool idea, and I'd love to eventually support custom sections.

The trouble with having baked-in languages all derive from configuration from config files is that we'd lose the eventual ability to add optimizations for each of them.

One example is that when nvm is used for managing your node version, you can use node --version to check the version, but NVM also generates a unique bin path for each node installation. That being the case, we can cache the Node version being used as long as $NVM_BIN hasn't changed. No spinning up of processes required.

Good brainstorming! 👱⛈

Edit: Oh! @sirMerr said something similar. 🙃

Was this page helpful?
0 / 5 - 0 ratings