Open andychu opened 4 years ago
Shorter description: Oil could be a shell that runs in something other than a terminal.
The terminal interface is an optional layer on top.
@akinomyoga might be interested because this is an alternative architecture for something like ble.sh
. Rather than having the line editor run in the same process, it could e a separate process controlling an Oil interpreter (two processes total).
I'd be interested in where that falls down. Of course it has to have a lot of hooks to expose its state. But you also have the benefit of not saving and restoring state.
I imagine we can come up with some sort of simple protocol over stdin/stdout. QSN #582 will definitely be a part of it.
problems: Signals need special handling. Would Ctrl-C, etc. translate to an IPC command, or would it have to be forwarded? The wrapper could "delegate" it.
Thinking about this a bit more, the signal issue is tough because you would want to simultaneously:
wait()
for a process to end (all shells including Oil do this in a blocking manner)So basically this may involve changing shell to an async model, which is hard... But could be worth it. It's a big change.
The other option is to use a separate thread for processing IPC requests, but threads and fork don't mix... so this could be hard. Threads also introduce some runtime dependencies.
Third option: IPC commands could be accompanied by signals? e.g. the protocol is to send a signal, and the process knows to wake up and read a command from stdin? (or a socket, etc.)
Related to #663 too (provide APIs that allow people to write their own line editor)
Previous discussions:
(Arcan) https://lobste.rs/s/brzfq6/does_anyone_use_mouse_with_terminal
(8 months ago) https://news.ycombinator.com/item?id=23521664
(October 2020) https://news.ycombinator.com/item?id=24873319
(January 2021) https://news.ycombinator.com/item?id=25925230
nyellin (Feb 2021): https://news.ycombinator.com/item?id=26123142
Some working code (in Rust):
https://oilshell.zulipchat.com/#narrow/stream/266977-shell-gui/topic/Lobste.2Ers.20followup
https://oilshell.zulipchat.com/#narrow/stream/266977-shell-gui/topic/async.20xv6
I'm absolutely interested in the idea of a GUI for a shell which runs outside of a terminal. If I were to prototype this, I might start with the popular Golang sh package, which I'm sure you're familiar with. It exposes a reasonable API for an interpreter with serviceable POSIX compatibility. However, proper support for Bash as a language, without its messy internals, would be extraordinarily useful.
Some off-the-cuff feature requirements from the shell might be:
Set the stdin, stdout, stderr of an individual "command" (i.e. whatever is entered in a single prompt) before executing it.
I don't think it's appropriate for the UI of the shell to share a terminal with the commands that run in it. (Well, I don't think it's appropriate for the UI to have anything to do with a terminal.) The UI can allocate buffers however it wants and provide handles to the shell. For example, I think a smart GUI for a shell should be able to: allocate a new pty for every command, set the pts as stdin/out, interpret the output, render it as rich text like HTML, and cleanup the pty once the command is finished.
Parse the syntax of commands provided separately from -- and before -- executing them.
If I enter a function into a prompt, I don't want to wait until executing it to discover a syntax error. Fish's interpreter does this, and this is the model of the sh package I linked to above. I'm sure Oil's interpreter works this way, but exposing it is important. I don't think this kind of static analysis should happen at the UI level.
Query any information about the state of the shell that would be necessary to implement autocomplete, such as what the current directory is and what shell commands are available to execute.
The UI can handle the more complicated completion details, like flags associated with particular shell commands.
Hooks to perform job control/send signals to a running job.
I'm less sure about whose responsibility job control needs to be. (Certainly not the tty driver, though -- there wouldn't necessarily be a controlling terminal at all. 🙂) The shell could provide high-level hooks (e.g. "suspend job") or may be able to get away with simply providing the UI with the process groups of running jobs.
Thanks for the feedback!
I don't believe existing shells can be divorced of the terminal easily, but Oil is factored so it shouldn't be necessary.
I think you could also get far with the shfmt package, although Go has a runtime issue that would make me wary: https://lobste.rs/s/hj3np3/mvdan_sh_posix_shell_go#c_qszuer
Still that's not a reason not to try it and I would be very interested in the results of that experiment.
I just pinged Greg Morenz here since he has a promising experiment: https://oilshell.zulipchat.com/#narrow/stream/266977-shell-gui
It's a similar "librarification" as the work above.
Yes querying the PWD is a good point, and necessary.
Yeah this is one of the most fiddly areas. I think it can be done and Oil is pretty principled about it. I just fixed a bug with signals where wait
wasn't interruptible.
I think signals are the most compelling argument for the "IPC" approach, vs. the "API" approach. i.e. basically the UI could talk to the shell "server" over a socket or pipe.
Anyway I'd be interested to see an experiment with either shfmt or Oil... Oil has some weirdness where it's both Python and C++, we can talk about it :) shfmt is less complete as a runtime as far as I understand. I think Oil is significantly more compatible / featureful / etc. but I haven't looked at shfmt in awhile.
Oh here is the original discussion with Greg: https://lobste.rs/s/bl7sla/what_are_you_doing_this_weekend#c_f62nl3
I think this will happen "eventually", people just have to work on it :)
I think there is some value to running it in the browser, but I would like multiple clients, not just a browser one.
I think Jupyter uses ZeroMQ for all the fiddly IPC stuff? I wasn't sure I wanted to take that dependency but I'm open to ideas
Yeah I need to read this: https://jupyter-client.readthedocs.io/en/stable/messaging.html
I think there is a jupyter bash plugin but it's not very tightly integrated and I don't think it sees that much use (?) It probably doesn't have all the syntax analysis stuff. It would be good to do a comparison.
I guess the difference with Jupyter is that they're more like "one UI, multiple kernels" whereas this is sort of like "multiple UIs, the shell as a 'kernel' or library/server process"
Luckily, I don't care about POSIX compliance -- I just want a usable shell. But your goals are rightfully very different. :)
I use Fish on a daily basis, which is multithreaded and doesn't fork or really support subshells at all. I'm not a shell poweruser, but I haven't found that to be a problem so far. It means that stuff like echo (set foo 'bar') $foo
totally works, but I don't know that that's a bad thing; it's at least more intuitive than what happens with subshells. Apparently subshell blocks are a feature in progress: https://github.com/fish-shell/fish-shell/issues/1439
That said, I think the shfmt package implements subshells by just creating a new Runner object for them in the same process -- no forking necessary, and it does have an exec implementation.
Oil as a Jupyter kernel would make a lot of sense!
There definitely do exist multiple Jupyter clients, though maybe they all share some internals. The IPython CLI, Jupyter Notebook, and JupyterLab are all different clients from the Jupyter team. Google Colab comes to mind as a third-party alternative, but I'm sure there are others. JupyterLab already has a "console" mode/document type, which is a little more ergonomically friendly than a notebook when all you want is an interactive prompt. But, I could see it making sense to build a lightweight client which is really geared towards shells.
The Jupyter Bash Kernel just uses pexpect
and is pretty easy to break. :)
Just to summarize this conversation a bit more, we came up with the "entered command" abstraction, I proposed ui-eval
, ui-wait
, and ui-cancel
builtins on the OSH side.
However those could maybe be called ecmd-eval
, ecmd-wait
, ecmd-cancel
.
The core problem is the same one identified above (and partially addressed by the async xv6 experiment): the shell does a blocking wait()
on processes, but it also would have to be able to process the ecmd-cancel
message / builtin.
The GUI could send SIGUSR1
or something before every command to unblock the wait()
; however it would also need to process IPC channel commands after being unblocked
How about interactive-eval
, or something along those lines?
Whatever the API is, I think it's the facility that any kind of interactive client would be built on, including:
osh
without argumentsIf you decide that this should be done using language features, I think it would make sense to dogfood it by ripping out main_loop.Interactive() and replacing it with an OSH script. :)
Yes that makes sense, I think it would be osh --server
or osh --ui-server
or something.
There should be another loop like Interactive()
. It won't be replaced in the near term, but it would be an additional option
To solve the signal problem, I'm thinking that the dumbest simplest thing is just make the GUI send SIGINT when it sees Ctrl-C. It doesn't have to all be over and IPC channel
Commands go over an IPC channel, but signals are just signals. It's a different form of IPC. Thoughts?
(Another idea that might be overly general, it could be osh --coprocess
, tying into the coprocess protocol I put on hold a couple years ago)
I'm not sure I understand what the --server
flag changes. I understood interactive-eval
to be a command that you'd put in a script with its own read/eval loop.
And yeah, having the shell forward a SIGINT to children that it's waiting on would work, I think.
Hm yes I guess there are still some open options on what to try:
--server
since there would be an IPC command to cancel, but if we just use SIGINT
, then we don't really need that ?ble.sh
does in bash, is definitely a goal, but probably long term.I still agree with the idea of every command having its own terminal, and the prompt being separate from the terminal, in the GUI.
Is that a concrete "next step" we can try?
I don't know exactly how to pass a terminal to the Oil process, which forks the processes that need the terminal. That is, I imagine the GUI creates a terminal and tells Oil to use it for the ECMD.
My initial thinking is to use a Unix domain socket because it supports descriptor passing, but I haven't tried that. Maybe it can just be a string like /dev/tty10
but I don't know exactly how that works!
This is all separate from the serial/parallel/state discussion so maybe we can figure it out first ...
I think at some point we talked about the vim/tmux issue, i.e. using the "alternate screen".
So in your view maybe the GUI doesn't literally have a terminal. It only has enough to be able to show "batch" commands and not vim/tmux/gdb?
I think that could be a worthwhile place to start as well. The issue is that isatty()
would fail and then you wouldn't get color for compiler output, etc. right? Or shell output.
Say you start zsh or fish from the shell, or say you ssh to a machine with bash. You want to have some color I think
i.e. I guess this boils down to: is the GUI a terminal multiplexer or not? Making it one is obviously harder, but not making it one has some limitations.
(And it's different than a normal terminal multiplexer because it knows about prompt, and can know about state. The IPC command to dump state to the GUI is another reason I was thinking about --server
, but that can also be a shell builtin that prints to stdout.)
This has a bearing on the API, but I think that if we use descriptor passing maybe Oil doesn't have to care? Oil can be passed an FD for a terminal, or an FD for a pipe, and it will work the same? So that API is general enough for both purposes? I'd have to play with it
random google result about descriptor passing, I think there are probably better ones: https://news.ycombinator.com/item?id=24964966
https://stackoverflow.com/questions/28003921/sending-file-descriptor-by-linux-socket
Thinking aloud a little more: the thinking behind --server
is that it's "whatever is necessary to separate the prompt from the terminal". (which another person was pursuing as well)
The shell and the child processes share a terminal in all shells. This functionality isn't supported by -i
or batch mode in any existing shell.
We could also call it --no-prompt
but I don't think that's enough.
I also think there probably needs to be a distinction between "server/IPC commands" (interactive-eval
, ecmd-eval
, dump-state
, etc.) and "shell builtins" (like eval
).
The distinction is: regular eval
sends its output to a terminal only. But I think dump-state
has to send its output back to the GUI/TUI, not to a terminal.
In summary, reasons for --server
, --coprocess
, whatever:
dump-state
to communicate with GUI/TUI, rather than terminal for the user
Feedback welcome, this is all rough thinking, really needs a prototype!
Ahh okay, I was thinking that interactive-eval
(or whatever) was supposed to be a shell builtin. My mind is stuck on the command server that I wrote for my bash prototype.
I guess you were thinking of it as some sort of IPC-specific method?
So in your view maybe the GUI doesn't literally have a terminal. It only has enough to be able to show "batch" commands and not vim/tmux/gdb?
In my view, it's up to the GUI/client what the standard io of a command should be. A GUI might want to allocate a terminal for each command, a simple prompt in a terminal would just always use that same terminal, a Jupyter kernel might want to just use pipes.
I was curious about passing fds around with domain sockets too, for the same reason haha. In my bash prototype, I have to create a named pipe in /tmp
for stderr and have bash open it. I'm glad our thinking is kind of converging; it makes me feel a little less naïve about this stuff.
Although, we were talking about whether job control is really necessary. The idea of every command having its own terminal is only relevant if some commands might run in the background. If there explicitly won't be support for background jobs at all when running interactively, then maybe the command-stdin/out/err could just be passed to the shell as additional fds when the process is created and reused for every command.
OK I think I have an idea of how to do this, or at least get started. And a good name for it -- it should be --headless
.
dump-state
, then you can pass a pipe. Same with completion queries -- they can be parsed and dispalyed in the GUI, not the terminal.interactive-eval --stdin-fd X 'user command'
, then you can pass a terminal. (At least I think that's the way it works and we can test it)What would --headless
do, exactly? Is that different from the normal non-interactive/"batch" mode? A shell running a script is already "headless".
Even if the shell is blocked with no job control, I still see a use case for terminal-per-command. You could do something like jupter where the prompt is separate from the command output. And you can put the prompt at the top, etc. The UI can block until the headless shell returns
Yeah, the standard IO for an evaled command should definitely be separate from the stdio for the shell (or whatever channel the shell is reading commands from). But if the command can't continue to run in the background, then the stdio for the command could be recycled for the next command.
Regardless, being able to change this per command is still the more generic interface.
It's just a name for what I described above: doesn't print the prompt, FD passing, etc.
I think it would have to be --headless /tmp/oil-socket
. The FD passing requires a slightly different code path.
There's also the possibility of separating the shell's stderr
from the command's stderr
. For example, for syntax errors, if you type something like )oops
, vs. ls
producing an error.
I think it should be possible to prototype in Python, since the stdlib has all the syscalls: http://ptspts.blogspot.com/2013/08/how-to-send-unix-file-descriptors.html
I don't know exactly how to do the terminal thing; that's something we could talk about. I guess ssh -t
does something like this.
This looks like a good start:
https://www.uninformativ.de/blog/postings/2018-02-24/0/POSTING-en.html
Also: http://neugierig.org/software/blog/2016/07/terminal-emulators.html
I don't want to write a terminal emulator, just demonstrate that it's possible to hook up a child process to a terminal passed over the --headless
socket. That said I think there are a bunch of small terminal emulators out there
Hm actually there are several in Python here:
https://pypi.org/project/pyte/
https://github.com/selectel/pyte
Shows some syscalls: https://github.com/selectel/pyte/blob/master/examples/capture.py
Here is what I think the flow would look like:
osh --headless /tmp/oil-socket
. It can have many of these running at the same time/tmp/oil-socket
.
readfd in out err
. The GUI sends the descriptor AND the command to tell the shell to read it.ls /
, the GUI can run something like ui-eval --stdin_fd $in --stdout_fd $out --stderr_fd $err 'ls /'
. (Although care has to be taken not to clobber user variables; ble.sh
has that issue)/tmp/oil-socket
. When a given osh
process returns "done", then it's unblocked and can run another command.
SIGINT
should cancel the current ECMD and return "done". dump-state
command to get cwd
, ENV, $?
, $PS1
, ${FUNCNAME[@]}
, etc. In this case, it doesn't use a terminal -- it can use a pipe that the GUI itself will parse from.We can talk about concurrency more later, but I think this is a handful as is... i.e. might be months of work, and it could be marginally better / no worse than using tmux or xterm, etc. windows are blocked in those cases too.
(EDIT: I think this alternate --headless
main loop should be fast to prototype; I meant an end-to-end demo might be months of work :) But hopefully we can figure out if the concept is worthwhile well before that)
Goal: separate prompt, history, completion from the terminal. These things could be nicer in the GUI, e.g. work more like the browser or like devtools. The browser's URL bar has all those things: prompt, history, and completion.
When the shell is in headless mode, does it only accept special IPC commands and only use the shell interpreter when you run ui-eval? Or, are ui-eval
, readfd
, and dump-state
handled by the interpreter too?
Would calling dump-state
modify the status in $?
, for exmaple?
I wonder if it's worth taking inspiration from how REPLs are implemented for other non-shell languages, here. Or at least, I'm curious if you would know more about that sort of thing. I think the default interactive prompt in Python is built into the interpreter (here?) but there are also all sorts of other interactive python UIs, like bpython or ipython/jupyter. The command line interface for v8 apparently uses the same protocol to connect to the interpreter that devtools uses.
I'm having second thoughts about whether polling the state of the shell is really good enough or if the shell needs to push state changes to the client. For example, if you ran cd; sleep 10
, the GUI wouldn't know about the directory change until the end of the command. Is that an issue?
Really random sidenote, but one interesting thing about descriptor passing that I found is that the file descriptor is allocated when you parse the out-of-band message from the read. So if you're using a stream socket and the message is fragmented, you need to make sure that you only read the OOB message for the first read and not the second. Otherwise, it allocates two fds for the same open file.
I don't know exactly how to do the terminal thing; that's something we could talk about. I guess ssh -t does something like this.
Allocating terminals is totally up to the GUI; the shell just needs to know what stdin/out/err to run a command with. But if you want to try it out, creating a pseudoterminal is just: fd = open("/dev/ptmx")
, unlockpt(fd)
, and ptsname(fd)
to get the corresponding pts.
I'm just using a fairly simple terminal emulator library for my prototype. I'm sure there are plenty of similar colorizing libraries for Python.
Whew, there are a lot of threads going on here. I hope that wasn't too all-over-the-place.
Sorry for the delay, I wanted to get a prototype going before getting to deep. I had an idea that I believe would be 300-500 lines of code, e.g. in a file core/headless.py
, but I haven't yet tested that theory.
So let me try to answer the questions for now, with the caveat that it could all change based on a prototype. All of these are good questions!
https://github.com/oilshell/oil/wiki/How-Interactive-Shells-Work
$?
is a good point. Oil already has a specific mechanism for $?
, because $PS1
, $PS4
, and $PROMPT_COMMAND
have the same "clobbering problem.https://github.com/oilshell/oil/blob/master/core/state.py#L874
But there could be other conflicts with state. That is why I mentioned #704 , the subinterpreters bug.
set -g status-interval 1 # hacky asynchronous polling
If you have any running code for the descriptor passing, I'd like to see it! I haven't had time to play around with it yet.
(As for what I've been up to, I went on a nice tangent with git annex, which might play into the Oil project eventually)
But I hope that I can get something going with 300-500 lines of code... just to flush out some of the issues we've been talking about.
You might have a look at the pty
module in python's standard library. It provides openpty
and a couple abstractions built on top of this. What's nice about it is that it works across different flavors of unix -- namely Linux and OSX -- where there are some subtle differences.
If nothing else, the code would be worth reading the module source to see what calls it makes, and how it handles platform differences.
Yeah Oil's philosophy is to be completely portable. That's one reason there is a new headless shell feature:
The demo client uses the pty
module:
https://github.com/oilshell/oil/blob/master/client/headless_demo.py
To write a bash-like TUI, I think you don't need anything unportable. Terminals on all Unixes are compatible in that respect.
But a richer GUI may want to take advantage of platform specific features. So it will be nice to punt those elsewhere!
If you want to give it a try, check out #shell-gui
on Zulip and post a message!
This might be a bit "too much" but there is quite recent and very successful effort SixtyFPS marrying a special declarative language (no DOM, no web s**t) with something-like-JS-lang to set the declarative part in motion.
https://github.com/sixtyfpsui/sixtyfps/tree/master/examples
Maybe Oil could adopt the declarative part and swap the JS-like part for Oil itself...
You may think this is already true: isn't a shell already a reusable component that runs under a terminal? You pass data on stdin, and get results on stdout and stderr?
No, because shell also has command completion and other UI features tightly integrated into it.
I guess this is an alternative to having something like ble.sh in-process. Shell could be a separate process that responds to completion requests.
cd /
, etc.echo $P<TAB>
So an interesting use case here is to make a GUI where the terminal line stays at the top? That is, the prompt and command line itself isn't part of the terminal. You edit it separately
related threads:
https://lobste.rs/s/brzfq6/does_anyone_use_mouse_with_terminal (e.g. Arcan subthread)
Playing With Debuggers (Zulip)
Videos about Terminals, Urwid, etc. (Zulip)