cmdgui 0.1.1__tar.gz → 0.2.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.
cmdgui-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: cmdgui
3
+ Version: 0.2.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
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # cmdgui
19
+
20
+ Simple terminal GUIs in Python. Draw your layout as text, fill in the widgets,
21
+ and keep writing your program — the view runs on its own thread, so there's no
22
+ event loop to hand control to and no draw calls to make.
23
+
24
+ ```
25
+ ┌─ name ───────────────────┐
26
+ │quinn │[ Greet ] ON fast
27
+ ├─ fruit ──────────────────┼─ scores ──────────────────────────────────┐
28
+ │ apple │name score city │
29
+ │ banana │Ada 98 London │
30
+ │ cherry │Linus 87 Helsinki │
31
+ │ ├─ log ─────────────────────────────────────┤
32
+ │ │Hello, quinn! 👋 │
33
+ │ │picked cherry │
34
+ └──────────────────────────┴───────────────────────────────────────────┘
35
+ ████████████████████████████████░░░░░░░░░░░░░░░░ 67%[ ] done
36
+ ```
37
+
38
+ No dependencies. macOS and Linux; Windows support is written but untested.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install cmdgui
44
+ ```
45
+
46
+ ## Quick start
47
+
48
+ ```python
49
+ from cmdgui import View, Button, Stdout
50
+ import time
51
+
52
+ class App(View):
53
+ layout = """
54
+ start pause .
55
+ log - -
56
+ """
57
+ start = Button("Start", on_click=lambda: print("started"))
58
+ pause = Button("Pause", on_click=lambda: print("paused"))
59
+ log = Stdout()
60
+
61
+ view = App()
62
+
63
+ for i in range(100):
64
+ print(f"tick {i}") # shows up in the log box
65
+ time.sleep(0.5)
66
+ ```
67
+
68
+ Press `q` to quit (or Ctrl+C). Because the widgets are class attributes, your
69
+ editor knows `view.start` is a `Button` — autocomplete and type checking work.
70
+
71
+ ## What's in it
72
+
73
+ - **Layouts drawn as text**: each word is a grid cell, `-` and `|` stretch a widget
74
+ across cells, and borders between neighbours join up.
75
+ - **Widgets**: text, labels, buttons, one-line and multi-line text boxes (with password
76
+ mode and history), checkboxes, toggles, radio buttons, dropdowns, sliders, progress
77
+ bars, menus, folding trees, tables, tabs, and a box that shows everything you print.
78
+ - **Popups**: dialogs, dropdowns and command menus that float over the layout.
79
+ - **Mouse and keyboard**: clicks, dragging, the scroll wheel, Tab between widgets, and
80
+ your own key bindings.
81
+ - **Timers**: `view.every(1, tick)` and `view.after(5, fn)`, no threads needed.
82
+ - **Themes**: restyle any part, with 73 named colors, the 256-color palette, or exact
83
+ hex colors.
84
+ - **Your own widgets**: subclass `Widget`, draw into a canvas, and it works in layouts.
85
+
86
+ ## Documentation
87
+
88
+ - [Overview](https://github.com/olliez-mods/cmdgui/blob/main/docs/index.md): ways to build a view, changing widgets, timers, running and quitting
89
+ - [Layouts](https://github.com/olliez-mods/cmdgui/blob/main/docs/layouts.md)
90
+ - [Widgets](https://github.com/olliez-mods/cmdgui/blob/main/docs/widgets.md)
91
+ - [Popups](https://github.com/olliez-mods/cmdgui/blob/main/docs/popups.md)
92
+ - [Keys and focus](https://github.com/olliez-mods/cmdgui/blob/main/docs/keys-and-focus.md)
93
+ - [Themes and colors](https://github.com/olliez-mods/cmdgui/blob/main/docs/themes.md)
94
+ - [Your own widgets](https://github.com/olliez-mods/cmdgui/blob/main/docs/custom-widgets.md)
95
+
96
+ The [examples](https://github.com/olliez-mods/cmdgui/tree/main/examples) folder has a
97
+ widget demo, a dashboard, popups, tabs, a console and a file browser.
cmdgui-0.2.0/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # cmdgui
2
+
3
+ Simple terminal GUIs in Python. Draw your layout as text, fill in the widgets,
4
+ and keep writing your program — the view runs on its own thread, so there's no
5
+ event loop to hand control to and no draw calls to make.
6
+
7
+ ```
8
+ ┌─ name ───────────────────┐
9
+ │quinn │[ Greet ] ON fast
10
+ ├─ fruit ──────────────────┼─ scores ──────────────────────────────────┐
11
+ │ apple │name score city │
12
+ │ banana │Ada 98 London │
13
+ │ cherry │Linus 87 Helsinki │
14
+ │ ├─ log ─────────────────────────────────────┤
15
+ │ │Hello, quinn! 👋 │
16
+ │ │picked cherry │
17
+ └──────────────────────────┴───────────────────────────────────────────┘
18
+ ████████████████████████████████░░░░░░░░░░░░░░░░ 67%[ ] done
19
+ ```
20
+
21
+ No dependencies. macOS and Linux; Windows support is written but untested.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install cmdgui
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```python
32
+ from cmdgui import View, Button, Stdout
33
+ import time
34
+
35
+ class App(View):
36
+ layout = """
37
+ start pause .
38
+ log - -
39
+ """
40
+ start = Button("Start", on_click=lambda: print("started"))
41
+ pause = Button("Pause", on_click=lambda: print("paused"))
42
+ log = Stdout()
43
+
44
+ view = App()
45
+
46
+ for i in range(100):
47
+ print(f"tick {i}") # shows up in the log box
48
+ time.sleep(0.5)
49
+ ```
50
+
51
+ Press `q` to quit (or Ctrl+C). Because the widgets are class attributes, your
52
+ editor knows `view.start` is a `Button` — autocomplete and type checking work.
53
+
54
+ ## What's in it
55
+
56
+ - **Layouts drawn as text**: each word is a grid cell, `-` and `|` stretch a widget
57
+ across cells, and borders between neighbours join up.
58
+ - **Widgets**: text, labels, buttons, one-line and multi-line text boxes (with password
59
+ mode and history), checkboxes, toggles, radio buttons, dropdowns, sliders, progress
60
+ bars, menus, folding trees, tables, tabs, and a box that shows everything you print.
61
+ - **Popups**: dialogs, dropdowns and command menus that float over the layout.
62
+ - **Mouse and keyboard**: clicks, dragging, the scroll wheel, Tab between widgets, and
63
+ your own key bindings.
64
+ - **Timers**: `view.every(1, tick)` and `view.after(5, fn)`, no threads needed.
65
+ - **Themes**: restyle any part, with 73 named colors, the 256-color palette, or exact
66
+ hex colors.
67
+ - **Your own widgets**: subclass `Widget`, draw into a canvas, and it works in layouts.
68
+
69
+ ## Documentation
70
+
71
+ - [Overview](https://github.com/olliez-mods/cmdgui/blob/main/docs/index.md): ways to build a view, changing widgets, timers, running and quitting
72
+ - [Layouts](https://github.com/olliez-mods/cmdgui/blob/main/docs/layouts.md)
73
+ - [Widgets](https://github.com/olliez-mods/cmdgui/blob/main/docs/widgets.md)
74
+ - [Popups](https://github.com/olliez-mods/cmdgui/blob/main/docs/popups.md)
75
+ - [Keys and focus](https://github.com/olliez-mods/cmdgui/blob/main/docs/keys-and-focus.md)
76
+ - [Themes and colors](https://github.com/olliez-mods/cmdgui/blob/main/docs/themes.md)
77
+ - [Your own widgets](https://github.com/olliez-mods/cmdgui/blob/main/docs/custom-widgets.md)
78
+
79
+ The [examples](https://github.com/olliez-mods/cmdgui/tree/main/examples) folder has a
80
+ widget demo, a dashboard, popups, tabs, a console and a file browser.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cmdgui"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "Simple terminal GUIs: draw your layout as text, the view runs itself"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,14 @@
1
+ from .view import View, Popup, Panel, Timer
2
+ from .widgets import (
3
+ Widget, Text, Label, Button, TextInput, TextArea, ProgressBar, Slider, Checkbox, Toggle,
4
+ RadioGroup, Select, Menu, Tree, Table, Tabs, Stdout, DrawMouse, DEFAULT_THEME, field,
5
+ )
6
+ from .inputs import Input, mouse
7
+ from .layout import LayoutError
8
+ from .shorts import Canvas, style, styled, Color, ColorName
9
+
10
+ __all__ = [
11
+ "View", "Popup", "Panel", "Timer", "Widget", "Text", "Label", "Button", "TextInput", "TextArea", "ProgressBar",
12
+ "Slider", "Checkbox", "Toggle", "RadioGroup", "Select", "Menu", "Tree", "Table", "Tabs", "Stdout", "DrawMouse", "DEFAULT_THEME", "field",
13
+ "Input", "mouse", "LayoutError", "Canvas", "style", "styled", "Color", "ColorName",
14
+ ]
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import sys
3
3
  import unicodedata
4
+ from typing import Literal, Optional, Tuple, Union, get_args
4
5
 
5
6
  ESC = "\x1b["
6
7
  RESET = ESC + "0m"
@@ -26,11 +27,58 @@ LINE_CHARS = {
26
27
  UP | DOWN | LEFT | RIGHT: "┼",
27
28
  }
28
29
 
30
+ # The basic colors (and 'bright_' versions) use the terminal's own palette, so
31
+ # they match the user's theme
29
32
  COLORS = {
30
33
  "black": 0, "red": 1, "green": 2, "yellow": 3,
31
34
  "blue": 4, "magenta": 5, "cyan": 6, "white": 7,
32
35
  }
33
36
 
37
+ # More colors, as numbers in the 256-color palette (supported by nearly every terminal).
38
+ # "grey" works wherever "gray" does.
39
+ EXTRA_COLORS = {
40
+ # reds and pinks
41
+ "dark_red": 88, "maroon": 52, "crimson": 161, "scarlet": 196, "coral": 203,
42
+ "salmon": 209, "rose": 211, "pink": 218, "hot_pink": 205, "deep_pink": 198,
43
+ # oranges, yellows and browns
44
+ "orange": 208, "dark_orange": 166, "amber": 214, "gold": 220, "lemon": 227,
45
+ "cream": 229, "peach": 216, "tan": 180, "khaki": 186, "brown": 94,
46
+ "rust": 130, "copper": 173,
47
+ # greens
48
+ "lime": 118, "chartreuse": 112, "olive": 100, "dark_green": 28, "forest": 22,
49
+ "emerald": 35, "sea_green": 72, "mint": 121, "pale_green": 157,
50
+ # blues and cyans
51
+ "teal": 30, "turquoise": 44, "aqua": 51, "sky": 117, "light_blue": 153,
52
+ "steel_blue": 67, "cornflower": 69, "royal_blue": 63, "dodger_blue": 33,
53
+ "dark_blue": 19, "navy": 17, "slate": 60,
54
+ # purples
55
+ "indigo": 54, "purple": 93, "dark_purple": 53, "violet": 177, "lavender": 183,
56
+ "plum": 176, "orchid": 170, "fuchsia": 201,
57
+ # grays
58
+ "charcoal": 236, "dark_gray": 238, "gray": 244, "silver": 249,
59
+ "light_gray": 252, "snow": 255,
60
+ }
61
+
62
+ # Every color name, so editors can autocomplete style(fg="...") and catch typos.
63
+ # Keep in step with COLORS and EXTRA_COLORS (checked below).
64
+ ColorName = Literal[
65
+ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "bright_black",
66
+ "bright_red", "bright_green", "bright_yellow", "bright_blue", "bright_magenta",
67
+ "bright_cyan", "bright_white",
68
+ "dark_red", "maroon", "crimson", "scarlet", "coral", "salmon", "rose", "pink",
69
+ "hot_pink", "deep_pink", "orange", "dark_orange", "amber", "gold", "lemon", "cream",
70
+ "peach", "tan", "khaki", "brown", "rust", "copper", "lime", "chartreuse", "olive",
71
+ "dark_green", "forest", "emerald", "sea_green", "mint", "pale_green", "teal",
72
+ "turquoise", "aqua", "sky", "light_blue", "steel_blue", "cornflower", "royal_blue",
73
+ "dodger_blue", "dark_blue", "navy", "slate", "indigo", "purple", "dark_purple",
74
+ "violet", "lavender", "plum", "orchid", "fuchsia", "charcoal", "dark_gray", "gray",
75
+ "silver", "light_gray", "snow",
76
+ ]
77
+ # A color: a name, a 256-color palette number, "#rrggbb", or (r, g, b)
78
+ Color = Union[ColorName, str, int, Tuple[int, int, int]]
79
+ assert set(get_args(ColorName)) == set(COLORS) | {"bright_" + c for c in COLORS} | set(EXTRA_COLORS), \
80
+ "ColorName is out of date with COLORS / EXTRA_COLORS"
81
+
34
82
  # --- Screen / cursor ------------------------------------------------------
35
83
 
36
84
  def screen_size():
@@ -53,16 +101,21 @@ def write(s):
53
101
 
54
102
  # --- Styling ----------------------------------------------------------------
55
103
 
56
- def style(fg=None, bg=None, bold=False, dim=False, italic=False, underline=False, reverse=False):
57
- """Escape code for a style. Colors are names from COLORS, prefixed 'bright_' for bright."""
104
+ def style(fg: Optional[Color] = None, bg: Optional[Color] = None, bold: bool = False, dim: bool = False,
105
+ italic: bool = False, underline: bool = False, reverse: bool = False) -> str:
106
+ """Escape code for a style. A color can be:
107
+ a name from COLORS, or 'bright_' + one of those: "red", "bright_red"
108
+ a name from EXTRA_COLORS: "orange", "teal", "lavender"
109
+ a number in the 256-color palette: 208
110
+ a hex string or (r, g, b) for exact colors, if the terminal has true color: "#ff8800" """
58
111
  codes = []
59
112
  if bold: codes.append("1")
60
113
  if dim: codes.append("2")
61
114
  if italic: codes.append("3")
62
115
  if underline: codes.append("4")
63
116
  if reverse: codes.append("7")
64
- if fg: codes.append(str(_color_code(fg, 30)))
65
- if bg: codes.append(str(_color_code(bg, 40)))
117
+ if fg is not None: codes.append(_color_code(fg, 30))
118
+ if bg is not None: codes.append(_color_code(bg, 40))
66
119
  return f"{ESC}{';'.join(codes)}m" if codes else ""
67
120
 
68
121
  def styled(text, **kwargs):
@@ -70,9 +123,28 @@ def styled(text, **kwargs):
70
123
  s = style(**kwargs)
71
124
  return f"{s}{text}{RESET}" if s else text
72
125
 
73
- def _color_code(name, base):
74
- if name.startswith("bright_"): return COLORS[name[7:]] + base + 60
75
- return COLORS[name] + base
126
+ def _color_code(color, base):
127
+ """The SGR code for a color, base is 30 for the foreground or 40 for the background."""
128
+ extended = base + 8 # 38 / 48 start a 256-color or true color code
129
+ if isinstance(color, int) and not isinstance(color, bool) and 0 <= color <= 255:
130
+ return f"{extended};5;{color}"
131
+ if isinstance(color, (tuple, list)) and len(color) == 3:
132
+ return f"{extended};2;" + ";".join(str(max(0, min(255, int(v)))) for v in color)
133
+ if isinstance(color, str):
134
+ name = color.lower().replace("grey", "gray").replace(" ", "_")
135
+ if name.startswith("#") and len(name) == 7:
136
+ try:
137
+ return f"{extended};2;{int(name[1:3], 16)};{int(name[3:5], 16)};{int(name[5:7], 16)}"
138
+ except ValueError:
139
+ pass
140
+ elif name in COLORS:
141
+ return str(COLORS[name] + base)
142
+ elif name.startswith("bright_") and name[7:] in COLORS:
143
+ return str(COLORS[name[7:]] + base + 60)
144
+ elif name in EXTRA_COLORS:
145
+ return f"{extended};5;{EXTRA_COLORS[name]}"
146
+ raise ValueError(f"unknown color {color!r}: use a name from COLORS or EXTRA_COLORS "
147
+ f"(with 'bright_' for the basic ones), 0-255, '#rrggbb' or (r, g, b)")
76
148
 
77
149
  # --- Text width -------------------------------------------------------------
78
150
  # Emoji and CJK characters take two columns, combining accents take none.
@@ -190,6 +262,10 @@ class Canvas:
190
262
  x += self.put(x, y, char, style)
191
263
  return x
192
264
 
265
+ def restyle(self, style=""):
266
+ """Give every cell the same style, keeping the characters."""
267
+ self.styles = [[style] * self.width for _ in range(self.height)]
268
+
193
269
  def fill(self, x=0, y=0, w=None, h=None, char=" ", style=""):
194
270
  """Fill a rectangle (the whole canvas by default)."""
195
271
  w = self.width if w is None else w