diff options
author | Mike Crute <mike@crute.us> | 2017-09-30 20:34:41 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2017-09-30 23:09:18 +0000 |
commit | 095351609ff0bbf9abdbf7a86b09cb6afc8f83b0 (patch) | |
tree | 686e683f40944da9f179d0d55edca40fe9215fb6 | |
parent | 1e9bc9d34c210925004eeeea4533a14185078db6 (diff) | |
download | pydora-095351609ff0bbf9abdbf7a86b09cb6afc8f83b0.tar.bz2 pydora-095351609ff0bbf9abdbf7a86b09cb6afc8f83b0.tar.xz pydora-095351609ff0bbf9abdbf7a86b09cb6afc8f83b0.zip |
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.
-rw-r--r-- | pydora/configure.py | 9 | ||||
-rw-r--r-- | pydora/player.py | 62 | ||||
-rw-r--r-- | 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): | |||
108 | self.builder = PydoraConfigFileBuilder() | 108 | self.builder = PydoraConfigFileBuilder() |
109 | 109 | ||
110 | self.cfg = ConfigParser() | 110 | self.cfg = ConfigParser() |
111 | self.screen = Screen() | ||
111 | 112 | ||
112 | if self.builder.file_exists: | 113 | if self.builder.file_exists: |
113 | self.read_config() | 114 | self.read_config() |
@@ -116,11 +117,11 @@ class Configurator(object): | |||
116 | self.cfg.add_section("api") | 117 | self.cfg.add_section("api") |
117 | 118 | ||
118 | def fail(self, message): | 119 | def fail(self, message): |
119 | print(Screen.print_error(message)) | 120 | print(self.screen.print_error(message)) |
120 | sys.exit(1) | 121 | sys.exit(1) |
121 | 122 | ||
122 | def finished(self, message): | 123 | def finished(self, message): |
123 | Screen.print_success(message) | 124 | self.screen.print_success(message) |
124 | sys.exit(0) | 125 | sys.exit(0) |
125 | 126 | ||
126 | def print_message(self, message): | 127 | def print_message(self, message): |
@@ -133,10 +134,10 @@ class Configurator(object): | |||
133 | self.fail("Error loading config file. Unable to continue.") | 134 | self.fail("Error loading config file. Unable to continue.") |
134 | 135 | ||
135 | def get_value(self, section, key, prompt): | 136 | def get_value(self, section, key, prompt): |
136 | self.cfg.set(section, key, Screen.get_string(prompt)) | 137 | self.cfg.set(section, key, self.screen.get_string(prompt)) |
137 | 138 | ||
138 | def get_password(self, section, key, prompt): | 139 | def get_password(self, section, key, prompt): |
139 | self.cfg.set(section, key, Screen.get_password(prompt)) | 140 | self.cfg.set(section, key, self.screen.get_password(prompt)) |
140 | 141 | ||
141 | def set_static_value(self, section, key, value): | 142 | def set_static_value(self, section, key, value): |
142 | self.cfg.set(section, key, value) | 143 | 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): | |||
64 | 64 | ||
65 | def __init__(self): | 65 | def __init__(self): |
66 | self.client = None | 66 | self.client = None |
67 | self.screen = Screen() | ||
67 | 68 | ||
68 | def get_player(self): | 69 | def get_player(self): |
69 | try: | 70 | try: |
70 | player = VLCPlayer(self, sys.stdin) | 71 | player = VLCPlayer(self, sys.stdin) |
71 | Screen.print_success("Using VLC") | 72 | self.screen.print_success("Using VLC") |
72 | return player | 73 | return player |
73 | except PlayerUnusable: | 74 | except PlayerUnusable: |
74 | pass | 75 | pass |
75 | 76 | ||
76 | try: | 77 | try: |
77 | player = MPG123Player(self, sys.stdin) | 78 | player = MPG123Player(self, sys.stdin) |
78 | Screen.print_success("Using mpg123") | 79 | self.screen.print_success("Using mpg123") |
79 | return player | 80 | return player |
80 | except PlayerUnusable: | 81 | except PlayerUnusable: |
81 | pass | 82 | pass |
82 | 83 | ||
83 | Screen.print_error("Unable to find a player") | 84 | self.screen.print_error("Unable to find a player") |
84 | sys.exit(1) | 85 | sys.exit(1) |
85 | 86 | ||
86 | def get_client(self): | 87 | def get_client(self): |
@@ -94,22 +95,22 @@ class PlayerApp(object): | |||
94 | return builder.build() | 95 | return builder.build() |
95 | 96 | ||
96 | if not self.client: | 97 | if not self.client: |
97 | Screen.print_error("No valid config found") | 98 | self.screen.print_error("No valid config found") |
98 | sys.exit(1) | 99 | sys.exit(1) |
99 | 100 | ||
100 | def station_selection_menu(self, error=None): | 101 | def station_selection_menu(self, error=None): |
101 | """Format a station menu and make the user select a station | 102 | """Format a station menu and make the user select a station |
102 | """ | 103 | """ |
103 | Screen.clear() | 104 | self.screen.clear() |
104 | 105 | ||
105 | if error: | 106 | if error: |
106 | Screen.print_error("{}\n".format(error)) | 107 | self.screen.print_error("{}\n".format(error)) |
107 | 108 | ||
108 | for i, station in enumerate(self.stations): | 109 | for i, station in enumerate(self.stations): |
109 | i = "{:>3}".format(i) | 110 | i = "{:>3}".format(i) |
110 | print(u"{}: {}".format(Colors.yellow(i), station.name)) | 111 | print(u"{}: {}".format(Colors.yellow(i), station.name)) |
111 | 112 | ||
112 | return self.stations[Screen.get_integer("Station: ")] | 113 | return self.stations[self.screen.get_integer("Station: ")] |
113 | 114 | ||
114 | def play(self, song): | 115 | def play(self, song): |
115 | """Play callback | 116 | """Play callback |
@@ -122,7 +123,7 @@ class PlayerApp(object): | |||
122 | 123 | ||
123 | def skip_song(self, song): | 124 | def skip_song(self, song): |
124 | if song.is_ad: | 125 | if song.is_ad: |
125 | Screen.print_error("Cannot skip advertisements") | 126 | self.screen.print_error("Cannot skip advertisements") |
126 | else: | 127 | else: |
127 | self.player.stop() | 128 | self.player.stop() |
128 | 129 | ||
@@ -135,61 +136,63 @@ class PlayerApp(object): | |||
135 | def dislike_song(self, song): | 136 | def dislike_song(self, song): |
136 | try: | 137 | try: |
137 | if song.thumbs_down(): | 138 | if song.thumbs_down(): |
138 | Screen.print_success("Track disliked") | 139 | self.screen.print_success("Track disliked") |
139 | self.player.stop() | 140 | self.player.stop() |
140 | else: | 141 | else: |
141 | Screen.print_error("Failed to dislike track") | 142 | self.screen.print_error("Failed to dislike track") |
142 | except NotImplementedError: | 143 | except NotImplementedError: |
143 | Screen.print_error("Cannot dislike this type of track") | 144 | self.screen.print_error("Cannot dislike this type of track") |
144 | 145 | ||
145 | def like_song(self, song): | 146 | def like_song(self, song): |
146 | try: | 147 | try: |
147 | if song.thumbs_up(): | 148 | if song.thumbs_up(): |
148 | Screen.print_success("Track liked") | 149 | self.screen.print_success("Track liked") |
149 | else: | 150 | else: |
150 | Screen.print_error("Failed to like track") | 151 | self.screen.print_error("Failed to like track") |
151 | except NotImplementedError: | 152 | except NotImplementedError: |
152 | Screen.print_error("Cannot like this type of track") | 153 | self.screen.print_error("Cannot like this type of track") |
153 | 154 | ||
154 | def bookmark_song(self, song): | 155 | def bookmark_song(self, song): |
155 | try: | 156 | try: |
156 | if song.bookmark_song(): | 157 | if song.bookmark_song(): |
157 | Screen.print_success("Bookmarked song") | 158 | self.screen.print_success("Bookmarked song") |
158 | else: | 159 | else: |
159 | Screen.print_error("Failed to bookmark song") | 160 | self.screen.print_error("Failed to bookmark song") |
160 | except NotImplementedError: | 161 | except NotImplementedError: |
161 | Screen.print_error("Cannot bookmark this type of track") | 162 | self.screen.print_error("Cannot bookmark this type of track") |
162 | 163 | ||
163 | def bookmark_artist(self, song): | 164 | def bookmark_artist(self, song): |
164 | try: | 165 | try: |
165 | if song.bookmark_artist(): | 166 | if song.bookmark_artist(): |
166 | Screen.print_success("Bookmarked artist") | 167 | self.screen.print_success("Bookmarked artist") |
167 | else: | 168 | else: |
168 | Screen.print_error("Failed to bookmark artis") | 169 | self.screen.print_error("Failed to bookmark artis") |
169 | except NotImplementedError: | 170 | except NotImplementedError: |
170 | Screen.print_error("Cannot bookmark artist for this type of track") | 171 | self.screen.print_error( |
172 | "Cannot bookmark artist for this type of track") | ||
171 | 173 | ||
172 | def sleep_song(self, song): | 174 | def sleep_song(self, song): |
173 | try: | 175 | try: |
174 | if song.sleep(): | 176 | if song.sleep(): |
175 | Screen.print_success("Song will not be played for 30 days") | 177 | self.screen.print_success( |
178 | "Song will not be played for 30 days") | ||
176 | self.player.stop() | 179 | self.player.stop() |
177 | else: | 180 | else: |
178 | Screen.print_error("Failed to sleep song") | 181 | self.screen.print_error("Failed to sleep song") |
179 | except NotImplementedError: | 182 | except NotImplementedError: |
180 | Screen.print_error("Cannot sleep this type of track") | 183 | self.screen.print_error("Cannot sleep this type of track") |
181 | 184 | ||
182 | def raise_volume(self, song): | 185 | def raise_volume(self, song): |
183 | try: | 186 | try: |
184 | self.player.raise_volume() | 187 | self.player.raise_volume() |
185 | except NotImplementedError: | 188 | except NotImplementedError: |
186 | Screen.print_error("Cannot sleep this type of track") | 189 | self.screen.print_error("Cannot sleep this type of track") |
187 | 190 | ||
188 | def lower_volume(self, song): | 191 | def lower_volume(self, song): |
189 | try: | 192 | try: |
190 | self.player.lower_volume() | 193 | self.player.lower_volume() |
191 | except NotImplementedError: | 194 | except NotImplementedError: |
192 | Screen.print_error("Cannot sleep this type of track") | 195 | self.screen.print_error("Cannot sleep this type of track") |
193 | 196 | ||
194 | def quit(self, song): | 197 | def quit(self, song): |
195 | self.player.end_station() | 198 | self.player.end_station() |
@@ -209,20 +212,21 @@ class PlayerApp(object): | |||
209 | try: | 212 | try: |
210 | cmd = getattr(self, self.CMD_MAP[input][1]) | 213 | cmd = getattr(self, self.CMD_MAP[input][1]) |
211 | except (IndexError, KeyError): | 214 | except (IndexError, KeyError): |
212 | return Screen.print_error("Invalid command {!r}!".format(input)) | 215 | return self.screen.print_error( |
216 | "Invalid command {!r}!".format(input)) | ||
213 | 217 | ||
214 | cmd(song) | 218 | cmd(song) |
215 | 219 | ||
216 | def pre_poll(self): | 220 | def pre_poll(self): |
217 | Screen.set_echo(False) | 221 | self.screen.set_echo(False) |
218 | 222 | ||
219 | def post_poll(self): | 223 | def post_poll(self): |
220 | Screen.set_echo(True) | 224 | self.screen.set_echo(True) |
221 | 225 | ||
222 | def pre_flight_checks(self): | 226 | def pre_flight_checks(self): |
223 | # See #52, this key no longer passes some server-side check | 227 | # See #52, this key no longer passes some server-side check |
224 | if self.client.partner_user == "iphone": | 228 | if self.client.partner_user == "iphone": |
225 | Screen.print_error(( | 229 | self.screen.print_error(( |
226 | "The `iphone` partner key set is no longer compatible with " | 230 | "The `iphone` partner key set is no longer compatible with " |
227 | "pydora. Please re-run pydora-configure to re-generate " | 231 | "pydora. Please re-run pydora-configure to re-generate " |
228 | "your config file before continuing.")) | 232 | "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 | |||
6 | import subprocess | 6 | import subprocess |
7 | from pandora.py2compat import input | 7 | from pandora.py2compat import input |
8 | 8 | ||
9 | try: | ||
10 | import termios | ||
11 | except ImportError: | ||
12 | # Windows does not have a termios module | ||
13 | termios = None | ||
14 | 9 | ||
10 | class TerminalPlatformUnsupported(Exception): | ||
11 | """Platform-specific functionality is not supported | ||
15 | 12 | ||
13 | Raised by code that can not be used to interact with the terminal on this | ||
14 | platform. | ||
15 | """ | ||
16 | pass | ||
16 | 17 | ||
17 | 18 | ||
18 | class Colors(object): | 19 | class Colors(object): |
@@ -35,25 +36,100 @@ class Colors(object): | |||
35 | white = __wrap_with("37") | 36 | white = __wrap_with("37") |
36 | 37 | ||
37 | 38 | ||
38 | class Screen(object): | 39 | class PosixEchoControl(object): |
40 | """Posix Console Echo Control Driver | ||
39 | 41 | ||
40 | @staticmethod | 42 | Uses termios on POSIX compliant platforms to control console echo. Is not |
41 | def set_echo(enabled): | 43 | supported on Windows as termios is not available and will throw a |
42 | if not termios: | 44 | TerminalPlatformUnsupported exception if contructed on Windows. |
43 | return | 45 | """ |
46 | |||
47 | def __init__(self): | ||
48 | try: | ||
49 | import termios | ||
50 | self.termios = termios | ||
51 | except ImportError: | ||
52 | raise TerminalPlatformUnsupported("POSIX not supported") | ||
44 | 53 | ||
54 | def set_echo(self, enabled): | ||
45 | handle = sys.stdin.fileno() | 55 | handle = sys.stdin.fileno() |
46 | if not os.isatty(handle): | 56 | if not os.isatty(handle): |
47 | return | 57 | return |
48 | 58 | ||
49 | attrs = termios.tcgetattr(handle) | 59 | attrs = self.termios.tcgetattr(handle) |
60 | |||
61 | if enabled: | ||
62 | attrs[3] |= self.termios.ECHO | ||
63 | else: | ||
64 | attrs[3] &= ~self.termios.ECHO | ||
65 | |||
66 | self.termios.tcsetattr(handle, self.termios.TCSANOW, attrs) | ||
67 | |||
68 | |||
69 | class Win32EchoControl(object): | ||
70 | """Windows Console Echo Control Driver | ||
71 | |||
72 | This uses the console API from WinCon.h and ctypes to control console echo | ||
73 | on Windows clients. It is not possible to construct this class on | ||
74 | non-Windows systems, on those systems it will throw a | ||
75 | TerminalPlatformUnsupported exception. | ||
76 | """ | ||
77 | |||
78 | STD_INPUT_HANDLE = -10 | ||
79 | ENABLE_ECHO_INPUT = 0x4 | ||
80 | DISABLE_ECHO_INPUT = ~ENABLE_ECHO_INPUT | ||
81 | |||
82 | def __init__(self): | ||
83 | import ctypes | ||
84 | |||
85 | if not hasattr(ctypes, "windll"): | ||
86 | raise TerminalPlatformUnsupported("Windows not supported") | ||
87 | |||
88 | from ctypes import wintypes | ||
89 | |||
90 | self.ctypes = ctypes | ||
91 | self.wintypes = wintypes | ||
92 | self.kernel32 = ctypes.windll.kernel32 | ||
93 | |||
94 | def _GetStdHandle(self, handle): | ||
95 | return self.kernel32.GetStdHandle(handle) | ||
96 | |||
97 | def _GetConsoleMode(self, handle): | ||
98 | mode = self.wintypes.DWORD() | ||
99 | self.kernel32.GetConsoleMode(handle, self.ctypes.byref(mode)) | ||
100 | return mode.value | ||
101 | |||
102 | def _SetConsoleMode(self, handle, value): | ||
103 | self.kernel32.SetConsoleMode(handle, value) | ||
104 | |||
105 | def set_echo(self, enabled): | ||
106 | stdin = self._GetStdHandle(self.STD_INPUT_HANDLE) | ||
107 | mode = self._GetConsoleMode(stdin) | ||
50 | 108 | ||
51 | if enabled: | 109 | if enabled: |
52 | attrs[3] |= termios.ECHO | 110 | self._SetConsoleMode(stdin, mode | self.ENABLE_ECHO_INPUT) |
53 | else: | 111 | else: |
54 | attrs[3] &= ~termios.ECHO | 112 | self._SetConsoleMode(stdin, mode & self.DISABLE_ECHO_INPUT) |
113 | |||
114 | |||
115 | class Screen(object): | ||
116 | |||
117 | def __init__(self): | ||
118 | try: | ||
119 | self._echo_driver = PosixEchoControl() | ||
120 | except TerminalPlatformUnsupported: | ||
121 | pass | ||
122 | |||
123 | try: | ||
124 | self._echo_driver = Win32EchoControl() | ||
125 | except TerminalPlatformUnsupported: | ||
126 | pass | ||
127 | |||
128 | if not self._echo_driver: | ||
129 | raise TerminalPlatformUnsupported("No supported terminal driver") | ||
55 | 130 | ||
56 | termios.tcsetattr(handle, termios.TCSANOW, attrs) | 131 | def set_echo(self, enabled): |
132 | self._echo_driver.set_echo(enabled) | ||
57 | 133 | ||
58 | @staticmethod | 134 | @staticmethod |
59 | def clear(): | 135 | def clear(): |