130 lines
4.0 KiB
Python
130 lines
4.0 KiB
Python
from __future__ import annotations
|
||
import asyncio
|
||
import time
|
||
import sys
|
||
from typing import Optional
|
||
|
||
from .kernel import EventBus, Event, EventType
|
||
|
||
|
||
try:
|
||
import msvcrt
|
||
|
||
HAS_MSVCRT = True
|
||
except ImportError:
|
||
HAS_MSVCRT = False
|
||
|
||
|
||
class InputBuffer:
|
||
def __init__(self):
|
||
self.text = ""
|
||
self.last_key_time = time.time()
|
||
self.pause_start: Optional[float] = None
|
||
self.sentence_boundaries = {'.', '!', '?', '。', '!', '?', '\n'}
|
||
self._committed = ""
|
||
|
||
def add_char(self, char: str):
|
||
now = time.time()
|
||
pause = now - self.last_key_time
|
||
if pause > 0.3 and self.text:
|
||
self.pause_start = now
|
||
else:
|
||
self.pause_start = None
|
||
self.last_key_time = now
|
||
|
||
if char == '\r':
|
||
self._committed = self.text
|
||
self.text = ""
|
||
return True
|
||
elif char == '\b' or char == '\x7f':
|
||
self.text = self.text[:-1]
|
||
else:
|
||
self.text += char
|
||
return False
|
||
|
||
@property
|
||
def current_pause(self) -> float:
|
||
if not self.text:
|
||
return 0.0
|
||
return time.time() - self.last_key_time
|
||
|
||
@property
|
||
def is_at_sentence_end(self) -> bool:
|
||
return bool(self.text) and self.text[-1] in self.sentence_boundaries
|
||
|
||
@property
|
||
def committed(self) -> str:
|
||
return self._committed
|
||
|
||
def reset_committed(self):
|
||
self._committed = ""
|
||
|
||
def __repr__(self):
|
||
return f"InputBuffer(text='{self.text}', pause={self.current_pause:.2f}s)"
|
||
|
||
|
||
class StreamInput:
|
||
def __init__(self, bus: EventBus, buffer: Optional[InputBuffer] = None):
|
||
self.bus = bus
|
||
self.buffer = buffer or InputBuffer()
|
||
self._running = False
|
||
|
||
async def listen_cli(self, on_sentence=None):
|
||
self._running = True
|
||
if not HAS_MSVCRT:
|
||
print("[Air] 当前环境不支持实时按键捕获,使用标准输入模式。")
|
||
await self._listen_fallback(on_sentence)
|
||
return
|
||
|
||
while self._running:
|
||
if msvcrt.kbhit():
|
||
ch = msvcrt.getwch()
|
||
if ch == '\x03':
|
||
raise KeyboardInterrupt
|
||
if ch == '\xe0':
|
||
ch2 = msvcrt.getwch()
|
||
if ch2 == 'K':
|
||
continue
|
||
if ch2 == 'M':
|
||
continue
|
||
continue
|
||
is_commit = self.buffer.add_char(ch)
|
||
await self.bus.emit(Event(
|
||
EventType.USER_INPUT_CHANGE,
|
||
{
|
||
"text": self.buffer.text,
|
||
"pause": self.buffer.current_pause,
|
||
"is_end": self.buffer.is_at_sentence_end or is_commit,
|
||
},
|
||
))
|
||
if is_commit and self.buffer.committed.strip():
|
||
text = self.buffer.committed.strip()
|
||
self.buffer.reset_committed()
|
||
await self.bus.emit(Event(EventType.USER_MESSAGE, text))
|
||
if on_sentence:
|
||
await on_sentence(text)
|
||
else:
|
||
await asyncio.sleep(0.05)
|
||
if self.buffer.text and self.buffer.current_pause > 3.0:
|
||
await self.bus.emit(Event(
|
||
EventType.USER_PAUSE,
|
||
{
|
||
"text": self.buffer.text,
|
||
"pause": self.buffer.current_pause,
|
||
},
|
||
))
|
||
|
||
async def _listen_fallback(self, on_sentence=None):
|
||
while self._running:
|
||
line = await asyncio.get_event_loop().run_in_executor(
|
||
None, sys.stdin.readline
|
||
)
|
||
line = line.strip()
|
||
if line:
|
||
await self.bus.emit(Event(EventType.USER_MESSAGE, line))
|
||
if on_sentence:
|
||
await on_sentence(line)
|
||
|
||
def stop(self):
|
||
self._running = False
|