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

Improve the API of REPL.TerminalMenus #35915

Merged
merged 21 commits into from
Jun 9, 2020
Merged

Conversation

timholy
Copy link
Sponsor Member

@timholy timholy commented May 17, 2020

This eliminates camelCase, fixes a typo (which unfortunately was documented), adds a const and type info to the CONFIG dict, and does some renaming/consolidation. Finally, it eliminates a dangerous escape hatch from one of the tests.

EDIT: the most recent version fixes #31414, fixes #32349, closes #30043, and closes #32344

stdlib/REPL/src/TerminalMenus/util.jl Outdated Show resolved Hide resolved
stdlib/REPL/test/TerminalMenus/runtests.jl Show resolved Hide resolved
@rfourquet rfourquet added the REPL Julia's REPL (Read Eval Print Loop) label May 17, 2020
@timholy
Copy link
Sponsor Member Author

timholy commented May 17, 2020

OK, the more I dug the more I found to fix. I tackled this because I was looking to create a new menu type, which gave me an opportunity to exercise the abstraction API. Perhaps not surprisingly I discovered some issues. Two issues are:

  • it turns out options(m) was never used other than to calculate the number of items in the menu. It seems more reasonable, then, to support an interface that allows you to just pass this number rather than a whole vector of strings. (For the menu type I am implementing, it's not a static list so not having to generate a bunch of strings that will just be thrown away seems like a win.)
  • there's a bad leak in the API, subtypes of AbstractMenu are expected to access TerminalMenus.CONFIG to get the characters for the cursor position and, for a multiple-selection menu, the strings that indicate the selection state. They are then expected to print these characters as well as any data associated with the menu options itself. It's quite weird to have the configuration be at the level of TerminalMenus but to have the printing be at the level of the subtype and then have the subtype need to access private data of TerminalMenus.

I've tried to clean this up here, but it's still not the API we probably want for 2.0:

  • if TerminalMenus maintains the settings for these character, it seems it should handle their printing and dispatch writeline only for the remainder of the info about the menu item
  • if the subtypes are going to be responsible for printing the cursor marker and selection state strings, it seems these are options you should pass when you construct the menu rather than a configuration option for TerminalMenus as a whole.

But I don't see a non-breaking way to get to either of these. So this does the next best thing (?) and passes a copy of these settings back to writeline.

I think I've set this up so that all users of the legacy interface should continue to work, but that's not actually tested.

timholy added a commit to JuliaGraphics/ColorTypes.jl that referenced this pull request May 18, 2020
This removes 80 invalidations, ref JuliaLang/julia#35915

This will be breaking so I've bumped the version in anticipation.
We might want to merge this only when we're ready to release the
next breaking version.
timholy added a commit to JuliaGraphics/ColorTypes.jl that referenced this pull request May 18, 2020
This removes 80 invalidations, ref JuliaLang/julia#35915

This will be breaking so I've bumped the version in anticipation.
We might want to merge this only when we're ready to release the
next breaking version.
timholy added 15 commits June 2, 2020 14:47
This eliminates camelCase, fixes a typo (which unfortunately
was documented), adds a `const` and type info to the CONFIG dict,
and does some renaming/consolidation. Finally, it eliminates a
dangerous escape hatch from one of the tests.
The old `writeLine` forced the user to take the (undocumented)
step of looking up the cursor character in the internal CONFIG Dict.
We can use the name change as an opportunity to improve the API.
The only way `options` was being used was to count the number,
so it seems a bit silly to force the user to pass a vector.
This will make it easier to support dynamic menus.
For menus whose number of options might change as a consequence of
user input (e.g., foldable menus), this prevents indexing errors.
This should enhance the support for dynamic menus that might change
the number of options offered.
@timholy
Copy link
Sponsor Member Author

timholy commented Jun 2, 2020

I think this has reached a satisfying point. Changes since the last update:

  • I've added an intermediate abstract type, ConfiguredMenu, that resolves the API "leak" I describe above. Subtypes store the configuration in the menu itself, rather than in a global variable.
  • The printing of the "cursor"-selected indicator is handled automatically, just like it already was for the "up arrow" and "down arrow" characters. This ends the odd asymmetry among these indicators.
  • I've made the printing algorithm more robust for dynamic menus that might change their number of options. It now passes a state variable that allows it to know how many lines it needs to back up to erase the previous menu.
  • I've added a more thorough test of navigation and menu-printing (the new dynamic_menu.jl test file)
  • I've added "legacy" tests that use the original Julia 1.0 AbstractMenu API, and verified that they still work.

With one important exception, this AbstractMenu API now preserves backward compatibility while developing a cleaner interface for the future. The exception, unfortunately, is in the exported RadioMenu and MultiSelectMenu types, which I've migrated over to the new API. That means that instead of configuring them via TerminalMenus.config, you configure them at construction time. If we want to preserve compatibility, we should export the OldRadioMenu and OldMultiSelectMenu that I added as test examples, and figure out some new name for the new implementations.

Thoughts?

@timholy timholy changed the title Improve style in REPL.TerminalMenus Improve the API of REPL.TerminalMenus Jun 3, 2020
@tomyun
Copy link
Contributor

tomyun commented Jun 3, 2020

Nice to see this moving forward! I was just wondering if we could use this chance for incorporating upstream changes made after split (merge?) circa early 2018. A notable change I can think of is single choice menu (nick-paul/TerminalMenus.jl#20) which was a sole reason prevented me from using the REPL version of this package.

@timholy
Copy link
Sponsor Member Author

timholy commented Jun 3, 2020

That can be viewed as a bugfix, so it would be fine to put that in any time. That said, you might as well wait for this to merge?

Copy link
Sponsor Member

@KristofferC KristofferC left a comment

Choose a reason for hiding this comment

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

Looks like a nice improvement to the API to me.

@timholy
Copy link
Sponsor Member Author

timholy commented Jun 8, 2020

Thanks, @KristofferC!

@timholy
Copy link
Sponsor Member Author

timholy commented Jun 8, 2020

Added NEWS, will merge if CI passes.

@timholy timholy merged commit 36270e9 into master Jun 9, 2020
@timholy timholy deleted the teh/cleanup_terminalmenus branch June 9, 2020 07:34
@yakir12
Copy link

yakir12 commented Jun 9, 2020

(about the news, if this indeed allows for adding default option/s, shouldn't that be mentioned in the news?)

@timholy
Copy link
Sponsor Member Author

timholy commented Jun 9, 2020

Maybe. I'm not actually sure what you mean by "adding default options," can you clarify?

Note docs for TerminalMenus are at https://docs.julialang.org/en/v1.6-dev/stdlib/REPL/#TerminalMenus-1. Amazingly, they've already been updated for this PR.

@yakir12
Copy link

yakir12 commented Jun 9, 2020

I'm only asking because this #32349 closed by this PR. As stated in that issue, default options would allow for the prompt to have the cursor already set to some default option for the RadioMenu, and for some default options in MultiSelectMenu to be pre-checked. This is useful when you are near certain about what the user will choose and want to save the extra scrolling and clicking by allowing the user to simply press done.

@timholy
Copy link
Sponsor Member Author

timholy commented Jun 9, 2020

Oh, got it. Yes, I suppose the individual API additions are NEWS-worthy. See if #36210 helps (and feel free to edit it).

@yakir12
Copy link

yakir12 commented Jun 9, 2020

Perfect! Thank you very much.

simeonschaub pushed a commit to simeonschaub/julia that referenced this pull request Aug 11, 2020
This overhaul of the `AbstractMenu` extension interface of `REPL.TerminalMenus` addresses several concerns:

- Printing the "paging" navigation indicators was handled by TerminalMenus, but the subtype methods were expected to print the cursor indicator and, for multiple-selection menus, the selection status indicators in a manner that preserved alignment. There was no obvious reason for the inconsistency.
- Printing these indicators relied on accessing a mutable private global variable in TerminalMenus. It was therefore not possible to use different settings simultaneously for two different menus (e.g. a main menu and its submenus).
- The API required that subtype methods supply a list of strings for each menu-option when really it only needed to know how many options were available. This was particularly problematic for large, dynamic menus whose options might change and for which lists of options would have to be reallocated regularly but were then thrown away after checking their length.

This deprecates the old interface in a backward-compatible manner, so is non-breaking.
@GunnarFarneback
Copy link
Contributor

I'm late to discover the wonders of TerminalMenus and I was delighted to find this PR when faced with the limitations of the API in Julia 1.5. However, I still see one glaring omission, namely that the cursor position is a local variable in the request loop. For reading purposes you can catch it from the writeline calls but there is no way to control it after the initialization.

Things that would be possible with a controllable cursor include jumping to a related option after a custom key press or seamlessly moving into a submenu related to the option at the cursor.

Ideally the cursor should have been a mandatory field in the menu struct. That's not possible to introduce now with respect to backwards compatibility of course. One possibility is to use a cursor field from the struct if it exists and fall back to a local variable otherwise, but I don't immediately see how to implement that in an elegant way. A simpler but less complete approach would be to allow keypress to return an Int if you want to change the cursor from that call (which is probably where it's most interesting).

@timholy, any thoughts?

@timholy
Copy link
Sponsor Member Author

timholy commented Nov 11, 2020

I agree that it would have been nice to allow cursor to be part of the menu. Indeed, the hardest part of this cleanup was doing it in a way that maintained backwards compatibility while still moving us towards a cleaner implementation.

As an alternative to supporting an arbitrary Int as a return value for keypress, what about supporting Union{Int,RefValue{Int}} for cursor? Then anyone who wants to observe the value from the outside can do so.

@GunnarFarneback
Copy link
Contributor

Ah, a Ref in the call to request. Yes, that's a clever approach. I'll do something with that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
REPL Julia's REPL (Read Eval Print Loop)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adding defaults to TerminalMenus julep: Allow passing an initial cursor to request() AbstractMenu.jl
7 participants