cli-ih 0.6.3__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli_ih/__init__.py +1 -0
- cli_ih/asyncClient.py +86 -17
- cli_ih/client.py +76 -24
- cli_ih/utils.py +51 -0
- {cli_ih-0.6.3.dist-info → cli_ih-0.7.0.dist-info}/METADATA +1 -1
- cli_ih-0.7.0.dist-info/RECORD +9 -0
- {cli_ih-0.6.3.dist-info → cli_ih-0.7.0.dist-info}/WHEEL +1 -1
- cli_ih-0.6.3.dist-info/RECORD +0 -8
- {cli_ih-0.6.3.dist-info → cli_ih-0.7.0.dist-info}/top_level.txt +0 -0
cli_ih/__init__.py
CHANGED
cli_ih/asyncClient.py
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
from typing import Coroutine, Callable, Any
|
|
2
2
|
from .exceptions import HandlerClosed
|
|
3
|
-
import
|
|
3
|
+
from .utils import safe_print as print, register_handler
|
|
4
|
+
import logging, warnings, asyncio, inspect, threading, sys, shutil, msvcrt
|
|
4
5
|
|
|
5
6
|
class AsyncInputHandler:
|
|
6
|
-
def __init__(self, cursor = "", *, logger: logging.Logger | None = None, register_defaults: bool = True):
|
|
7
|
+
def __init__(self, cursor = "", thread_mode: bool = True, *, logger: logging.Logger | None = None, register_defaults: bool = True):
|
|
8
|
+
register_handler(self)
|
|
7
9
|
self.commands = {}
|
|
8
10
|
self.is_running = False
|
|
11
|
+
self.thread_mode = thread_mode
|
|
9
12
|
self.cursor = f"{cursor.strip()} " if cursor else ""
|
|
10
13
|
self.global_logger = logger if logger else None
|
|
11
14
|
self.logger = logger.getChild("InputHandler") if logger else None
|
|
12
15
|
self.register_defaults = register_defaults
|
|
16
|
+
self.print_lock = threading.Lock()
|
|
17
|
+
self.input_buffer = ""
|
|
18
|
+
self.processing_command = False
|
|
19
|
+
|
|
13
20
|
if self.register_defaults:
|
|
14
21
|
self.register_default_commands()
|
|
15
22
|
else:
|
|
@@ -71,23 +78,71 @@ class AsyncInputHandler:
|
|
|
71
78
|
return func
|
|
72
79
|
return decorator
|
|
73
80
|
|
|
74
|
-
|
|
75
|
-
"""Starts the input handler loop in a
|
|
81
|
+
def start(self):
|
|
82
|
+
"""Starts the input handler loop. Runs in a thread if thread_mode is True, otherwise blocks."""
|
|
76
83
|
self.is_running = True
|
|
84
|
+
if self.thread_mode:
|
|
85
|
+
thread = threading.Thread(target=self._start_thread, daemon=True)
|
|
86
|
+
thread.start()
|
|
87
|
+
else:
|
|
88
|
+
self._start_thread()
|
|
89
|
+
|
|
90
|
+
def _start_thread(self):
|
|
91
|
+
asyncio.run(self._run())
|
|
92
|
+
|
|
93
|
+
async def _run(self):
|
|
94
|
+
"""Starts the input handler loop in a separate thread if thread mode is enabled."""
|
|
77
95
|
loop = asyncio.get_running_loop()
|
|
78
96
|
input_queue = asyncio.Queue()
|
|
79
97
|
|
|
80
98
|
def _input_worker():
|
|
99
|
+
# Initial prompt
|
|
100
|
+
with self.print_lock:
|
|
101
|
+
sys.stdout.write(self.cursor)
|
|
102
|
+
sys.stdout.flush()
|
|
103
|
+
|
|
81
104
|
while self.is_running:
|
|
82
105
|
try:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
if msvcrt.kbhit():
|
|
107
|
+
char = msvcrt.getwch()
|
|
108
|
+
|
|
109
|
+
if char == '\r': # Enter
|
|
110
|
+
with self.print_lock:
|
|
111
|
+
sys.stdout.write('\n')
|
|
112
|
+
sys.stdout.flush()
|
|
113
|
+
text = self.input_buffer
|
|
114
|
+
self.input_buffer = ""
|
|
115
|
+
loop.call_soon_threadsafe(input_queue.put_nowait, text)
|
|
116
|
+
|
|
117
|
+
elif char == '\x08': # Backspace
|
|
118
|
+
if len(self.input_buffer) > 0:
|
|
119
|
+
self.input_buffer = self.input_buffer[:-1]
|
|
120
|
+
with self.print_lock:
|
|
121
|
+
sys.stdout.write('\b \b')
|
|
122
|
+
sys.stdout.flush()
|
|
123
|
+
|
|
124
|
+
elif char == '\x03': # Ctrl+C
|
|
125
|
+
loop.call_soon_threadsafe(input_queue.put_nowait, KeyboardInterrupt)
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
# Verify printable
|
|
130
|
+
if char.isprintable():
|
|
131
|
+
self.input_buffer += char
|
|
132
|
+
with self.print_lock:
|
|
133
|
+
sys.stdout.write(char)
|
|
134
|
+
sys.stdout.flush()
|
|
135
|
+
else:
|
|
136
|
+
pass
|
|
137
|
+
# Small sleep to prevent high CPU usage,
|
|
138
|
+
# but we are in a dedicated thread so it's fine-ish,
|
|
139
|
+
# actually explicit sleep is good.
|
|
140
|
+
import time
|
|
141
|
+
time.sleep(0.01)
|
|
142
|
+
|
|
88
143
|
except Exception:
|
|
89
144
|
break
|
|
90
|
-
|
|
145
|
+
|
|
91
146
|
thread = threading.Thread(target=_input_worker, daemon=True)
|
|
92
147
|
thread.start()
|
|
93
148
|
|
|
@@ -106,26 +161,32 @@ class AsyncInputHandler:
|
|
|
106
161
|
|
|
107
162
|
try:
|
|
108
163
|
sig = inspect.signature(func)
|
|
164
|
+
has_var_args = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in sig.parameters.values())
|
|
165
|
+
if has_var_args:
|
|
166
|
+
final_args = args
|
|
167
|
+
else:
|
|
168
|
+
params = [p for p in sig.parameters.values() if p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY)]
|
|
169
|
+
final_args = args[:len(params)]
|
|
109
170
|
if is_legacy:
|
|
110
|
-
sig.bind(
|
|
171
|
+
sig.bind(final_args)
|
|
111
172
|
else:
|
|
112
|
-
sig.bind(*
|
|
173
|
+
sig.bind(*final_args)
|
|
113
174
|
except TypeError as e:
|
|
114
|
-
self.__warning(f"Argument error for legacy command '{name}': {e}")
|
|
175
|
+
self.__warning(f"Argument error for {"legacy " if is_legacy else ""}command '{name}': {e}")
|
|
115
176
|
return
|
|
116
177
|
|
|
117
178
|
try:
|
|
118
179
|
if is_legacy:
|
|
119
180
|
warnings.warn("This way of running commands id Deprecated. And should be changed to the new decorator way.", DeprecationWarning, 2)
|
|
120
181
|
if inspect.iscoroutinefunction(func):
|
|
121
|
-
await func(
|
|
182
|
+
await func(final_args)
|
|
122
183
|
else:
|
|
123
|
-
await asyncio.to_thread(func,
|
|
184
|
+
await asyncio.to_thread(func, final_args)
|
|
124
185
|
else:
|
|
125
186
|
if inspect.iscoroutinefunction(func):
|
|
126
|
-
await func(*
|
|
187
|
+
await func(*final_args)
|
|
127
188
|
else:
|
|
128
|
-
await asyncio.to_thread(func, *
|
|
189
|
+
await asyncio.to_thread(func, *final_args)
|
|
129
190
|
|
|
130
191
|
except HandlerClosed as e:
|
|
131
192
|
raise e
|
|
@@ -146,6 +207,7 @@ class AsyncInputHandler:
|
|
|
146
207
|
if not user_input:
|
|
147
208
|
continue
|
|
148
209
|
|
|
210
|
+
self.processing_command = True
|
|
149
211
|
cmdargs = user_input.split(' ')
|
|
150
212
|
command_name = cmdargs[0].lower()
|
|
151
213
|
args = cmdargs[1:]
|
|
@@ -153,6 +215,13 @@ class AsyncInputHandler:
|
|
|
153
215
|
await _run_command(self.commands, command_name, args)
|
|
154
216
|
else:
|
|
155
217
|
self.__warning(f"Unknown command: '{command_name}'")
|
|
218
|
+
self.processing_command = False
|
|
219
|
+
|
|
220
|
+
# Reprompt after command execution
|
|
221
|
+
with self.print_lock:
|
|
222
|
+
sys.stdout.write(self.cursor)
|
|
223
|
+
sys.stdout.flush()
|
|
224
|
+
|
|
156
225
|
except EOFError:
|
|
157
226
|
self.__error("Input ended unexpectedly.")
|
|
158
227
|
break
|
cli_ih/client.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from typing import Callable
|
|
2
2
|
from .exceptions import HandlerClosed
|
|
3
|
-
import
|
|
3
|
+
from .utils import safe_print as print, register_handler
|
|
4
|
+
import logging, warnings, sys, shutil, threading, msvcrt
|
|
4
5
|
|
|
5
6
|
class InputHandler:
|
|
6
7
|
def __init__(self, thread_mode = True, cursor = "", *, logger: logging.Logger | None = None, register_defaults: bool = True):
|
|
8
|
+
register_handler(self)
|
|
7
9
|
self.commands = {}
|
|
8
10
|
self.is_running = False
|
|
9
11
|
self.thread_mode = thread_mode
|
|
@@ -12,6 +14,10 @@ class InputHandler:
|
|
|
12
14
|
self.global_logger = logger if logger else None
|
|
13
15
|
self.logger = logger.getChild("InputHandler") if logger else None
|
|
14
16
|
self.register_defaults = register_defaults
|
|
17
|
+
self.print_lock = threading.Lock()
|
|
18
|
+
self.input_buffer = ""
|
|
19
|
+
self.processing_command = False
|
|
20
|
+
|
|
15
21
|
if self.register_defaults:
|
|
16
22
|
self.register_default_commands()
|
|
17
23
|
else:
|
|
@@ -85,30 +91,37 @@ class InputHandler:
|
|
|
85
91
|
func = command.get("cmd")
|
|
86
92
|
is_legacy = command.get("legacy", False)
|
|
87
93
|
if callable(func):
|
|
94
|
+
sig = inspect.signature(func)
|
|
95
|
+
has_var_args = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in sig.parameters.values())
|
|
96
|
+
|
|
97
|
+
if has_var_args:
|
|
98
|
+
final_args = args
|
|
99
|
+
else:
|
|
100
|
+
params = [p for p in sig.parameters.values() if p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY)]
|
|
101
|
+
final_args = args[:len(params)]
|
|
102
|
+
|
|
88
103
|
if is_legacy:
|
|
89
104
|
try:
|
|
90
|
-
sig
|
|
91
|
-
sig.bind(args)
|
|
105
|
+
sig.bind(final_args)
|
|
92
106
|
except TypeError as e:
|
|
93
107
|
self.__warning(f"Argument error for legacy command '{name}': {e}")
|
|
94
108
|
return
|
|
95
109
|
|
|
96
110
|
try:
|
|
97
111
|
warnings.warn("This way of running commands id Deprecated. And should be changed to the new decorator way.", DeprecationWarning, 2)
|
|
98
|
-
func(
|
|
112
|
+
func(final_args)
|
|
99
113
|
except HandlerClosed as e:
|
|
100
114
|
raise e
|
|
101
115
|
except Exception as e:
|
|
102
116
|
self.__exeption(f"An error occurred in legacy command '{name}'", e)
|
|
103
117
|
else:
|
|
104
118
|
try:
|
|
105
|
-
sig
|
|
106
|
-
sig.bind(*args)
|
|
119
|
+
sig.bind(*final_args)
|
|
107
120
|
except TypeError as e:
|
|
108
121
|
self.__warning(f"Argument error for command '{name}': {e}")
|
|
109
122
|
return
|
|
110
123
|
try:
|
|
111
|
-
func(*
|
|
124
|
+
func(*final_args)
|
|
112
125
|
except HandlerClosed as e:
|
|
113
126
|
raise e
|
|
114
127
|
except Exception as e:
|
|
@@ -123,26 +136,65 @@ class InputHandler:
|
|
|
123
136
|
"""Continuously listens for user input and processes commands."""
|
|
124
137
|
while self.is_running:
|
|
125
138
|
try:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
# Initial prompt
|
|
140
|
+
with self.print_lock:
|
|
141
|
+
sys.stdout.write(self.cursor)
|
|
142
|
+
sys.stdout.flush()
|
|
143
|
+
|
|
144
|
+
while self.is_running:
|
|
145
|
+
if msvcrt.kbhit():
|
|
146
|
+
char = msvcrt.getwch()
|
|
147
|
+
|
|
148
|
+
if char == '\r': # Enter
|
|
149
|
+
with self.print_lock:
|
|
150
|
+
sys.stdout.write('\n')
|
|
151
|
+
sys.stdout.flush()
|
|
152
|
+
text = self.input_buffer
|
|
153
|
+
self.input_buffer = ""
|
|
154
|
+
|
|
155
|
+
if text:
|
|
156
|
+
self.processing_command = True
|
|
157
|
+
cmdargs = text.split(' ')
|
|
158
|
+
command_name = cmdargs[0].lower()
|
|
159
|
+
args = cmdargs[1:]
|
|
160
|
+
if command_name in self.commands:
|
|
161
|
+
_run_command(self.commands, command_name, args)
|
|
162
|
+
else:
|
|
163
|
+
self.__warning(f"Unknown command: '{command_name}'")
|
|
164
|
+
self.processing_command = False
|
|
165
|
+
|
|
166
|
+
# Break inner loop to reprompt
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
elif char == '\x08': # Backspace
|
|
170
|
+
if len(self.input_buffer) > 0:
|
|
171
|
+
self.input_buffer = self.input_buffer[:-1]
|
|
172
|
+
with self.print_lock:
|
|
173
|
+
sys.stdout.write('\b \b')
|
|
174
|
+
sys.stdout.flush()
|
|
175
|
+
|
|
176
|
+
elif char == '\x03': # Ctrl+C
|
|
177
|
+
self.__error("Input interrupted.")
|
|
178
|
+
self.is_running = False
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
# Verify printable
|
|
183
|
+
if char.isprintable():
|
|
184
|
+
self.input_buffer += char
|
|
185
|
+
with self.print_lock:
|
|
186
|
+
sys.stdout.write(char)
|
|
187
|
+
sys.stdout.flush()
|
|
188
|
+
else:
|
|
189
|
+
import time
|
|
190
|
+
time.sleep(0.01)
|
|
191
|
+
|
|
143
192
|
except HandlerClosed:
|
|
144
193
|
self.__info("Input Handler exited.")
|
|
145
194
|
break
|
|
195
|
+
except Exception as e:
|
|
196
|
+
self.__exeption("Input loop error", e)
|
|
197
|
+
break
|
|
146
198
|
self.is_running = False
|
|
147
199
|
if self.thread_mode:
|
|
148
200
|
self.thread = threading.Thread(target=_thread, daemon=True)
|
cli_ih/utils.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
_HANDLER = None
|
|
5
|
+
|
|
6
|
+
def register_handler(handler):
|
|
7
|
+
global _HANDLER
|
|
8
|
+
_HANDLER = handler
|
|
9
|
+
|
|
10
|
+
def safe_print(msg: str, cursor: str | None = None, input_buffer: str | None = None):
|
|
11
|
+
"""
|
|
12
|
+
Prints a message safely while preserving the current input buffer and cursor.
|
|
13
|
+
This ensures logs appear above the input line appropriately.
|
|
14
|
+
"""
|
|
15
|
+
if cursor is None and input_buffer is None and _HANDLER is not None:
|
|
16
|
+
lock = None
|
|
17
|
+
try:
|
|
18
|
+
# Try to acquire lock if available to prevent race conditions
|
|
19
|
+
lock = getattr(_HANDLER, "print_lock", None)
|
|
20
|
+
if lock:
|
|
21
|
+
lock.acquire()
|
|
22
|
+
|
|
23
|
+
cursor = str(getattr(_HANDLER, "cursor", ""))
|
|
24
|
+
input_buffer = str(getattr(_HANDLER, "input_buffer", ""))
|
|
25
|
+
processing_command = getattr(_HANDLER, "processing_command", False)
|
|
26
|
+
|
|
27
|
+
if processing_command:
|
|
28
|
+
cursor = ""
|
|
29
|
+
input_buffer = ""
|
|
30
|
+
|
|
31
|
+
_do_safe_print(msg, cursor, input_buffer)
|
|
32
|
+
finally:
|
|
33
|
+
if lock:
|
|
34
|
+
lock.release()
|
|
35
|
+
else:
|
|
36
|
+
# Fallback or explicit arguments
|
|
37
|
+
_do_safe_print(msg, cursor or "", input_buffer or "")
|
|
38
|
+
|
|
39
|
+
def _do_safe_print(msg: str, cursor: str, input_buffer: str):
|
|
40
|
+
try:
|
|
41
|
+
columns = shutil.get_terminal_size().columns
|
|
42
|
+
except:
|
|
43
|
+
columns = 80
|
|
44
|
+
|
|
45
|
+
# Clear line
|
|
46
|
+
sys.stdout.write('\r' + ' ' * (columns - 1) + '\r')
|
|
47
|
+
# Print message
|
|
48
|
+
sys.stdout.write(f"{msg}\n")
|
|
49
|
+
# Reprint cursor and buffer
|
|
50
|
+
sys.stdout.write(f"{cursor}{input_buffer}")
|
|
51
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
cli_ih/__init__.py,sha256=wCwv9grczxpu7EXqunCdFgAWlx3XEZVw4O4U3fs45PY,278
|
|
2
|
+
cli_ih/asyncClient.py,sha256=L7nCURM15Y9BIrSyfdeLB8PPddr1OF3IA8NWY26HyQQ,11171
|
|
3
|
+
cli_ih/client.py,sha256=GWZH5YX2dDV7A2usgnX9xDm8xVjwLSj09sh5vfHSW-Y,10573
|
|
4
|
+
cli_ih/exceptions.py,sha256=0xqaMCVu-yEURIrB6wEhbf6kfdsRmODJBoDsQTOp29s,156
|
|
5
|
+
cli_ih/utils.py,sha256=HstJPjjlTfVKz68sHU_whSBsOoBKhxizCcycNyQeK2k,1685
|
|
6
|
+
cli_ih-0.7.0.dist-info/METADATA,sha256=yu19a6SGAo79STCAgcxBVU4cqbvcYgIZ_KE7hyl_OcY,2789
|
|
7
|
+
cli_ih-0.7.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
+
cli_ih-0.7.0.dist-info/top_level.txt,sha256=Ve1CRLNXhPyPSkpN0xLu26roh30LQCpNzkF61BZYfk0,7
|
|
9
|
+
cli_ih-0.7.0.dist-info/RECORD,,
|
cli_ih-0.6.3.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
cli_ih/__init__.py,sha256=0dtZNjrIlZMny9M0s6ygYTuo-9sM7BCSJHkBMDYkn8g,247
|
|
2
|
-
cli_ih/asyncClient.py,sha256=VNjBwO5Y0IVl5Gv_oq-GR0GpmnM2jk6gBH08z9VjaKM,7939
|
|
3
|
-
cli_ih/client.py,sha256=WGlA8sxvF173eKEhVbrk_Z3TjHRHIBZzum1rsvE9rsY,7786
|
|
4
|
-
cli_ih/exceptions.py,sha256=0xqaMCVu-yEURIrB6wEhbf6kfdsRmODJBoDsQTOp29s,156
|
|
5
|
-
cli_ih-0.6.3.dist-info/METADATA,sha256=9Qyw0eFfA3RQp9WxRnoY2cJucjqQBcu29kLJrO2odYc,2789
|
|
6
|
-
cli_ih-0.6.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
cli_ih-0.6.3.dist-info/top_level.txt,sha256=Ve1CRLNXhPyPSkpN0xLu26roh30LQCpNzkF61BZYfk0,7
|
|
8
|
-
cli_ih-0.6.3.dist-info/RECORD,,
|
|
File without changes
|