cleaned up some stuff and added Documentation in README.md

This commit is contained in:
icaema 2023-11-16 20:00:06 -05:00
parent 7984b55856
commit 03be2c5f3a
8 changed files with 286 additions and 232 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
pip install -r requirements.txt ################################################################
# 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
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 variaiables 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, "$")
``` ```

View File

@ -1,28 +1,38 @@
from pytermrender import * from pytermrender import *
from colorama import Fore, Back, Style
Screen = TermScreen(height=24, width=80, framerate=30) Screen = TermScreen(height=24, width=80, framerate=30)
#not strictly needed, but useful
def tick():
Screen.frame_no+=1
def setup(): def setup():
print("Setup") print("Setup")
print("Clearing") print("Clearing")
clearScreen() clearScreen()
pass return
def teardown(): def teardown():
clearScreen()
print("Teardown") print("Teardown")
print("Teardown Complete") print("Teardown Complete")
pass return
#not strictly needed, but useful
def tick():
Screen.frame_no+=1
return
def draw(): def draw():
clearScreen() clearScreen()
clearAllBuffers() clearAllBuffers()
frame = Screen.frame_no
drawBox(0,0,Screen.width,Screen.height,'#') drawBox(0,0,Screen.width,Screen.height,'#')
printBuffer(1,1,str(Screen.framerate)+'fps '+str(Screen.frame_no)) 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() run()

View File

@ -1,59 +0,0 @@
from pytermrender import *
from colorama import Fore, Back, Style
BPM=112
BEAT_SUB=16
FRAMERATE=(BPM*BEAT_SUB/60)
Screen = TermScreen(height=24, width=64, framerate=FRAMERATE)
cursorX=1
cooldown=0
def tick():
global cursorX,cooldown
Screen.frame_no+=1
cursorX=cursorX+1
if cursorX >= Screen.width-1:
cursorX=0
if cursorX % 12 == 0:
cooldown=8
else:
cooldown-=1
return
def setup():
print("Setup")
print("Clearing")
clearScreen()
return
def teardown():
print("Teardown")
print("Teardown Complete")
return
def draw():
clearScreen()
clearAllBuffers()
for y in range(Screen.height):
for x in range(Screen.width):
if x == 0 or x == Screen.width-1:
Screen.buffer_char[y][x]='#'
if y == 0 or y == Screen.height-1:
Screen.buffer_char[y][x]='#'
printBuffer(0,3,"1234567890123456789012345678901234567890123456789012345678901234")
printBuffer(0,4,"0 1 2 3 4 5 6 ")
#printBuffer(0,4,"2 6 8 6 4 6 8 6 2 6 8 6 4 6 8 6 ")
clearLine(5)
Screen.buffer_char[5][cursorX] = "x"
if cooldown > 0:
Screen.buffer_color[5][cursorX] = Fore.RED
printBuffer(1,1,Screen.frame_no)
printBuffer(8,1,Screen.framerate)
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.

View File

@ -1,17 +1,12 @@
import __main__ import __main__, time, math, functools
import time, math from colorama import Style
from colorama import Fore, Back, Style
import functools
#from dataclasses import dataclass
printr = functools.partial(print, end="")
## Definitions ## Definitions
printr = functools.partial(print, end="")
class RenderException(Exception): class RenderException(Exception):
pass pass
class TermScreen: class TermScreen:
width: int width: int
height: int height: int
@ -19,12 +14,15 @@ class TermScreen:
frame_no: int = 0 frame_no: int = 0
buffers: list = ["char", "color"] buffers: list = ["char", "color"]
tickers: dict tickers: dict
state: dict
def __init__(self, *, width: int = 80, height: int = 24, framerate: float = 30.0): def __init__(self, *, width: int = 80, height: int = 24, framerate: float = 30.0):
self.width=width self.width=width
self.height=height self.height=height
self.framerate=framerate self.framerate=framerate
self.buffer_char = [[" "]*self.width for i in range(self.height)] 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.buffer_color = [[Style.RESET_ALL]*self.width for i in range(self.height)]
self.tickers = {}
self.state = {}
return return
def reset_buffer(self, buffer="char"): def reset_buffer(self, buffer="char"):
@ -37,6 +35,31 @@ class TermScreen:
self.buffer_color = [[Style.RESET_ALL]*self.width for i in range(self.height)] self.buffer_color = [[Style.RESET_ALL]*self.width for i in range(self.height)]
return 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): def render(self):
for y in range(self.height): for y in range(self.height):
for x in range(self.width): for x in range(self.width):
@ -45,6 +68,9 @@ class TermScreen:
print() #print \n print() #print \n
return return
def framerateMaker(func, perSec): def framerateMaker(func, perSec):
interval = 1.0 / perSec interval = 1.0 / perSec
while True: while True:
@ -66,11 +92,11 @@ def renderLoop():
## Library Functions ## Library Functions
def clearScreen(): def clearScreen():
printr('\033[2J') printr('\033[2J\r')
def clearBuffer(buf="char"): def clearBuffer(buffer=TermScreen.buffers[0]):
global Screen global Screen
Screen.reset_buffer(buf) Screen.reset_buffer(buffer)
return return
def clearAllBuffers(): def clearAllBuffers():
@ -79,19 +105,28 @@ def clearAllBuffers():
Screen.reset_buffer(buf) Screen.reset_buffer(buf)
return 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): def printBuffer(x, y, val):
""" Print a string into the char buffer """
global Screen global Screen
string=str(val) string=str(val)
if len(string) > Screen.width-x: if len(string) > Screen.width-x:
raise RenderException("Too Long to fit in screen") raise RenderException("Too Long to fit in screen")
for i, char in enumerate(string): for i, char in enumerate(string):
Screen.buffer_char[y][x+i]=char Screen.buffer_char[y][x+i]=char
return
def clearLine(y):
global Screen
Screen.buffer_char[y]=[" "]*Screen.width
def drawBox(x,y,width,height,char): def drawBox(x,y,width,height,char):
""" Draw a box in the char buffer """
global Screen global Screen
if x+width > Screen.width or y+height > Screen.height: if x+width > Screen.width or y+height > Screen.height:
raise RenderException("Box too BIG") raise RenderException("Box too BIG")
@ -99,17 +134,16 @@ def drawBox(x,y,width,height,char):
for w_y in range(height): for w_y in range(height):
if (w_x==0 or w_x==width-1) or (w_y==0 or w_y==height-1): 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 Screen.buffer_char[y+w_y][x+w_x] = char
return
#################### ####################
def tick(): def tick():
global Screen global Screen
Screen.frame_no+=1 Screen.frame_no+=1
def setup(): def setup():
print("Setup") print("Default Setup")
clearScreen() clearScreen()
clearAllBuffers() clearAllBuffers()
return return

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()