init
This commit is contained in:
commit
09d4c52043
19 changed files with 542 additions and 0 deletions
0
sandbox/__init__.py
Normal file
0
sandbox/__init__.py
Normal file
0
sandbox/cli/__init__.py
Normal file
0
sandbox/cli/__init__.py
Normal file
66
sandbox/cli/prompt.py
Normal file
66
sandbox/cli/prompt.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'''
|
||||
===== Prompt Handling =====
|
||||
This file implements the Prompt class, allowing for
|
||||
inheritance and instantiation of Prompt objects.
|
||||
The CLI class uses these instead of containing the
|
||||
logic immediately within itself.
|
||||
'''
|
||||
|
||||
from typing import Any
|
||||
|
||||
from noether.cli.style import *
|
||||
from noether.lib.structs import Result
|
||||
|
||||
class Prompt:
|
||||
DEFAULT_PROMPT = 'DEF> '
|
||||
def __init__(self) -> None:
|
||||
self._prompt = self.DEFAULT_PROMPT
|
||||
|
||||
'''
|
||||
Prompt and await command via stdin.
|
||||
'''
|
||||
def prompt(self):
|
||||
command = self.__request()
|
||||
result = self._parse(command)
|
||||
if result.success:
|
||||
self._exec(result.value)
|
||||
else:
|
||||
self.__parse_error(result)
|
||||
|
||||
'''
|
||||
!! OVERRIDE ON INHERITANCE !!
|
||||
Handles the parsing of a given command.
|
||||
'''
|
||||
def _parse(self, command: str) -> Result:
|
||||
return Result.succeed(command)
|
||||
|
||||
def __parse_error(self, error: Result) -> None:
|
||||
err = f' ↳ {style(error.reason, Effect.ITALICS)}'
|
||||
print(style(err, Effect.DIM))
|
||||
|
||||
'''
|
||||
!! OVERRIDE ON INHERITANCE !!
|
||||
Handles the execution of a command that
|
||||
was successfully parsed by a Prompt inheritor.
|
||||
'''
|
||||
def _exec(self, command: Any) -> None:
|
||||
pass
|
||||
|
||||
'''
|
||||
Internal use only. Handles a raw request with no validation.
|
||||
'''
|
||||
def __request(self) -> None:
|
||||
print(self.__get_prompt(), end='', flush=True)
|
||||
return input()
|
||||
|
||||
'''
|
||||
!! OVERRIDE ON INHERITANCE !!
|
||||
'''
|
||||
def _style_prompt(self, prompt: str) -> str:
|
||||
return prompt
|
||||
|
||||
def __get_prompt(self) -> str:
|
||||
return self._style_prompt(self._prompt)
|
||||
|
||||
def set_prompt(self, prompt: str) -> None:
|
||||
self._prompt = prompt
|
||||
234
sandbox/cli/style.py
Normal file
234
sandbox/cli/style.py
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
'''
|
||||
=== ANSI Escape Sequences ===
|
||||
This file exists to organise and name
|
||||
all important ANSI escape sequences
|
||||
for management of the CLI.
|
||||
'''
|
||||
|
||||
from sys import stdout
|
||||
from enum import StrEnum
|
||||
|
||||
# Removes ALL set colors/styling
|
||||
RESET = '\x1b[0m'
|
||||
|
||||
'''
|
||||
Implements text foreground coloring.
|
||||
'''
|
||||
class Color(StrEnum):
|
||||
BLACK = '\x1b[30m'
|
||||
RED = '\x1b[31m'
|
||||
GREEN = '\x1b[32m'
|
||||
YELLOW = '\x1b[33m'
|
||||
BLUE = '\x1b[34m'
|
||||
MAGENTA = '\x1b[35m'
|
||||
CYAN = '\x1b[36m'
|
||||
WHITE = '\x1b[37m'
|
||||
|
||||
'''
|
||||
Handles the application of a Color to a given text.
|
||||
Unset the `temp` flag to leave this style on (after the given text)
|
||||
'''
|
||||
@staticmethod
|
||||
def apply(color: 'Color', text: str, temp: bool = True) -> str:
|
||||
return color + text + Color.WHITE if temp else ''
|
||||
|
||||
'''
|
||||
Implements text effects.
|
||||
'''
|
||||
class Effect(StrEnum):
|
||||
BOLD = '\x1b[1m'
|
||||
DIM = '\x1b[2m'
|
||||
ITALICS = '\x1b[3m'
|
||||
UNDERLINE = '\x1b[4m'
|
||||
BLINK = '\x1b[5m'
|
||||
REVERSE = '\x1b[7m'
|
||||
HIDE = '\x1b[8m'
|
||||
|
||||
'''
|
||||
Returns the opposite ESC code to a given Effect.
|
||||
ie the opposite of BOLD '\x1b[1m]' is '\x1b[21m'
|
||||
'''
|
||||
@staticmethod
|
||||
def _inverse(effect: 'Effect') -> str:
|
||||
return f'{effect[:2]}2{effect[2:]}'
|
||||
|
||||
'''
|
||||
Handles the application of a Effect to a given text.
|
||||
Unset the `temp` flag to leave this style on (after the given text)
|
||||
'''
|
||||
@staticmethod
|
||||
def apply(effect: 'Effect', text: str, temp: bool = True) -> str:
|
||||
return effect + text + Effect._inverse(effect) if temp else ''
|
||||
|
||||
'''
|
||||
Applies styling (color/effects/etc) to a given string
|
||||
Unset the `temp` flag to leave this style on (after the given text)
|
||||
'''
|
||||
def style(text: str, *args: Color | Effect, temp: bool = True) -> str:
|
||||
# unsures we don't add redundant "set white" commands
|
||||
unset_color = False
|
||||
for arg in args:
|
||||
unset = temp
|
||||
if isinstance(arg, Color):
|
||||
unset_color |= temp
|
||||
unset = False
|
||||
text = type(arg).apply(arg, text, temp=True)
|
||||
if unset_color:
|
||||
text += Color.WHITE
|
||||
return text
|
||||
|
||||
|
||||
'''
|
||||
Implements cursor movement functionality.
|
||||
NOTE:
|
||||
The Cursor class currently has no ability
|
||||
to handle EXACT line (row) numbers. Currently
|
||||
I'm assuming all functionality of noether can
|
||||
be implemented solely via relative movements.
|
||||
'''
|
||||
class Cursor(StrEnum):
|
||||
# SAVE current / RESTORE last saved cursor position
|
||||
_SAVE = f'\x1b[7'
|
||||
_RESTORE = f'\x1b[8'
|
||||
|
||||
_MV_UP = 'A'
|
||||
_MV_DOWN = 'B'
|
||||
_MV_LEFT = 'C'
|
||||
_MV_RIGHT = 'D'
|
||||
# NEXT/PREV are the same as DOWN/UP (respectively)
|
||||
# except that the cursor will reset to column 0
|
||||
_MV_NEXT = 'E'
|
||||
_MV_PREV = 'F'
|
||||
# move cursor to the start of the current line
|
||||
_MV_START = '\r' # there is no ESC CSI for carriage return
|
||||
|
||||
_MV_COLUMN = 'G'
|
||||
|
||||
'''
|
||||
Generates an ESC code sequence corresponding
|
||||
to a horizontal movement relative to the
|
||||
cursor's current vertical position.
|
||||
+ive = right
|
||||
-ive = left
|
||||
'''
|
||||
@staticmethod
|
||||
def _XMOVE_REL(n: int) -> str:
|
||||
if n > 0:
|
||||
return f'\x1b[{n}{Cursor._MV_RIGHT}'
|
||||
if n < 0:
|
||||
return f'\x1b[{n}{Cursor._MV_LEFT}'
|
||||
return ''
|
||||
|
||||
'''
|
||||
Generates an ESC code sequence corresponding
|
||||
to a horizontal movement to an EXACT column.
|
||||
'''
|
||||
@staticmethod
|
||||
def _XMOVE(n: int) -> str:
|
||||
return f'\x1b[{n}{Cursor._MV_COLUMN}'
|
||||
|
||||
'''
|
||||
Generates an ESC code sequence corresponding
|
||||
to a vertical movement relative to the
|
||||
cursor's current vertical position.
|
||||
+ive = down
|
||||
-ive = up
|
||||
'''
|
||||
@staticmethod
|
||||
def _YMOVE_REL(n: int, reset: bool = False) -> str:
|
||||
if n > 0:
|
||||
if reset:
|
||||
return f'\x1b[{n}{Cursor._MV_NEXT}'
|
||||
return f'\x1b[{n}{Cursor._MV_DOWN}'
|
||||
if n < 0:
|
||||
if reset:
|
||||
return f'\x1b[{n}{Cursor._MV_PREV}'
|
||||
return f'\x1b[{n}{Cursor._MV_UP}'
|
||||
return ''
|
||||
|
||||
'''
|
||||
Sets the cursor column (horizontal) position to an exact value.
|
||||
NOTE: does NOT flush stdout buffer
|
||||
'''
|
||||
@staticmethod
|
||||
def set_x(n: int) -> None:
|
||||
stdout.write(Cursor.XMOVE(n))
|
||||
|
||||
'''
|
||||
Moves the cursor left/right n columns (relative).
|
||||
NOTE: does NOT flush stdout buffer
|
||||
'''
|
||||
@staticmethod
|
||||
def move_x(n: int) -> None:
|
||||
stdout.write(Cursor._XMOVE_REL(n))
|
||||
|
||||
'''
|
||||
Moves the cursor up/down n rows and resets the
|
||||
cursor to be at the start of the line
|
||||
NOTE: does NOT flush stdout buffer
|
||||
'''
|
||||
@staticmethod
|
||||
def move_y(n: int, reset: bool = True) -> None:
|
||||
stdout.write(Cursor._YMOVE_REL(n, reset=reset))
|
||||
|
||||
'''
|
||||
Saves the current cursor position.
|
||||
NOTE: does NOT flush stdout buffer
|
||||
'''
|
||||
@staticmethod
|
||||
def save() -> None:
|
||||
stdout.write(Cursor._SAVE)
|
||||
|
||||
'''
|
||||
Restores the cursor position to a saved position.
|
||||
NOTE: does NOT flush stdout buffer
|
||||
'''
|
||||
@staticmethod
|
||||
def restore() -> None:
|
||||
stdout.write(Cursor._RESTORE)
|
||||
|
||||
'''
|
||||
Handles erasing content displayed on the screen.
|
||||
NOTE that the cursor position is NOT updated
|
||||
via these sequences. The \r code should be given after.
|
||||
'''
|
||||
class Erase(StrEnum):
|
||||
# erase everything from the current cursor
|
||||
# position to the START/END of the screen
|
||||
_SCR_AFTER = '\x1b[0J'
|
||||
_SCR_BEFORE = '\x1b[1J'
|
||||
_SCR_ALL = '\x1b[3J' # erase screen and delete all saved cursors
|
||||
|
||||
# erase everything from the current cursor
|
||||
# position to the START/END of the current line
|
||||
_LINE_AFTER = '\x1b[0K'
|
||||
_LINE_BEFORE = '\x1b[1K'
|
||||
_LINE_ALL = '\x1b[2K'
|
||||
|
||||
'''
|
||||
Erase characters on the entire screen.
|
||||
Set `before` flag to only erase before the cursor,
|
||||
set `after` flag to only erase after the cursor.
|
||||
'''
|
||||
@staticmethod
|
||||
def screen(before: bool = False, after: bool = False) -> None:
|
||||
if before:
|
||||
stdout.write(Erase._SCR_BEFORE)
|
||||
elif after:
|
||||
stdout.write(Erase._SCR_AFTER)
|
||||
else:
|
||||
stdout.write(Erase._SCR_ALL)
|
||||
|
||||
'''
|
||||
Erase characters on the current line.
|
||||
Set `before` flag to only erase before the cursor,
|
||||
set `after` flag to only erase after the cursor.
|
||||
'''
|
||||
@staticmethod
|
||||
def line(before: bool = False, after: bool = False) -> None:
|
||||
if before:
|
||||
stdout.write(Erase._LINE_BEFORE)
|
||||
elif after:
|
||||
stdout.write(Erase._LINE_AFTER)
|
||||
else:
|
||||
stdout.write(Erase._LINE_ALL)
|
||||
0
sandbox/cmd/__init__.py
Normal file
0
sandbox/cmd/__init__.py
Normal file
60
sandbox/cmd/cycsub.py
Normal file
60
sandbox/cmd/cycsub.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from sys import stdout
|
||||
|
||||
from noether.cli.style import *
|
||||
from noether.cli.prompt import *
|
||||
from noether.lib.structs import Result
|
||||
|
||||
from noether.lib.util import digits
|
||||
from noether.lib.groups import cyclic_subgrp
|
||||
from noether.lib.primes import totient, is_prime
|
||||
|
||||
class cycsub(Prompt):
|
||||
DEFAULT_PROMPT = style('[n]: ', Color.BLUE)
|
||||
def __init__(self, ignore_zero: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.ignore_zero = ignore_zero
|
||||
|
||||
def _parse(self, command: str) -> int:
|
||||
try:
|
||||
return Result.succeed(int(command))
|
||||
except ValueError:
|
||||
return Result.fail('Not an integer.')
|
||||
|
||||
def _exec(self, n: int) -> None:
|
||||
phi = totient(n)
|
||||
lpadding = digits(n)
|
||||
rpadding = digits(phi)
|
||||
if is_prime(n, phi=n):
|
||||
Cursor.save()
|
||||
Cursor.move_y(-1, reset=False)
|
||||
Cursor.set_x(len(self.DEFAULT_PROMPT) + lpadding + 1)
|
||||
stdout.write(style('[PRIME]', Color.MAGENTA, Effect.BOLD))
|
||||
Cursor.restore()
|
||||
stdout.flush()
|
||||
|
||||
# keeps track of all primitive roots
|
||||
# (note that there will be exactly totient(phi) of them)
|
||||
proots = []
|
||||
for g in range(n):
|
||||
G, order, periodic = cyclic_subgrp(g, n, ignore_zero=self.ignore_zero)
|
||||
primitive = (order == phi) # primitive root
|
||||
|
||||
lpad = ' ' * (lpadding - digits(a))
|
||||
rpad = ' ' * (rpadding - digits(order))
|
||||
|
||||
color_g = Color.RED
|
||||
style_G = []
|
||||
style_order = []
|
||||
if primitive:
|
||||
color_g = Color.GREEN
|
||||
style_G = [Color.GREEN, Effect.BOLD]
|
||||
style_order = [Color.YELLOW, Effect.BOLD]
|
||||
proots.append(g)
|
||||
elif gcd(g, n) == 1:
|
||||
color_g = Color.Yellow
|
||||
line = style(f'{lpad}{g}', color_g, Effect.BOLD) + \
|
||||
'-> ' + \
|
||||
style(f'{order}{rpad} ', *style_order) + \
|
||||
'| ' + \
|
||||
style(str(G), *style_G)
|
||||
print(line, flush=True)
|
||||
3
sandbox/exceptions.py
Normal file
3
sandbox/exceptions.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class PromptParseError(Exception):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(message)
|
||||
Loading…
Add table
Add a link
Reference in a new issue