Created
April 1, 2024 00:09
-
-
Save qexat/d5e7cd84406291b4672a752854135cbc to your computer and use it in GitHub Desktop.
idk i'm just having fun
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# pyright: reportUnusedCallResult = false | |
# Requires outspin: | |
# pip install outspin | |
from __future__ import annotations | |
import io | |
import os | |
import shutil | |
import typing | |
from outspin import get_key # pyright: ignore[reportMissingTypeStubs] | |
if typing.TYPE_CHECKING: | |
from collections.abc import Generator | |
def check_int() -> Generator[bool, str, None]: | |
""" | |
Pseudo-server to check if a string contains an integer. | |
TODO: add support for sign (-/+) | |
>>> checker = check_int() | |
>>> checker.send("42") | |
True | |
>>> checker.send("42.0") | |
False | |
""" | |
def _check_int() -> Generator[bool, str, None]: | |
is_int = True | |
while True: | |
string = yield is_int | |
is_int = string.isdecimal() | |
generator = _check_int() | |
generator.send(None) # pyright: ignore[reportArgumentType] | |
return generator | |
def write(string: str) -> None: | |
""" | |
Helper because this kind of `print` is used a lot in the code below. | |
""" | |
print(string, end="", flush=True) | |
def report_is_int(is_int: bool) -> None: | |
""" | |
Handy function to print below the input whether it's an integer or not. | |
""" | |
write(f"\n\x1b[2KIs integer: \x1b[1;96m{is_int}\x1b[22;39m") | |
def main() -> int: | |
lbuffer = io.StringIO() | |
# for practicality, the right buffer is stored backwards, so to get its | |
# actual contents, we need to reverse the result of `getvalue()` | |
rbuffer = io.StringIO() | |
s_sel = 0 # start of selection | |
e_sel = 0 # end of selection | |
checker = check_int() | |
while True: | |
try: | |
char = get_key() | |
except KeyboardInterrupt: | |
break | |
if char == "^D": | |
return os.EX_OK | |
if char == "enter": | |
break | |
lcontents = lbuffer.getvalue() | |
rcontents = rbuffer.getvalue()[::-1] | |
# the full buffer is the two buffers combined | |
contents = lcontents + rcontents | |
# the cursor is at the end of the left buffer | |
pos = len(lcontents) | |
# effectively the selection span size but we treat as a | |
# "is there a selection" for now | |
sel = e_sel - s_sel | |
match char: | |
case "backspace": | |
if sel: | |
# delete the selection from both buffers | |
lbuffer = io.StringIO(lcontents[:s_sel] + lcontents[e_sel + 1 :]) | |
rbuffer = io.StringIO(rcontents[e_sel - s_sel :][::-1]) | |
lbuffer.seek(0, os.SEEK_END) | |
rbuffer.seek(0, os.SEEK_END) | |
# /!\ we reset selection because we dumped it! | |
s_sel = e_sel = 0 | |
else: | |
# remove the last character from the left buffer and moves | |
# the cursor to the left | |
lbuffer = io.StringIO(lcontents[:-1]) | |
pos = max(0, pos - 1) | |
lbuffer.seek(0, os.SEEK_END) | |
case "left": | |
# reset selection as shift is not held | |
s_sel = e_sel = 0 | |
# only if lcontents is not empty then we can move to the left | |
# (there are characters to be consumed) -- otherwise we are | |
# already at the "leftest" position and we can't move further | |
if lcontents: | |
consumed = lcontents[-1] | |
pos -= 1 | |
lbuffer = io.StringIO(lcontents[:-1]) | |
lbuffer.seek(0, os.SEEK_END) | |
rbuffer.write(consumed) | |
case "right": | |
# reset selection as shift is not held | |
s_sel = e_sel = 0 | |
# only if rcontents is not empty then we can move to the right | |
# (there are characters to be consumed) -- otherwise we are | |
# already at the "rightest" position and we can't move further | |
if rcontents: | |
consumed = rcontents[0] | |
pos += 1 | |
rbuffer = io.StringIO(rcontents[::-1][:-1]) | |
rbuffer.seek(0, os.SEEK_END) | |
lbuffer.write(consumed) | |
case "shift+left": | |
# no lcontents => nothing to select on the left from | |
if lcontents: | |
# not rsel => there was no selection before so we fix the | |
# selection end to where we started (i.e. the cursor pos) | |
if not e_sel: | |
e_sel = pos | |
# same code than for a simple move to the left | |
consumed = lcontents[-1] | |
pos -= 1 | |
lbuffer = io.StringIO(lcontents[:-1]) | |
lbuffer.seek(0, os.SEEK_END) | |
rbuffer.write(consumed) | |
# selection start is the new position | |
s_sel = pos | |
case "shift+right": | |
# TODO: handle shift+right | |
# it is not as trivial as mirroring shift+left because we need | |
# to handle many edge cases where shift+left and shift+right | |
# would interfere | |
pass | |
case _: # TODO: handle more control sequences | |
# we yeet the current selection if there is one | |
# (it's gonna be replaced with the typed character) | |
if sel: | |
lbuffer = io.StringIO(lcontents[:s_sel] + lcontents[e_sel + 1 :]) | |
rbuffer = io.StringIO(rcontents[e_sel - s_sel :][::-1]) | |
lbuffer.seek(0, os.SEEK_END) | |
rbuffer.seek(0, os.SEEK_END) | |
s_sel = e_sel = 0 | |
# prevent writing more characters if it takes the whole terminal width | |
if len(contents) < shutil.get_terminal_size().columns or sel: | |
echar = " " if char == "space" else char | |
pos += lbuffer.write(echar) | |
contents = lbuffer.getvalue() + rbuffer.getvalue()[::-1] | |
sel = e_sel - s_sel | |
is_int = checker.send(contents) | |
# 1 = red, 2 = green | |
color = 2 if is_int else 1 | |
write(f"\r\x1b[2K\x1b[1;3{color}m") | |
# we write every character one by one because we need to special-case | |
# the selected ones in order to make them stand out | |
for index, char in enumerate(contents): | |
if index == s_sel: | |
write("\x1b[7m") # enable inverted fg/bg | |
if index == e_sel: | |
write("\x1b[27m") # disable inverted fg/bg | |
write(char) | |
write("\x1b[22;27;39m") # so it doesn't leak across lines | |
report_is_int(is_int) | |
# move the stdout cursor to where it should be | |
write(f"\x1b[A\x1b[{pos + 1}G") | |
contents = lbuffer.getvalue() + rbuffer.getvalue()[::-1] | |
is_int = checker.send(contents) | |
report_is_int(is_int) | |
print() # make sure the program ends with a newline | |
return os.EX_OK if is_int else os.EX_DATAERR | |
if __name__ == "__main__": | |
raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment