How can I include user privilege indicators in shell prompts while keeping syntax highlighting enabled in codeblocks?

Using the CLI compiler typt (0.13.1), I’ve been writing notes about using shell commands on Linux, with syntax highlighting enabled.

Example snippet, to be run by a regular user:

#raw("chmod test.sh", lang: "sh")

Example snippet, to be run by with root privileges:

#raw("chown user test.sh", lang: "sh")
Output screenshot

typst-raw-sh

I would like to include the symbol $ for commands that can be run by regular users and # for commands that require root privileges at the beginning of each line. (This is a convention used by the Arch Wiki. See this page as an example.)

Now, I’m wondering if it’s possible to do this with correct syntax highlighting. By ‘correct’, I’m thinking of how the Markdown renderer handles it here on Typst forums when a codeblock has the filetype set to shell or console instead of sh:

$ chmod test.sh
# chown user test.sh

I’ve tried changing the lang option to shell or console in Typst, but doing so in disables syntax highlighting for the current raw block altogether.

Currently, if I want to keep syntax highlighting on, I have to use sh, where $ is marked as a command and everything after it as arguments:

Example snippet, to be run by a regular user:

#raw("$ chmod test.sh", lang: "sh")
Output screenshot

typst-raw-sh-p

And #, together with everything after it, is marked as a comment:

Example snippet, to be run with root privileges:

#raw("# chown user test.sh", lang: "sh")
Output screenshot

typst-raw-sh-pr

If you want to use it for only oneliners:

#show raw: set text(font: "Fira Mono")

#let user(command) = emph(raw("$ ")) + raw(command, lang: "sh")
#let root(command) = emph(raw("# ")) + raw(command, lang: "sh")

Example snippet, to be run by a regular user:

`$ ````sh chmod test.sh```

#user("chmod test.sh")

Example snippet, to be run by with root privileges:

`# ````sh chown user test.sh```

#root("chown user test.sh")

image

For multi-line stuff you can do something like this:

#show raw: set text(font: "Fira Mono")

#let user(command) = {
  show raw.line: it => emph(text(font: "Fira Mono", "$ ")) + it
  command
}
#let root(command) = {
  show raw.line: it => emph(text(font: "Fira Mono", "# ")) + it
  command
}

Example snippet, to be run by a regular user:

#user(```sh
chmod this
chmod that
```)

Example snippet, to be run by with root privileges:

#root(```sh
chown user this
chown user that
```)

image

Or if you want to write the appropriate prefix yourself, here is a little trick (note that you need to specify an absolute font size):

#show raw: set text(font: "Fira Mono", size: 10pt)

#show raw.where(lang: "console"): it => {
  let user = emph(raw("$ "))
  let root = emph(raw("# "))
  it
    .text
    .split("\n")
    .map(line => {
      let starts(pattern) = line.starts-with(pattern)
      let prefix = if starts("$ ") { user } else if starts("# ") { root }
      let text = line.replace(regex("^[$#] "), "")
      prefix + raw(text, lang: "sh")
    })
    .join("\n")
}

```console
$ chmod this
$ chmod that
# chown user this
# chown user that
```

image

I tried using show raw.line inside show raw.where(lang: "console"), but I hit infinite show rule recursion and I don’t know if it is fixable. So this approach is the second one, and it actually works. But multi-line commands probably will not be highlighted properly. Maybe with enough dedication a more resilient solution can be found.

1 Like

Thank you for your reply! Out of these solutions, the third one seems the most appealing (where I provide $ or # on each line myself).

The only (minor) inconvenient part about it is this part you mentioned:

note that you need to specify an absolute font size

By that, did you mean that it has to be specified manually in pt or em, or that it can’t depend on whatever font size is set globally throughout the document?

I’ve tried replicating the default size by giving the console block 1.11em and compared it with a regular code block, like so:

#set text(size: 10pt)

Regular fence:

```
# chown user test.sh
$ chmod this
$ chmod that
# chown user this
# chown user that
```

#show raw: set text(font: "DejaVu Sans Mono", size: 1.11em)

#show raw.where(lang: "console"): it => {
  let user = emph(raw("$ "))
  let root = emph(raw("# "))
  it
    .text
    .split("\n")
    .map(line => {
      let starts(pattern) = line.starts-with(pattern)
      let prefix = if starts("$ ") { user } else if starts("# ") { root }
      let text = line.replace(regex("^[$#] "), "")
      prefix + raw(text, lang: "sh")
    })
    .join("\n")
}

Console fence:

```console
# chown user test.sh
$ chmod this
$ chmod that
# chown user this
# chown user that
```

I don’t know how accurate this setting is (can it be even more accurate?), but it seems to work fine. So, if I change the font size in the first line to something else, like 14pt, the console block scales to it.

Output screenshot

This is just my observation, so if you happen to know of any pitfalls about this that I’m not aware of, feel free to let me know.

See Length Type – Typst Documentation and here is an example without the absolute font size set:

code
#show raw: set text(font: "Fira Mono")

#show raw.where(lang: "console"): it => {
  let user = emph(raw("$ "))
  let root = emph(raw("# "))
  it
    .text
    .split("\n")
    .map(line => {
      let starts(pattern) = line.starts-with(pattern)
      let prefix = if starts("$ ") { user } else if starts("# ") { root }
      let text = line.replace(regex("^[$#] "), "")
      prefix + raw(text, lang: "sh")
    })
    .join("\n")
}

```
$ chmod this
$ chmod that
# chown user this
# chown user that
```

```console
$ chmod this
$ chmod that
# chown user this
# chown user that
```
output

image

After a quick search I didn’t find a place where this issue was discussed, but I think by default the raw font size is like 0.9em, which means nested raw elements will have different font sizes due to 0.9 ⋅ 0.9 which doesn’t happen with absolute length value. You might be able to change the solution so that this issue doesn’t affect it.