cmdgui 0.1.0__py3-none-any.whl

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,263 @@
1
+ Metadata-Version: 2.4
2
+ Name: cmdgui
3
+ Version: 0.1.0
4
+ Summary: Simple terminal GUIs: draw your layout as text, the view runs itself
5
+ Author: olliez-mods
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://github.com/olliez-mods/cmdgui
8
+ Keywords: terminal,tui,gui,cli,widgets
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Environment :: Console
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+
16
+ # cmdgui
17
+
18
+ Simple terminal GUIs in Python. Draw your layout as text, fill in the widgets,
19
+ and keep writing your program — the view runs on its own thread, so there's no
20
+ event loop to hand control to and no draw calls to make.
21
+
22
+ ```
23
+ ┌─ name ───────────────────┐
24
+ │quinn │[ Greet ] ON fast
25
+ ├─ fruit ──────────────────┼─ scores ──────────────────────────────────┐
26
+ │ apple │name score city │
27
+ │ banana │Ada 98 London │
28
+ │ cherry │Linus 87 Helsinki │
29
+ │ ├─ log ─────────────────────────────────────┤
30
+ │ │Hello, quinn! 👋 │
31
+ │ │picked cherry │
32
+ └──────────────────────────┴───────────────────────────────────────────┘
33
+ ████████████████████████████████░░░░░░░░░░░░░░░░ 67%[ ] done
34
+ ```
35
+
36
+ No dependencies. macOS and Linux; Windows support is written but untested.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install cmdgui
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ from cmdgui import View, Button, Stdout
48
+ import time
49
+
50
+ class App(View):
51
+ layout = """
52
+ start pause .
53
+ log - -
54
+ """
55
+ start = Button("Start", on_click=lambda: print("started"))
56
+ pause = Button("Pause", on_click=lambda: print("paused"))
57
+ log = Stdout()
58
+
59
+ view = App()
60
+
61
+ for i in range(100):
62
+ print(f"tick {i}") # shows up in the log box
63
+ time.sleep(0.5)
64
+ ```
65
+
66
+ Press `q` to quit (or Ctrl+C). Because the widgets are class attributes, your
67
+ editor knows `view.start` is a `Button` — autocomplete and type checking work.
68
+
69
+ ### Three ways to build a view
70
+
71
+ They can be mixed freely:
72
+
73
+ ```python
74
+ from cmdgui import View, Label
75
+
76
+ # 1. A subclass: everything in one place, fully typed in your editor
77
+ class App(View):
78
+ layout = "heading \n stdout"
79
+ heading = Label("Hello", align="center")
80
+ view = App()
81
+
82
+ # 2. Widgets passed in: quick one-off views
83
+ view = View("heading \n stdout", heading=Label("Hello", align="center"))
84
+
85
+ # 3. Types in the layout, configured afterwards
86
+ view = View("label[heading] \n stdout")
87
+ view.heading.set(text="Hello", align="center")
88
+ ```
89
+
90
+ A keyword argument overrides a subclass's widget of the same name. Each view made
91
+ from a subclass gets its own copies of the widgets. For typed access to widgets
92
+ declared in the layout string, use `view.get("heading", Label)`.
93
+
94
+ ## Layouts
95
+
96
+ Each word is a grid cell. Write `type[name]` to create a widget:
97
+
98
+ | Token | Meaning |
99
+ |---|---|
100
+ | `button[start]` | a `button` widget named `start` |
101
+ | `heading` | the widget you passed in (or declared on the class) as `heading` |
102
+ | `stdout` | otherwise: a widget whose name is its type |
103
+ | `start` | the existing widget `start` again — it spans into this cell |
104
+ | `-` | same as the cell to the left |
105
+ | `\|` | same as the cell above |
106
+ | `.` | empty cell |
107
+ | `{b}` / `{nb}` | on the first cell of a widget: force a border on / off |
108
+
109
+ Every widget has to cover a rectangle. Get widgets with `view.start` or `view["start"]`.
110
+
111
+ **Sizes** come from the widgets: a button wants to be 1 line tall, so its row is
112
+ 1 line tall; everything else shares the remaining space. If the terminal is too
113
+ small for the layout's minimum size, the view shows a message until it's resized.
114
+
115
+ **Borders** are drawn by the view, and neighbours share a single line with the
116
+ right junctions (`├ ┬ ┼`). The border title is the widget's name, or its `title`.
117
+ The focused widget's border is highlighted.
118
+
119
+ ## Widgets
120
+
121
+ | Type | Class | What it does |
122
+ |---|---|---|
123
+ | `text` | `Text` | Wrapped text. `text`, `align` (`left`/`center`/`right`) |
124
+ | `label` | `Label` | One line of text |
125
+ | `button` | `Button` | `text`, `on_click(fn)`. Click, or Enter/Space when focused |
126
+ | `text_input` | `TextInput` | `value`, `placeholder`, `on_submit(fn)`, `on_change(fn)` |
127
+ | `progress_bar` | `ProgressBar` | `value` from 0 to 1 |
128
+ | `checkbox` | `Checkbox` | `text`, `checked`, `on_change(fn)` |
129
+ | `toggle` | `Toggle` | An on/off switch, same API as `Checkbox` |
130
+ | `menu` | `Menu` | `items`, `selected`, `on_select(fn(index, item))` |
131
+ | `table` | `Table` | `columns`, `rows`. Scroll with the mouse wheel |
132
+ | `stdout` | `Stdout` | Everything printed, stderr in red. Scroll with the mouse wheel. `clear()` |
133
+
134
+ Every widget also takes `border`, `title`, `preferred_width` and `preferred_height`.
135
+ The first positional argument is the main content: `Label("text")`, `Menu(items)`,
136
+ `Table(columns)`, `ProgressBar(0.5)`. Callbacks can be passed in (`on_click=`) or set
137
+ later (`button.on_click(fn)`).
138
+
139
+ Setting an attribute redraws the widget: `view.bar.value = 0.5` just works, and
140
+ `widget.set(text=..., align=...)` changes several at once. If you change a list in
141
+ place (`view.menu.items.append(...)`), call `widget.refresh()`.
142
+
143
+ ## Popups
144
+
145
+ A popup floats over the layout. It has its own layout and widgets, just like a view,
146
+ and sizes itself to fit them:
147
+
148
+ ```python
149
+ from cmdgui import View, Popup, Label, Button, Stdout
150
+
151
+ class Confirm(Popup):
152
+ layout = """
153
+ message -
154
+ yes no
155
+ """
156
+ title = "Clear the log?"
157
+ message = Label("This can't be undone.")
158
+ yes = Button("Clear")
159
+ no = Button("Cancel")
160
+
161
+ def init(self): # wire up the popup's own widgets
162
+ self.no.on_click(self.close)
163
+
164
+ class App(View):
165
+ layout = "clear \n log"
166
+ clear = Button("Clear log", on_click=lambda: view.show(view.confirm))
167
+ log = Stdout()
168
+ confirm = Confirm() # popups can live on the view too
169
+
170
+ view = App()
171
+ view.confirm.yes.on_click(lambda: (view.log.clear(), view.confirm.close()))
172
+ ```
173
+
174
+ - `view.show(popup)` opens it in the middle, or use `below=widget`, `above=widget` or
175
+ `at=(x, y)`. It flips to the other side if there's no room.
176
+ - `popup.close()`, `popup.is_open`, `popup.on_close(fn)`.
177
+ - `view.alert("Saved!", title="Done")` shows a message with an OK button.
178
+ - A popup with one widget doesn't need a layout: `Popup(Menu(items))`, or a subclass
179
+ with a single widget.
180
+
181
+ Settings, as class attributes or constructor arguments:
182
+
183
+ | Setting | Default | |
184
+ |---|---|---|
185
+ | `modal` | `True` | blocks clicks and focus for everything underneath |
186
+ | `close_on_escape` | `True` | |
187
+ | `close_on_outside_click` | `False` | for a modal popup the click just closes it; otherwise it goes through too |
188
+ | `keep_typing` | `False` | the focused text box keeps getting typed text, while arrows and Enter go to the popup (autocomplete, command menus) |
189
+ | `border`, `title` | `True`, `None` | |
190
+ | `width`, `height` | `None` | outer size; `None` fits the content |
191
+
192
+ Inside a popup, a widget's border title is only shown if you set its `title`.
193
+ `examples/simple_console.py` has a `/` command menu and `examples/popups.py` has a dialog and a dropdown.
194
+
195
+ ## Keys and focus
196
+
197
+ - **Tab** / **Shift+Tab** or a click moves focus between widgets that take input.
198
+ - Key presses go to the focused widget; typed characters go to a focused text box first.
199
+ - `view.on_key("ctrl+s", save)` binds a key. Key names: letters, `enter`, `escape`,
200
+ `tab`, `backspace`, `delete`, `up`/`down`/`left`/`right`, `home`/`end`,
201
+ `page_up`/`page_down`, `ctrl+a`, `alt+x`, `ctrl+up`, ...
202
+ - `View(layout, quit_key="q")` sets the quit key; `quit_key=None` turns it off.
203
+
204
+ ## Ending the program
205
+
206
+ ```python
207
+ with View("...") as view:
208
+ ...
209
+ view.wait() # blocks until q, view.quit() or Ctrl+C
210
+ ```
211
+
212
+ Without `wait()`, the quit key ends your program as if it had finished.
213
+ If anything raises — in your code or inside a callback — the terminal is put back
214
+ to normal first, so the traceback is visible.
215
+
216
+ ## Your own widgets
217
+
218
+ ```python
219
+ from cmdgui import Widget, View, field
220
+
221
+ class Clock(Widget):
222
+ time: str = field(default="", kw_only=False) # fields become constructor arguments
223
+ show_seconds: bool = True
224
+ history: list = field(default_factory=list) # a fresh list for each clock
225
+
226
+ preferred_height = 1 # 5, "5+" (at least), "5-10" (between), or None
227
+ border = True
228
+
229
+ def init(self): # other setup (not constructor arguments)
230
+ self.ticks = 0
231
+
232
+ def draw(self, c): # c is a Canvas exactly the widget's size
233
+ c.text(0, 0, self.time)
234
+
235
+ def on_input(self, input): # mouse, stdout, and keys (while focused)
236
+ pass
237
+
238
+ view = View("clock") # registered automatically as "clock"
239
+ view = View("now", now=Clock("12:00"))
240
+ ```
241
+
242
+ Annotated attributes are fields: keyword arguments by default, positional with
243
+ `field(kw_only=False)`. Editors autocomplete and type-check them like a dataclass.
244
+
245
+ Override `content_size()` to return the `(width, height)` your content wants, so
246
+ popups can size themselves around it (`None` for either means "don't care").
247
+
248
+ `Canvas` has `put`, `text`, `fill` and `border`; `self.mouse_pos()` and
249
+ `self.mouse_over()` give the mouse relative to the widget; `self.theme("key")`
250
+ gets a style from the theme.
251
+
252
+ ## Themes
253
+
254
+ ```python
255
+ from cmdgui import View, style
256
+
257
+ view = View("...", theme={
258
+ "border_focus": style(fg="magenta", bold=True),
259
+ "selected": style(fg="black", bg="yellow"),
260
+ })
261
+ ```
262
+
263
+ See `DEFAULT_THEME` in `cmdgui.widgets` for every key.
@@ -0,0 +1,11 @@
1
+ cmdgui/__init__.py,sha256=nZ1vGolNskOAVf7e1Q8dlJ0k6oPhjkBoMMBrjLs_8pY,536
2
+ cmdgui/_platform.py,sha256=7rEI5HW2ys2NapUxZT_LuYvXWn19QpA7VXftKBfxeZw,5320
3
+ cmdgui/inputs.py,sha256=YDms1p_Z1sU2A7lUlrIicA8A2YvStmB8iqE3LfNEWyU,7443
4
+ cmdgui/layout.py,sha256=FGzCWMbaOsjoBoBDzDsitlA03tM8ISs6uOh24IklwAE,10010
5
+ cmdgui/shorts.py,sha256=cOei9vUc1mlStnY1bosEENOjd-kz860jDEZbJ4szOiE,11599
6
+ cmdgui/view.py,sha256=HQeoilSoh1NQrQna-Em9gGOkWM30LGjcEDIw1RvYMWk,30698
7
+ cmdgui/widgets.py,sha256=e86L0UyV0GVg5prlTr9RgACUc6EDkfV1JCmsjAVcV6I,24994
8
+ cmdgui-0.1.0.dist-info/METADATA,sha256=YzkMah80UEr-vxzADcxpyerzL0u0VbNn30SPUcDCAag,10207
9
+ cmdgui-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ cmdgui-0.1.0.dist-info/top_level.txt,sha256=GpcATSXO7fsU2P79H65jF7LP5Jc_NzqqPSecIo6hgtk,7
11
+ cmdgui-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ cmdgui