Merge pull request 'Merge package branch into Main' (#1) from package into main

This commit is contained in:
icaema 2023-11-17 01:03:59 +00:00
commit f59fa2c532
8 changed files with 450 additions and 258 deletions

1
.gitignore vendored
View File

@ -53,3 +53,4 @@ env.bak
venv.bak venv.bak
pyvenv.cfg pyvenv.cfg
bin bin
__pycache__

125
README.md
View File

@ -1,7 +1,124 @@
# Pyterm Render # Python Term Render
A quick to use framework for making terminal based graphics in python. A quick to use and dead simple framework for making terminal based graphics in python.
``` ```
python3 -m venv . example_music.py
################################################################
# FPS: 29.866 Frame: 251 Sample: Wavtapper - Frums #
# [3, 3, 7, 14, 29, 59] #
================================================================
1234567890123456789012345678901234567890123456789012345678901234
0 1 | 2 3 | 4 | 5 6
$
X X X X X X
X Y Z Z
# #
################################################################
```
## Quick start
```
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
``` python3 example.py
```
## Usage
Programs take the following form,
```
from pytermrender import *
from colorama import Fore, Back, Style
Screen = TermScreen(height=24, width=80, framerate=30) # Screen must be defined
def setup():
print("Setup")
print("Clearing")
clearScreen()
def teardown():
clearScreen()
print("Teardown")
print("Teardown Complete")
def tick():
Screen.frame_no+=1
def draw():
clearScreen()
clearAllBuffers()
frame = Screen.frame_no
drawBox(0,0,Screen.width,Screen.height,'#')
printBuffer(1,1,str(Screen.framerate)+'fps -'+str(frame)+'-')
for ix, c in enumerate(str(frame)):
putBuffer(8+ix,1, Fore.BLUE, buffer="color")
run()
```
import everything from the pytermrender lib and define setup, teardown, tick, and draw. then just call run().
The setup function is called on program start, and teardown is called on Ctrl+c.
The tick function is called every frame before the draw function.
It can be used to do the program logic.
The draw function is called after the tick function and is where the program should actually manipulate the screen buffers.
As of now, there are only two buffers, "char" and "color".
Both of these buffers get printed to the screen every frame.
Use the char buffer for printable characters and the color buffer for non-printable characters, like ANSI char sequences.
## Library Reference
Pytermrender provides several library functions for quick usage:
```
clearScreen():
| Clear the terminal
clearAllBuffers():
| Clear all buffers
clearBuffer(buffer="char"):
| Clear a specific buffer
| buffer can be one of [ "char", "color" ]
clearLine(y, buffer="char"):
| Clear a line of a buffer
| buffer can be one of [ "char", "color" ]
putBuffer(x, y, char, *, buffer="char"):
| Put a char into a specific location in a buffer
| buffer can be one of [ "char", "color" ]
| NOTE: multi-char strings can be used, but can cause unintended results
| If you want to put multi-char strings, use printBuffer
printBuffer(x, y, string):
| Print a string into the char buffer
drawBox(x, y, width, height, char):
| Draw a box in the char buffer with the supplied char
```
Pytermrender also provides a convienent way to store state inside the Screen object.
```
Screen.tickers
| a dictionary to store variables that change every frame.
Screen.state
| a dictionary to store variables that are more static.
```
Example usage as follows:
```
tick():
global Screen
Screen.tickers["music"][0]=(Screen.frame_no//BEAT_SUB)//4
Screen.state["cursorActive"]=True
draw():
bar=Screen.tickers["music"][0]
cursorActive=Screen.state["cursorActive"]
if cursorActive:
putBuffer(0, 0, "$")
```

38
example.py Normal file
View File

@ -0,0 +1,38 @@
from pytermrender import *
from colorama import Fore, Back, Style
Screen = TermScreen(height=24, width=80, framerate=30)
def setup():
print("Setup")
print("Clearing")
clearScreen()
return
def teardown():
clearScreen()
print("Teardown")
print("Teardown Complete")
return
#not strictly needed, but useful
def tick():
Screen.frame_no+=1
return
def draw():
clearScreen()
clearAllBuffers()
frame = Screen.frame_no
drawBox(0,0,Screen.width,Screen.height,'#')
printBuffer(1,1,str(Screen.framerate)+'fps -'+str(frame)+'-')
for ix, c in enumerate(str(frame)):
putBuffer(8+ix,1, Fore.BLUE, buffer="color")
return
run()

95
example_music.py Normal file
View File

@ -0,0 +1,95 @@
from pytermrender import *
from colorama import Fore, Back, Style
from just_playback import Playback
import math
BPM=112
BEAT_SUB=16
FRAMERATE=(BPM*BEAT_SUB/60)
Screen = TermScreen(height=20, width=64, framerate=FRAMERATE)
playback = Playback()
def setup():
global Screen
print("Setup")
playback.load_file('media/WavetapperSample.wav')
playback.loop_at_end(True)
playback.play()
playback.set_volume(0.5)
Screen.tickers["music"] = {}
print("Clearing Screen")
clearScreen()
return
def teardown():
clearScreen();
print("Teardown")
print("Stopping Playback")
playback.stop()
print("Teardown Complete")
return
def tick():
global Screen
Screen.frame_no+=1
Screen.tickers["music"][0]=(Screen.frame_no//BEAT_SUB)//4 # bar
Screen.tickers["music"][1]=(Screen.frame_no//BEAT_SUB//1)%4 # beat
Screen.tickers["music"][2]=(Screen.frame_no//(BEAT_SUB//2))%8 # 8th
Screen.tickers["music"][3]=(Screen.frame_no//(BEAT_SUB//4))%16 # 16th
Screen.tickers["music"][4]=(Screen.frame_no//(BEAT_SUB//8))%32 # 32nd
Screen.tickers["music"][5]=(Screen.frame_no//(BEAT_SUB//16))%64 # 64th
Screen.state["cursorX"]=Screen.tickers["music"][5]
if Screen.tickers["music"][1] in [0,2]:
Screen.state["cursorActive"]=True
else:
Screen.state["cursorActive"]=False
return
def draw():
clearScreen()
clearAllBuffers()
cursorX=Screen.state["cursorX"]
cursorActive=Screen.state["cursorActive"]
bar=Screen.tickers["music"][0]
drawBox(0,0, Screen.width,Screen.height, '#')
printBuffer(2, 1, "FPS: "+str(Screen.framerate)[:6])
printBuffer(15, 1, "Frame: "+str(Screen.frame_no))
printBuffer(37, 1, "Sample: Wavtapper - Frums")
printBuffer(2, 2, [x for x in Screen.tickers["music"].values()])
printBuffer(0, 3, "="*Screen.width)
printBuffer(0, 4, "1234567890123456789012345678901234567890123456789012345678901234")
printBuffer(0, 5, "0 1 | 2 3 | 4 | 5 6 ")
clearLine(6)
putBuffer(cursorX, 6, "$")
if cursorActive:
putBuffer(cursorX, 6, Fore.GREEN, buffer="color")
clearLine(7)
printBuffer(0,7,"X X X X X X")
clearLine(8)
match bar % 4:
case 0:
printBuffer(52,8,"X")
case 1:
printBuffer(52,8,"X Y")
case 2:
printBuffer(52,8,"X")
case 3:
printBuffer(52,8,"X Y Z Z")
for ix in range(cursorX):
putBuffer(ix, 7, Fore.RED, buffer="color")
putBuffer(ix, 8, Fore.BLUE, buffer="color")
return
run()

BIN
media/WavetapperSample.wav Normal file

Binary file not shown.

195
pytermrender.py Normal file
View File

@ -0,0 +1,195 @@
import __main__, time, math, functools
from colorama import Style
## Definitions
printr = functools.partial(print, end="")
class RenderException(Exception):
pass
class TermScreen:
width: int
height: int
framerate: float
frame_no: int = 0
buffers: list = ["char", "color"]
tickers: dict
state: dict
def __init__(self, *, width: int = 80, height: int = 24, framerate: float = 30.0):
self.width=width
self.height=height
self.framerate=framerate
self.buffer_char = [[" "]*self.width for i in range(self.height)]
self.buffer_color = [[Style.RESET_ALL]*self.width for i in range(self.height)]
self.tickers = {}
self.state = {}
return
def reset_buffer(self, buffer="char"):
if buffer not in self.buffers:
raise RenderException("Buffer Does not exist")
match buffer:
case "char":
self.buffer_char = [[" "]*self.width for i in range(self.height)]
case "color":
self.buffer_color = [[Style.RESET_ALL]*self.width for i in range(self.height)]
return
def reset_buffer_line(self, line=0, buffer="char"):
if buffer not in self.buffers:
raise RenderException("Buffer Does not exist")
if line >= self.height:
raise RenderException("Line is out of range of height")
match buffer:
case "char":
self.buffer_char[line] = [" "]*self.width
case "color":
self.buffer_color[line] = [Style.RESET_ALL]*self.width
return
def put_buffer(self, x, y, val, buffer="char"):
if buffer not in self.buffers:
raise RenderException("Buffer Does not exist")
if x >= self.width or y >= self.height:
raise RenderException("Point is out of range of the buffer")
char=str(val)
match buffer:
case "char":
self.buffer_char[y][x] = char
case "color":
self.buffer_color[y][x] = char
return
def render(self):
for y in range(self.height):
for x in range(self.width):
printr(self.buffer_color[y][x])
printr(self.buffer_char[y][x])
print() #print \n
return
def framerateMaker(func, perSec):
interval = 1.0 / perSec
while True:
start_time = time.time()
func()
elapsed = time.time() - start_time
if elapsed < interval:
time.sleep(interval-elapsed)
return
def renderLoop():
global Screen
pro_tick()
pro_draw()
Screen.render()
return
################
## Library Functions
def clearScreen():
printr('\033[2J\r')
def clearBuffer(buffer=TermScreen.buffers[0]):
global Screen
Screen.reset_buffer(buffer)
return
def clearAllBuffers():
global Screen
for buf in Screen.buffers:
Screen.reset_buffer(buf)
return
def clearLine(y, buffer=TermScreen.buffers[0]):
global Screen
Screen.reset_buffer_line(y, buffer)
return
def putBuffer(x, y, val, *, buffer=TermScreen.buffers[0]):
global Screen
Screen.put_buffer(x, y, val, buffer)
return
def printBuffer(x, y, val):
""" Print a string into the char buffer """
global Screen
string=str(val)
if len(string) > Screen.width-x:
raise RenderException("Too Long to fit in screen")
for i, char in enumerate(string):
Screen.buffer_char[y][x+i]=char
return
def drawBox(x,y,width,height,char):
""" Draw a box in the char buffer """
global Screen
if x+width > Screen.width or y+height > Screen.height:
raise RenderException("Box too BIG")
for w_x in range(width):
for w_y in range(height):
if (w_x==0 or w_x==width-1) or (w_y==0 or w_y==height-1):
Screen.buffer_char[y+w_y][x+w_x] = char
return
####################
def tick():
global Screen
Screen.frame_no+=1
def setup():
print("Default Setup")
clearScreen()
clearAllBuffers()
return
def teardown():
print("Default Teardown")
return
def draw():
return
def run():
global Screen
global pro_tick, pro_draw
if hasattr(__main__, "Screen"):
Screen = __main__.Screen
else:
raise RenderException("Screen Not defined")
if hasattr(__main__, "setup"):
pro_setup = __main__.setup
else:
pro_setup = setup
if hasattr(__main__, "teardown"):
pro_teardown = __main__.teardown
else:
pro_teardown = teardown
if hasattr(__main__, "tick"):
pro_tick = __main__.tick
else:
pro_tick = tick
if hasattr(__main__, "draw"):
pro_draw = __main__.draw
else:
pro_draw = draw
pro_setup()
try:
framerateMaker(renderLoop, Screen.framerate)
except KeyboardInterrupt:
pass
pro_teardown()

View File

@ -1,110 +0,0 @@
import time, math
from colorama import Fore, Back, Style
import functools
printr = functools.partial(print, end="")
###############
FRAMERATE=30
HEIGHT=24
WIDTH=80
###############
FRAME_NO=0
def clearBuffer():
global screen_buffer,screen_buffer_color
screen_buffer = [[" "]*WIDTH for y in range(HEIGHT)]
def clearBufferColor():
global screen_buffer,screen_buffer_color
screen_buffer_color = [[Style.RESET_ALL]*WIDTH for y in range(HEIGHT)]
def clearAllBuffers():
clearBuffer()
clearBufferColor()
def clearScreen():
printr('\033[2J')
def render(clear=True):
if clear:
clearScreen()
for y in range(HEIGHT):
for x in range(WIDTH):
printr(screen_buffer_color[y][x])
printr(screen_buffer[y][x])
print() #print \n
def renderLoop():
trackers()
draw()
render()
def framerateMaker(func, perSec):
interval = 1.0 / perSec
while True:
start_time = time.time()
func()
elapsed = time.time() - start_time
if elapsed < interval:
time.sleep(interval-elapsed)
################
class RenderException(Exception):
pass
def printBuffer(x, y, val):
global screen_buffer
string=str(val)
if len(string) > WIDTH-x:
raise RenderException("Too Long")
for i, l in enumerate(string):
screen_buffer[y][x+i]=l
def clearLine(y):
global screen_buffer
screen_buffer[y]=[" "]*WIDTH
def drawBox(x,y,width,height,char):
if x+width > WIDTH or y+height > HEIGHT:
raise RenderException("Box too BIG")
for w_x in range(width):
for w_y in range(height):
if (w_x==0 or w_x==width-1) or (w_y==0 or w_y==height-1):
screen_buffer[y+w_y][x+w_x] = char
####################
def trackers():
global FRAME_NO
FRAME_NO+=1
def main():
setup()
try:
framerateMaker(renderLoop, FRAMERATE)
pass
except KeyboardInterrupt:
pass
teardown()
def setup():
print("Setup")
print("Clearing")
clearScreen()
clearAllBuffers()
pass
def teardown():
global dat
print("Teardown")
print("Teardown Complete")
pass
def draw():
global FRAME_NO,screen_buffer,screen_buffer_color
clearAllBuffers()
drawBox(0,0,WIDTH,HEIGHT,'#')
printBuffer(1,1,str(FRAMERATE)+'fps '+str(FRAME_NO))
if __name__ == "__main__":
main()

View File

@ -1,144 +0,0 @@
import time
import math
import functools
printr = functools.partial(print, end="")
from just_playback import Playback
playback = Playback()
from colorama import Fore, Back, Style
###############
BPM=112
BEAT_SUB=16
FRAMERATE=(BPM*BEAT_SUB/60)
HEIGHT=24
WIDTH=64
###############
FRAME_NO=0
MUSIC_TIME=[0 for i in range(int(math.log2(BEAT_SUB)))]
###############
def clearBuffer():
global screen_buffer,screen_buffer_color
screen_buffer = [[" "]*WIDTH for y in range(HEIGHT)]
def clearBufferColor():
global screen_buffer,screen_buffer_color
screen_buffer_color = [[Style.RESET_ALL]*WIDTH for y in range(HEIGHT)]
def clearAllBuffers():
clearBuffer()
clearBufferColor()
def clearScreen():
printr('\033[2J')
def render(clear=True):
if clear:
clearScreen()
for y in range(HEIGHT):
for x in range(WIDTH):
printr(screen_buffer_color[y][x])
printr(screen_buffer[y][x])
print() #print \n
def renderLoop():
trackers()
draw()
render()
def framerateMaker(func, perSec):
interval = 1.0 / perSec
while True:
start_time = time.time()
func()
elapsed = time.time() - start_time
if elapsed < interval:
time.sleep(interval-elapsed)
################
def printBuffer(x, y, val):
global screen_buffer
string=str(val)
if len(string) > WIDTH-x:
return
for i, l in enumerate(string):
screen_buffer[y][x+i]=l
pass
def clearLine(y):
global screen_buffer
screen_buffer[y]=[" "]*WIDTH
pass
####################
def trackers():
global FRAME_NO
global MUSIC_TIME
FRAME_NO+=1
MUSIC_TIME[0]=(FRAME_NO//BEAT_SUB)//4
MUSIC_TIME[1]=(FRAME_NO//BEAT_SUB//1)%4
MUSIC_TIME[2]=(FRAME_NO//(BEAT_SUB//2))%8
MUSIC_TIME[3]=(FRAME_NO//(BEAT_SUB//4))%16
#MUSIC_TIME[1]=(FRAME_NO//(BEAT_SUB*int(math.log2(BEAT_SUB//8))))%4
# MUSIC_TIME[2]=(FRAME_NO//8)%8
# for i in range(len(MUSIC_TIME)):
# MUSIC_TIME[i]=(FRAME_NO//2**(i+1)) % 2**(i)
def main():
setup()
try:
framerateMaker(renderLoop, FRAMERATE)
pass
except KeyboardInterrupt:
pass
teardown()
def setup():
global playback
print("Setup")
print("Clearing")
clearScreen()
clearAllBuffers()
#playback.load_file('')
#playback.play()
def teardown():
print("Teardown")
print("Stopping Playback")
#playback.stop()
print("Teardown Complete")
pass
cursorX=1
cooldown=[0]*4*4
def draw():
global screen_buffer,screen_buffer_color
global MUSIC_TIME
global cursorX,cooldown
clearAllBuffers()
for y in range(HEIGHT):
for x in range(WIDTH):
if x == 0 or x == WIDTH-1:
screen_buffer[y][x]='#'
if y == 0 or y == HEIGHT-1:
screen_buffer[y][x]='#'
printBuffer(0,3,"1234567890123456789012345678901234567890123456789012345678901234")
printBuffer(0,4,"0 1 2 3 4 5 6 ")
clearLine(5)
if cursorX >= WIDTH:
cursorX=0
screen_buffer[5][cursorX] = "x"
printBuffer(1,1,FRAME_NO)
printBuffer(6,1,FRAMERATE)
printBuffer(1,2,MUSIC_TIME)
cursorX=cursorX+1
if __name__ == "__main__":
main()