Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I paginate picocli results like Git does it? #1252

Closed
jlengrand opened this issue Nov 4, 2020 · 8 comments
Closed

How can I paginate picocli results like Git does it? #1252

jlengrand opened this issue Nov 4, 2020 · 8 comments

Comments

@jlengrand
Copy link

Hey there,

I am creating an issue based on a question I raised a few weeks back on Twitter.

I currently have a fun little API that returns results from the Star Wars API. Depending on the query, it returns a lot of results.
I would like to be able to present the results paginated, like Git does it. Like @remkop already mentions it on Twitter, it would be best to leverage already existing infrastructure on the system rather than implement a local pagination.

I've been fiddling around with code for the past few days but I'm afraid I didn't manage to come up with anything conclusive yet. The twitter thread seems to mention IExecutionHandler but I couldn't find anything related in the source code of picocli, so I have been looking at creating a custom IParseResultHandler instead. Past that, I also ran into stdin forwarding issues with ProcessBuilder, so it's hard to come with a minimal reproducible example.

Any help moving in the right direction would be most appreciated!

@jlengrand
Copy link
Author

jlengrand commented Nov 4, 2020

Just to be clear, I do have some code lying around that attempts to do what is described in the Twitter thread, I simply didn't get it to function as expected just yet.

You can find my attempt here

Most of the noteworthy code is quite small :

    private fun executionStrategy(parseResult: ParseResult): Int {
        println("in there")
        val processBuilder = ProcessBuilder("less")
        val process = processBuilder.start()
        this.spec.commandLine().out = PrintWriter(process.outputStream)
        return RunLast().execute(parseResult)
    }

    companion object{
        @JvmStatic
        fun main(args: Array<String>){
            val app = SwaCLIPaginateDummy()
            exitProcess(CommandLine(app)
                    .setExecutionStrategy(app::executionStrategy)
                    .execute(*args))
        }
    }

I tried to strip down as many things as possible in there. Example to be ran with the "planets" argument to trigger the printing of the dummy string

@remkop
Copy link
Owner

remkop commented Nov 4, 2020

Thank you for raising this!
There are other commitments that I have to take care of first, but I will take a look.

EDIT: sorry, on Twitter I said IExecutionHandler, I meant IExecutionStrategy. But it looks like you already figured that out. 👍

@jlengrand
Copy link
Author

jlengrand commented Nov 5, 2020

No hurry really, this is not a priority. I keep tinkering with it and I think someone used to the library might be going faster. Thanks!

@remkop
Copy link
Owner

remkop commented Nov 5, 2020

No success yet. One small thing:
The planets command should not write to STDOUT, but to the output stream of the commandline:

@Command(name = "planets", description = ["Search for planets"])
class PlanetsCommandPaginateDummy : Callable<Int> {

    @Spec
    lateinit var spec: CommandSpec

    override fun call(): Int {
        spec.commandLine().out.println(dummyLongString)
        return 0
    }
}

I tried writing to a temp file, but no joy yet...

@remkop
Copy link
Owner

remkop commented Nov 5, 2020

I got it to work with a temp file and by calling inheritIO() on the ProcessBuilder, and calling waitFor() on the Process. You may want to delete the temp file when done.

package nl.lengrand.swacli

import picocli.CommandLine
import picocli.CommandLine.*
import picocli.CommandLine.Model.CommandSpec
import java.io.*
import java.nio.file.Files
import java.util.concurrent.Callable
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess

const val dummyLongString =
        """
 Found 60 total results for that query  
Showing 10 results
 Tatooine
    Climate : arid
    Diameter (km) : 10465
    Gravity : 1 standard
    Orbital period : 304
 Alderaan
    Climate : temperate
    Diameter (km) : 12500
    Gravity : 1 standard
    Orbital period : 364
 Yavin IV
    Climate : temperate, tropical
    Diameter (km) : 10200
    Gravity : 1 standard
    Orbital period : 4818
 Hoth
    Climate : frozen
    Diameter (km) : 7200
    Gravity : 1.1 standard
    Orbital period : 549
 Dagobah
    Climate : murky
    Diameter (km) : 8900
    Gravity : N/A
    Orbital period : 341
 Bespin
    Climate : temperate
    Diameter (km) : 118000
    Gravity : 1.5 (surface), 1 standard (Cloud City)
    Orbital period : 5110
 Endor
    Climate : temperate
    Diameter (km) : 4900
    Gravity : 0.85 standard
    Orbital period : 402
 Naboo
    Climate : temperate
    Diameter (km) : 12120
    Gravity : 1 standard
    Orbital period : 312
 Coruscant
    Climate : temperate
    Diameter (km) : 12240
    Gravity : 1 standard
    Orbital period : 368
 Kamino
    Climate : temperate
    Diameter (km) : 19720
    Gravity : 1 standard
    Orbital period : 463
"""

@Command(
        mixinStandardHelpOptions = true,
        subcommands = [PlanetsCommandPaginateDummy::class, HelpCommand::class]
)
class SwaCLIPaginateDummy : Callable<Int> {
    @Spec
    lateinit var spec: CommandSpec

    private fun executionStrategy(parseResult: ParseResult): Int {
        println("in there")
        val file = Files.createTempFile("pico", ".tmp").toFile()
        this.spec.commandLine().out = PrintWriter(FileWriter(file), true)
        println("writing to ${file}")

        val result = RunLast().execute(parseResult)
        this.spec.commandLine().out.flush() // may not be needed

        println("starting `less ${file.absolutePath}`")
        val processBuilder = ProcessBuilder("less", file.absolutePath).inheritIO()
        val process = processBuilder.start()
        process.waitFor()
        return result
    }

    override fun call(): Int {
        spec.commandLine().usage(System.out)
        return 0
    }

    companion object{
        @JvmStatic
        fun main(args: Array<String>){
            val app = SwaCLIPaginateDummy()
            exitProcess(CommandLine(app)
                    .setExecutionStrategy(app::executionStrategy)
                    .execute(*args))
        }
    }
}

@Command(name = "planets", description = ["Search for planets"])
class PlanetsCommandPaginateDummy : Callable<Int> {

    @Spec
    lateinit var spec: CommandSpec

    override fun call(): Int {
        spec.commandLine().out.println(dummyLongString)
        return 0
    }
}

@jlengrand
Copy link
Author

jlengrand commented Nov 6, 2020

Thanks for the help! I didn't think about using a temp file, good idea

I can confirm indeed that this does the trick. A few limitations I can see when testing :

  • The default usage seems to be using less as well currently. I meant to use less only for non help / version commands.
  • The color output is being lost when forwarding to the less process. This works as intended, the example listed here does not use Ansi code but they work just fine.
  • It is visible to the user that we stream to a temporary file, while it is not in, say Git for example.

image

Thanks again, I don't expect you to test all the comments here :), I'm listing them in case someone else ends up here at some point. I'll be looking if I can improve those myself.

@jlengrand
Copy link
Author

jlengrand commented Nov 6, 2020

Ok, so getting around the usage and help part is quite simple. You gotta avoid the mambo jambo in case no subcommand is detected.

    private fun executionStrategy(parseResult: ParseResult): Int {

        if (!parseResult.hasSubcommand())
            return RunLast().execute(parseResult)

        val file = Files.createTempFile("pico", ".tmp").toFile()
        this.spec.commandLine().out = PrintWriter(FileWriter(file), true)

        val result = RunLast().execute(parseResult)
        this.spec.commandLine().out.flush() // may not be needed

        val processBuilder = ProcessBuilder("less", file.absolutePath).inheritIO()
        val process = processBuilder.start()
        process.waitFor()

        return result
    }

would work in that case. It could become more complex in a real life application.

@jlengrand
Copy link
Author

Thanks for all the help @remkop. I used your changes and the working example can be found here https://github.com/jlengrand/swacli/blob/main/src/main/kotlin/nl/lengrand/swacli/SwaCLIPaginate.kt.

It is possible to play around with the less options to get more or less info in the output for those that would want to dive deeper into it :).

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

No branches or pull requests

2 participants