Skip to content

๐Ÿ› biome breaks characters in stdin modeย #2988

Description

@ton1517

Environment information

CLI:
  Version:                      1.7.3
  Color support:                true

Platform:
  CPU Architecture:             aarch64
  OS:                           macos

Environment:
  BIOME_LOG_DIR:                unset
  NO_COLOR:                     unset
  TERM:                         "screen-256color"
  JS_RUNTIME_VERSION:           "v20.12.2"
  JS_RUNTIME_NAME:              "node"
  NODE_PACKAGE_MANAGER:         "yarn/4.1.0"

Biome Configuration:
  Status:                       Loaded successfully
  Formatter disabled:           false
  Linter disabled:              false
  Organize imports disabled:    false
  VCS disabled:                 false

Workspace:
  Open Documents:               0

What happened?

When using the stdin mode in combination with โ€œ--colors off", some characters are displayed with broken characters.

image

I'm not a rustacean but have investigated the code.

The broken characters are Combining character which is represented by multiple code points.

fn main() {
    println!("{}", "โš ๏ธ".escape_unicode()); // \u{26a0}\u{fe0f}
    println!("{}", "aฬ‚".escape_unicode()); // \u{61}\u{302}
    println!("{}", "๏ฝถ๏พž".escape_unicode()); // \u{ff76}\u{ff9e}
    println!("{}", "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฑ".escape_unicode()); // \u{1f468}\u{1f3fb}\u{200d}\u{1f9b1}
}

--colors off or not using terminal will result in ColorChoice::Never.

fn compute_color(colors: ColorMode) -> (ColorChoice, ColorChoice) {
match colors {
ColorMode::Enabled => (ColorChoice::Always, ColorChoice::Always),
ColorMode::Disabled => (ColorChoice::Never, ColorChoice::Never),
ColorMode::Auto => {
let stdout = if io::stdout().is_terminal() {
ColorChoice::Auto
} else {
ColorChoice::Never
};
let stderr = if io::stderr().is_terminal() {
ColorChoice::Auto
} else {
ColorChoice::Never
};
(stdout, stderr)
}
}
}

If you run biome format in stdin mode, the entire code is output here.

console.append(markup! {
{output}
});

Finally, this write_str is called.

fn write_str(&mut self, content: &str) -> fmt::Result {

In the write_str method, convert characters in the case of Windows or ColorChoice::Never.
By grapheme.chars().nth(0), all combining characters output only the first code point.

if cfg!(windows) || !self.writer.supports_color() {
let is_ascii = grapheme_is_ascii(grapheme);
if !is_ascii {
let replacement = unicode_to_ascii(grapheme.chars().nth(0).unwrap());
replacement.encode_utf8(&mut buffer);

codesandbox

https://codesandbox.io/p/devbox/serene-swirles-6z4jfd?embed=1&file=%2Fsrc%2Findex.js%3A5%2C1&workspaceId=6833e942-7d98-4ed2-8f40-9a265aa14215

related issues

Expected result

The output of the code should not be converted anything.

Code of Conduct

  • I agree to follow Biome's Code of Conduct

Activity

  1. ematipico commented on May 26, 2024

    @ematipico
    Member

    Your investigation will be VERY USEFUL! Thank you!

    As a workaround, you can use --colors=force.

  2. added
    S-Bug-confirmedStatus: report has been confirmed as a valid bug
    S-Help-wantedStatus: you're familiar with the code base and want to help the project
    on May 26, 2024
  3. ton1517 commented on May 26, 2024

    @ton1517
    Author

    @ematipico I usually run biome check --apply --stdin-file-path $FILENAME in vim.
    When I add --colors force, the characters are not broken, but the terminal format code are inserted at the beginning and end of the file.

    vim

  4. melMass commented on Nov 18, 2024

    @melMass

    I'm experiencing the terminal code issue too now on neovim/macos
    image

    The formatter I use, conform, use these args, and in my options I'm adding --colors=force to this default set

  5. walkerdb commented on Apr 24, 2025

    @walkerdb

    @ematipico we are also running into this issue at Notion, where we use biome in a node process to auto-format and lint --fix template-generated strings of typescript code via child_process.spawn before we write them to file. Our "workaround" so far has been to write the code to temp files and use biome's normal --write mode instead of its stdin approach, but this is slower and also means we have to put the temp file in a similar location to the final source so that the right lint rules are applied, which is more painful to manage.

    The core issue seems not to be the color handling choice from termcolor but rather the conflation of colors and emoji in these two parts of the code

    if cfg!(windows) || !self.writer.supports_color() {
    let is_ascii = grapheme.is_ascii();
    if !is_ascii {
    let replacement = unicode_to_ascii(grapheme.chars().nth(0).unwrap());
    replacement.encode_utf8(&mut buffer);
    if let Err(err) = self.writer.write_all(&buffer[..replacement.len_utf8()]) {
    self.error = Err(err);
    return Err(fmt::Error);
    }
    continue;
    }
    };

    /// Replace emoji characters with similar but more widely supported ASCII
    /// characters
    fn unicode_to_ascii(c: char) -> char {
    match c {
    '\u{2714}' => '\u{221a}',
    '\u{2139}' => 'i',
    '\u{26a0}' => '!',
    '\u{2716}' => '\u{00d7}',
    _ => c,
    }
    }

    Colors are generally terminal-only escape characters and will not appear in a source-code file, so they're fine to retain or strip based on a terminal's color support.

    Emoji though have semantic meaning and are real parts of the source - by mutating emoji biome is changing the original source in a way that breaks the intention of the code's author. Whether or not a terminal will correctly display the emoji codepoints isn't really relevant, since what we care about is staying true to the content of the source file, not making the output easier to sight-read. It also means that the end-result output is different between the file-handling side of biome and the stdin side of biome, which leads to some confusion.

    Just to get a sense of y'all's thinking on this, how would you feel about a PR that removes the emoji-mutating code from the stdout processor? Or that adds some option to retain emoji codepoints while still making other color decisions per termcolor?

  6. ematipico commented on Apr 25, 2025

    @ematipico
    Member

    Thank you, @walkerdb, for chiming in! Please send a PR; everyone would appreciate it. As for the solution, I don't have strong opinions (or knowledge).

    Considering the use case, I believe it's mostly likely to have characters that would break than emojis that would break, so maybe an option to keep emoji codepoints should be fine? Again, I don't have much knowledge, so feel free to take the initiative and go for a solution that suits your needs

  7. ahmafi commented on Sep 15, 2025

    @ahmafi

    I didn't know that biome's LSP supports code formatting, if you want to use Biome in NeoVim, you can just use the formatter from the LSP, you don't need something like Conform that uses stdin to format the file. If you are using Conform, you can use the lsp_format = "prefer" or lsp_format = "first" for the languages that are using Biome.
    I couldn't get the organize-imports to work with LSP though.

  8. lagomcoder commented on Oct 22, 2025

    @lagomcoder

    Can confirm, diacritics are gone too:

    echo "anvaฬˆndarvillkoren" | tee /tmp/test.txt |  npx @biomejs/biome format --colors off --stdin-file-path=/tmp/test.txt
    
  9. kiyoon commented on Dec 9, 2025

    @kiyoon

    I have the same issue. Any workarounds?

  10. ematipico commented on Dec 9, 2025

    @ematipico
    Member

    I vibe-coded a possible solution. I might create a preview release so you can test it #8410

  11. Pajn commented on Dec 17, 2025

    @Pajn

    This issue should be reopened, the PR does not solve the whole problem, characters like โš  are still incorrectly replaced when they appear in the source code.
    Biome should only tweak its own output depending on the terminal, source code should appear as-is unless there is a lint or formatting rule to change it.

  12. ematipico commented on Dec 17, 2025

    @ematipico
    Member

    PRs are welcome then. I tried with my limited knowledge but it didn't work

  13. kiyoon commented on Dec 18, 2025

    @kiyoon

    I also still have the same issue with biome 2.3.10

  14. kiyoon commented on Dec 18, 2025

    @kiyoon

    } else if !self.writer.supports_color() {
    // On non-Windows with colors disabled:
    // Only convert single-codepoint graphemes (diagnostic symbols)
    // Multi-codepoint graphemes (like emoji with modifiers) are preserved for source code fidelity
    let chars: Vec<char> = grapheme.chars().collect();
    if chars.len() == 1 {
    let replacement = unicode_to_ascii(chars[0]);
    replacement.encode_utf8(&mut buffer);
    if let Err(err) = self.writer.write_all(&buffer[..replacement.len_utf8()]) {
    self.error = Err(err);
    return Err(fmt::Error);
    }
    continue;
    }
    // Multi-codepoint graphemes fall through to be written as-is below
    }

    This is clearly the problem. In the PR #8410, you modified certain emojis because they can be problematic in some terminals. The solution is that don't do it just because color is disabled, or rather, do it on actual diagnostic symbols and not the source code if they can be separated.

    I would just remove that block and it should be good. (at least for non-Windows)

  15. ematipico commented on Mar 21, 2026

    @ematipico
    Member

    Closing. The issues has been fixed in one of latest versions of Biome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-CLIArea: CLIS-Bug-confirmedStatus: report has been confirmed as a valid bugS-Help-wantedStatus: you're familiar with the code base and want to help the project

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions