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 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
@@ -0,0 +1,8 @@
1
+ # CLICanvas
2
+
3
+ CLICanvas is a python library that deals with terminal-based user input.
4
+ From a TUI framework to custom input functions.
5
+
6
+ ## Features
7
+
8
+ - Custom input function and modes, including a mirror of readline and getpass
@@ -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 = []
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,4 @@
1
+ inputkit>=1.0.2
2
+ regex>=2026.2.28
3
+
4
+ [thonny]
@@ -0,0 +1 @@
1
+ clicanvas