Skip to content

Text-based UI support #503

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

Closed
wants to merge 4 commits into from
Closed

Text-based UI support #503

wants to merge 4 commits into from

Conversation

st0012
Copy link
Member

@st0012 st0012 commented Jan 27, 2022

This PR adds the TUI support proposed in #305.

Usage

  • tui opens TUI with a default top window (locals)
  • tui on [type] opens TUI with a type top window (current has locals and src)
    • tui on src,locals can open 2 windows
  • tui off closes TUI

Demo

Single-window
(The top window shows the current frame's local variables)

TUI.mov

Multi-window

TUI.-.stacked.view.mov

Specs

These specs will be improved in the future.

When disabled (default)

It doesn't affect any existing funcionality.

When enabled with tui command

  • The screen will be separated into top frame and the REPL
    • The top frame
      • Is not scrollable
      • Has one window to display current frame's source code (src) or local variables (locals).
      • Has a fixed height of 15 lines (13 lines for content)
        • I'll make it configurable once this has been accepted
    • The REPL
      • Is not scrollable either
  • Can be disabled with tui off

Future Plans

More window types

  • breakpoints - the list of registered breakpoints
  • tracers - the list of activated tracers
  • trace - tracers' output will be directed here
  • threads
  • frames - nearby frames (it's unlikely we'll be able to show all frames)

Multi-window support

  • tui on [w1:h1,w2:h2] opens windows with specified types. We'll start from stack layout. Example: tui on src:20,locals:10
----------
|  src   |
|        |
----------
| locals |
----------
|  REPL  |
----------
  • RUBY_DEBUG_TUI_WINDOWS=w1:h1,w2,h2 - configures default windows.
  • RUBY_DEBUG_TUI_HEIGHT=15 - configures the default window height.

Scroll support

We can add scroll support to both the REPL and the top frame. But probably only command or key-binding scrolling at first.

Implementation notes

Additional data channel

TUI requires frequent data exchange between the Session and ThreadClient. More specifically:

  1. Session needs to tell TC what type of the data it needs with additional information like how many lines is expected.
  2. TC needs to give the Session requested TUI data, like local variables.

For 2), we already pass @internal_info in every TC -> Session events so we can use it directly.

However, we currently don't have anything for 1). So in this PR I added change to let every Session -> TC request also carry a metadata payload. It's a Hash that can carry some general information, like TUI info. I think this can also help simplify the implementation of #422.

Separate frame data's collection/display logic

Currently, frame data like file source, local variables...etc. are printed during collection. This is enough for the current local console but is not convenient when we need to process the data differently, like DAP server or the TUI. So I also extracted get_* methods (return collections) from some of the show_* methods (show collections). Part of this change was done in #465 and merged here and can be merged it separately.

(I know technically puts in ThreadClient doesn't "print" the data immediately. But it's hard to get the collection back from the @output variable as a collection anyway.)

TODO: Reduce screen refreshing

Currently, screen is refreshed on each UI#puts call. So lines.each {|l| @ui.puts(l)} will refresh screen multiple times. If we can change that into @ui.puts(lines), we can avoid unnecessary refreshes. It won't be a huge change (mainly here and maybe a few other places), but I want to wait for the general design is approved before making further changes.

@st0012
Copy link
Member Author

st0012 commented Jan 27, 2022

@ko1 this implementation requires some extractions made in #465, so it'd be great if we can finish its discussion.

@st0012 st0012 force-pushed the tui branch 3 times, most recently from b66a47c to 3dc8521 Compare January 28, 2022 14:20
@st0012 st0012 marked this pull request as ready for review January 28, 2022 17:10
@st0012 st0012 force-pushed the tui branch 3 times, most recently from 605e44d to 3412e69 Compare January 31, 2022 18:59
@ono-max
Copy link
Member

ono-max commented Feb 1, 2022

Because I've never seen Expected the debuggee program to finish. as random errors, this PR's changes may cause failing tests.

I apologize for giving you detailed informations.

https://github.com/ruby/debug/runs/5010975029?check_suite_focus=true

@st0012
Copy link
Member Author

st0012 commented Feb 1, 2022

@ono-max Yeah it's weird but I don't see how this PR can cause the detaching test failure. The DAP-related changes are:

@ko1
Copy link
Collaborator

ko1 commented Feb 5, 2022

I read your patch.

  • we can introduce metadata or some more communication path between Session <-> TC
  • Also we can accept to introduce getters instead of printers (show_... methods).
  • Now your patch hard codes TUI mode into sources, and I don't think it is good idea for maintenance. So current patch is difficult to accept. Could you design extended general (extensible) mechanism at first?

Others:

  • it is only for locals. Can we extend it on remote mode? I don't want to introduce difference between local and remote REPL as possible.
  • Where the stdout/err outputs?

@st0012 st0012 force-pushed the tui branch 2 times, most recently from c7dde90 to cf604c2 Compare February 5, 2022 23:29
@st0012
Copy link
Member Author

st0012 commented Feb 6, 2022

it is only for locals. Can we extend it on remote mode? I don't want to introduce difference between local and remote REPL as possible.

I can explore it and it should be possible. But because it requires redrawing the screen repeatedly, the result won't be good in remote consoles under current implementation (with bigger overhead to transfer data). I prefer to wait until optimizations like Reduce screen refreshing is implemented.

Another reason to wait would be to have a more complete test framework (or helpers) for TUI.

Where the stdout/err outputs?

Do you mean we should preserve the stdout/err output after the debug session is finished?

@ko1
Copy link
Collaborator

ko1 commented Feb 21, 2022

Do you mean we should preserve the stdout/err output after the debug session is finished?

My question is, what happens when the program prints something to stdout/err?

@st0012
Copy link
Member Author

st0012 commented Mar 2, 2022

My question is, what happens when the program prints something to stdout/err?

Currently, program's output to stdout/err won't be displayed because they don't trigger the screen refresh. I'm still finding the least impactful way to pipe program output to the console screen.

@st0012 st0012 force-pushed the tui branch 5 times, most recently from 9f0af2d to 629f1cc Compare March 19, 2022 22:32
@st0012
Copy link
Member Author

st0012 commented Mar 19, 2022

@ko1 I've added stderr/stdout support. I've tested these scenarios in the demo:

  • stdout from user program
  • stdout from debugger console
  • stderr from debugger console
  • stdout resumed after TUI is turned off
2022-03-19.22.34.35.mov

def deactivate
super
clear_screen!
$stdout.reopen(@original_stdout)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will reconnect the $stdout object to the original stdout object's stream. So both STDOUT and $stdout will be resumed.

$stdout.define_singleton_method(:write) do |*args|
original_stdout.write(*args)
tui_console.puts(args.join)
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to listen to stdout's printing events and reflect the message on the TUI window as well. The patch will be cleared after #deactivate is called (see below).


tui_console = self
original_stdout = @original_stdout
original_stderr = @original_stderr
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locals are for referencing from define_singleton_method (can't use ivar there), and ivars are for referencing from #deactivate.

true
end

def store_prev_line(line)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mainly for storing the user input line.

@ko1
Copy link
Collaborator

ko1 commented Mar 20, 2022

  • what the plan do you want to extend it with more information?
  • do you plan to extend to accept key combination like gdb's TUI mode?
  • Could you squash commits if the commits can be merged semantically? It will help me to review them.
  • I couldn't review all code, so I need to more time for it.

Minor comments:

I tried with the script:

# target.rb

def rec n
  rec n - 1 if n > 0
  a = b = c = d = e = f = g = h = i = j = k = l = m = n = o = p = q = r = s = t = u = nil
  bb
end

rec 100
  • I can't see the src pane. any setting is needed? I can see the lvars pane and prompt
  • When there are many local variables, it seems to show vars as possible.

image

  • when the window size is changed, the output seems broken.

image

  • I didn't use get_xxx terminology in thread_client.rb so if we have another good name, it seems better (but I couldn't find out another name).

@st0012
Copy link
Member Author

st0012 commented Mar 20, 2022

First of all, thank you for taking the time reviewing this PR 🙏

  • what the plan do you want to extend it with more information?

at the very least, I hope we support these:

  • breakpoints - the list of registered breakpoints
  • tracers - the list of activated tracers
  • trace - tracers' output will be directed here
  • threads
  • frames - nearby frames (it's unlikely we'll be able to show all frames)

(I've also listed them in the description)

and users should be able to use tui on threads,src to open TUI with the selected windows.

  • do you plan to extend to accept key combination like gdb's TUI mode?

yes. I'm not sure how to prioritize it with vertical window split though. I think we need some discussion around it.

  • Could you squash commits if the commits can be merged semantically? It will help me to review them.

of course. will let you know when done.

I tried with the script:

# target.rb

def rec n
  rec n - 1 if n > 0
  a = b = c = d = e = f = g = h = i = j = k = l = m = n = o = p = q = r = s = t = u = nil
  bb
end

rec 100
  • I can't see the src pane. any setting is needed? I can see the lvars pane and prompt

you can use tui on src to switch to src pane, or tui on locals,src to open both panes.

  • When there are many local variables, it seems to show vars as possible.

image

do you mean it doesn't show as many locals as possible? that's because currently the window height is fixed.

I think the best way to address this issue is to have a key-binding for scrolling. I'll write a plan for that.

  • when the window size is changed, the output seems broken.

yes that's a known issue.

  • I didn't use get_xxx terminology in thread_client.rb so if we have another good name, it seems better (but I couldn't find out another name).

how about collect_xxx?

@st0012
Copy link
Member Author

st0012 commented Mar 20, 2022

@ko1 I've reduced the number of commits to just 4. and the first 2 commits can actually be merged separately (that'll help reducing conflicts).

@ko1
Copy link
Collaborator

ko1 commented Mar 20, 2022

(I've also listed them in the description)

Sorry I missed it.

do you plan to extend to accept key combination like gdb's TUI mode?

yes. I'm not sure how to prioritize it with vertical window split though. I think we need some discussion around it.
...
I think the best way to address this issue is to have a key-binding for scrolling. I'll write a plan for that.

Yes I'm considering how to scroll the window to access more information like editors. If the window is small and information are many (for example ivar, frames, srcs) the importance of scroll is high.

st0012 added 4 commits March 30, 2022 20:53
We should pass tc (ThreadClient)'s data in the Session in a uniformed way:
either all with ivar or all with local variable. And since the local variable
approach has been rejects (ruby#293), we may want to go with @ivar.

This change for unification is needed for later changes.
@st0012
Copy link
Member Author

st0012 commented Apr 9, 2022

@ko1 I've checked Ruby TUI applications like ruby_jard and Ruco to see how they handle keyboard input. I think ruby_jard implements input capturing directly from IO, while Ruco is built on top of curses.

So I think to support key-binding, we can't use reline's readline method anymore and need to implement a lower level of input handling. This can affect how features like autocompletion are supported under different modes.

IMO, the principles should be

  • We need to support the same set of input features (e.g. hints and autocompletion) in normal/TUI mode.
  • They can't have on different implementations (reline vs a completely custom one) for the sake of maintenance.
  • For whatever implementation we choose, it shouldn't rely on any 3rd party gem or C-library.
    • So curses is off of the table.

That leaves us with these options:

  1. Use custom implementation to replace reline for both normal and TUI mode.
    • This clearly requires a lot of work.
  2. Support key-binding detection in reline that also works with the readline method.
    • I think it's the ideal option but I'm not sure if that's in the scope of reline.

Do you have any thoughts on this?

@ko1
Copy link
Collaborator

ko1 commented Apr 17, 2022

  1. Use custom readline on TUI mode? (simple one)

is another idea.

Controlling terminal is very difficult task so I don't recommend you to dive into this area.
Also maybe I can't accept complex solution because I don't want to maintain it.

I think it is more reasonable to make a CLI DAP client in another gem and connect with debug.gem.

@st0012
Copy link
Member Author

st0012 commented Apr 17, 2022

I agree that the debugger shouldn't maintain complicated terminal controlling mechanism. I'm not too keen to dive into that now either.

And I just found that GDB uses ncurses to implement TUI instead of building everything from scratch. So perhaps bringing the curses gem in would be a possible solution here.

But if that's not acceptable, I don't think building another UI on top of DAP is what I'd pursuit. My goal for this TUI are:

  • Convenient to use: just a tui command can spin it up.
  • Operations are still performed from REPL and can directly see stdout/stderr.

Which would be hard to achieve with DAP's architecture.

@st0012
Copy link
Member Author

st0012 commented Apr 17, 2022

@ko1 Since the chance of this PR getting merged is small, I won't proceed on it anymore. Do you think the first 2 refactoring commits worth merging? Otherwise I'll just close it.

@ko1
Copy link
Collaborator

ko1 commented Apr 21, 2022

Could you make another PR for that?

@st0012
Copy link
Member Author

st0012 commented Apr 21, 2022

@ko1 Of course, here's the PR: #628

@st0012 st0012 closed this Apr 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants