Skip to content

Commit 3861b21

Browse files
authored
Allow prompt_toolkit for input (#64)
1 parent f9eb63b commit 3861b21

10 files changed

Lines changed: 125 additions & 58 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ dev = [
5656
"pre-commit",
5757
"pytest",
5858
]
59+
full = [
60+
"prompt-toolkit", # Alternate interface, akin to GNU Readline
61+
]
5962

6063
[project.scripts]
6164
trepan3k = "trepan.__main__:main"

test/unit/cmdhelper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ def no_history():
7474
return d, cp
7575

7676

77-
def readline_yes(prompt=None) -> str:
77+
def readline_yes(prompt=None, use_raw: bool=False) -> str:
7878
return "Y"
7979

8080

81-
def readline_no(prompt=None) -> str:
81+
def readline_no(prompt=None, use_raw: bool=False) -> str:
8282
return "N"

test/unit/interfaces/test_intf_user.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def no_history():
2424

2525
for s in ["y", "Y", "Yes", " YES "]:
2626

27-
def readline_with_prompt(prompt=None):
27+
def readline_with_prompt(prompt=None, use_raw: bool=False):
2828
return readline(s)
2929

3030
u.input.readline = readline_with_prompt
@@ -33,7 +33,7 @@ def readline_with_prompt(prompt=None):
3333
pass
3434
for s in ["n", "N", "No", " NO "]:
3535

36-
def readline_with_prompt(prompt=None):
36+
def readline_with_prompt(prompt=None, use_raw: bool=False):
3737
return readline(s)
3838

3939
u.input.readline = readline_with_prompt

trepan/debugger.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,13 @@ def completer(text: str, state):
9999

100100
# How are I/O for this debugger handled? This should
101101
# be set before calling DebuggerCore.
102-
interface_opts = {
103-
"complete": completer,
102+
interface_opts = opts.get("interface_opts", {
104103
"debugger_name": "trepan3k",
105-
}
104+
"readline": opts.get("readline")
105+
})
106+
if "complete" not in interface_opts:
107+
interface_opts["complete"] = completer
108+
106109
# FIXME when I pass in opts=opts things break
107110

108111
inp = opts.get("input", None) if opts else None

trepan/inout/input.py

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright (C) 2009-2010, 2013-2015, 2017, 2023 Rocky Bernstein
3+
# Copyright (C) 2009-2010, 2013-2015, 2017, 2023-2024 Rocky Bernstein
44
# <rocky@gnu.org>
55
#
66
# This program is free software: you can redistribute it and/or modify
@@ -20,79 +20,74 @@
2020
import io
2121
import sys
2222

23-
from trepan import misc as Mmisc
2423
from trepan.inout import base as Mbase
2524

26-
27-
def readline_importable() -> bool:
28-
try:
29-
import readline # NOQA
30-
31-
return True
32-
except ImportError:
33-
return False
34-
return # Not reached
35-
25+
try:
26+
from prompt_toolkit import PromptSession, HTML
27+
from prompt_toolkit.styles import Style
28+
from prompt_toolkit.history import FileHistory
29+
except:
30+
PromptSession = lambda history: None
31+
FileHistory = lambda history: None
32+
HTML = lambda string: string
3633

3734
class DebuggerUserInput(Mbase.DebuggerInputBase):
3835
"""Debugger input connected to what we think of as a end-user input
3936
as opposed to a relay mechanism to another process. Input could be
4037
interactive terminal, but it might be file input."""
4138

4239
def __init__(self, inp=None, opts=None):
43-
self.input = inp or sys.stdin
44-
self.line_edit = None # Our name for GNU readline capability
45-
self.open(self.input, opts)
40+
41+
if opts and opts.get("readline") == "prompt_toolkit":
42+
self.session = PromptSession(history=FileHistory(opts.get("histfile")))
43+
self.input = self.session.input
44+
self.session.enable_history_search = True
45+
self.line_edit = True
46+
self.closed = False
47+
self.use_raw = False
48+
else:
49+
self.session = None
50+
self.input = inp or sys.stdin
51+
self.line_edit = None # Our name for GNU readline capability
52+
self.open(self.input, opts)
53+
4654
return
4755

4856
def close(self):
4957
self.input.close()
5058
self.closed = True
5159
return
5260

53-
DEFAULT_OPEN_READ_OPTS = {
54-
"use_raw": True,
55-
"try_readline": True,
56-
}
57-
5861
def use_history(self):
59-
return self.use_raw and readline_importable()
62+
return self.use_raw
6063

6164
def open(self, inp, opts={}):
6265
"""Use this to set where to read from.
6366
64-
Set opts['try_lineedit'] if you want this input to interact
65-
with GNU-like readline library. By default, we will assume to
66-
try importing and using readline. If readline is not
67-
importable, line editing is not available whether or not
68-
opts['try_readline'] is set.
69-
7067
Set opts['use_raw'] if input should use Python's use_raw(). If
7168
however 'inp' is a string and opts['use_raw'] is not set, we
7269
will assume no raw output. Note that an individual readline
7370
may override the setting.
7471
"""
75-
get_option = lambda key: Mmisc.option_set(
76-
opts, key, self.DEFAULT_OPEN_READ_OPTS
77-
)
7872
if (
7973
isinstance(inp, io.TextIOWrapper)
8074
or isinstance(inp, io.StringIO)
8175
or hasattr(inp, "isatty")
8276
and inp.isatty()
8377
):
84-
self.use_raw = get_option("use_raw")
78+
self.use_raw = opts and opts.get("use_raw", False)
8579
elif isinstance(inp, "string".__class__): # FIXME
8680
if opts is None:
8781
self.use_raw = False
8882
else:
89-
self.use_raw = get_option("use_raw")
83+
self.use_raw = opts.get("use_raw", False)
9084
pass
9185
inp = open(inp, "r")
9286
else:
9387
raise IOError("Invalid input type (%s) for %s" % (type(inp), inp))
9488
self.input = inp
95-
self.line_edit = get_option("try_readline") and readline_importable()
89+
self.line_edit = bool(opts and opts.get("readline"))
90+
9691
self.closed = False
9792
return
9893

@@ -107,6 +102,12 @@ def readline(self, use_raw=None, prompt=""):
107102
initialization is used.
108103
"""
109104
# FIXME we don't do command completion.
105+
if self.session:
106+
# Using prompt_toolkit
107+
html_prompt = HTML(f"<u>{prompt.strip()}</u> ")
108+
line = self.session.prompt(html_prompt, style=Style.from_dict({"": ""}))
109+
return line.rstrip("\n")
110+
110111
if use_raw is None:
111112
use_raw = self.use_raw
112113
pass
@@ -131,7 +132,6 @@ def readline(self, use_raw=None, prompt=""):
131132

132133
# Demo
133134
if __name__ == "__main__":
134-
print("readline importable: ", readline_importable())
135135
inp = DebuggerUserInput(__file__)
136136
line = inp.readline()
137137
print(line)

trepan/inout/scriptin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ def __init__(self, inp, opts=None):
3030
self.input = None
3131
self.line_edit = False # Our name for GNU readline capability
3232
self.name = None
33+
self.opts = opts
34+
self.session = None
3335
self.open(inp, opts)
3436
return
3537

trepan/inout/stringarray.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright (C) 2009, 2013-2014, 2023 Rocky Bernstein
3+
# Copyright (C) 2009, 2013-2014, 2023-2024 Rocky Bernstein
44
# <rocky@gnu.org>
55
#
66
# This program is free software: you can redistribute it and/or modify
@@ -27,8 +27,10 @@ class StringArrayInput(Mbase.DebuggerInputBase):
2727
even simpler."""
2828

2929
def __init__(self, inp=[], opts=None):
30-
self.input = inp
3130
self.closed = False
31+
self.input = inp
32+
self.opts = opts
33+
self.session = None
3234
return
3335

3436
def close(self):

trepan/interfaces/user.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,33 +43,33 @@
4343
write_history_file,
4444
)
4545
except ImportError:
46-
pass
46+
pass
4747

4848

4949
class UserInterface(TrepanInterface):
5050
"""Interface when communicating with the user in the same
5151
process as the debugged program."""
5252

5353
def __init__(self, inp=None, out=None, opts={}):
54-
user_opts = DEFAULT_USER_SETTINGS.copy()
55-
user_opts.update(opts)
54+
self.user_opts = DEFAULT_USER_SETTINGS.copy()
55+
self.user_opts.update(opts)
5656

5757
atexit.register(self.finalize)
5858
self.interactive = True # Or at least so we think initially
59-
self.input = inp or DebuggerUserInput()
59+
self.input = inp or DebuggerUserInput(self.user_opts.get("input", {}), self.user_opts)
6060
self.output = out or DebuggerUserOutput()
61-
self.debugger_name = user_opts.get("debugger_name", "trepan3k")
61+
self.debugger_name = self.user_opts.get("debugger_name", "trepan3k")
6262

6363
if self.input.use_history():
64-
self.complete = user_opts["complete"]
64+
self.complete = self.user_opts["complete"]
6565
if self.complete:
6666
parse_and_bind("tab: complete")
6767
set_completer(self.complete)
6868
pass
69-
self.histfile = user_opts["histfile"]
69+
self.histfile = self.user_opts["histfile"]
7070
if self.histfile:
7171
try:
72-
read_history_file(histfile)
72+
read_history_file(self.histfile)
7373
except IOError:
7474
pass
7575
except Exception:
@@ -158,16 +158,14 @@ def read_command(self, prompt=""):
158158
return line
159159

160160
def readline(self, prompt=""):
161+
use_raw = hasattr(self.input, "use_raw") and self.input.use_raw
161162
if (
162-
hasattr(self.input, "use_raw")
163-
and not self.input.use_raw
164-
and prompt
165-
and len(prompt) > 0
163+
use_raw and prompt and len(prompt) > 0 or not self.readline == "prompt_toolkit"
166164
):
167165
self.output.write(prompt)
168166
self.output.flush()
169167
pass
170-
return self.input.readline(prompt=prompt)
168+
return self.input.readline(prompt=prompt, use_raw=use_raw)
171169

172170
pass
173171

trepan/options.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@
2727
from trepan.inout.output import DebuggerUserOutput
2828
from trepan.lib.file import readable
2929

30+
try:
31+
import prompt_toolkit # NOQA
32+
have_prompt_toolkit = True
33+
except ImportError:
34+
have_prompt_toolkit = False
35+
36+
try:
37+
import prompt_toolkit # NOQA
38+
have_gnu_readline = True
39+
except ImportError:
40+
have_gnu_readline = False
41+
3042

3143
def default_configfile(base_filename: str) -> str:
3244
"""Return fully expanded configuration filename location for
@@ -313,6 +325,39 @@ def process_options(pkg_version: str, sys_argv: str, option_list=None):
313325
"--annotate", default=0, type="int", help="Use annotations to work inside emacs"
314326
)
315327

328+
readline = None
329+
if have_gnu_readline:
330+
optparser.add_option(
331+
"--gnu-readline",
332+
dest="use_gnu_readline",
333+
action="store_true",
334+
default=True,
335+
help="Try using GNU-Readline",
336+
)
337+
optparser.add_option(
338+
"--no-gnu-readline",
339+
dest="use_gnu_readline",
340+
action="store_false",
341+
default=True,
342+
help="Do not use GNU-Readline",
343+
)
344+
if have_prompt_toolkit:
345+
optparser.add_option(
346+
"--prompt-toolkit",
347+
dest="use_prompt_toolkit",
348+
action="store_true",
349+
default=True,
350+
help="Try using prompt_toolkit",
351+
)
352+
optparser.add_option(
353+
"--no-prompt-toolkit",
354+
dest="use_prompt_toolkit",
355+
action="store_false",
356+
default=True,
357+
help="Do not use prompt_toolkit",
358+
)
359+
360+
316361
# Set up to stop on the first non-option because that's the name
317362
# of the script to be debugged on arguments following that are
318363
# that scripts options that should be left untouched. We would
@@ -323,7 +368,20 @@ def process_options(pkg_version: str, sys_argv: str, option_list=None):
323368

324369
sys.argv = list(sys_argv)
325370
(opts, sys.argv) = optparser.parse_args(sys_argv[1:])
326-
dbg_opts = {"from_ipython": opts.from_ipython}
371+
if hasattr(opts, "use_prompt_toolkit") and opts.use_prompt_toolkit:
372+
readline = "prompt_toolkit"
373+
elif hasattr(opts, "use_gnu_readline") and opts.use_gnu_readline:
374+
readline = "gnu_readline"
375+
else:
376+
readline = None
377+
378+
dbg_opts = {
379+
"from_ipython": opts.from_ipython,
380+
"interface_opts": {
381+
"readline": readline,
382+
"debugger_name": "trepan3k",
383+
}
384+
}
327385

328386
# Handle debugger startup command files: --nx (-n) and --command.
329387
dbg_initfiles = []

trepan/processor/cmdproc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@ def set_prompt(self, prompt="trepan3k"):
270270
pass
271271
self.prompt_str = f"{'(' * self.debug_nest}{prompt}{')' * self.debug_nest}"
272272
highlight = self.debugger.settings["highlight"]
273-
if highlight and highlight in ("light", "dark"):
273+
using_prompt_toolkit = self.intf[-1].input.session is not None
274+
if not using_prompt_toolkit and highlight and highlight in ("light", "dark"):
274275
self.prompt_str = colorize("underline", self.prompt_str)
275276
self.prompt_str += " "
276277

0 commit comments

Comments
 (0)