I started brainstorming in markdown :smiley:
# Rocli
## Potential goals
- Rocli is the best way to build modern CLI applications with the Roc programming language.
- Rocli is a modular CLI platform for Roc.
## Outline
- A Rust-based Roc platform
- Features:
- I/O (stdin/out/err & files)
- Complete error handling
- Command parsing
- Flag parsing
- Argument parsing
- `requires`:
- `main : Request -> Task {} []`
- `interface`s:
- `File`
- `exposes`:
- `append : Str, Str -> Task {} [ FileNotFound, PermissionDenied ]`
- `read : Str -> Task Str [ FileNotFound, PermissionDenied ]`
- `write : Str, Str -> Task {} [ DirectoryNotFound, PermissionDenied ]`
- `Parser`
- `exposes`:
- `appendHelpFlag : CommandSchema -> CommandSchema`
- `ArgumentSchema : { name: Str, description: Str }`
- `ArgumentValue : { contents: Str, schema: ArgumentSchema }`
- `CommandSchema : { name: Str, shortName: Str, description: Str, flags: List FlagSchema, arguments: List ArgumentSchema }`
- `CommandValue : { arguments: List Argument, flags: List Flag, schema: CommandSchema }`
- `FlagSchema : { name: Str, isRequired: Bool, shortName: Str }`
- `FlagValue : { contents: Str, wasUsed: Bool, schema: FlagSchema }`
- `parseCommand : Request, List CommandSchema -> (Result CommandValue [ NoSchemaMatched ])`
- `Request`
- `exposes`:
- `Request : { executablePath: Str, rawArguments: List Str }`
- `Std`
- `exposes`:
- `readInLine : Task Str [ BrokenPipe ]`
- `writeErr : Str -> Task {} [ BrokenPipe ]`
- `writeErrLine : Str -> Task {} [ BrokenPipe ]`
- `writeOut : Str -> Task {} [ BrokenPipe ]`
- `writeOutLine : Str -> Task {} [ BrokenPipe ]`
- `Task`
- `exposes`:
- `Task ok err : Effect.Effect (Result ok err)`
- `attempt : Task a b, (Result a b -> Task c d) -> Task c d`
- `await : Task a err, (a -> Task b err) -> Task b err`
- `fail : err -> Task * err`
- `map : Task a err, (a -> b) -> Task b err`
- `onFail : Task ok a, (a -> Task ok b) -> Task ok b`
- `succeed : val -> Task val *`
## TODO
- Subcommand parsing
- Man page generation
- Autocomplete for shell(s) (Bash, etc.)
- Config handling
- Network requests
- Alias handling
- Testing helper module
- Rocli CLI
- Code generation
- Testing helper commands?
I'm imagining something that would be competitive with cobra and oclif, both of which have pleasant DX.
What do y'all think of this feature set? What do y'all think of this approach to the interfaces?
I really like rust's structopt or argh
I think auto generating the help message is important, as well as bash completions
cobra is a good example as well
https://docs.rs/structopt/latest/structopt/
Ah, so it's CLI codegen for Rust?
the question is, could something like that just be a library built on top of a generic platform intended to run in a terminal?
JanCVanB said:
Ah, so it's CLI codegen for Rust?
sort of, you mostly just define all the possible args and flags as a struct or nested enums and structs
Yes, my first idea for Rocli (name TBD, or not lol) was to have it be a set of repos - one for the platform, one for a parser library, one for a codgen tool, etc.
However, since a platform comes with interface exposures anyways, I'm not sure that splitting it up is actually valuable
here's an example where I used structopt:
https://github.com/rvcas/checkers/blob/main/src/main.rs#L27
roc itself uses clap
structopt is built on top of clap
Lucas Rosa said:
the question is, could something like that just be a library built on top of a generic platform intended to run in a terminal?
This is a great question. Because this project is initially "a production-grade version of examples/cli", I'm anchored to the term "CLI" as referring to something more than a simple executable. However, what is actually the correct term/pattern here?
Should this be split into two projects, where one is a robust platform for creating executables* and the other is a robust library for creating advanced CLIs with flags, args, etc?
(* I can't decide what the best noun for this - executable? terminal-based app? minimal CLI?)
I expect that...
a) even simple executables would benefit from some of the less-advanced features like arg parsing
(ex: ./my_executable ./path/to/my_input_file.txt)
b) some more-advanced features would be platform-dependent
(ex: the file I/O required for caching an API token in a config file)
c) there are not many other directions an executable/terminal-focused platform could go in, other than supporting more-advanced CLI features
However, I suppose that it could be nice to separate the advanced features like command/arg/flag parsing so that they can be re-used in non-OS-terminal environments... though I can't think of any.
Then again, any Roc-only interfaces could simply be imported in any module, regardless of platform, right?
As you can see, I'm going back and forth on this :laughing:
I should also state that I expect the Parser interface to be a Roc-only module, which doesn't use any of the platform's internals or Tasks.
(same with all non-I/O modules)
I'm also particularly interested in a platform for console / TUI apps - think terminal editors/etc. I'd like to rewrite this tool I've been working on in Roc: https://github.com/joshuawarner32/debug_buf_viewer (screenshot in this issue: https://github.com/rtfeldman/roc/pull/2213)
I've been thinking of making a platform that exposes more or less the same interface the crossterm rust crate.
This doesn't necessarily have to be the same platform as for other CLI apps, but maybe?
Wow! Great idea, that's a perfect rebuttal to my (c) expectation above.
Should all of the following 3 apps be served by the same platform+libraries repo?
What I/O Tasks would be required for #3's cursor movement & cursor-aware text formatting? (ex: highlighting the currently-hovered line/word) Is that just stdin?
More broad Roc question: Since a Roc app can only use one platform, does that mean that a production-grade CLI platform would need to support all possible forms of I/O? stdin/out/err, files, network (HTTP, TCP, UDP), ... what else? Does that scale well? Could unrelated helpful libraries for things like network request abstraction (ex: a high-level GraphQL client) be platform-agnostic, or would they need to be platform-specific?
What I/O Tasks would be required for #3's cursor movement & cursor-aware text formatting?
TUIs are complicated and filled with historical baggage. :grimacing: stdin/stdout are definitely required and give you _most_ of what you need; but that's not all unfortunately. You also need access to some APIs that allow switching terminal modes ("raw mode") - and being able to intercept/handle SIGWINCH ("window size changed") is really useful.
Ideally, we could come up with an API that really cleanly abstracts all of this.
Like, it'd be cool if the same platform interface could also be used to write apps that cross-compile into the browser, without also loading some big terminal emulator.
That'd require encapsulating formatting and cursor movement commands, rather than having them be encoded on stdout.
Hmmm, do you mean like abstracting the CLI platform's interface to be generic enough that other/complementary Roc platforms could provide the same interface for different underlying execution/interaction environments?
Yeah; I want to write this TUI tool, and then be able to (later) embed it in a web page or something to make it accessible to more people
Whoa
Usually formatting is encoded as ANSI codes - e.g. \e[33m for "please switch to writing text in yellow" - but if we could just have then Task interface accept (Text, Formatting) output (where formatting is some tag union), that'd be ideal.
Commands for cursor movement are encoded similarly.
One thing that's _realy_ complicated about writing a "modern"/"real" TUI app is handling proper localization / unicode - and knowing whether a terminal will render 日本 as two character slots or four (or something else), is a bit of a crapshoot. Similar for things like arabic (right to left ordering).
All of this is quite a challenging problem, much of which isn't used for like 80% of terminal apps.
That sounds cool, but such an abstraction will probably add overhead/confusion for simple CLIs. Therefore, I imagine that an abstraction library might be the best solution for that:
CLI platform X --> CLI app A
CLI platform X --> Cross-env abstraction library Y --> Cross-env app B
Browser platform Z --> Cross-env abstraction library Y --> Cross-env app B? C?
Browser platform Z --> Browser app D
(however, I don't know much about platforms, so I'm just guessing here)
In terms of functionality that various apps need, I'd put things into four buckets:
stdout or stderr. They might optionally spit out things in various colors, but nothing else.cargo, where it progress bars for all the different downloads/compilations going on at that time.grep the output. You're going to get garbage.1/2/3 _could_ be combined into one - as perhaps could 2/3/4 - but 1 & 4 should probably always remain separate.
But yes, I think you're 100% right in terms of building compatibility layers.
@Joshua Warner For use cases 1, 2, and 3, what do you think of the above interface proposals?
(with some new TODOs added for 2&3's output colors, redrawn text, loading bars, etc.)
I'm new to FP, and I imagine there are multiple patterns that could be employed here to simplify the interface and support modularity
A few thoughts, in no particular order:
I'd highly recommend looking at the interface WASI provides (in the web-assembly world), as a model of a well-designed API for dealing with all these things.
nice good points
With file handles, I advise enforcing scope. So instead of openFile and requiring a closeFile later, have a withFile. That way leaks can't happen
WithFile would take a lambda
These are good points, I'll revise the interfaces and post a v2 :)
@Brendan Hansknecht Like this? withFile : Str, (File -> Task {} []) -> Task {} [ FileNotFound, PermissionDenied ]
@Joshua Warner If file I/O is improved with something like withFile, would stdin reading look like this? withStdin : (File -> Task {} []) -> Task {} [ BrokenPipe ]
Something like that, though I guess the error would need to be passed into the lambda?
I would expose the stdin file handle as an argument passed to main (probably part of a larger object)
Maybe not though. Maybe the lambda should be selfed contained and the error is exposed elsewhere
I would expect the lambda to only handle the nominal case, with error handling outside the withFile call
Yeah. Just trying to figure out the exact error handling process if the file doesn't exist and you want to try a different one
@Joshua Warner Interesting, since they don't really need to be opened or closed
Exactly!
@Joshua Warner Regarding the Request bikeshedding, my intent for that was to provide a low-level exposure of execution context - essentially what did the user request: "./path/to/my_executable --flag arg arg" ==> Request { executablePath: "./path/to/my_executable", rawArguments: "--flag arg arg" }
Then you can tell if a given function reads/writes to the terminal just by looking at it's signature (i.e. does it take the handle to read/write to as an argument) - which seems like a useful property for making code auditable
Re Request: Ahhh intersting; why not expose that as part of Args?
I was thinking some developers may not want the fancy arg parsing
Yep, makes sense. But from an 'api understandability' perspective, I think it belongs somewhere in the same module/namespace as the rest of the arg parsing
I'd still highly recommend taking a look at WASI for inspiration: https://github.com/bytecodealliance/wasmtime/blob/main/docs/WASI-overview.md
That's a shining example of this sort of interface 'done right', IMO
And since it's ambiguous where commands end and args begin... gh repo clone https://github.com/cli/cli
Arg parsing is a _hard problem_ I wouldn't put too much weight on solving it at the same time as everything else.
Regarding stream handles, should those look significantly different than file handles?
Regarding bytes vs. strings, should file I/O support bytes as well?
Regarding env vars, yes. :laughing:
Regarding a project/platform name, any ideas?
Regarding stream handles, should those look significantly different than file handles?
There's a pretty big design space for how to represent file handles in a type system, and what they can do. Here are some of the things that you can do (in posix) on a file handle:
I'm of the opinion that actually encoding these in the type system somehow is good
I'd also encourage "keeping it _reasonably_ simple" - i.e. don't actually try to support all of that
Whether you can read/write/seek are all independent properties of a file handle
Well, kinda. I guess a bunch of the combinations are not actually useful in practice.
Wowee!
A file handle that you can only seek but not read or write is not super useful.
I can imagine certain executables utilizing those features
Knowing the ins and outs of using file systems in creative ways has been a pretty critical part of my job for the last few years ;)
Assuming a good typing system and clean interfaces, which would be best:
I think Java has a pretty good design here. They have InputStreams, OutputStreams, and (IIRC) RandomAccessFiles
(I'm so happy we have your domain knowledge here)
RandomAccessFile could then be specialized into readable, writable, and readable+writable
RAF is a file in RAM?
Lost when the handle is lost?
random access, as in "you can call seek()" on it
Oh, nevermind
An (IMO) very underappreciated thing you can do with handles - across basically all systems these days - is open a handle to a directory. With a directory handle you can do things like:
openat(<dir_handle>, "logfile.001")mv /var/logs/myapplogs /home/me/cache/myapplogsopenat(<dir_handle>, "logfile.002"), it creates a file at /home/me/cache/myapplogs/logfile.002 - which is probably exactly what the user wanted anyway.The really key thing about handles is that they "follow" moves - both for normal file handles and directory handles like the above.
For a simple CLI, would this only apply for files that are moved after the execution starts but before it finishes?
Yep. To be fair, it's not a commonly-used property. But it can save your bum sometimes.
(Unrelated, is this thread taking up too much space in this chatroom? I don't know the etiquette here.)
@Brendan Hansknecht Should we move this discussion elsewhere?
This is a fine space for it. I think it would be super useful to make a summary doc/gist so that people can see the current plan
That way as things change, we can update and discuss the paired doc
I'm working on that now, also considering starting a repo for it (would a gist be better at first? I've never used gists)
Here's one pretty general thing to consider. In terms of "ways apps access the filesystem", there's a bunch of different access patterns apps use. A web server will tend to use the FS in a very different pattern from a database server, which will be different still from things like a compiler, or a utility for working with .zip files.
The posix interface has to serve _all_ of those needs - which in practice means it serves _none_ of them perfectly.
Also, I think as a base goal, I think you may want to look at 3 versions of the api.
1) Super simple, just load the entire thing into string or bytes.
2) Streaming, basically can pipe bytes in for writing or pipe bytes out for reading
3) Random access file.
I think 1 is the most important for simple things, but 2 become very important for representing more things and deal with large inputs.
3 is very important in some cases, but for a lot of starter cli apps, it may not be needed at all.
And, expanding on that above thought just a little bit more: With Roc, I think we have the opportunity to make several platforms that aren't under the pressure to be everything to everyone. A database will be written based on a different Platform than a compiler. And that's ok. In fact, it's _good_ since having a very specific target app in mind can make the interface you design much better.
All true, which raises the question of what types of apps this platform is aiming to support?
I'd encourage starting with a thing you want to write in Roc and then writing the platform that would be perfect for that thing. And then going from there.
To that end, it would be a cobra/oclif competitor
Oh, huh! TIL. Gonna have to look into that.
Designed for packaging a set of Roc functions (aka commands) into a structured, terminal-friendly way (help, man, etc.)
If those are pure functions, your job is super easy
If you're trying to be the thing that everyone uses to write commands for the terminal, you've really got your work cut out for you.
This is why I'd recommend starting from the _app_ that you want to support and working from there.
Supporting unzip -p a/b/c.txt file.zip > c.txt will have very different requirements from... I dunno, math 1+2
App #1: math 1+2
One thing that's interesting here is that Roc's platform concept is so fundamentally different from other programming languages.
For example, cobra is THE (clear best) way of writing CLIs in Golang. oclif is THE (clear best) way of writing CLIs in Node.js.
However, neither of those libraries handle file I/O. That's delegated to your file I/O library of choice, since each library can interact with the underlying OS/platform independently.
For a Roc platform to ergonomically provide the same level of abstractions as cobra or oclif do, would it also need to support every I/O & side effect your app could possibly need? It seems so, which is such a tall order.
It really sounds to me like what you _actually_ want to build is a library for arg parsing
Therefore, is a cobra competitor or oclif competitor impossible in the Roc ecosystem?
(not a platform)
Maybe! Perhaps schema-based I/O parsing/formatting is truly independent of the underlying I/O system.
However, I would still love to see+use a community-supported CLI-building platform that is optimized for real-world use (file I/O, error handling, etc.), since roc/examples/cli seems optimized more for simplicity/education (which it does very well).
I've started prototyping it here: https://github.com/JanCVanB/Rocli :smile:
Joshua Warner said:
It really sounds to me like what you _actually_ want to build is a library for arg parsing
Your words have been bouncing around my head this week :grinning_face_with_smiling_eyes: For now, I agree. I'm currently redesigning the library to be platform-agnostic, and maybe as a future step it will make sense to add a more-powerful CLI platform to go along with it. That might be where @Erwin Kuhn comes in!
Last updated: Jun 16 2026 at 16:19 UTC