Skip to content

Instantly share code, notes, and snippets.

@qexat
Created April 1, 2024 00:09
Show Gist options
  • Save qexat/d5e7cd84406291b4672a752854135cbc to your computer and use it in GitHub Desktop.
Save qexat/d5e7cd84406291b4672a752854135cbc to your computer and use it in GitHub Desktop.
idk i'm just having fun
#!/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