From 095351609ff0bbf9abdbf7a86b09cb6afc8f83b0 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sat, 30 Sep 2017 20:34:41 +0000 Subject: Add Windows echo driver This makes the previously stateless Screen class stateful because it now needs to construct and hold a reference to the platform specific echo driver. --- pydora/configure.py | 9 +++-- pydora/player.py | 62 ++++++++++++++++--------------- pydora/utils.py | 104 +++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 128 insertions(+), 47 deletions(-) diff --git a/pydora/configure.py b/pydora/configure.py index 01de8df..7d3c754 100644 --- a/pydora/configure.py +++ b/pydora/configure.py @@ -108,6 +108,7 @@ class Configurator(object): self.builder = PydoraConfigFileBuilder() self.cfg = ConfigParser() + self.screen = Screen() if self.builder.file_exists: self.read_config() @@ -116,11 +117,11 @@ class Configurator(object): self.cfg.add_section("api") def fail(self, message): - print(Screen.print_error(message)) + print(self.screen.print_error(message)) sys.exit(1) def finished(self, message): - Screen.print_success(message) + self.screen.print_success(message) sys.exit(0) def print_message(self, message): @@ -133,10 +134,10 @@ class Configurator(object): self.fail("Error loading config file. Unable to continue.") def get_value(self, section, key, prompt): - self.cfg.set(section, key, Screen.get_string(prompt)) + self.cfg.set(section, key, self.screen.get_string(prompt)) def get_password(self, section, key, prompt): - self.cfg.set(section, key, Screen.get_password(prompt)) + self.cfg.set(section, key, self.screen.get_password(prompt)) def set_static_value(self, section, key, value): self.cfg.set(section, key, value) diff --git a/pydora/player.py b/pydora/player.py index 653fb35..991468a 100644 --- a/pydora/player.py +++ b/pydora/player.py @@ -64,23 +64,24 @@ class PlayerApp(object): def __init__(self): self.client = None + self.screen = Screen() def get_player(self): try: player = VLCPlayer(self, sys.stdin) - Screen.print_success("Using VLC") + self.screen.print_success("Using VLC") return player except PlayerUnusable: pass try: player = MPG123Player(self, sys.stdin) - Screen.print_success("Using mpg123") + self.screen.print_success("Using mpg123") return player except PlayerUnusable: pass - Screen.print_error("Unable to find a player") + self.screen.print_error("Unable to find a player") sys.exit(1) def get_client(self): @@ -94,22 +95,22 @@ class PlayerApp(object): return builder.build() if not self.client: - Screen.print_error("No valid config found") + self.screen.print_error("No valid config found") sys.exit(1) def station_selection_menu(self, error=None): """Format a station menu and make the user select a station """ - Screen.clear() + self.screen.clear() if error: - Screen.print_error("{}\n".format(error)) + self.screen.print_error("{}\n".format(error)) for i, station in enumerate(self.stations): i = "{:>3}".format(i) print(u"{}: {}".format(Colors.yellow(i), station.name)) - return self.stations[Screen.get_integer("Station: ")] + return self.stations[self.screen.get_integer("Station: ")] def play(self, song): """Play callback @@ -122,7 +123,7 @@ class PlayerApp(object): def skip_song(self, song): if song.is_ad: - Screen.print_error("Cannot skip advertisements") + self.screen.print_error("Cannot skip advertisements") else: self.player.stop() @@ -135,61 +136,63 @@ class PlayerApp(object): def dislike_song(self, song): try: if song.thumbs_down(): - Screen.print_success("Track disliked") + self.screen.print_success("Track disliked") self.player.stop() else: - Screen.print_error("Failed to dislike track") + self.screen.print_error("Failed to dislike track") except NotImplementedError: - Screen.print_error("Cannot dislike this type of track") + self.screen.print_error("Cannot dislike this type of track") def like_song(self, song): try: if song.thumbs_up(): - Screen.print_success("Track liked") + self.screen.print_success("Track liked") else: - Screen.print_error("Failed to like track") + self.screen.print_error("Failed to like track") except NotImplementedError: - Screen.print_error("Cannot like this type of track") + self.screen.print_error("Cannot like this type of track") def bookmark_song(self, song): try: if song.bookmark_song(): - Screen.print_success("Bookmarked song") + self.screen.print_success("Bookmarked song") else: - Screen.print_error("Failed to bookmark song") + self.screen.print_error("Failed to bookmark song") except NotImplementedError: - Screen.print_error("Cannot bookmark this type of track") + self.screen.print_error("Cannot bookmark this type of track") def bookmark_artist(self, song): try: if song.bookmark_artist(): - Screen.print_success("Bookmarked artist") + self.screen.print_success("Bookmarked artist") else: - Screen.print_error("Failed to bookmark artis") + self.screen.print_error("Failed to bookmark artis") except NotImplementedError: - Screen.print_error("Cannot bookmark artist for this type of track") + self.screen.print_error( + "Cannot bookmark artist for this type of track") def sleep_song(self, song): try: if song.sleep(): - Screen.print_success("Song will not be played for 30 days") + self.screen.print_success( + "Song will not be played for 30 days") self.player.stop() else: - Screen.print_error("Failed to sleep song") + self.screen.print_error("Failed to sleep song") except NotImplementedError: - Screen.print_error("Cannot sleep this type of track") + self.screen.print_error("Cannot sleep this type of track") def raise_volume(self, song): try: self.player.raise_volume() except NotImplementedError: - Screen.print_error("Cannot sleep this type of track") + self.screen.print_error("Cannot sleep this type of track") def lower_volume(self, song): try: self.player.lower_volume() except NotImplementedError: - Screen.print_error("Cannot sleep this type of track") + self.screen.print_error("Cannot sleep this type of track") def quit(self, song): self.player.end_station() @@ -209,20 +212,21 @@ class PlayerApp(object): try: cmd = getattr(self, self.CMD_MAP[input][1]) except (IndexError, KeyError): - return Screen.print_error("Invalid command {!r}!".format(input)) + return self.screen.print_error( + "Invalid command {!r}!".format(input)) cmd(song) def pre_poll(self): - Screen.set_echo(False) + self.screen.set_echo(False) def post_poll(self): - Screen.set_echo(True) + self.screen.set_echo(True) def pre_flight_checks(self): # See #52, this key no longer passes some server-side check if self.client.partner_user == "iphone": - Screen.print_error(( + self.screen.print_error(( "The `iphone` partner key set is no longer compatible with " "pydora. Please re-run pydora-configure to re-generate " "your config file before continuing.")) diff --git a/pydora/utils.py b/pydora/utils.py index faa18e5..6c608a7 100644 --- a/pydora/utils.py +++ b/pydora/utils.py @@ -6,13 +6,14 @@ import getpass import subprocess from pandora.py2compat import input -try: - import termios -except ImportError: - # Windows does not have a termios module - termios = None +class TerminalPlatformUnsupported(Exception): + """Platform-specific functionality is not supported + Raised by code that can not be used to interact with the terminal on this + platform. + """ + pass class Colors(object): @@ -35,25 +36,100 @@ class Colors(object): white = __wrap_with("37") -class Screen(object): +class PosixEchoControl(object): + """Posix Console Echo Control Driver - @staticmethod - def set_echo(enabled): - if not termios: - return + Uses termios on POSIX compliant platforms to control console echo. Is not + supported on Windows as termios is not available and will throw a + TerminalPlatformUnsupported exception if contructed on Windows. + """ + + def __init__(self): + try: + import termios + self.termios = termios + except ImportError: + raise TerminalPlatformUnsupported("POSIX not supported") + def set_echo(self, enabled): handle = sys.stdin.fileno() if not os.isatty(handle): return - attrs = termios.tcgetattr(handle) + attrs = self.termios.tcgetattr(handle) + + if enabled: + attrs[3] |= self.termios.ECHO + else: + attrs[3] &= ~self.termios.ECHO + + self.termios.tcsetattr(handle, self.termios.TCSANOW, attrs) + + +class Win32EchoControl(object): + """Windows Console Echo Control Driver + + This uses the console API from WinCon.h and ctypes to control console echo + on Windows clients. It is not possible to construct this class on + non-Windows systems, on those systems it will throw a + TerminalPlatformUnsupported exception. + """ + + STD_INPUT_HANDLE = -10 + ENABLE_ECHO_INPUT = 0x4 + DISABLE_ECHO_INPUT = ~ENABLE_ECHO_INPUT + + def __init__(self): + import ctypes + + if not hasattr(ctypes, "windll"): + raise TerminalPlatformUnsupported("Windows not supported") + + from ctypes import wintypes + + self.ctypes = ctypes + self.wintypes = wintypes + self.kernel32 = ctypes.windll.kernel32 + + def _GetStdHandle(self, handle): + return self.kernel32.GetStdHandle(handle) + + def _GetConsoleMode(self, handle): + mode = self.wintypes.DWORD() + self.kernel32.GetConsoleMode(handle, self.ctypes.byref(mode)) + return mode.value + + def _SetConsoleMode(self, handle, value): + self.kernel32.SetConsoleMode(handle, value) + + def set_echo(self, enabled): + stdin = self._GetStdHandle(self.STD_INPUT_HANDLE) + mode = self._GetConsoleMode(stdin) if enabled: - attrs[3] |= termios.ECHO + self._SetConsoleMode(stdin, mode | self.ENABLE_ECHO_INPUT) else: - attrs[3] &= ~termios.ECHO + self._SetConsoleMode(stdin, mode & self.DISABLE_ECHO_INPUT) + + +class Screen(object): + + def __init__(self): + try: + self._echo_driver = PosixEchoControl() + except TerminalPlatformUnsupported: + pass + + try: + self._echo_driver = Win32EchoControl() + except TerminalPlatformUnsupported: + pass + + if not self._echo_driver: + raise TerminalPlatformUnsupported("No supported terminal driver") - termios.tcsetattr(handle, termios.TCSANOW, attrs) + def set_echo(self, enabled): + self._echo_driver.set_echo(enabled) @staticmethod def clear(): -- cgit v1.2.3