cmdgui 0.1.1__tar.gz → 0.3.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.
Files changed (37) hide show
  1. cmdgui-0.3.0/PKG-INFO +106 -0
  2. cmdgui-0.3.0/README.md +89 -0
  3. {cmdgui-0.1.1 → cmdgui-0.3.0}/pyproject.toml +1 -1
  4. cmdgui-0.3.0/src/cmdgui/__init__.py +14 -0
  5. {cmdgui-0.1.1 → cmdgui-0.3.0}/src/cmdgui/_platform.py +5 -2
  6. cmdgui-0.3.0/src/cmdgui/clipboard.py +53 -0
  7. {cmdgui-0.1.1 → cmdgui-0.3.0}/src/cmdgui/inputs.py +32 -3
  8. cmdgui-0.3.0/src/cmdgui/layout.py +444 -0
  9. cmdgui-0.3.0/src/cmdgui/raster.py +429 -0
  10. cmdgui-0.3.0/src/cmdgui/shorts.py +539 -0
  11. cmdgui-0.3.0/src/cmdgui/view.py +1348 -0
  12. cmdgui-0.3.0/src/cmdgui/widgets/__init__.py +14 -0
  13. cmdgui-0.3.0/src/cmdgui/widgets/base.py +353 -0
  14. cmdgui-0.3.0/src/cmdgui/widgets/container.py +83 -0
  15. cmdgui-0.3.0/src/cmdgui/widgets/controls.py +294 -0
  16. cmdgui-0.3.0/src/cmdgui/widgets/dates.py +231 -0
  17. cmdgui-0.3.0/src/cmdgui/widgets/graphics.py +282 -0
  18. cmdgui-0.3.0/src/cmdgui/widgets/lists.py +391 -0
  19. cmdgui-0.3.0/src/cmdgui/widgets/log.py +204 -0
  20. cmdgui-0.3.0/src/cmdgui/widgets/stdout.py +87 -0
  21. cmdgui-0.3.0/src/cmdgui/widgets/tabs.py +213 -0
  22. cmdgui-0.3.0/src/cmdgui/widgets/text.py +443 -0
  23. cmdgui-0.3.0/src/cmdgui.egg-info/PKG-INFO +106 -0
  24. cmdgui-0.3.0/src/cmdgui.egg-info/SOURCES.txt +26 -0
  25. cmdgui-0.1.1/PKG-INFO +0 -265
  26. cmdgui-0.1.1/README.md +0 -248
  27. cmdgui-0.1.1/src/cmdgui/__init__.py +0 -14
  28. cmdgui-0.1.1/src/cmdgui/layout.py +0 -249
  29. cmdgui-0.1.1/src/cmdgui/shorts.py +0 -307
  30. cmdgui-0.1.1/src/cmdgui/view.py +0 -713
  31. cmdgui-0.1.1/src/cmdgui/widgets.py +0 -577
  32. cmdgui-0.1.1/src/cmdgui.egg-info/PKG-INFO +0 -265
  33. cmdgui-0.1.1/src/cmdgui.egg-info/SOURCES.txt +0 -14
  34. {cmdgui-0.1.1 → cmdgui-0.3.0}/LICENSE +0 -0
  35. {cmdgui-0.1.1 → cmdgui-0.3.0}/setup.cfg +0 -0
  36. {cmdgui-0.1.1 → cmdgui-0.3.0}/src/cmdgui.egg-info/dependency_links.txt +0 -0
  37. {cmdgui-0.1.1 → cmdgui-0.3.0}/src/cmdgui.egg-info/top_level.txt +0 -0
cmdgui-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: cmdgui
3
+ Version: 0.3.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, a calendar and date picker, tabs, split panes to
78
+ drag, a log viewer with levels and search, a box that shows everything you print, and
79
+ a pixel canvas with lines, shapes and curves.
80
+ - **Styled text**: `"[bold red]Error:[/] couldn't open [cyan]notes.txt[/]"` in labels,
81
+ buttons and text.
82
+ - **Text boxes that behave**: select with the mouse or Shift, copy, cut and paste,
83
+ pasting in one go, and dim hints before, after and ahead of the cursor for autocomplete.
84
+ - **Inline views**: draw in a few rows under the prompt instead of the whole screen, with
85
+ printed text scrolling above.
86
+ - **Popups**: dialogs, dropdowns and command menus that float over the layout.
87
+ - **Mouse and keyboard**: clicks, dragging, the scroll wheel, Tab between widgets, and
88
+ your own key bindings.
89
+ - **Timers**: `view.every(1, tick)` and `view.after(5, fn)`, no threads needed.
90
+ - **Themes**: restyle any part, with 73 named colors, the 256-color palette, or exact
91
+ hex colors.
92
+ - **Your own widgets**: subclass `Widget`, draw into a canvas, and it works in layouts.
93
+
94
+ ## Documentation
95
+
96
+ - [Overview](https://github.com/olliez-mods/cmdgui/blob/main/docs/index.md): ways to build a view, changing widgets, timers, running and quitting
97
+ - [Layouts](https://github.com/olliez-mods/cmdgui/blob/main/docs/layouts.md)
98
+ - [Widgets](https://github.com/olliez-mods/cmdgui/blob/main/docs/widgets.md)
99
+ - [Popups](https://github.com/olliez-mods/cmdgui/blob/main/docs/popups.md)
100
+ - [Keys and focus](https://github.com/olliez-mods/cmdgui/blob/main/docs/keys-and-focus.md)
101
+ - [Themes and colors](https://github.com/olliez-mods/cmdgui/blob/main/docs/themes.md)
102
+ - [Your own widgets](https://github.com/olliez-mods/cmdgui/blob/main/docs/custom-widgets.md)
103
+
104
+ The [examples](https://github.com/olliez-mods/cmdgui/tree/main/examples) folder has a
105
+ widget demo, a dashboard, popups, tabs, a console, a file browser, pixel graphics, a
106
+ log viewer with split panes, date pickers, and an inline progress display.
cmdgui-0.3.0/README.md ADDED
@@ -0,0 +1,89 @@
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, a calendar and date picker, tabs, split panes to
61
+ drag, a log viewer with levels and search, a box that shows everything you print, and
62
+ a pixel canvas with lines, shapes and curves.
63
+ - **Styled text**: `"[bold red]Error:[/] couldn't open [cyan]notes.txt[/]"` in labels,
64
+ buttons and text.
65
+ - **Text boxes that behave**: select with the mouse or Shift, copy, cut and paste,
66
+ pasting in one go, and dim hints before, after and ahead of the cursor for autocomplete.
67
+ - **Inline views**: draw in a few rows under the prompt instead of the whole screen, with
68
+ printed text scrolling above.
69
+ - **Popups**: dialogs, dropdowns and command menus that float over the layout.
70
+ - **Mouse and keyboard**: clicks, dragging, the scroll wheel, Tab between widgets, and
71
+ your own key bindings.
72
+ - **Timers**: `view.every(1, tick)` and `view.after(5, fn)`, no threads needed.
73
+ - **Themes**: restyle any part, with 73 named colors, the 256-color palette, or exact
74
+ hex colors.
75
+ - **Your own widgets**: subclass `Widget`, draw into a canvas, and it works in layouts.
76
+
77
+ ## Documentation
78
+
79
+ - [Overview](https://github.com/olliez-mods/cmdgui/blob/main/docs/index.md): ways to build a view, changing widgets, timers, running and quitting
80
+ - [Layouts](https://github.com/olliez-mods/cmdgui/blob/main/docs/layouts.md)
81
+ - [Widgets](https://github.com/olliez-mods/cmdgui/blob/main/docs/widgets.md)
82
+ - [Popups](https://github.com/olliez-mods/cmdgui/blob/main/docs/popups.md)
83
+ - [Keys and focus](https://github.com/olliez-mods/cmdgui/blob/main/docs/keys-and-focus.md)
84
+ - [Themes and colors](https://github.com/olliez-mods/cmdgui/blob/main/docs/themes.md)
85
+ - [Your own widgets](https://github.com/olliez-mods/cmdgui/blob/main/docs/custom-widgets.md)
86
+
87
+ The [examples](https://github.com/olliez-mods/cmdgui/tree/main/examples) folder has a
88
+ widget demo, a dashboard, popups, tabs, a console, a file browser, pixel graphics, a
89
+ log viewer with split panes, date pickers, and an inline progress display.
@@ -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.3.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, Edge, Timer
2
+ from .widgets import (
3
+ Widget, Text, Label, Button, TextInput, TextArea, ProgressBar, Slider, Checkbox, Toggle,
4
+ RadioGroup, Select, Menu, Tree, Table, Tabs, Graphics, Calendar, DatePicker, Log, 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", "Edge", "Timer", "Widget", "Text", "Label", "Button", "TextInput", "TextArea", "ProgressBar",
12
+ "Slider", "Checkbox", "Toggle", "RadioGroup", "Select", "Menu", "Tree", "Table", "Tabs", "Graphics", "Calendar", "DatePicker", "Log", "Stdout", "DrawMouse", "DEFAULT_THEME", "field",
13
+ "Input", "mouse", "LayoutError", "Canvas", "style", "styled", "Color", "ColorName",
14
+ ]
@@ -22,8 +22,11 @@ if not WINDOWS:
22
22
  fd = sys.stdin.fileno()
23
23
  self._saved = termios.tcgetattr(fd)
24
24
  attrs = termios.tcgetattr(fd)
25
- # No line buffering, no echo. ISIG stays on so Ctrl+C still raises KeyboardInterrupt.
26
- attrs[3] &= ~(termios.ICANON | termios.ECHO)
25
+ # No line buffering or echo. Ctrl+C, Ctrl+V, Ctrl+S and so on come through as keys
26
+ # (ISIG, IEXTEN, IXON off): the view turns Ctrl+C back into KeyboardInterrupt when
27
+ # it isn't copying, and Ctrl+S no longer freezes the output
28
+ attrs[3] &= ~(termios.ICANON | termios.ECHO | termios.ISIG | termios.IEXTEN)
29
+ attrs[0] &= ~termios.IXON
27
30
  termios.tcsetattr(fd, termios.TCSANOW, attrs)
28
31
 
29
32
  def disable(self):
@@ -0,0 +1,53 @@
1
+ """Copying to the system clipboard. A terminal app can't reach the clipboard
2
+ directly, so copy() tries both ways there are:
3
+
4
+ - OSC 52, an escape code asking the terminal to copy. Works over SSH. Most modern
5
+ terminals support it (kitty, WezTerm, Ghostty, Alacritty, Windows Terminal, foot,
6
+ iTerm2 once allowed in its settings); macOS's Terminal.app doesn't.
7
+ - the system's own tool (pbcopy, wl-copy, xclip, xsel, clip), when running locally.
8
+
9
+ Pasting from the system clipboard is done by the terminal (Cmd+V, or Ctrl+Shift+V),
10
+ which sends the text to the app as a paste. paste() gives what was last copied in
11
+ this program, for Ctrl+V."""
12
+ from __future__ import annotations
13
+
14
+ import base64
15
+ import os
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ from .shorts import write
20
+
21
+ _last = "" # the last text copied, for paste()
22
+
23
+ # Clipboard tools, most likely first: (command, needs)
24
+ TOOLS = [
25
+ (["pbcopy"], "darwin"),
26
+ (["wl-copy"], "WAYLAND_DISPLAY"),
27
+ (["xclip", "-selection", "clipboard"], "DISPLAY"),
28
+ (["xsel", "--clipboard", "--input"], "DISPLAY"),
29
+ (["clip"], "win32"),
30
+ ]
31
+
32
+
33
+ def copy(text: str) -> None:
34
+ """Put text on the system clipboard (as far as the terminal allows), and keep it
35
+ for paste()."""
36
+ global _last
37
+ _last = text
38
+ write("\x1b]52;c;" + base64.b64encode(text.encode("utf-8")).decode("ascii") + "\x07")
39
+ if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_TTY"):
40
+ return # the tools would copy on the remote machine, not yours
41
+ for command, needs in TOOLS:
42
+ if (needs == sys.platform or os.environ.get(needs)) and shutil.which(command[0]):
43
+ try:
44
+ subprocess.run(command, input=text.encode("utf-8"), timeout=2, check=False,
45
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
46
+ except (OSError, subprocess.SubprocessError):
47
+ continue
48
+ return
49
+
50
+
51
+ def paste() -> str:
52
+ """The text last copied with copy() in this program."""
53
+ return _last
@@ -1,6 +1,7 @@
1
1
  import queue
2
2
  import re
3
3
  import sys
4
+ import time
4
5
 
5
6
  from .shorts import ESC, write
6
7
  from ._platform import Terminal
@@ -57,6 +58,10 @@ def update_state(input):
57
58
  # 1006 = SGR encoding, which gives readable decimal coordinates with no size limit
58
59
  MOUSE_ON = ESC + "?1003h" + ESC + "?1006h"
59
60
  MOUSE_OFF = ESC + "?1003l" + ESC + "?1006l"
61
+ # Bracketed paste: the terminal wraps pasted text in these, so it arrives as one
62
+ # "paste" input instead of keys (a pasted newline isn't Enter)
63
+ PASTE_ON, PASTE_OFF = ESC + "?2004h", ESC + "?2004l"
64
+ PASTE_START, PASTE_END = "\x1b[200~", "\x1b[201~"
60
65
 
61
66
  # SGR mouse report: ESC [ < button ; x ; y then M (press/move) or m (release)
62
67
  SGR_MOUSE = re.compile(r"\x1b\[<(\d+);(\d+);(\d+)([Mm])")
@@ -105,7 +110,7 @@ def enable():
105
110
  """Raw keyboard mode, mouse reporting, and capture print output."""
106
111
  _stderr_log.clear()
107
112
  _terminal.enable()
108
- write(MOUSE_ON)
113
+ write(MOUSE_ON + PASTE_ON)
109
114
  sys.stdout = StreamInterceptor(sys.stdout, "stdout")
110
115
  sys.stderr = StreamInterceptor(sys.stderr, "stderr")
111
116
 
@@ -116,7 +121,7 @@ def disable():
116
121
  sys.stdout = sys.stdout.real
117
122
  if isinstance(sys.stderr, StreamInterceptor):
118
123
  sys.stderr = sys.stderr.real
119
- write(MOUSE_OFF)
124
+ write(MOUSE_OFF + PASTE_OFF)
120
125
  _terminal.disable()
121
126
 
122
127
 
@@ -125,6 +130,23 @@ def captured_stderr():
125
130
  return "".join(_stderr_log)
126
131
 
127
132
 
133
+ CURSOR_REPORT = re.compile(r"\x1b\[(\d+);(\d+)R")
134
+
135
+ def cursor_position(timeout=0.5):
136
+ """Ask the terminal where the cursor is: (x, y), 0-based, or None if it doesn't say.
137
+ Call after enable() and before reading inputs; anything typed meanwhile is kept."""
138
+ global _buffer
139
+ write(ESC + "6n")
140
+ end = time.monotonic() + timeout
141
+ while time.monotonic() < end:
142
+ _buffer += _terminal.read(max(0.0, end - time.monotonic()))
143
+ match = CURSOR_REPORT.search(_buffer)
144
+ if match:
145
+ _buffer = _buffer[:match.start()] + _buffer[match.end():]
146
+ return int(match.group(2)) - 1, int(match.group(1)) - 1
147
+ return None
148
+
149
+
128
150
  def wake():
129
151
  """Make read_inputs() return straight away. Safe from any thread."""
130
152
  _terminal.wake()
@@ -146,7 +168,14 @@ def read_inputs(timeout=0.1):
146
168
  inputs.append(Input(kind, {"text": text}))
147
169
 
148
170
  while _buffer:
149
- if _buffer.startswith("\x1b[<"):
171
+ if _buffer.startswith(PASTE_START):
172
+ end = _buffer.find(PASTE_END)
173
+ if end < 0:
174
+ break # the rest of the paste is still coming
175
+ text = _buffer[len(PASTE_START):end].replace("\r\n", "\n").replace("\r", "\n")
176
+ inputs.append(Input("paste", {"text": text}))
177
+ _buffer = _buffer[end + len(PASTE_END):]
178
+ elif _buffer.startswith("\x1b[<"):
150
179
  match = SGR_MOUSE.match(_buffer)
151
180
  if not match:
152
181
  break # incomplete mouse report, wait for the rest