From 8bfa376bb5570738214f2b009e15df1938370998 Mon Sep 17 00:00:00 2001 From: Ben Winston Date: Wed, 30 Mar 2022 23:47:21 -0400 Subject: rename project roku-tui --- README.md | 14 +--- roku-cli.py | 218 ------------------------------------------------------------ roku_tui.py | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 230 deletions(-) delete mode 100755 roku-cli.py create mode 100755 roku_tui.py diff --git a/README.md b/README.md index 9627cb4..8078cfc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# roku-cli +# roku-tui a command line TUI remote for Roku ## basic usage @@ -13,6 +13,7 @@ Keys on the left map to standard Roku remote buttons on the right * `<` -> Rewind * `>` -> Fast-Forward +For help inside the application, type '?'. To quit the application, type `q`. ## typing mode @@ -27,14 +28,3 @@ be interpreted literally and sent to the Roku. To exit typing mode, press `ENTER` or `ESC`. NOTE: this doesn't work with all text interfaces. - -## install -Replace the default IP address in `roku.config` with the IP address of -the Roku, then run: - -``` -make -sudo make install -``` - -Program can be started using `roku`. diff --git a/roku-cli.py b/roku-cli.py deleted file mode 100755 index 5312631..0000000 --- a/roku-cli.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env python3 - -import urllib.request -import time -import curses -from configparser import ConfigParser -from pathlib import Path -import xml.etree.ElementTree as ET - -class RokuConfig: - - def __init__(self, config_file): - config = ConfigParser() - config.read(config_file) - self.ip = config['general']['roku_ip'] - -class RemoteKey: - - def __init__(self, hotkey, label, endpoint, y, x): - self.hotkey = hotkey - self.label = label - self.endpoint = endpoint - self.y = y - self.x = x - -class RokuRemote: - - def __init__(self, ip): - self.base_url = 'http://{}:8060/'.format(ip) - self.setup_keys() - - def setup_keys(self): - self.keys = {} - - # first row - self.keys['b'] = RemoteKey('b', 'Back', 'keypress/back', 1, 1) - self.keys['h'] = RemoteKey('h', 'Home', 'keypress/home', 1, 10) - - # second row - self.keys['r'] = RemoteKey('r', 'Repl', 'keypress/instantreplay', 13, 1) - self.keys['*'] = RemoteKey('*', 'Star', 'keypress/info', 13, 10) - - # arrow keys - self.keys['KEY_UP'] = RemoteKey('KEY_UP', '^', 'keypress/up', 4, 7) - self.keys['KEY_LEFT'] = RemoteKey('KEY_LEFT', '<', 'keypress/left', 7, 1) - self.keys['\n'] = RemoteKey('\n', 'OK!', 'keypress/select', 7, 6) - self.keys['KEY_RIGHT'] = RemoteKey('KEY_RIGHT', '>', 'keypress/right', 7, 13) - self.keys['KEY_DOWN'] = RemoteKey('KEY_DOWN', 'v', 'keypress/down', 10, 7) - - # third row - self.keys['<'] = RemoteKey('<', '<<', 'keypress/rev', 16, 1) - self.keys['p'] = RemoteKey('p', 'P', 'keypress/play', 16, 7) - self.keys['>'] = RemoteKey('>', '>>', 'keypress/fwd', 16, 12) - -def draw_key(stdscr, key, pressed=False): - color = 1 if not pressed else 2 - draw_rect_key(stdscr, key.y, key.x, key.label, color) - -def press_key(stdscr, key, base_url): - draw_key(stdscr, key, True) - stdscr.refresh() - request_url = base_url + key.endpoint - urllib.request.urlopen(request_url, b'') - time.sleep(0.1) - -def draw_rect_key(stdscr, y, x, text, colors): - iw = len(text) + 2 - ow = iw + 2 - - owf = "{:^" + str(ow) + "}" - iwf = "{:^" + str(iw) + "}" - - # draw the box - stdscr.addstr(y, x, owf.format('-' * iw)) - stdscr.addstr(y + 1, x, "|") - stdscr.addstr(y + 1, x + ow - 1, "|") - stdscr.addstr(y + 2, x, owf.format('-' * iw)) - - # draw the inside - if colors == 1: - stdscr.addstr(y + 1, x + 1, iwf.format(text)) - elif colors == 2: - stdscr.addstr(y + 1, x + 1, iwf.format(text), curses.A_STANDOUT) - - -def status(stdscr): - stdscr.addstr(20, 1, "~*~ roku-cli ~*~") - -def search_loop(stdscr, base_url): - # setup for search - stdscr.addstr(22, 1, '/') - min_x = 2 - cur_x = min_x - stdscr.move(22, cur_x) - curses.curs_set(True) - while 1: - stdscr.refresh() - letter = stdscr.getkey() - if letter in ('\n', 'KEY_ESC'): - stdscr.move(22, 0) - stdscr.clrtoeol() - curses.curs_set(False) - return - elif letter == '\x7f': - if cur_x > min_x: - cur_x -= 1 - stdscr.addstr(22, cur_x, " ") - stdscr.move(22, cur_x) - request_url = base_url + 'keypress/Backspace' - else: - stdscr.addstr(22, cur_x, letter) - cur_x += 1 - if letter == ' ': - letter = '%20' - request_url = base_url + 'keypress/Lit_' + letter - urllib.request.urlopen(request_url, b'') - -def help_toggle(stdscr): - - help_lines = ["'h' -> Home", - "'b' -> Back", - "'p' -> Play/Pause", - "'r' -> Replay", - "'*' -> Options (star key)", - "arrow keys -> navigation", - "ENTER -> OK", - "'<' -> Rewind", - "'>' -> Fast-Forward", - "'/' -> Toggle typing mode (ENTER or ESC to leave)", - "'?' -> Show this help screen", - "'q' -> Quit", - "", - "Press any key to return to remote"] - - stdscr.clear() - status(stdscr) - for i in range(1,len(help_lines)+1): - stdscr.addstr(i, 1, help_lines[i - 1]) - stdscr.getch() # wait for keypress - stdscr.clear() - status(stdscr) - -def remote_loop(stdscr, remote): - while 1: - # draw all the keys - for k in remote.keys: - draw_key(stdscr, remote.keys[k]) - - # listen for an input - c = stdscr.getkey() - - # if the input is in the mapped keys, press the key - if c in remote.keys: - press_key(stdscr, remote.keys[c], remote.base_url) - elif c == '/': - search_loop(stdscr, remote.base_url) - elif c == '?': - help_toggle(stdscr) - elif c == 'q': - break - -def main(stdscr, config): - - init_curses() - stdscr.clear() - - # create the remote - remote = RokuRemote(config.ip) - - status(stdscr) - - remote_loop(stdscr, remote) - -def init_curses(): - # set up colors - curses.use_default_colors() - - # clear the screen and hide the cursor - curses.curs_set(False) - -def get_device_name(base_url): - request_url = base_url + "query/device-info" - out = urllib.request.urlopen(request_url) - response = out.read().decode() - root = ET.fromstring(response) - return root.findall("model-name")[0].text - -def init_run(config_file): - print("Welcome to roku-cli!") - print("Please enter the IP address of the Roku device you wish to control.") - print("This can be found in the Settings > Network > About menu, ") - print("or via your router.") - print("You will only need to do this once.") - print("If you wish to change the IP, modify the file at ~/.config/roku/roku.config") - print() - ip = input("IP Address: ") - remote = RokuRemote(ip) - - # if this returns, the IP is valid - device_name = get_device_name(remote.base_url) - - # write the config, create the directory if necessary - if not config_file.parent.exists(): - config_file.parent.mkdir() - config = ConfigParser() - config["general"] = {"roku_ip": ip} - with open(str(config_file), 'w') as cf: - config.write(cf) - -def startup(): - config_file = Path('~/.config/roku/roku.config').expanduser() - if not config_file.exists(): - init_run(config_file) - config = RokuConfig(config_file) - curses.wrapper(main, config) - -if __name__ == '__main__': - startup() diff --git a/roku_tui.py b/roku_tui.py new file mode 100755 index 0000000..5312631 --- /dev/null +++ b/roku_tui.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 + +import urllib.request +import time +import curses +from configparser import ConfigParser +from pathlib import Path +import xml.etree.ElementTree as ET + +class RokuConfig: + + def __init__(self, config_file): + config = ConfigParser() + config.read(config_file) + self.ip = config['general']['roku_ip'] + +class RemoteKey: + + def __init__(self, hotkey, label, endpoint, y, x): + self.hotkey = hotkey + self.label = label + self.endpoint = endpoint + self.y = y + self.x = x + +class RokuRemote: + + def __init__(self, ip): + self.base_url = 'http://{}:8060/'.format(ip) + self.setup_keys() + + def setup_keys(self): + self.keys = {} + + # first row + self.keys['b'] = RemoteKey('b', 'Back', 'keypress/back', 1, 1) + self.keys['h'] = RemoteKey('h', 'Home', 'keypress/home', 1, 10) + + # second row + self.keys['r'] = RemoteKey('r', 'Repl', 'keypress/instantreplay', 13, 1) + self.keys['*'] = RemoteKey('*', 'Star', 'keypress/info', 13, 10) + + # arrow keys + self.keys['KEY_UP'] = RemoteKey('KEY_UP', '^', 'keypress/up', 4, 7) + self.keys['KEY_LEFT'] = RemoteKey('KEY_LEFT', '<', 'keypress/left', 7, 1) + self.keys['\n'] = RemoteKey('\n', 'OK!', 'keypress/select', 7, 6) + self.keys['KEY_RIGHT'] = RemoteKey('KEY_RIGHT', '>', 'keypress/right', 7, 13) + self.keys['KEY_DOWN'] = RemoteKey('KEY_DOWN', 'v', 'keypress/down', 10, 7) + + # third row + self.keys['<'] = RemoteKey('<', '<<', 'keypress/rev', 16, 1) + self.keys['p'] = RemoteKey('p', 'P', 'keypress/play', 16, 7) + self.keys['>'] = RemoteKey('>', '>>', 'keypress/fwd', 16, 12) + +def draw_key(stdscr, key, pressed=False): + color = 1 if not pressed else 2 + draw_rect_key(stdscr, key.y, key.x, key.label, color) + +def press_key(stdscr, key, base_url): + draw_key(stdscr, key, True) + stdscr.refresh() + request_url = base_url + key.endpoint + urllib.request.urlopen(request_url, b'') + time.sleep(0.1) + +def draw_rect_key(stdscr, y, x, text, colors): + iw = len(text) + 2 + ow = iw + 2 + + owf = "{:^" + str(ow) + "}" + iwf = "{:^" + str(iw) + "}" + + # draw the box + stdscr.addstr(y, x, owf.format('-' * iw)) + stdscr.addstr(y + 1, x, "|") + stdscr.addstr(y + 1, x + ow - 1, "|") + stdscr.addstr(y + 2, x, owf.format('-' * iw)) + + # draw the inside + if colors == 1: + stdscr.addstr(y + 1, x + 1, iwf.format(text)) + elif colors == 2: + stdscr.addstr(y + 1, x + 1, iwf.format(text), curses.A_STANDOUT) + + +def status(stdscr): + stdscr.addstr(20, 1, "~*~ roku-cli ~*~") + +def search_loop(stdscr, base_url): + # setup for search + stdscr.addstr(22, 1, '/') + min_x = 2 + cur_x = min_x + stdscr.move(22, cur_x) + curses.curs_set(True) + while 1: + stdscr.refresh() + letter = stdscr.getkey() + if letter in ('\n', 'KEY_ESC'): + stdscr.move(22, 0) + stdscr.clrtoeol() + curses.curs_set(False) + return + elif letter == '\x7f': + if cur_x > min_x: + cur_x -= 1 + stdscr.addstr(22, cur_x, " ") + stdscr.move(22, cur_x) + request_url = base_url + 'keypress/Backspace' + else: + stdscr.addstr(22, cur_x, letter) + cur_x += 1 + if letter == ' ': + letter = '%20' + request_url = base_url + 'keypress/Lit_' + letter + urllib.request.urlopen(request_url, b'') + +def help_toggle(stdscr): + + help_lines = ["'h' -> Home", + "'b' -> Back", + "'p' -> Play/Pause", + "'r' -> Replay", + "'*' -> Options (star key)", + "arrow keys -> navigation", + "ENTER -> OK", + "'<' -> Rewind", + "'>' -> Fast-Forward", + "'/' -> Toggle typing mode (ENTER or ESC to leave)", + "'?' -> Show this help screen", + "'q' -> Quit", + "", + "Press any key to return to remote"] + + stdscr.clear() + status(stdscr) + for i in range(1,len(help_lines)+1): + stdscr.addstr(i, 1, help_lines[i - 1]) + stdscr.getch() # wait for keypress + stdscr.clear() + status(stdscr) + +def remote_loop(stdscr, remote): + while 1: + # draw all the keys + for k in remote.keys: + draw_key(stdscr, remote.keys[k]) + + # listen for an input + c = stdscr.getkey() + + # if the input is in the mapped keys, press the key + if c in remote.keys: + press_key(stdscr, remote.keys[c], remote.base_url) + elif c == '/': + search_loop(stdscr, remote.base_url) + elif c == '?': + help_toggle(stdscr) + elif c == 'q': + break + +def main(stdscr, config): + + init_curses() + stdscr.clear() + + # create the remote + remote = RokuRemote(config.ip) + + status(stdscr) + + remote_loop(stdscr, remote) + +def init_curses(): + # set up colors + curses.use_default_colors() + + # clear the screen and hide the cursor + curses.curs_set(False) + +def get_device_name(base_url): + request_url = base_url + "query/device-info" + out = urllib.request.urlopen(request_url) + response = out.read().decode() + root = ET.fromstring(response) + return root.findall("model-name")[0].text + +def init_run(config_file): + print("Welcome to roku-cli!") + print("Please enter the IP address of the Roku device you wish to control.") + print("This can be found in the Settings > Network > About menu, ") + print("or via your router.") + print("You will only need to do this once.") + print("If you wish to change the IP, modify the file at ~/.config/roku/roku.config") + print() + ip = input("IP Address: ") + remote = RokuRemote(ip) + + # if this returns, the IP is valid + device_name = get_device_name(remote.base_url) + + # write the config, create the directory if necessary + if not config_file.parent.exists(): + config_file.parent.mkdir() + config = ConfigParser() + config["general"] = {"roku_ip": ip} + with open(str(config_file), 'w') as cf: + config.write(cf) + +def startup(): + config_file = Path('~/.config/roku/roku.config').expanduser() + if not config_file.exists(): + init_run(config_file) + config = RokuConfig(config_file) + curses.wrapper(main, config) + +if __name__ == '__main__': + startup() -- cgit v1.2.3