-
Notifications
You must be signed in to change notification settings - Fork 132
Expand file tree
/
Copy pathgetting_started.py
More file actions
executable file
·231 lines (195 loc) · 8.68 KB
/
Copy pathgetting_started.py
File metadata and controls
executable file
·231 lines (195 loc) · 8.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#!/usr/bin/env python3
"""An example cmd2 application demonstrating many common features.
Features demonstrated include all of the following:
1) Colorizing/stylizing output
2) Persistent history
3) How to run an initialization script at startup
4) How to group and categorize commands when displaying them in help
5) Opting-in to using the ipy command to run an IPython shell
6) Allowing access to your application in py and ipy
7) Displaying an intro banner upon starting your application
8) Using a custom prompt
9) How to make custom attributes settable at runtime
10) Shortcuts for commands
11) Persistent bottom toolbar with realtime status updates
12) Right prompt which displays contextual information
13) Background thread to update the content displayed by the bottom toolbar outside of the UI thread to keep things responsive
14) Using preloop() and postloop() hooks to start and stop a background thread
15) Using the with_annotated decorator to parse typed command arguments
16) Using the with_argparser decorator to parse command arguments with a custom parser
"""
import argparse
import datetime
import pathlib
import sys
import threading
from typing import Annotated
from prompt_toolkit.application import get_app
from prompt_toolkit.formatted_text import AnyFormattedText
from rich.style import Style
from rich.text import Text
import cmd2
from cmd2 import (
Color,
stylize,
)
from cmd2.annotated import Option
class BasicApp(cmd2.Cmd):
"""Cmd2 application to demonstrate many common features."""
DEFAULT_CATEGORY = "My Custom Commands"
def __init__(self) -> None:
"""Initialize the cmd2 application."""
# Startup script that defines a couple aliases for running shell commands
alias_script = pathlib.Path(__file__).absolute().parent / ".cmd2rc"
# Create a shortcut for one of our commands
shortcuts = cmd2.DEFAULT_SHORTCUTS
shortcuts.update({"&": "intro"})
super().__init__(
auto_suggest=True,
enable_bottom_toolbar=True,
enable_rprompt=True,
include_ipy=True,
persistent_history_file="cmd2_history.dat",
refresh_interval=0.5, # refresh the UI twice a second to keep the bottom toolbar timestamp current
shortcuts=shortcuts,
startup_script=str(alias_script),
)
# Prints an intro banner once upon application startup
self.intro = (
stylize(
"Welcome to cmd2!",
style=Style(color=Color.GREEN1, bgcolor=Color.GRAY0, bold=True),
)
+ " Note the full Unicode support: 😇 💩"
+ " and the persistent bottom bar with realtime status updates!"
)
# Show this as the prompt when asking for input
self.prompt = "myapp> "
# Allow access to your application in py and ipy via self
self.self_in_py = True
# Color to output text in with echo command
self.foreground_color = Color.CYAN.value
# Make echo_fg settable at runtime
fg_colors = [c.value for c in Color]
self.add_settable(
cmd2.Settable(
"foreground_color",
str,
Text.assemble(
"Foreground color to use with echo command ",
"(Options: ",
Text("Green", Style(color=Color.GREEN)),
", ",
Text("Red", Style(color=Color.RED)),
", ",
Text("Blue", Style(color=Color.BLUE)),
", ...)",
),
self,
choices=fg_colors,
)
)
# Initialize background thread state for the bottom toolbar
self._toolbar_state = {"now": ""}
self._toolbar_lock = threading.Lock()
self._stop_thread_event = threading.Event()
self._toolbar_thread: threading.Thread | None = None
def _update_toolbar_state(self) -> None:
"""Background thread worker to update toolbar state continuously."""
while not self._stop_thread_event.is_set():
# Get the current time in ISO format with 0.01s precision
dt = datetime.datetime.now(datetime.timezone.utc).astimezone()
now = dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4] + dt.strftime("%z")
with self._toolbar_lock:
self._toolbar_state["now"] = now
# Sleep to yield CPU, polling 4 times a second
self._stop_thread_event.wait(0.25)
def preloop(self) -> None:
"""Hook method executed once when the cmdloop() method is called."""
self._stop_thread_event.clear()
self._toolbar_thread = threading.Thread(target=self._update_toolbar_state, daemon=True)
self._toolbar_thread.start()
def postloop(self) -> None:
"""Hook method executed once when the cmdloop() method is about to return."""
if self._toolbar_thread and self._toolbar_thread.is_alive():
self._stop_thread_event.set()
self._toolbar_thread.join()
def get_bottom_toolbar(self) -> AnyFormattedText:
left_text = sys.argv[0]
with self._toolbar_lock:
now = self._toolbar_state.get("now", "")
# Fetch the terminal width to calculate padding for right-alignment.
# If called outside a running app loop (e.g., in unit tests), get_app()
# safely returns a dummy app with an 80-column fallback.
cols = get_app().output.get_size().columns
padding_size = cols - len(left_text) - len(now)
if padding_size < 1:
padding_size = 1
padding = " " * padding_size
# Return formatted text for prompt-toolkit
return [
("ansigreen", left_text),
("", padding),
("ansicyan", now),
]
def get_rprompt(self) -> AnyFormattedText:
current_working_directory = pathlib.Path.cwd()
style = "bg:ansired fg:ansiwhite"
text = f"cwd={current_working_directory}"
return [(style, text)]
@cmd2.with_annotated
def do_cat(
self,
path: pathlib.Path, # Required positional argument with type annotation, tab-completes filesystem paths automatically
numbered: Annotated[ # Optional flag argument with type annotation, default value, and help text
bool, Option("-n", "--number", help_text="prefix each line with its number")
] = False,
) -> None:
"""Print a file's contents. `path` tab-completes filesystem paths automatically.
Try:
cat <TAB> # path completes files/dirs -- no completer wired
cat notes.txt
cat notes.txt -n # -n / --number, declared via Option metadata
cat notes.txt --no-number
"""
text = path.read_text()
lines = text.splitlines()
if numbered:
numbered_lines = []
for index, line in enumerate(lines, start=1):
numbered_lines.append(f"{index}: {line}")
self.ppaged("\n".join(numbered_lines))
else:
# Just print the contents using a pager
self.ppaged(path.read_text())
def do_intro(self, _: cmd2.Statement) -> None:
"""Display the intro banner.
This command uses raw statement parsing. In general, we strongly recommend against this approach. But since this
command effectively takes no arguments, it is safe to use raw statement parsing here.
The & key is also used as a shortcut for this command, so you can also type & to display the intro banner.
"""
self.poutput(self.intro)
@staticmethod
def _build_echo_parser() -> cmd2.Cmd2ArgumentParser:
"""Parser factory method for use with the echo command."""
echo_parser = cmd2.Cmd2ArgumentParser(description="Command that echoes input.")
echo_parser.add_argument("-u", "--upper", action="store_true", help="uppercase the output")
echo_parser.add_argument("-r", "--repeat", type=int, default=1, help="output [n] times")
echo_parser.add_argument("words", nargs="+", help="words to print")
return echo_parser
@cmd2.with_argparser(_build_echo_parser)
def do_echo(self, args: argparse.Namespace) -> None:
"""Command using with_argparser decorator for parsing arguments."""
output_str = " ".join(args.words)
if args.upper:
output_str = output_str.upper()
for _ in range(args.repeat):
self.poutput(
stylize(
output_str,
style=Style(color=self.foreground_color),
)
)
if __name__ == "__main__":
app = BasicApp()
sys.exit(app.cmdloop())