diff --git a/.gitignore b/.gitignore index e4b1250..aae4f19 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ env.bak venv.bak pyvenv.cfg bin +__pycache__ diff --git a/README.md b/README.md index b231fea..b4717fd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,124 @@ -# Pyterm Render -A quick to use framework for making terminal based graphics in python. +# Python Term Render +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 -``` \ No newline at end of file +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, "$") +``` diff --git a/example.py b/example.py index b574066..25f5bec 100644 --- a/example.py +++ b/example.py @@ -1,28 +1,38 @@ from pytermrender import * +from colorama import Fore, Back, Style + Screen = TermScreen(height=24, width=80, framerate=30) -#not strictly needed, but useful -def tick(): - Screen.frame_no+=1 def setup(): print("Setup") print("Clearing") clearScreen() - pass + return def teardown(): + clearScreen() print("Teardown") print("Teardown Complete") - pass + 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(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() diff --git a/example2.py b/example2.py deleted file mode 100644 index 423303d..0000000 --- a/example2.py +++ /dev/null @@ -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() diff --git a/example_music.py b/example_music.py new file mode 100644 index 0000000..a53ec07 --- /dev/null +++ b/example_music.py @@ -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() diff --git a/media/WavetapperSample.wav b/media/WavetapperSample.wav new file mode 100644 index 0000000..cca2595 Binary files /dev/null and b/media/WavetapperSample.wav differ diff --git a/pytermrender.py b/pytermrender.py index 1f7ecff..7f32746 100644 --- a/pytermrender.py +++ b/pytermrender.py @@ -1,17 +1,12 @@ -import __main__ -import time, math -from colorama import Fore, Back, Style -import functools -#from dataclasses import dataclass -printr = functools.partial(print, end="") +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 @@ -19,14 +14,17 @@ class TermScreen: 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") @@ -37,6 +35,31 @@ class TermScreen: 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): @@ -45,6 +68,9 @@ class TermScreen: print() #print \n return + + + def framerateMaker(func, perSec): interval = 1.0 / perSec while True: @@ -66,11 +92,11 @@ def renderLoop(): ## Library Functions def clearScreen(): - printr('\033[2J') + printr('\033[2J\r') -def clearBuffer(buf="char"): +def clearBuffer(buffer=TermScreen.buffers[0]): global Screen - Screen.reset_buffer(buf) + Screen.reset_buffer(buffer) return def clearAllBuffers(): @@ -79,19 +105,28 @@ def clearAllBuffers(): 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 - -def clearLine(y): - global Screen - Screen.buffer_char[y]=[" "]*Screen.width + 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") @@ -99,17 +134,16 @@ def drawBox(x,y,width,height,char): 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("Setup") + print("Default Setup") clearScreen() clearAllBuffers() return diff --git a/termrender_audio.py b/termrender_audio.py deleted file mode 100644 index f1cdf9f..0000000 --- a/termrender_audio.py +++ /dev/null @@ -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()