clicanvas 0.2__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.
- clicanvas-0.2/PKG-INFO +18 -0
- clicanvas-0.2/README.md +8 -0
- clicanvas-0.2/pyproject.toml +27 -0
- clicanvas-0.2/setup.cfg +4 -0
- clicanvas-0.2/src/clicanvas/__init__.py +101 -0
- clicanvas-0.2/src/clicanvas/input/__init__.py +22 -0
- clicanvas-0.2/src/clicanvas/input/base.py +119 -0
- clicanvas-0.2/src/clicanvas/input/confirm.py +18 -0
- clicanvas-0.2/src/clicanvas/input/constrained.py +96 -0
- clicanvas-0.2/src/clicanvas/input/getpass.py +110 -0
- clicanvas-0.2/src/clicanvas/input/readline.py +237 -0
- clicanvas-0.2/src/clicanvas/microwidgets/__init__.py +7 -0
- clicanvas-0.2/src/clicanvas/microwidgets/_lib.py +26 -0
- clicanvas-0.2/src/clicanvas/microwidgets/checkbox.py +70 -0
- clicanvas-0.2/src/clicanvas/microwidgets/incrementer.py +61 -0
- clicanvas-0.2/src/clicanvas/microwidgets/menu.py +47 -0
- clicanvas-0.2/src/clicanvas/microwidgets/slider.py +63 -0
- clicanvas-0.2/src/clicanvas.egg-info/PKG-INFO +18 -0
- clicanvas-0.2/src/clicanvas.egg-info/SOURCES.txt +20 -0
- clicanvas-0.2/src/clicanvas.egg-info/dependency_links.txt +1 -0
- clicanvas-0.2/src/clicanvas.egg-info/requires.txt +4 -0
- clicanvas-0.2/src/clicanvas.egg-info/top_level.txt +1 -0
clicanvas-0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clicanvas
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: A python library that deals with terminal-based user input. From a TUI framework to custom input functions.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: inputkit>=1.0.2
|
|
8
|
+
Requires-Dist: regex>=2026.2.28
|
|
9
|
+
Provides-Extra: thonny
|
|
10
|
+
|
|
11
|
+
# CLICanvas
|
|
12
|
+
|
|
13
|
+
CLICanvas is a python library that deals with terminal-based user input.
|
|
14
|
+
From a TUI framework to custom input functions.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Custom input function and modes, including a mirror of readline and getpass
|
clicanvas-0.2/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "clicanvas"
|
|
7
|
+
version = "0.2"
|
|
8
|
+
description = "A python library that deals with terminal-based user input. From a TUI framework to custom input functions."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"inputkit>=1.0.2",
|
|
13
|
+
"regex>=2026.2.28",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
thonny = []
|
|
18
|
+
|
|
19
|
+
[tool.uv.sources]
|
|
20
|
+
tester = { git = "https://github.com/Minemario64/Testing-Suite.git" }
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
package-dir = {"" = "src"}
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
exclude = []
|
clicanvas-0.2/setup.cfg
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from importlib.metadata import metadata
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
def isOptionFlagEnabled(flag: str) -> bool:
|
|
5
|
+
try:
|
|
6
|
+
m = metadata("clicanvas")
|
|
7
|
+
requested = m.get_all("Provides-Extra") or []
|
|
8
|
+
return flag in requested
|
|
9
|
+
|
|
10
|
+
except Exception:
|
|
11
|
+
return False
|
|
12
|
+
|
|
13
|
+
if os.name == "nt" and isOptionFlagEnabled("thonny"):
|
|
14
|
+
import os, sys, subprocess
|
|
15
|
+
import ctypes, ctypes.wintypes as wintypes
|
|
16
|
+
|
|
17
|
+
@lambda _: _()
|
|
18
|
+
def ensureOutOfThonny():
|
|
19
|
+
if sys.stdout.__class__.__name__ == "FakeOutputStream":
|
|
20
|
+
subprocess.Popen(["python", os.path.abspath(sys.argv[0])], creationflags=subprocess.CREATE_NEW_CONSOLE)
|
|
21
|
+
print("Re-launching in external console...")
|
|
22
|
+
exit()
|
|
23
|
+
|
|
24
|
+
def isANSIEnabled():
|
|
25
|
+
kernel32 = ctypes.windll.kernel32
|
|
26
|
+
h = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE = -11
|
|
27
|
+
mode = ctypes.c_uint()
|
|
28
|
+
if kernel32.GetConsoleMode(h, ctypes.byref(mode)) == 0:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
return bool(mode.value & 0x0004) # ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
32
|
+
|
|
33
|
+
def enableANSI():
|
|
34
|
+
kernel32 = ctypes.windll.kernel32
|
|
35
|
+
h = kernel32.GetStdHandle(-11)
|
|
36
|
+
mode = ctypes.c_uint()
|
|
37
|
+
kernel32.GetConsoleMode(h, ctypes.byref(mode))
|
|
38
|
+
kernel32.SetConsoleMode(h, mode.value | 0x0004)
|
|
39
|
+
|
|
40
|
+
if not isANSIEnabled():
|
|
41
|
+
enableANSI()
|
|
42
|
+
|
|
43
|
+
class COORD(ctypes.Structure):
|
|
44
|
+
_fields_ = [("X", wintypes.SHORT),
|
|
45
|
+
("Y", wintypes.SHORT)]
|
|
46
|
+
|
|
47
|
+
class SMALL_RECT(ctypes.Structure):
|
|
48
|
+
_fields_ = [("Left", wintypes.SHORT),
|
|
49
|
+
("Top", wintypes.SHORT),
|
|
50
|
+
("Right", wintypes.SHORT),
|
|
51
|
+
("Bottom", wintypes.SHORT)]
|
|
52
|
+
|
|
53
|
+
class CONSOLE_SCREEN_BUFFER_INFOEX(ctypes.Structure):
|
|
54
|
+
_fields_ = [
|
|
55
|
+
("cbSize", wintypes.ULONG),
|
|
56
|
+
("dwSize", COORD),
|
|
57
|
+
("dwCursorPosition", COORD),
|
|
58
|
+
("wAttributes", wintypes.WORD),
|
|
59
|
+
("srWindow", SMALL_RECT),
|
|
60
|
+
("dwMaximumWindowSize", COORD),
|
|
61
|
+
("wPopupAttributes", wintypes.WORD),
|
|
62
|
+
("bFullscreenSupported", wintypes.BOOL),
|
|
63
|
+
("ColorTable", wintypes.DWORD * 16)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
kernel32 = ctypes.windll.kernel32
|
|
67
|
+
STDOUT = -11
|
|
68
|
+
handle = kernel32.GetStdHandle(STDOUT)
|
|
69
|
+
|
|
70
|
+
info = CONSOLE_SCREEN_BUFFER_INFOEX()
|
|
71
|
+
info.cbSize = ctypes.sizeof(CONSOLE_SCREEN_BUFFER_INFOEX)
|
|
72
|
+
|
|
73
|
+
kernel32.GetConsoleScreenBufferInfoEx(handle, ctypes.byref(info))
|
|
74
|
+
|
|
75
|
+
def rgb(r: int, g: int, b: int) -> int:
|
|
76
|
+
return (b << 16) | (g << 8) | r
|
|
77
|
+
|
|
78
|
+
# Color Palette
|
|
79
|
+
info.ColorTable[0] = rgb(24, 31, 40)
|
|
80
|
+
info.ColorTable[4] = rgb(186, 7, 50)
|
|
81
|
+
info.ColorTable[2] = rgb(26, 122, 62)
|
|
82
|
+
info.ColorTable[6] = rgb(249, 163, 27)
|
|
83
|
+
info.ColorTable[1] = rgb(40, 92, 196)
|
|
84
|
+
info.ColorTable[5] = rgb(102, 25, 174)
|
|
85
|
+
info.ColorTable[3] = rgb(0, 130, 125)
|
|
86
|
+
info.ColorTable[7] = rgb(139, 147, 175)
|
|
87
|
+
info.ColorTable[8] = rgb(51, 57, 65)
|
|
88
|
+
info.ColorTable[12] = rgb(235, 58, 100)
|
|
89
|
+
info.ColorTable[10] = rgb(57, 219, 117)
|
|
90
|
+
info.ColorTable[14] = rgb(255, 252, 64)
|
|
91
|
+
info.ColorTable[9] = rgb(36, 159, 222)
|
|
92
|
+
info.ColorTable[13] = rgb(161, 47, 215)
|
|
93
|
+
info.ColorTable[11] = rgb(40, 194, 149)
|
|
94
|
+
info.ColorTable[15] = rgb(255, 255, 255)
|
|
95
|
+
|
|
96
|
+
info.wAttributes = 0x07
|
|
97
|
+
|
|
98
|
+
kernel32.SetConsoleScreenBufferInfoEx(handle, ctypes.byref(info))
|
|
99
|
+
|
|
100
|
+
sys.stdout.write("\x1b[2J\x1b[3J\x1b[H")
|
|
101
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .base import input as input
|
|
2
|
+
from .readline import input as readline, loadHistory, saveHistory, maxHistory
|
|
3
|
+
from .getpass import input as getpass
|
|
4
|
+
from .confirm import input as confirm
|
|
5
|
+
|
|
6
|
+
__all__ = ["input", "readline", "loadHistory", "saveHistory", "getpass", "confirm", "customInput", "getpassConfirm", "getpassCheck", "maxHistory"] # pyright: ignore[reportUnsupportedDunderAll]
|
|
7
|
+
|
|
8
|
+
def __getattr__(name: str):
|
|
9
|
+
match name:
|
|
10
|
+
case "customInput":
|
|
11
|
+
from .base import customInput
|
|
12
|
+
return customInput
|
|
13
|
+
|
|
14
|
+
case "getpassConfirm":
|
|
15
|
+
from .getpass import confirm
|
|
16
|
+
return confirm
|
|
17
|
+
|
|
18
|
+
case "getpassCheck":
|
|
19
|
+
from .getpass import check
|
|
20
|
+
return check
|
|
21
|
+
|
|
22
|
+
raise AttributeError(name=name)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from inputkit import Key, handleInput
|
|
2
|
+
from typing import Callable, overload, Any
|
|
3
|
+
from io import StringIO
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
VERSION = "1.0.0"
|
|
7
|
+
|
|
8
|
+
_ANSI_START = "\x1b["
|
|
9
|
+
|
|
10
|
+
def input(prompt: str, voidCtrlC: bool = True) -> str:
|
|
11
|
+
sys.stdout.write(prompt)
|
|
12
|
+
sys.stdout.flush()
|
|
13
|
+
inputBuf: StringIO = StringIO()
|
|
14
|
+
cur: int = 0
|
|
15
|
+
@handleInput(hideCursor=False)
|
|
16
|
+
def inpHandler(key: Key | str) -> bool:
|
|
17
|
+
nonlocal cur
|
|
18
|
+
if isinstance(key, str):
|
|
19
|
+
inputBuf.write(key)
|
|
20
|
+
cur += 1
|
|
21
|
+
sys.stdout.write(key)
|
|
22
|
+
sys.stdout.flush()
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
match key:
|
|
26
|
+
case Key.BACKSPACE:
|
|
27
|
+
forwardInput = inputBuf.read()
|
|
28
|
+
cur -= 1
|
|
29
|
+
if cur == -1:
|
|
30
|
+
cur = 0
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
inputBuf.seek(cur)
|
|
34
|
+
inputBuf.truncate()
|
|
35
|
+
inputBuf.write(forwardInput)
|
|
36
|
+
inputBuf.seek(cur)
|
|
37
|
+
sys.stdout.write(f"{_ANSI_START}D{' '*(len(forwardInput)+1)}{_ANSI_START}{len(forwardInput)+1}D{f'{forwardInput}{_ANSI_START}{len(forwardInput)}D' if forwardInput else ''}")
|
|
38
|
+
sys.stdout.flush()
|
|
39
|
+
|
|
40
|
+
case Key.DEL:
|
|
41
|
+
inputBuf.seek(cur+1)
|
|
42
|
+
forwardInput = inputBuf.read()
|
|
43
|
+
inputBuf.seek(cur)
|
|
44
|
+
inputBuf.truncate()
|
|
45
|
+
inputBuf.write(forwardInput)
|
|
46
|
+
inputBuf.seek(cur)
|
|
47
|
+
sys.stdout.write(f"{' '*(len(forwardInput)+1)}{_ANSI_START}{len(forwardInput)+1}D{f'{forwardInput}{_ANSI_START}{len(forwardInput)}D' if forwardInput else ''}")
|
|
48
|
+
sys.stdout.flush()
|
|
49
|
+
|
|
50
|
+
case Key.ENTER:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
case Key.CTRL_C:
|
|
54
|
+
if not voidCtrlC:
|
|
55
|
+
raise KeyboardInterrupt
|
|
56
|
+
|
|
57
|
+
case Key.RIGHT:
|
|
58
|
+
inputBuf.seek(0, 2)
|
|
59
|
+
bufLength = inputBuf.tell()
|
|
60
|
+
if cur == bufLength:
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
cur += 1
|
|
64
|
+
inputBuf.seek(cur)
|
|
65
|
+
sys.stdout.write("\x1b[C")
|
|
66
|
+
sys.stdout.flush()
|
|
67
|
+
|
|
68
|
+
case Key.LEFT:
|
|
69
|
+
cur -= 1
|
|
70
|
+
if cur < 0:
|
|
71
|
+
cur = 0
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
inputBuf.seek(cur)
|
|
75
|
+
sys.stdout.write("\x1b[D")
|
|
76
|
+
sys.stdout.flush()
|
|
77
|
+
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
print()
|
|
81
|
+
|
|
82
|
+
res = inputBuf.getvalue()
|
|
83
|
+
|
|
84
|
+
return res
|
|
85
|
+
|
|
86
|
+
@overload
|
|
87
|
+
def customInput(prompt: str, validators: list[Callable[[str], bool]], failResponse: str = "Try Again.",*, inputFunc: Callable[[str], str] = input) -> str: ...
|
|
88
|
+
|
|
89
|
+
@overload
|
|
90
|
+
def customInput(prompt: str, validators: list[Callable[[str], bool]], failResponse: str = "Try Again.",*, transformers: list[Callable], inputFunc: Callable[[str], str] = input) -> Any: ...
|
|
91
|
+
|
|
92
|
+
def customInput(prompt: str, validators: list[Callable[[str], bool]], failResponse: str = "Try Again.",*, inputFunc: Callable[[str], str] = input, transformers: list[Callable] | None = None) -> str | Any:
|
|
93
|
+
while True:
|
|
94
|
+
inp = inputFunc(prompt)
|
|
95
|
+
if all([validator(inp) for validator in validators]):
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
print(failResponse)
|
|
99
|
+
|
|
100
|
+
if transformers is None:
|
|
101
|
+
return inp
|
|
102
|
+
|
|
103
|
+
res: Any = inp
|
|
104
|
+
for transformer in transformers:
|
|
105
|
+
res = transformer(res)
|
|
106
|
+
|
|
107
|
+
return res
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
print(repr(input("Hello: ")))
|
|
111
|
+
def isInt(input: str) -> bool:
|
|
112
|
+
try:
|
|
113
|
+
int(input)
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
except Exception:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
print(customInput("Input an integer: ", [isInt], "Input an integer. Try Again.", transformers=[int]))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .base import customInput
|
|
2
|
+
|
|
3
|
+
VERSION = "1.0.0"
|
|
4
|
+
|
|
5
|
+
def isYesOrNo(text: str) -> bool:
|
|
6
|
+
if text.lower() in ['y', "n", "ye", "no", "yes", "yea", "yeah", "noo"]:
|
|
7
|
+
return True
|
|
8
|
+
|
|
9
|
+
return False
|
|
10
|
+
|
|
11
|
+
def strToBool(text: str) -> bool:
|
|
12
|
+
if text in ['y', 'ye', 'yes', 'yea', 'yeah']:
|
|
13
|
+
return True
|
|
14
|
+
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
def input(prompt: str) -> bool:
|
|
18
|
+
return customInput(prompt, [isYesOrNo], "Must be yes or no. Try Again", transformers=[lambda text: text.lower(), strToBool])
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from inputkit import Key, handleInput
|
|
2
|
+
from io import StringIO
|
|
3
|
+
import sys
|
|
4
|
+
import regex as rgx
|
|
5
|
+
|
|
6
|
+
VERSION = "1.0.0"
|
|
7
|
+
|
|
8
|
+
_ANSI_START = "\x1b["
|
|
9
|
+
|
|
10
|
+
def input(prompt: str, rgxRestrictions: list[rgx.Pattern], voidCtrlC: bool = True) -> str:
|
|
11
|
+
sys.stdout.write(prompt)
|
|
12
|
+
sys.stdout.flush()
|
|
13
|
+
inputBuf: StringIO = StringIO()
|
|
14
|
+
cur: int = 0
|
|
15
|
+
@handleInput(hideCursor=False)
|
|
16
|
+
def inpHandler(key: Key | str) -> bool:
|
|
17
|
+
nonlocal cur
|
|
18
|
+
if isinstance(key, str):
|
|
19
|
+
cur += 1
|
|
20
|
+
inputBuf.write(key)
|
|
21
|
+
if not any([pattern.match(inputBuf.getvalue(), partial=True) for pattern in rgxRestrictions]):
|
|
22
|
+
cur -= 1
|
|
23
|
+
inputBuf.seek(cur)
|
|
24
|
+
inputBuf.truncate()
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
sys.stdout.write(key)
|
|
28
|
+
sys.stdout.flush()
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
match key:
|
|
32
|
+
case Key.BACKSPACE:
|
|
33
|
+
forwardInput = inputBuf.read()
|
|
34
|
+
cur -= 1
|
|
35
|
+
if cur == -1:
|
|
36
|
+
cur = 0
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
inputBuf.seek(cur)
|
|
40
|
+
inputBuf.truncate()
|
|
41
|
+
inputBuf.write(forwardInput)
|
|
42
|
+
inputBuf.seek(cur)
|
|
43
|
+
sys.stdout.write(f"{_ANSI_START}D{' '*(len(forwardInput)+1)}{_ANSI_START}{len(forwardInput)+1}D{f'{forwardInput}{_ANSI_START}{len(forwardInput)}D' if forwardInput else ''}")
|
|
44
|
+
sys.stdout.flush()
|
|
45
|
+
|
|
46
|
+
case Key.DEL:
|
|
47
|
+
inputBuf.seek(cur+1)
|
|
48
|
+
forwardInput = inputBuf.read()
|
|
49
|
+
inputBuf.seek(cur)
|
|
50
|
+
inputBuf.truncate()
|
|
51
|
+
inputBuf.write(forwardInput)
|
|
52
|
+
inputBuf.seek(cur)
|
|
53
|
+
sys.stdout.write(f"{' '*(len(forwardInput)+1)}{_ANSI_START}{len(forwardInput)+1}D{f'{forwardInput}{_ANSI_START}{len(forwardInput)}D' if forwardInput else ''}")
|
|
54
|
+
sys.stdout.flush()
|
|
55
|
+
|
|
56
|
+
case Key.ENTER:
|
|
57
|
+
if not any([pattern.match(inputBuf.getvalue(), partial=True) for pattern in rgxRestrictions]):
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
case Key.CTRL_C:
|
|
63
|
+
if not voidCtrlC:
|
|
64
|
+
raise KeyboardInterrupt
|
|
65
|
+
|
|
66
|
+
case Key.RIGHT:
|
|
67
|
+
inputBuf.seek(0, 2)
|
|
68
|
+
bufLength = inputBuf.tell()
|
|
69
|
+
if cur == bufLength:
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
cur += 1
|
|
73
|
+
inputBuf.seek(cur)
|
|
74
|
+
sys.stdout.write("\x1b[C")
|
|
75
|
+
sys.stdout.flush()
|
|
76
|
+
|
|
77
|
+
case Key.LEFT:
|
|
78
|
+
cur -= 1
|
|
79
|
+
if cur < 0:
|
|
80
|
+
cur = 0
|
|
81
|
+
|
|
82
|
+
else:
|
|
83
|
+
inputBuf.seek(cur)
|
|
84
|
+
sys.stdout.write("\x1b[D")
|
|
85
|
+
sys.stdout.flush()
|
|
86
|
+
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
print()
|
|
90
|
+
|
|
91
|
+
res = inputBuf.getvalue()
|
|
92
|
+
|
|
93
|
+
return res
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
print(repr(input("Hello: ", [rgx.compile(r"rock", rgx.I), rgx.compile(r"paper", rgx.I), rgx.compile(r"scissors", rgx.I)])))
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from inputkit import Key, handleInput
|
|
2
|
+
from typing import overload
|
|
3
|
+
from io import StringIO
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
VERSION = "1.0.0"
|
|
7
|
+
|
|
8
|
+
_ANSI_START = "\x1b["
|
|
9
|
+
|
|
10
|
+
def input(prompt: str = "Password: ", voidCtrlC: bool = True) -> str:
|
|
11
|
+
sys.stdout.write(prompt)
|
|
12
|
+
sys.stdout.flush()
|
|
13
|
+
inputBuf: StringIO = StringIO()
|
|
14
|
+
cur: int = 0
|
|
15
|
+
@handleInput(hideCursor=True)
|
|
16
|
+
def inpHandler(key: Key | str) -> bool:
|
|
17
|
+
nonlocal cur
|
|
18
|
+
if isinstance(key, str):
|
|
19
|
+
inputBuf.write(key)
|
|
20
|
+
cur += 1
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
match key:
|
|
24
|
+
case Key.BACKSPACE:
|
|
25
|
+
forwardInput = inputBuf.read()
|
|
26
|
+
cur -= 1
|
|
27
|
+
if cur == -1:
|
|
28
|
+
cur = 0
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
inputBuf.seek(cur)
|
|
32
|
+
inputBuf.truncate()
|
|
33
|
+
inputBuf.write(forwardInput)
|
|
34
|
+
inputBuf.seek(cur)
|
|
35
|
+
|
|
36
|
+
case Key.DEL:
|
|
37
|
+
inputBuf.seek(cur+1)
|
|
38
|
+
forwardInput = inputBuf.read()
|
|
39
|
+
inputBuf.seek(cur)
|
|
40
|
+
inputBuf.truncate()
|
|
41
|
+
inputBuf.write(forwardInput)
|
|
42
|
+
inputBuf.seek(cur)
|
|
43
|
+
|
|
44
|
+
case Key.ENTER:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
case Key.CTRL_C:
|
|
48
|
+
if not voidCtrlC:
|
|
49
|
+
raise KeyboardInterrupt
|
|
50
|
+
|
|
51
|
+
case Key.RIGHT:
|
|
52
|
+
inputBuf.seek(0, 2)
|
|
53
|
+
bufLength = inputBuf.tell()
|
|
54
|
+
if cur == bufLength:
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
cur += 1
|
|
58
|
+
inputBuf.seek(cur)
|
|
59
|
+
|
|
60
|
+
case Key.LEFT:
|
|
61
|
+
cur -= 1
|
|
62
|
+
if cur < 0:
|
|
63
|
+
cur = 0
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
inputBuf.seek(cur)
|
|
67
|
+
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
print()
|
|
71
|
+
|
|
72
|
+
res = inputBuf.getvalue()
|
|
73
|
+
|
|
74
|
+
return res
|
|
75
|
+
|
|
76
|
+
@overload
|
|
77
|
+
def confirm() -> str: ...
|
|
78
|
+
|
|
79
|
+
@overload
|
|
80
|
+
def confirm(prompt: str = "Password: ", confirmPrompt: str = "Confirm password: ") -> str: ...
|
|
81
|
+
|
|
82
|
+
@overload
|
|
83
|
+
def confirm(prompt: str = "Password: ", confirmPrompt: str = "Confirm password: ", retry: bool = False) -> str | None: ...
|
|
84
|
+
|
|
85
|
+
def confirm(prompt: str = "Password: ", confirmPrompt: str = "Confirm password: ", retry: bool = True) -> str | None:
|
|
86
|
+
while True:
|
|
87
|
+
password: str = input(prompt)
|
|
88
|
+
confirmedPassword: str = input(confirmPrompt)
|
|
89
|
+
if not (password == confirmedPassword):
|
|
90
|
+
if not retry:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
return password
|
|
97
|
+
|
|
98
|
+
def check(password: str, prompt: str = "Password: ", tries: int = 3, color: bool = True) -> bool:
|
|
99
|
+
for attempt in range(tries):
|
|
100
|
+
attemptedInput = input(prompt)
|
|
101
|
+
if attemptedInput == password:
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
if attempt < tries - 1:
|
|
105
|
+
print(f"{f'{_ANSI_START}31m' if color else ''}Incorrect. Try Again{f'{_ANSI_START}0m' if color else ''}")
|
|
106
|
+
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
password = check("123456")
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from inputkit import Key, handleInput
|
|
2
|
+
from typing import Iterable, overload, Any, TextIO
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from io import StringIO
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
VERSION = "1.0.0"
|
|
8
|
+
|
|
9
|
+
_ANSI_START = "\x1b["
|
|
10
|
+
|
|
11
|
+
class maxList(list):
|
|
12
|
+
@overload
|
|
13
|
+
def __init__(self,*, maxLen: int = 50) -> None: ...
|
|
14
|
+
|
|
15
|
+
@overload
|
|
16
|
+
def __init__(self, iterable: Iterable,*, maxLen: int = 50) -> None: ...
|
|
17
|
+
|
|
18
|
+
def __init__(self, iterable: Iterable | None = None,*, maxLen: int = 50) -> None:
|
|
19
|
+
self.__maxLen: int = maxLen
|
|
20
|
+
if iterable is None:
|
|
21
|
+
super().__init__()
|
|
22
|
+
else:
|
|
23
|
+
super().__init__(iterable)
|
|
24
|
+
|
|
25
|
+
def changeMaxLen(self, value: int) -> None:
|
|
26
|
+
self.__maxLen = value
|
|
27
|
+
if len(self) > self.__maxLen:
|
|
28
|
+
while len(self) > self.__maxLen:
|
|
29
|
+
self.pop(0)
|
|
30
|
+
|
|
31
|
+
def shiftUntilLen(self, length: int) -> None:
|
|
32
|
+
while len(self) > length:
|
|
33
|
+
self.pop()
|
|
34
|
+
|
|
35
|
+
def append(self, object: Any) -> None:
|
|
36
|
+
if len(self) >= self.__maxLen:
|
|
37
|
+
self.shiftUntilLen(self.__maxLen - 1)
|
|
38
|
+
|
|
39
|
+
return super().insert(0, object)
|
|
40
|
+
|
|
41
|
+
def extend(self, iterable: Iterable) -> None:
|
|
42
|
+
if len(iterable) > self.__maxLen: # pyright: ignore[reportArgumentType]
|
|
43
|
+
raise ValueError("Iterable too large")
|
|
44
|
+
|
|
45
|
+
elif len(iterable) == self.__maxLen: # pyright: ignore[reportArgumentType]
|
|
46
|
+
self.clear()
|
|
47
|
+
super().extend(iterable)
|
|
48
|
+
|
|
49
|
+
if (len(self) + len(iterable)) >= self.__maxLen: # pyright: ignore[reportArgumentType]
|
|
50
|
+
self.shiftUntilLen(len(self) - len(iterable)) # pyright: ignore[reportArgumentType]
|
|
51
|
+
|
|
52
|
+
self.reverse()
|
|
53
|
+
super().extend(iterable)
|
|
54
|
+
self.reverse()
|
|
55
|
+
|
|
56
|
+
_HIST = maxList([])
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
def maxHistory() -> int: ...
|
|
60
|
+
|
|
61
|
+
@overload
|
|
62
|
+
def maxHistory(value: int) -> None: ...
|
|
63
|
+
|
|
64
|
+
def maxHistory(value: int | None = None) -> int | None:
|
|
65
|
+
if value is None:
|
|
66
|
+
return _HIST._maxList__maxLen # pyright: ignore[reportAttributeAccessIssue]
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
_HIST.changeMaxLen(value)
|
|
70
|
+
|
|
71
|
+
def loadHistory(file: str | Path | TextIO) -> None:
|
|
72
|
+
if isinstance(file, Path):
|
|
73
|
+
if not file.is_file():
|
|
74
|
+
raise ValueError("Path object must be a file.")
|
|
75
|
+
|
|
76
|
+
if isinstance(file, str | Path):
|
|
77
|
+
with (open(file, "r", encoding='utf8') if isinstance(file, str) else file.open("r", encoding='utf8')) as fileObj:
|
|
78
|
+
_HIST.clear()
|
|
79
|
+
_HIST.extend(fileObj.read().splitlines())
|
|
80
|
+
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
elif isinstance(file, TextIO):
|
|
84
|
+
_HIST.clear()
|
|
85
|
+
cursor: int = file.tell()
|
|
86
|
+
file.seek(0)
|
|
87
|
+
_HIST.extend(file.read().splitlines())
|
|
88
|
+
file.seek(cursor)
|
|
89
|
+
|
|
90
|
+
def saveHistory(filepath: str | Path) -> None:
|
|
91
|
+
if isinstance(filepath, Path):
|
|
92
|
+
if filepath.is_dir():
|
|
93
|
+
raise ValueError("Path must be able to be a file.")
|
|
94
|
+
|
|
95
|
+
if not (pathObj := (Path().joinpath(filepath) if isinstance(filepath, str) else filepath)).exists():
|
|
96
|
+
pathObj.touch()
|
|
97
|
+
|
|
98
|
+
with pathObj.open("w") as file:
|
|
99
|
+
file.write("\n".join(_HIST))
|
|
100
|
+
|
|
101
|
+
def input(prompt: str, voidCtrlC: bool = True, connectHistory: bool = True) -> str:
|
|
102
|
+
sys.stdout.write(prompt)
|
|
103
|
+
sys.stdout.flush()
|
|
104
|
+
newInputText: str = ''
|
|
105
|
+
inputBuf: StringIO = StringIO()
|
|
106
|
+
cur: int = 0
|
|
107
|
+
historyIdx: int = -1
|
|
108
|
+
@handleInput(hideCursor=False)
|
|
109
|
+
def inpHandler(key: Key | str) -> bool:
|
|
110
|
+
nonlocal newInputText
|
|
111
|
+
nonlocal cur
|
|
112
|
+
nonlocal historyIdx
|
|
113
|
+
if isinstance(key, str):
|
|
114
|
+
inputBuf.write(key)
|
|
115
|
+
cur += 1
|
|
116
|
+
sys.stdout.write(key)
|
|
117
|
+
sys.stdout.flush()
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
match key:
|
|
121
|
+
case Key.BACKSPACE:
|
|
122
|
+
forwardInput = inputBuf.read()
|
|
123
|
+
cur -= 1
|
|
124
|
+
if cur == -1:
|
|
125
|
+
cur = 0
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
inputBuf.seek(cur)
|
|
129
|
+
inputBuf.truncate()
|
|
130
|
+
inputBuf.write(forwardInput)
|
|
131
|
+
inputBuf.seek(cur)
|
|
132
|
+
sys.stdout.write(f"{_ANSI_START}D{' '*(len(forwardInput)+1)}{_ANSI_START}{len(forwardInput)+1}D{f'{forwardInput}{_ANSI_START}{len(forwardInput)}D' if forwardInput else ''}")
|
|
133
|
+
sys.stdout.flush()
|
|
134
|
+
|
|
135
|
+
case Key.DEL:
|
|
136
|
+
inputBuf.seek(cur+1)
|
|
137
|
+
forwardInput = inputBuf.read()
|
|
138
|
+
inputBuf.seek(cur)
|
|
139
|
+
inputBuf.truncate()
|
|
140
|
+
inputBuf.write(forwardInput)
|
|
141
|
+
inputBuf.seek(cur)
|
|
142
|
+
sys.stdout.write(f"{' '*(len(forwardInput)+1)}{_ANSI_START}{len(forwardInput)+1}D{f'{forwardInput}{_ANSI_START}{len(forwardInput)}D' if forwardInput else ''}")
|
|
143
|
+
sys.stdout.flush()
|
|
144
|
+
|
|
145
|
+
case Key.ENTER:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
case Key.CTRL_C:
|
|
149
|
+
if not voidCtrlC:
|
|
150
|
+
raise KeyboardInterrupt
|
|
151
|
+
|
|
152
|
+
case Key.RIGHT:
|
|
153
|
+
inputBuf.seek(0, 2)
|
|
154
|
+
bufLength = inputBuf.tell()
|
|
155
|
+
if cur == bufLength:
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
cur += 1
|
|
159
|
+
inputBuf.seek(cur)
|
|
160
|
+
sys.stdout.write("\x1b[C")
|
|
161
|
+
sys.stdout.flush()
|
|
162
|
+
|
|
163
|
+
case Key.LEFT:
|
|
164
|
+
cur -= 1
|
|
165
|
+
if cur < 0:
|
|
166
|
+
cur = 0
|
|
167
|
+
|
|
168
|
+
else:
|
|
169
|
+
inputBuf.seek(cur)
|
|
170
|
+
sys.stdout.write("\x1b[D")
|
|
171
|
+
sys.stdout.flush()
|
|
172
|
+
|
|
173
|
+
case Key.UP | Key.DOWN:
|
|
174
|
+
if connectHistory:
|
|
175
|
+
if historyIdx == -1:
|
|
176
|
+
newInputText = inputBuf.getvalue()
|
|
177
|
+
|
|
178
|
+
historyIdx += 1 if key == Key.UP else -1
|
|
179
|
+
if historyIdx < -1:
|
|
180
|
+
historyIdx = -1
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
elif historyIdx == -1:
|
|
184
|
+
inputBuf.seek(0, 2)
|
|
185
|
+
bufLength = inputBuf.tell()
|
|
186
|
+
inputBuf.seek(cur)
|
|
187
|
+
|
|
188
|
+
# chr(32) is space, is there because of a syntax error for 3.10
|
|
189
|
+
sys.stdout.write(f"{f'{_ANSI_START}{bufLength}D{chr(32)*bufLength}{_ANSI_START}{bufLength}D' if bufLength > 0 else ''}{newInputText}")
|
|
190
|
+
sys.stdout.flush()
|
|
191
|
+
|
|
192
|
+
inputBuf.seek(0)
|
|
193
|
+
inputBuf.truncate(0)
|
|
194
|
+
inputBuf.write(newInputText)
|
|
195
|
+
cur = inputBuf.tell()
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
inputBuf.seek(0, 2)
|
|
200
|
+
bufLength = inputBuf.tell()
|
|
201
|
+
inputBuf.seek(cur)
|
|
202
|
+
|
|
203
|
+
# chr(32) is space, is there because of a syntax error for 3.10
|
|
204
|
+
sys.stdout.write(f"{f'{_ANSI_START}{bufLength}D{chr(32)*bufLength}{_ANSI_START}{bufLength}D' if bufLength > 0 else ''}{_HIST[historyIdx]}")
|
|
205
|
+
sys.stdout.flush()
|
|
206
|
+
|
|
207
|
+
inputBuf.seek(0)
|
|
208
|
+
inputBuf.truncate(0)
|
|
209
|
+
inputBuf.write(_HIST[historyIdx])
|
|
210
|
+
cur = inputBuf.tell()
|
|
211
|
+
|
|
212
|
+
except IndexError:
|
|
213
|
+
historyIdx += 1
|
|
214
|
+
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
print()
|
|
218
|
+
|
|
219
|
+
res = inputBuf.getvalue()
|
|
220
|
+
if connectHistory:
|
|
221
|
+
_HIST.append(res)
|
|
222
|
+
|
|
223
|
+
return res
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
loadHistory(".hist")
|
|
227
|
+
import time
|
|
228
|
+
name = input("Name: ", connectHistory=False)
|
|
229
|
+
print(f"Hello, {name}!")
|
|
230
|
+
|
|
231
|
+
time.sleep(1)
|
|
232
|
+
confirm = input("Confirm?(y/n): ", connectHistory=False)
|
|
233
|
+
print("Confirmed!" if confirm in ["y", "yes", "ye", "yea", "yeah"] else "Denied.")
|
|
234
|
+
|
|
235
|
+
while True:
|
|
236
|
+
input(": ", voidCtrlC=False)
|
|
237
|
+
saveHistory(".hist")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .menu import menu
|
|
2
|
+
from .checkbox import checkbox, CheckboxChoice
|
|
3
|
+
from .incrementer import numInput
|
|
4
|
+
from .slider import percent
|
|
5
|
+
from ._lib import ANSIColor, HighlightMode
|
|
6
|
+
|
|
7
|
+
__all__ = ["menu", "checkbox", "CheckboxChoice", "numInput", "percent", "ANSIColor", "HighlightMode"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
class ANSIColor(IntEnum):
|
|
4
|
+
BLACK = 30
|
|
5
|
+
RED = 31
|
|
6
|
+
GREEN = 32
|
|
7
|
+
YELLOW = 33
|
|
8
|
+
BLUE = 34
|
|
9
|
+
MAGENTA = 35
|
|
10
|
+
CYAN = 36
|
|
11
|
+
WHITE = 37
|
|
12
|
+
BRIGHT_BLACK = 90
|
|
13
|
+
BRIGHT_RED = 91
|
|
14
|
+
BRIGHT_GREEN = 92
|
|
15
|
+
BRIGHT_YELLOW = 93
|
|
16
|
+
BRIGHT_BLUE = 94
|
|
17
|
+
BRIGHT_MAGENTA = 95
|
|
18
|
+
BRIGHT_CYAN = 96
|
|
19
|
+
BRIGHT_WHITE = 97
|
|
20
|
+
|
|
21
|
+
class HighlightMode(IntEnum):
|
|
22
|
+
INVERT = 7
|
|
23
|
+
COLOR = 0
|
|
24
|
+
BOLD = 1
|
|
25
|
+
|
|
26
|
+
_ANSI_START = "\x1b["
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from inputkit import Key, handleInput
|
|
2
|
+
from ._lib import ANSIColor, HighlightMode, _ANSI_START
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
def runImmediately(func: Callable) -> Callable:
|
|
8
|
+
func()
|
|
9
|
+
return func
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CheckboxChoice:
|
|
13
|
+
choice: object
|
|
14
|
+
selected: bool = False
|
|
15
|
+
|
|
16
|
+
VERSION = "1.0.0"
|
|
17
|
+
|
|
18
|
+
def checkbox(prompt: str, choices: list[CheckboxChoice], cursor: str = ">", color: ANSIColor | tuple[int, int, int] | None = None, highlightMode: HighlightMode = HighlightMode.INVERT, keepOnScreen: bool = False) -> list[object]:
|
|
19
|
+
if not choices:
|
|
20
|
+
return []
|
|
21
|
+
|
|
22
|
+
pos: int = 0
|
|
23
|
+
colorStr: str = f"\x1b[{color.value}m" if isinstance(color, ANSIColor) else f"\x1b[38;2;{color[0]};{color[1]};{color[2]}m" if isinstance(color, tuple) else ''
|
|
24
|
+
|
|
25
|
+
@runImmediately
|
|
26
|
+
def draw() -> None:
|
|
27
|
+
sys.stdout.write(f"{prompt}\r\n")
|
|
28
|
+
|
|
29
|
+
for i, choice in enumerate(choices):
|
|
30
|
+
sys.stdout.write(f"{colorStr if not (highlightMode == highlightMode.COLOR) else ''}{cursor if i == pos else " "*len(cursor)} {(f'{_ANSI_START}7m' if highlightMode == HighlightMode.INVERT else colorStr if highlightMode == highlightMode.COLOR else f"{_ANSI_START}1m" if highlightMode == highlightMode.BOLD else '') if i == pos else ""} [{"X" if choice.selected else " "}] {choice.choice} {f'{_ANSI_START}0m' if (colorStr) or (i == pos) else ""}\r\n")
|
|
31
|
+
|
|
32
|
+
sys.stdout.write(f"\r\n{colorStr if not (highlightMode == highlightMode.COLOR) else ''}{cursor if pos == len(choices) else " "*len(cursor)} {(f'{_ANSI_START}7m' if highlightMode == HighlightMode.INVERT else colorStr if highlightMode == highlightMode.COLOR else f"{_ANSI_START}1m" if highlightMode == highlightMode.BOLD else '') if pos == len(choices) else ""} Confirm {f'{_ANSI_START}0m' if (colorStr) or (pos == len(choices)) else ""}\r\n")
|
|
33
|
+
|
|
34
|
+
sys.stdout.flush()
|
|
35
|
+
|
|
36
|
+
@handleInput
|
|
37
|
+
def inputHandler(key: Key | str) -> bool:
|
|
38
|
+
nonlocal pos
|
|
39
|
+
match (key.lower() if isinstance(key, str) else key):
|
|
40
|
+
case Key.CTRL_Q | "q":
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
case Key.UP | Key.DOWN:
|
|
44
|
+
pos = (pos + (-1 if key == Key.UP else 1)) % (len(choices) + 1)
|
|
45
|
+
|
|
46
|
+
case Key.ENTER:
|
|
47
|
+
if pos == len(choices):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
choices[pos].selected = not choices[pos].selected
|
|
51
|
+
|
|
52
|
+
sys.stdout.write(f"\x1b[{len(choices)+3}A")
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
draw()
|
|
55
|
+
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
if not keepOnScreen:
|
|
59
|
+
for _ in range(len(choices) + 3):
|
|
60
|
+
sys.stdout.write(f"\x1b[A\x1b[2K")
|
|
61
|
+
|
|
62
|
+
sys.stdout.flush()
|
|
63
|
+
|
|
64
|
+
return [choice.choice for choice in choices if choice.selected]
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
print(checkbox("Select Features:", [
|
|
68
|
+
CheckboxChoice("Math", True),
|
|
69
|
+
CheckboxChoice("Logging")
|
|
70
|
+
], cursor="►", color=ANSIColor.BRIGHT_GREEN, highlightMode=HighlightMode.COLOR))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from inputkit import handleInput, Key
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
def _inSlice(s: slice, n: int):
|
|
5
|
+
start = s.start or 0
|
|
6
|
+
step = s.step or 1
|
|
7
|
+
stop = s.stop
|
|
8
|
+
|
|
9
|
+
if stop is None:
|
|
10
|
+
if step > 0:
|
|
11
|
+
return n >= start and (n - start) % step == 0
|
|
12
|
+
|
|
13
|
+
else:
|
|
14
|
+
return n <= start and (start - n) % (-step) == 0
|
|
15
|
+
|
|
16
|
+
if step > 0:
|
|
17
|
+
return start <= n < stop and (n - start) % step == 0
|
|
18
|
+
else:
|
|
19
|
+
return stop < n <= start and (start - n) % (-step) == 0
|
|
20
|
+
|
|
21
|
+
def numInput(prompt: str, range: slice, n: int | None = None) -> int:
|
|
22
|
+
"""Prompts the user in the terminal with a custom input for a number spinner like `Threads: [4] 🠑🠓`
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
prompt (str): The prompt.
|
|
26
|
+
range (slice): A slice for the min, max, and step for the limits of the input
|
|
27
|
+
n (int | None, optional): The starting number, and must be in the range. Defaults to the lowest number in the range.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
int: The number the user picked.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
if not _inSlice(range, n or range.start):
|
|
34
|
+
raise ValueError("n is not in range")
|
|
35
|
+
|
|
36
|
+
num: int = n or range.start
|
|
37
|
+
|
|
38
|
+
def draw():
|
|
39
|
+
sys.stdout.write(f"\r\x1b[2K{prompt} [{num}] \x1b[90m🠕🠗\x1b[0m")
|
|
40
|
+
|
|
41
|
+
draw()
|
|
42
|
+
|
|
43
|
+
@handleInput
|
|
44
|
+
def inputHandler(key: str | Key) -> bool:
|
|
45
|
+
nonlocal num
|
|
46
|
+
match key:
|
|
47
|
+
case Key.ENTER:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
case Key.UP | Key.DOWN:
|
|
51
|
+
n = (range.step or 1) * (-1 if key == Key.DOWN else 1)
|
|
52
|
+
if _inSlice(range, num + n):
|
|
53
|
+
num += n
|
|
54
|
+
|
|
55
|
+
draw()
|
|
56
|
+
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
print()
|
|
60
|
+
|
|
61
|
+
return num
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from inputkit import Key, handleInput
|
|
2
|
+
from ._lib import ANSIColor, HighlightMode, _ANSI_START
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
VERSION = "1.0.0"
|
|
6
|
+
|
|
7
|
+
def menu(prompt: str, choices: list[object], defaultIdx: int = 0, cursor: str = ">", color: ANSIColor | tuple[int, int, int] | None = None, highlightMode: HighlightMode = HighlightMode.INVERT) -> object:
|
|
8
|
+
if not choices:
|
|
9
|
+
return None
|
|
10
|
+
|
|
11
|
+
pos: int = defaultIdx % len(choices)
|
|
12
|
+
colorStr: str = f"\x1b[{color.value}m" if isinstance(color, ANSIColor) else f"\x1b[38;2;{color[0]};{color[1]};{color[2]}m" if isinstance(color, tuple) else ''
|
|
13
|
+
|
|
14
|
+
def draw() -> None:
|
|
15
|
+
sys.stdout.write(f"{prompt}\r\n")
|
|
16
|
+
|
|
17
|
+
for i, choice in enumerate(choices):
|
|
18
|
+
sys.stdout.write(f"{colorStr if not (highlightMode == highlightMode.COLOR) else ''}{f"{cursor}" if i == pos else " "} {(f"{_ANSI_START}7m" if highlightMode == HighlightMode.INVERT else colorStr if highlightMode == highlightMode.COLOR else f"{_ANSI_START}1m" if highlightMode == highlightMode.BOLD else '') if i == pos else ""} {choice} {f"{_ANSI_START}0m" if (colorStr) or (i == pos) else ""}\r\n")
|
|
19
|
+
|
|
20
|
+
sys.stdout.flush()
|
|
21
|
+
|
|
22
|
+
draw()
|
|
23
|
+
|
|
24
|
+
@handleInput
|
|
25
|
+
def inputHandler(key: Key | str) -> bool:
|
|
26
|
+
nonlocal pos
|
|
27
|
+
match key:
|
|
28
|
+
case Key.ENTER:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
case Key.UP:
|
|
32
|
+
pos = (pos - 1) % len(choices)
|
|
33
|
+
|
|
34
|
+
case Key.DOWN:
|
|
35
|
+
pos = (pos + 1) % len(choices)
|
|
36
|
+
|
|
37
|
+
sys.stdout.write(f"\x1b[{len(choices)+1}A")
|
|
38
|
+
sys.stdout.flush()
|
|
39
|
+
draw()
|
|
40
|
+
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
return choices[pos]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
print(menu("Select Language:", ["Python", "Rust", "Ruby", "Lua"], cursor="►", color=(255, 213, 64), highlightMode=HighlightMode.COLOR))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from inputkit import handleInput, Key
|
|
2
|
+
from io import StringIO
|
|
3
|
+
from ._lib import ANSIColor
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
VERSION = "1.0.0"
|
|
7
|
+
|
|
8
|
+
def percent(prompt: str, interval: float = 0.01, n: float = 0.0, width: int = 10, color: ANSIColor = ANSIColor.BRIGHT_WHITE) -> float:
|
|
9
|
+
"""Prompts the user and gets a percent, shown as a slider.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
prompt (str): The prompt
|
|
13
|
+
interval (float, optional): The change when moving the slider. Must be between 0.01 - 1. Defaults to 0.01.
|
|
14
|
+
n (float, optional): Initial percent. Must be between 0 - 1. Defaults to 0.0.
|
|
15
|
+
width (int, optional): The width of the slider. Defaults to 10.
|
|
16
|
+
color (ANSIColor, optional): The color of the edges of the slider, the percent amount displayed, and the color of the full values in the slider. Defaults to ANSIColor.BRIGHT_WHITE.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError: if n is not between 0 - 1
|
|
20
|
+
ValueError: if interval is not between 0.01 - 1
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
float: the float representation of the percent
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if not (0.0 <= n <= 1.0):
|
|
27
|
+
raise ValueError(f"n must be between 0 - 1, not {n}")
|
|
28
|
+
|
|
29
|
+
if not (0.01 <= interval <= 1.0):
|
|
30
|
+
raise ValueError(f"interval must be between 0.01 - 1, not {n}")
|
|
31
|
+
|
|
32
|
+
num: int = round(100 * n)
|
|
33
|
+
step: int = round(100 * interval)
|
|
34
|
+
|
|
35
|
+
def draw():
|
|
36
|
+
n = 100 / (width*2)
|
|
37
|
+
slide: StringIO = StringIO()
|
|
38
|
+
for i in range(1, width+1):
|
|
39
|
+
slide.write(f"\x1b[{color}m#" if num >= i * (n*2) else "\x1b[39m=" if (num >= (i-1) * (n*2)) and ((num / n) % 2 >= 1) else "\x1b[90m-" if (num > (i-1) * (n*2)) else " ")
|
|
40
|
+
|
|
41
|
+
sys.stdout.write(f"\r\x1b[2K{prompt} \x1b[{color}m[{slide.getvalue()}\x1b[{color}m] {num}%\x1b[0m")
|
|
42
|
+
|
|
43
|
+
draw()
|
|
44
|
+
|
|
45
|
+
@handleInput
|
|
46
|
+
def inputHandler(key: str | Key) -> bool:
|
|
47
|
+
nonlocal num
|
|
48
|
+
match key:
|
|
49
|
+
case Key.ENTER:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
case Key.LEFT | Key.RIGHT:
|
|
53
|
+
num += step if key == Key.RIGHT else -step
|
|
54
|
+
if not (0 <= num <= 100):
|
|
55
|
+
num -= step if key == Key.RIGHT else -step
|
|
56
|
+
|
|
57
|
+
draw()
|
|
58
|
+
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
print()
|
|
62
|
+
|
|
63
|
+
return num / 100
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clicanvas
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: A python library that deals with terminal-based user input. From a TUI framework to custom input functions.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: inputkit>=1.0.2
|
|
8
|
+
Requires-Dist: regex>=2026.2.28
|
|
9
|
+
Provides-Extra: thonny
|
|
10
|
+
|
|
11
|
+
# CLICanvas
|
|
12
|
+
|
|
13
|
+
CLICanvas is a python library that deals with terminal-based user input.
|
|
14
|
+
From a TUI framework to custom input functions.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Custom input function and modes, including a mirror of readline and getpass
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/clicanvas/__init__.py
|
|
4
|
+
src/clicanvas.egg-info/PKG-INFO
|
|
5
|
+
src/clicanvas.egg-info/SOURCES.txt
|
|
6
|
+
src/clicanvas.egg-info/dependency_links.txt
|
|
7
|
+
src/clicanvas.egg-info/requires.txt
|
|
8
|
+
src/clicanvas.egg-info/top_level.txt
|
|
9
|
+
src/clicanvas/input/__init__.py
|
|
10
|
+
src/clicanvas/input/base.py
|
|
11
|
+
src/clicanvas/input/confirm.py
|
|
12
|
+
src/clicanvas/input/constrained.py
|
|
13
|
+
src/clicanvas/input/getpass.py
|
|
14
|
+
src/clicanvas/input/readline.py
|
|
15
|
+
src/clicanvas/microwidgets/__init__.py
|
|
16
|
+
src/clicanvas/microwidgets/_lib.py
|
|
17
|
+
src/clicanvas/microwidgets/checkbox.py
|
|
18
|
+
src/clicanvas/microwidgets/incrementer.py
|
|
19
|
+
src/clicanvas/microwidgets/menu.py
|
|
20
|
+
src/clicanvas/microwidgets/slider.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clicanvas
|