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..4ab1176 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 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, "$") +``` diff --git a/example.py b/example.py new file mode 100644 index 0000000..25f5bec --- /dev/null +++ b/example.py @@ -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() 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 new file mode 100644 index 0000000..7f32746 --- /dev/null +++ b/pytermrender.py @@ -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() + diff --git a/termrender.py b/termrender.py deleted file mode 100644 index fddb51b..0000000 --- a/termrender.py +++ /dev/null @@ -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() 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()