ajalt / mordant

Multiplatform text styling for Kotlin command-line applications
https://ajalt.github.io/mordant/
Apache License 2.0
986 stars 34 forks source link

Best practices for dealing with interfering log statements #214

Open sschuberth opened 2 months ago

sschuberth commented 2 months ago

I'm wondering whether there are and hints / best practices on how to deal with log statements from third-party libraries while displaying a MultiProgressBarAnimation on the console. Log output from third-part libraries causes lines being printed to the console that mess up Mordant's rendering of progress bars etc.

As the log out is useful and should not be suppressed, would it maybe even make sense to have a Mordant-based log "appender" that can be used with common logging frameworks like Log4j or SLF4J to direct log output to a console are that Mordant "knows of" and thus does not mess with the progress bars?

sschuberth commented 2 months ago

I realize #99 might be a bit related, though I do not want to write to a file.

ajalt commented 2 months ago

I can see something like that being useful. On JVM at least, you can also redirect all of stdout through mordant: https://github.com/ajalt/mordant/issues/171#issuecomment-2131244827

sschuberth commented 2 months ago

Yeah, though a Mordant log appender which would allow you to redirect all log output e.g. to a Mordant table cell / panel would really be nice, I guess.

dimabran commented 2 months ago

A Mordant appender would be great

mikehearn commented 6 days ago

I have an internal app framework that wraps Mordant and it does this. At some point I hope to be able to contribute some of this stuff upstream, but unfortunately I don't have a whole lot of time to work on it. The gist is:

class RedirectingTerminalInterface(
        private val stdOut: PrintStream,
        private val stdErr: PrintStream,
        private val delegate: TerminalInterface
    ) : TerminalInterface by delegate {
        override fun completePrintRequest(request: PrintRequest) {
            val target = if (request.stderr) stdErr else stdOut
            if (request.trailingLinebreak) {
                if (request.text.isEmpty()) {
                    target.println()
                } else {
                    target.println(request.text)
                }
            } else {
                target.print(request.text)
            }
        }

        override fun readLineOrNull(hideInput: Boolean): String? {
            if (hideInput) {
                val console = System.console()
                if (console != null) {
                    // Workaround to a bug in macOS Terminal: if we don't send anything in the prompt to readPassword, the little "key" glyph
                    // that indicates the input isn't going to be echoed doesn't display consistently. So we send the ANSI "reset" escape, which
                    // doesn't really do anything.
                    //
                    // TODO(low): Is this still required after the upgrade to Mordant 2.6?
                    return console.readPassword("\u001B[m")?.concatToString()
                }
            }
            return readlnOrNull()
        }
    }

    private class PrintStreamWrapper(private val terminal: Terminal, target: PrintStream) : PrintStream(target) {
        override fun print(obj: Any?) {
            // TODO: Accept progress report objects.
            print(obj?.toString())
        }

        override fun print(s: String?) {
            // Work around a bug in Mordant.
            if (s != null && s.endsWith('\n'))
                terminal.println(s)
            else
                terminal.print(s)
        }

        override fun print(b: Boolean) {
            terminal.print(b)
        }

        override fun print(c: Char) {
            terminal.print(c)
        }

        override fun print(i: Int) {
            terminal.print(i)
        }

        override fun print(l: Long) {
            terminal.print(l)
        }

        override fun print(f: Float) {
            terminal.print(f)
        }

        override fun print(d: Double) {
            terminal.print(d)
        }

        override fun print(s: CharArray) {
            terminal.print(s)
        }

        override fun println() {
            terminal.println()
        }

        override fun println(x: Boolean) {
            terminal.println(x)
        }

        override fun println(x: Char) {
            terminal.println(x)
        }

        override fun println(x: Int) {
            terminal.println(x)
        }

        override fun println(x: Long) {
            terminal.println(x)
        }

        override fun println(x: Float) {
            terminal.println(x)
        }

        override fun println(x: Double) {
            terminal.println(x)
        }

        override fun println(x: CharArray) {
            terminal.println(x)
        }

        override fun println(x: String?) {
            terminal.println(x)
        }
    }

            val terminal = Terminal(terminalInterface = RedirectingTerminalInterface(System.out, System.err, Terminal().terminalInterface))

            // Ensure printing to stdout still works and pushes the messages _above_ the animation.
            System.setOut(PrintStreamWrapper(terminal, System.out))