codehs-utils 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .mypy_cache/
11
+ .pytest_cache/
12
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,510 @@
1
+ Metadata-Version: 2.5
2
+ Name: codehs-utils
3
+ Version: 1.0.0
4
+ Summary: A small terminal toolkit: colors, gradients, banners, keyboard/mouse input, and drawing for CodeHS-style Python environments.
5
+ Project-URL: Homepage, https://github.com/yourusername/codehs-utils
6
+ Project-URL: Issues, https://github.com/yourusername/codehs-utils/issues
7
+ Author-email: Your Name <you@example.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: ansi,codehs,colors,console,terminal,tui
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Terminals
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+
21
+ # codehs-utils
22
+
23
+ [![PyPI version](https://badge.fury.io/py/codehs-utils.svg)](https://badge.fury.io/py/codehs-utils)
24
+ [![Python versions](https://img.shields.io/pypi/pyversions/codehs-utils.svg)](https://pypi.org/project/codehs-utils/)
25
+
26
+ A Python library for terminal colors, gradients, banners, drawing, and keyboard/mouse input, originally built for CodeHS's Python environment.
27
+
28
+ ## Features
29
+
30
+ - RGB and named colors for foreground and background, plus mixing/lighten/darken
31
+ - Gradient text with built-in presets (rainbow, fire, ocean, sunset, and more)
32
+ - Composable text banners and boxes, alignable and laid out side by side
33
+ - Rectangles with fill and border drawing
34
+ - A half-block pixel canvas for square-pixel graphics
35
+ - Keyboard and mouse input, including click-and-drag mouse events
36
+ - Cursor and screen control (alternate screen, hide/show cursor, clear, resize-aware sizing)
37
+ - Clickable buttons with hover/press states
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install codehs-utils
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```python
48
+ import codehs_utils as c
49
+
50
+ # Colorful banner
51
+ print(c.banner("Hello, World!", color="white", background="blue"))
52
+
53
+ # Interactive app with mouse support
54
+ with c.app(mouse=True):
55
+ c.banner("Click anywhere, press ESC to quit").draw(1, 1)
56
+ for event in c.events(timeout=5):
57
+ if event is None or (event.kind == "key" and event.key == "ESC"):
58
+ break
59
+ ```
60
+
61
+ ## Documentation
62
+
63
+ ### Color Functions
64
+
65
+ ```python
66
+ from codehs_utils import ColorLike, ColorText, GradientText
67
+
68
+ # Using named colors, hex, or RGB tuples
69
+ red = ColorLike("red")
70
+ also_red = ColorLike("#ff0000")
71
+ also_red_too = ColorLike((255, 0, 0))
72
+
73
+ print(ColorText("Red text", foreground=red))
74
+
75
+ # Mixing and adjusting colors
76
+ lighter = red.lighten(0.3)
77
+ darker = red.darken(0.3)
78
+ contrast = red.contrast() # black or white, whichever reads better
79
+
80
+ # Background colors
81
+ print(ColorText("Green background", background="green"))
82
+ ```
83
+
84
+ ### Gradient Text
85
+
86
+ ```python
87
+ from codehs_utils import GradientText
88
+
89
+ # Built-in preset, as a shortcut method...
90
+ print(GradientText.rainbow("This text has rainbow colors!"))
91
+
92
+ # ...or via from_preset(), which every shortcut method calls internally
93
+ print(GradientText.from_preset("Same thing", preset="rainbow"))
94
+
95
+ # Custom stops
96
+ print(GradientText("Custom gradient", colors=["red", "orange", "yellow"]))
97
+
98
+ # Background gradient
99
+ print(GradientText("Rainbow background!", background_colors="rainbow"))
100
+ ```
101
+
102
+ Mixing two colors together (used internally by `.lighten()`/`.darken()`, also usable directly):
103
+
104
+ ```python
105
+ from codehs_utils import ColorLike
106
+
107
+ blend = ColorLike("red").mix("blue", t=0.5) # 50/50 blend
108
+ print(blend.to_hex())
109
+ ```
110
+
111
+ ### Style Methods
112
+
113
+ `ColorText` and `GradientText` share a set of chainable style methods (from their common `_StyleMixin`), so you can build up a piece of styled text step by step instead of passing everything to the constructor at once:
114
+
115
+ ```python
116
+ from codehs_utils import ColorText
117
+
118
+ msg = ColorText("warning!").bold().underline()
119
+ msg.set_background("red")
120
+ print(msg)
121
+ ```
122
+
123
+ Available chainable methods:
124
+ - `.bold()`, `.italic()`, `.underline()`, `.strikethrough()`, `.dim()`, `.blink()`, `.reverse()` — turn on a single style
125
+ - `.add_style(name)` / `.remove_style(name)` — turn a named style on/off
126
+ - `.set_styles(*names)` — replace the whole style list at once
127
+ - `.set_text(text)` — change the wrapped text
128
+ - `.set_background(color)` — set or clear the background color
129
+ - `.set_reset(bool)` — whether an ANSI reset code is appended after the text (default `True`)
130
+
131
+ Every method returns `self`, so calls can be chained.
132
+
133
+ `StyledText` concatenates several `ColorText`/`GradientText`/plain-string pieces into one object, and can be wrapped or aligned as a whole:
134
+
135
+ ```python
136
+ from codehs_utils import ColorText, GradientText, StyledText
137
+
138
+ line = StyledText([ColorText("Error: ", "red"), GradientText.fire("something broke")])
139
+ print(line)
140
+ print(line.align(40, "center"))
141
+ print(line.wrap(20))
142
+ ```
143
+
144
+ ### Cursor & Screen Control
145
+
146
+ ```python
147
+ from codehs_utils import (
148
+ clear_screen, clear_line, set_cursor_pos, set_cursor_row, set_cursor_col,
149
+ move_cursor, save_cursor_position, restore_cursor_position,
150
+ hide_cursor, show_cursor, get_cursor_position, get_terminal_size,
151
+ enter_alt_screen, leave_alt_screen, restore_terminal,
152
+ )
153
+
154
+ # Clear the screen, or just the current line
155
+ clear_screen()
156
+ clear_line(mode="to_end") # "full", "to_end", or "to_start"
157
+
158
+ # Move the cursor
159
+ set_cursor_pos(10, 5) # absolute: row 10, column 5
160
+ set_cursor_row(10)
161
+ set_cursor_col(5)
162
+ move_cursor(dx=2, dy=-1) # relative
163
+
164
+ # Save/restore cursor position
165
+ save_cursor_position()
166
+ restore_cursor_position()
167
+
168
+ # Hide/show the cursor
169
+ hide_cursor()
170
+ show_cursor()
171
+
172
+ # Read back state
173
+ row, col = get_cursor_position()
174
+ width, height = get_terminal_size()
175
+
176
+ # The alternate screen buffer (used internally by app())
177
+ enter_alt_screen()
178
+ leave_alt_screen()
179
+
180
+ # Manually undo everything (hide_cursor, alt screen, mouse tracking, raw
181
+ # mode) in one call — normally you'd just use app() instead
182
+ restore_terminal()
183
+ ```
184
+
185
+ Lower-level output helpers, used internally but available directly:
186
+
187
+ ```python
188
+ from codehs_utils import write, frame
189
+
190
+ write("some text") # like print(), but no trailing newline and no `sep`
191
+
192
+ # Batch several writes into one flush, avoiding flicker/tearing
193
+ with frame():
194
+ write("line one\n")
195
+ write("line two\n")
196
+ ```
197
+
198
+ ### Text Formatting
199
+
200
+ ```python
201
+ from codehs_utils import align_text, wrap_text
202
+
203
+ # Text alignment
204
+ text = "Hello World"
205
+ print(align_text(text, 20, "center")) # Center align in 20 characters
206
+ print(align_text(text, 20, "right")) # Right align
207
+ print(align_text(text, 20, "left")) # Left align
208
+
209
+ # Word wrapping (ANSI-aware, so styled text still wraps correctly)
210
+ print(wrap_text("A long line of text that needs wrapping", width=15))
211
+ ```
212
+
213
+ ### Banners & Boxes
214
+
215
+ ```python
216
+ from codehs_utils import banner, Banner, BannerRow
217
+
218
+ # A styled box of text
219
+ b = banner("Important Message", color="black", background="lightyellow")
220
+ print(b)
221
+
222
+ # Draw it at a specific position in an app()
223
+ b.draw(row=1, col=1)
224
+
225
+ # Lay banners out side by side
226
+ row = BannerRow([banner("One"), banner("Two"), banner("Three")])
227
+ print(row)
228
+
229
+ # Adjust spacing and vertical alignment between banners
230
+ row.set_gap(3).set_valign("middle")
231
+
232
+ # Align a whole banner (or row) within a wider field
233
+ print(b.align(width=40, align="right"))
234
+ ```
235
+
236
+ ### Rectangles & Pixels
237
+
238
+ ```python
239
+ from codehs_utils import Rect, fill_rect, set_pixel, get_pixel_size
240
+
241
+ # Fill a rectangle
242
+ rect = fill_rect(row=1, col=1, width=10, height=3, color="blue")
243
+
244
+ # Draw a border around a Rect
245
+ rect.draw_border(color="cyan", style="double")
246
+
247
+ # Half-block pixel canvas
248
+ width, height = get_pixel_size()
249
+ set_pixel(x=5, y=5, color="red")
250
+ ```
251
+
252
+ ### Keyboard & Mouse Input
253
+
254
+ ```python
255
+ from codehs_utils import get_key, get_keys, get_mouse_event, events, app, KeyEvent, MouseEvent
256
+
257
+ with app(mouse=True):
258
+ for event in events(timeout=None):
259
+ if isinstance(event, KeyEvent):
260
+ print("Key pressed:", event.key)
261
+ elif isinstance(event, MouseEvent):
262
+ print("Mouse:", event.type, event.button, event.x, event.y)
263
+
264
+ # Blocking single-key read (outside an app(), still uses raw/cbreak mode)
265
+ key = get_key(timeout=5)
266
+
267
+ # Drain every key pressed so far without blocking
268
+ keys = get_keys()
269
+
270
+ # Just the next mouse event
271
+ mouse_event = get_mouse_event(timeout=1)
272
+ ```
273
+
274
+ Mouse tracking can also be toggled manually, if you're not using `app()`:
275
+
276
+ ```python
277
+ from codehs_utils import enable_mouse_tracking, disable_mouse_tracking
278
+
279
+ enable_mouse_tracking()
280
+ disable_mouse_tracking()
281
+ ```
282
+
283
+ ## Advanced Usage
284
+
285
+ ### Custom Colors and Gradients
286
+
287
+ ```python
288
+ from codehs_utils import ColorLike, GradientText
289
+
290
+ # Create ColorLike objects directly
291
+ purple = ColorLike((128, 64, 192))
292
+ print(purple.to_hex())
293
+
294
+ # List and preview all built-in gradient presets
295
+ print(GradientText.list_presets())
296
+ GradientText.preview_presets()
297
+ ```
298
+
299
+ ### Clickable Buttons
300
+
301
+ ```python
302
+ from codehs_utils import Button, app, events
303
+
304
+ with app(mouse=True):
305
+ button = Button("Click me", row=2, col=2, on_click=lambda: print("Clicked!"))
306
+ button.draw()
307
+ for event in events(timeout=10):
308
+ if event is None:
309
+ break
310
+ # handle() updates hover/pressed state, redraws if it changed, fires
311
+ # on_click when appropriate, and returns True exactly on that click
312
+ button.handle(event)
313
+ ```
314
+
315
+ ### The `app()` Context Manager
316
+
317
+ ```python
318
+ from codehs_utils import app
319
+
320
+ with app(mouse=True, cursor=False, clear=True, alt_screen=True):
321
+ ... # Terminal state is restored automatically on exit, even on Ctrl+C
322
+ ```
323
+
324
+ ## Reference
325
+
326
+ ### Text Styles
327
+ Passed as `styles=[...]` to `ColorText`/`GradientText`, or via `.bold()`, `.italic()`, etc:
328
+ - `bold`
329
+ - `dim`
330
+ - `italic`
331
+ - `underline`
332
+ - `blink`
333
+ - `reverse`
334
+ - `strikethrough`
335
+
336
+ ### Border Styles
337
+ Passed as `style=` to `Rect.draw_border()`:
338
+ - `single` — `┌─┐ └─┘`
339
+ - `double` — `╔═╗ ╚═╝`
340
+ - `rounded` — `╭─╮ ╰─╯`
341
+ - `heavy` — `┏━┓ ┗━┛`
342
+ - `ascii` — `+-+ +-+`
343
+
344
+ ### Gradient Presets
345
+ Built into `GradientText`, usable by name (e.g. `GradientText.rainbow(...)`):
346
+ - `rainbow`, `fire`, `ocean`, `sunset`, `pastel`, `grayscale`, `neon`, `forest`, `mint`, `gold`
347
+
348
+ ## API Reference
349
+
350
+ Every public name is re-exported from the top level (`import codehs_utils as c`), so the module path below is just for grouping — you don't need to import from it directly.
351
+
352
+ ### Colors — `colors`
353
+
354
+ | Name | Description |
355
+ |---|---|
356
+ | `ColorLike(color)` | Parses a name, `"#hex"`, or `(r, g, b)` tuple into a normalized color |
357
+ | `ColorLike.rgb` | The parsed `(r, g, b)` tuple, or `None` |
358
+ | `ColorLike.to_hex()` | Returns `"#rrggbb"` |
359
+ | `ColorLike.luminance()` | Perceived brightness, 0.0–1.0 |
360
+ | `ColorLike.contrast()` | Black or white, whichever reads better on this color |
361
+ | `ColorLike.mix(other, t=0.5)` | Blends toward another color by ratio `t` |
362
+ | `ColorLike.lighten(amount=0.2)` / `.darken(amount=0.2)` | Mixes toward white / black |
363
+ | `ColorLike.print_samples()` | Prints every named color as a labeled banner |
364
+ | `ColorText(text, foreground=, background=, styles=, reset=)` | A single piece of solid-colored/styled text |
365
+ | `GradientText(text, colors=, color=, background=, background_colors=, styles=, reset=)` | Text with a foreground and/or background gradient |
366
+ | `GradientText.from_preset(text, preset="rainbow", **kwargs)` | Build from a named preset |
367
+ | `GradientText.rainbow(...)`, `.fire(...)`, `.ocean(...)`, `.sunset(...)`, `.pastel(...)`, `.grayscale(...)`, `.neon(...)`, `.forest(...)`, `.mint(...)`, `.gold(...)` | Shortcut constructors, one per built-in preset |
368
+ | `GradientText.list_presets()` | List of built-in preset names |
369
+ | `GradientText.preview_presets(sample_text=, print_output=True)` | Prints (and returns) a sample of every preset |
370
+ | `StyledText(parts=None)` | Concatenates strings/`ColorText`/`GradientText` into one object |
371
+ | `ColorSpec`, `GradientColors` | Type aliases used in the signatures above (not runtime objects) |
372
+
373
+ **Shared style methods** (on `ColorText` and `GradientText`, via `_StyleMixin`):
374
+
375
+ | Name | Description |
376
+ |---|---|
377
+ | `.bold()` `.dim()` `.italic()` `.underline()` `.blink()` `.reverse()` `.strikethrough()` | Turn on one style |
378
+ | `.add_style(name)` / `.remove_style(name)` | Turn a named style on/off |
379
+ | `.set_styles(*names)` | Replace the whole style list |
380
+ | `.set_text(text)` | Change the wrapped text |
381
+ | `.set_background(color)` | Set or clear the background color |
382
+ | `.set_reset(bool)` | Whether a trailing ANSI reset code is appended |
383
+ | `.set_color(color)` (`ColorText` only) | Set or clear the foreground color |
384
+ | `.set_colors(colors)` / `.set_background_gradient(colors)` (`GradientText` only) | Set the foreground / background gradient stops |
385
+
386
+ **`StyledText` methods:**
387
+
388
+ | Name | Description |
389
+ |---|---|
390
+ | `.wrap(width, collapse_space=True)` | ANSI-aware word wrap of the combined text |
391
+ | `.align(width, align="left", fillchar=" ")` | Align the combined text within `width` |
392
+
393
+ ### Text — `text`
394
+
395
+ | Name | Description |
396
+ |---|---|
397
+ | `wrap_text(text, width, collapse_space=True, break_long_words=True, preserve_newlines=True)` | ANSI-aware word wrapping |
398
+ | `align_text(text, width, align="left", fillchar=" ")` | `"left"`, `"right"`, `"center"`, or `"justify"` alignment |
399
+
400
+ ### Geometry — `geometry`
401
+
402
+ | Name | Description |
403
+ |---|---|
404
+ | `Rect(row, col, width, height)` | A rectangle; also returned by most drawing functions |
405
+ | `Rect.top` `.left` `.bottom` `.right` | Edge coordinates |
406
+ | `Rect.contains(x, y)` | Whether a point falls inside the rect |
407
+ | `Rect.fill(color=None, char=" ")` | Fill the rect (wraps `fill_rect`) |
408
+ | `Rect.draw_border(color=None, style="single", background=None)` | Draw a border around the rect |
409
+
410
+ ### Drawing — `drawing`
411
+
412
+ | Name | Description |
413
+ |---|---|
414
+ | `Banner(lines)` | A block of text padded to a uniform width |
415
+ | `Banner.height` | Number of lines |
416
+ | `Banner.draw(row, col)` | Draw at a position, returns the `Rect` drawn |
417
+ | `Banner.align(width, align="left", fillchar=" ")` | Align the banner within a wider field |
418
+ | `BannerRow(banners, gap=1, valign="top")` | Lay several banners out side by side |
419
+ | `BannerRow.width` `.height` | Combined dimensions |
420
+ | `BannerRow.set_gap(gap)` / `.set_valign(valign)` | Adjust spacing / vertical alignment |
421
+ | `BannerRow.draw(row, col)` / `.align(width, align=, fillchar=)` | Same as on `Banner` |
422
+ | `banner(text, width=None, color=, colors=, background=, background_colors=, styles=, padding=1, align="center")` | Build a styled `Banner` in one call |
423
+ | `fill_rect(row, col, width, height, color=None, char=" ", foreground=None)` | Fill a rectangle directly, returns a `Rect` |
424
+ | `get_pixel_size()` | Terminal size in half-block pixels (`(width, height * 2)`) |
425
+ | `set_pixel(x, y, color)` / `clear_pixel(x, y)` / `clear_pixels()` | Half-block pixel canvas |
426
+ | `Button(text, row, col, width=, background=, color=, hover_background=, hover_color=, pressed_background=, pressed_color=, padding=1, styles=, trigger="release", on_click=)` | A clickable button |
427
+ | `Button.draw()` | (Re)draw the button in its current state |
428
+ | `Button.handle(event)` | Update state from a `MouseEvent`, redraw if needed, fire `on_click`; returns `True` on a completed click |
429
+
430
+ ### Terminal — `terminal`
431
+
432
+ | Name | Description |
433
+ |---|---|
434
+ | `app(mouse=False, cursor=False, clear=True, alt_screen=True, catch_interrupt=True)` | Context manager: sets up and tears down an interactive session |
435
+ | `MouseEvent` | `.type`, `.button`, `.x`, `.y`, `.shift`, `.alt`, `.ctrl`; `.inside(rect)` |
436
+ | `KeyEvent` | `.key`; compares equal to a plain string |
437
+ | `get_key(timeout=None)` | Block for a single keypress |
438
+ | `get_keys()` / `get_key_nonblocking()` | Drain all buffered keys without blocking |
439
+ | `get_mouse_event(timeout=None)` / `get_mouse_event_nonblocking()` | Get the next mouse event |
440
+ | `get_event(timeout=None)` | Get the next key **or** mouse event |
441
+ | `events(timeout=None)` | Infinite iterator over `get_event()` |
442
+ | `get_cursor_position()` / `gcp` | Query the cursor's current position (`gcp` is an alias) |
443
+ | `get_terminal_size()` | `(width, height)` in characters |
444
+ | `clear_screen(scrollback=True)` / `clear_line(mode="full")` | Clear the screen / current line |
445
+ | `set_cursor_pos(row, col)` / `set_cursor_row(row)` / `set_cursor_col(col)` | Absolute cursor positioning |
446
+ | `move_cursor(dx=0, dy=0)` | Relative cursor movement |
447
+ | `save_cursor_position()` / `restore_cursor_position()` | Push/pop cursor position |
448
+ | `hide_cursor()` / `show_cursor()` | Toggle cursor visibility |
449
+ | `enter_alt_screen()` / `leave_alt_screen()` | Toggle the alternate screen buffer |
450
+ | `enable_mouse_tracking()` / `disable_mouse_tracking()` | Toggle mouse event reporting |
451
+ | `print_at(row, col, text, clear_to_end=False)` | Print (possibly multi-line) text at a position, returns a `Rect` |
452
+ | `restore_terminal()` | Undo cursor/mouse/alt-screen/raw-mode state in one call |
453
+
454
+ ### Output buffering — `_buffer` (re-exported)
455
+
456
+ | Name | Description |
457
+ |---|---|
458
+ | `write(*parts, sep="", flush=True)` | Like `print()`, but no newline and writes go through the frame buffer |
459
+ | `frame(sync=False)` | Context manager: batch writes into a single flush |
460
+
461
+ ## Requirements
462
+
463
+ - Python 3.8+
464
+ - No third-party dependencies
465
+
466
+ ## Examples
467
+
468
+ ### Create a Colorful Banner
469
+
470
+ ```python
471
+ from codehs_utils import banner, clear_screen, GradientText
472
+
473
+ clear_screen()
474
+ print(banner("WELCOME", color="black", background="lightgreen"))
475
+ print()
476
+ print(GradientText.rainbow("- Terminal Toolkit Demo -"))
477
+ print(GradientText.rainbow("=" * 50))
478
+ ```
479
+
480
+ ### Simple Paint Program
481
+
482
+ ```python
483
+ from codehs_utils import app, events, set_pixel
484
+
485
+ with app(mouse=True):
486
+ for event in events(timeout=None):
487
+ if event is None or (event.kind == "key" and event.key == "ESC"):
488
+ break
489
+ if event.kind == "mouse" and event.button == "left":
490
+ set_pixel(event.x, event.y, "red")
491
+ ```
492
+
493
+ ### Bordered Dialog Box
494
+
495
+ ```python
496
+ from codehs_utils import Rect, print_at
497
+
498
+ box = Rect(row=3, col=5, width=30, height=6)
499
+ box.fill(color="black")
500
+ box.draw_border(color="white", style="rounded")
501
+ print_at(box.row + 2, box.col + 2, "Press any key to continue...")
502
+ ```
503
+
504
+ ## License
505
+
506
+ MIT License - see LICENSE file for details.
507
+
508
+ ## Contributing
509
+
510
+ Contributions are welcome! Please feel free to submit a Pull Request.