├── .gitignore ├── LICENSE ├── README.rst ├── pybart ├── __init__.py ├── api.py ├── draw.py ├── main.py ├── settings.py └── utils.py ├── screenshot.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | 4 | build/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Eric Wang 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of pybart nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pybart 2 | ====== 3 | 4 | .. image:: https://img.shields.io/pypi/v/pybart.svg 5 | :target: https://pypi.python.org/pypi/pybart 6 | :alt: Version 7 | .. image:: https://img.shields.io/pypi/pyversions/pybart.svg 8 | :target: http://py3readiness.org/ 9 | :alt: Python versions 10 | .. image:: https://img.shields.io/pypi/wheel/pybart.svg 11 | :target: http://pythonwheels.com/ 12 | :alt: Wheel status 13 | .. image:: https://img.shields.io/pypi/l/pybart.svg 14 | :target: https://opensource.org/licenses/BSD-3-Clause 15 | :alt: License 16 | 17 | Real time BART (`Bay Area Rapid Transit `_) information 18 | in your terminal! 19 | 20 | .. image:: https://raw.githubusercontent.com/ericdwang/pybart/master/screenshot.png 21 | :alt: Screenshot 22 | 23 | Features 24 | -------- 25 | 26 | - Real time estimates and service advisories 27 | - `Curses-based `_ 28 | TUI with auto-refreshing and resizing 29 | - View multiple stations at the same time 30 | - Colors indicating transit lines, estimate times, and train lengths 31 | - Ability to configure a default set of stations 32 | - Other non-TUI commands like opening a map and getting the fare for a trip 33 | - Includes a low-level Python wrapper for the full BART API 34 | - No dependencies; built with only standard libraries 35 | 36 | Requirements 37 | ------------ 38 | 39 | - Python 2.6+ or Python 3.0+ with the ``curses`` module installed (i.e. not 40 | Windows) 41 | - Terminal with 256 color support to correctly display the Richmond-Fremont 42 | line as orange (magenta otherwise) 43 | 44 | - Note: this usually involves setting the ``TERM`` environment variable to 45 | ``xterm-256color`` 46 | 47 | Installation 48 | ------------ 49 | 50 | ``pip install pybart`` 51 | 52 | Usage 53 | ----- 54 | 55 | :: 56 | 57 | usage: bart [-h] [-v] {map,list,est,fare} ... 58 | 59 | Display real time BART estimates. 60 | 61 | optional arguments: 62 | -h, --help show this help message and exit 63 | -v, --version show program's version number and exit 64 | 65 | commands: 66 | {map,list,est,fare} 67 | map open station map in web browser 68 | list show list of stations and their abbreviations 69 | est display estimates for specified stations 70 | fare show fare for a trip between two stations 71 | 72 | examples: 73 | bart get estimates for $BART_STATIONS 74 | bart map open station map 75 | bart list list all stations 76 | bart est mcar get estimates for MacArthur station 77 | bart est embr cols get estimates for Embarcadero and Coliseum stations 78 | bart fare conc sfia get fare for a trip between Concord and SFO stations 79 | 80 | Configuration 81 | ------------- 82 | 83 | The following (optional) environment variables can be used to configure pybart: 84 | 85 | - ``BART_STATIONS`` - a comma-separated string (e.g. ``mcar,embr,cols``) 86 | specifying the default stations to use when running ``bart`` with no 87 | arguments. 88 | - ``BART_API_KEY`` - the BART API key to use when fetching information. A 89 | public one is used by default, but you can get your own 90 | `here `_. 91 | 92 | API 93 | --- 94 | 95 | Even though it doesn't use everything, pybart includes a low-level Python 96 | wrapper for the full 97 | `BART API `_ with 98 | ``pybart.api.BART``. Every call by default returns the root element of the XML 99 | response using 100 | `ElementTree `_. 101 | JSON is also supported but the format is currently in 102 | `beta `_. 103 | 104 | Example usage:: 105 | 106 | >>> from pybart.api import BART 107 | >>> bart = BART() # Uses the public API key by default 108 | >>> root = bart.stn.stninfo('dbrk') 109 | >>> station = root.find('stations').find('station') 110 | >>> print(station.find('address').text + ', ' + station.find('city').text) 111 | 2160 Shattuck Avenue, Berkeley 112 | >>> print(bart.version().find('apiVersion').text) 113 | 3.10 114 | >>> bart = BART(json_format=True) # Now with JSON 115 | >>> root = bart.stn.stninfo('dbrk') 116 | >>> station = root['stations']['station'] 117 | >>> print(station['address'] + ', ' + station['city']) 118 | 2160 Shattuck Avenue, Berkeley 119 | 120 | License 121 | ------- 122 | 123 | `BSD 3-Clause `_ 124 | -------------------------------------------------------------------------------- /pybart/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.5.1' 2 | -------------------------------------------------------------------------------- /pybart/api.py: -------------------------------------------------------------------------------- 1 | try: 2 | # Python 3 imports 3 | from urllib.error import URLError 4 | from urllib.parse import urlencode 5 | from urllib.request import urlopen 6 | except ImportError: 7 | # Python 2 imports 8 | from urllib import urlencode 9 | from urllib2 import URLError 10 | from urllib2 import urlopen 11 | 12 | import errno 13 | import json 14 | from contextlib import closing 15 | from xml.etree import cElementTree 16 | 17 | from pybart import settings 18 | 19 | 20 | api_key = settings.BART_API_KEY or settings.DEFAULT_API_KEY 21 | 22 | 23 | class BaseAPI(object): 24 | """Base wrapper for the individual BART APIs.""" 25 | BART_URL = 'https://api.bart.gov/api/{api}.aspx' 26 | 27 | api = '' 28 | base_url = '' 29 | json_format = False 30 | key = '' 31 | 32 | def __init__(self, key, json_format): 33 | self.base_url = self.BART_URL.format(api=self.api) 34 | self.key = key 35 | self.json_format = json_format 36 | 37 | def _get_response_root(self, url): 38 | """Get the root of the response from the specified URL. 39 | 40 | Raise a RuntimeError if there's no Internet connection or there was an 41 | error in the response. 42 | """ 43 | try: 44 | # Note: closing isn't necessary in Python 3 because urlopen returns 45 | # a context manager already 46 | with closing(urlopen(url)) as response: 47 | body = response.read() 48 | 49 | except URLError as error: 50 | # Treat interrupted signal calls as warnings 51 | if error.reason.errno == errno.EINTR: 52 | raise RuntimeWarning 53 | raise RuntimeError('No Internet connection.') 54 | 55 | # Note: the BART API returns XML for errors (even when requesting JSON) 56 | # so all responses need to be checked for XML 57 | try: 58 | root = cElementTree.fromstring(body) 59 | try: 60 | raise RuntimeError(root.find('.//error').find('details').text) 61 | except AttributeError: # XML with no errors 62 | return root 63 | except cElementTree.ParseError: # JSON 64 | return json.loads(body)['root'] 65 | 66 | 67 | def api_method(method): 68 | """Decorator for using method signatures to validate and make API calls.""" 69 | def wrapper(self, *args, **kwargs): 70 | # Validate arguments 71 | method(self, *args, **kwargs) 72 | 73 | # Convert positional arguments to keyword arguments 74 | kwargs.update(zip(method.__code__.co_varnames[1:], args)) 75 | 76 | # Use the method name for the command and add other parameters 77 | kwargs.update({'cmd': method.__name__, 'key': self.key}) 78 | if self.json_format: 79 | kwargs['json'] = 'y' 80 | 81 | # Make the request and parse the response 82 | return self._get_response_root(self.base_url + '?' + urlencode(kwargs)) 83 | 84 | return wrapper 85 | 86 | 87 | class AdvisoryAPI(BaseAPI): 88 | """API for advisories: https://api.bart.gov/docs/bsa/""" 89 | api = 'bsa' 90 | 91 | @api_method 92 | def bsa(self, orig=None): 93 | pass 94 | 95 | @api_method 96 | def count(self): 97 | pass 98 | 99 | @api_method 100 | def elev(self): 101 | pass 102 | 103 | 104 | class EstimateAPI(BaseAPI): 105 | """API for real time estimates: https://api.bart.gov/docs/etd/""" 106 | api = 'etd' 107 | 108 | @api_method 109 | def etd(self, orig, plat=None, dir=None): 110 | pass 111 | 112 | 113 | class RouteAPI(BaseAPI): 114 | """API for route information: https://api.bart.gov/docs/route/""" 115 | api = 'route' 116 | 117 | @api_method 118 | def routeinfo(self, route, sched=None, date=None): 119 | pass 120 | 121 | @api_method 122 | def routes(self, sched=None, date=None): 123 | pass 124 | 125 | 126 | class ScheduleAPI(BaseAPI): 127 | """API for schedule information: https://api.bart.gov/docs/sched/""" 128 | api = 'sched' 129 | 130 | @api_method 131 | def arrive(self, orig, dest, time=None, date=None, b=None, a=None, l=None): 132 | pass 133 | 134 | @api_method 135 | def depart(self, orig, dest, time=None, date=None, b=None, a=None, l=None): 136 | pass 137 | 138 | @api_method 139 | def fare(self, orig, dest): 140 | pass 141 | 142 | @api_method 143 | def holiday(self): 144 | pass 145 | 146 | @api_method 147 | def routesched(self, route, date=None, time=None, l=None, sched=None): 148 | pass 149 | 150 | @api_method 151 | def scheds(self): 152 | pass 153 | 154 | @api_method 155 | def special(self, l=None): 156 | pass 157 | 158 | @api_method 159 | def stnsched(self, orig, date=None): 160 | pass 161 | 162 | 163 | class StationAPI(BaseAPI): 164 | """API for station information: https://api.bart.gov/docs/stn/""" 165 | api = 'stn' 166 | 167 | @api_method 168 | def stnaccess(self, orig, l=None): 169 | pass 170 | 171 | @api_method 172 | def stninfo(self, orig): 173 | pass 174 | 175 | @api_method 176 | def stns(self): 177 | pass 178 | 179 | 180 | class VersionAPI(BaseAPI): 181 | """API for version information: https://api.bart.gov/docs/version/""" 182 | api = 'version' 183 | 184 | def __call__(self): 185 | """Allow calling the class directly since the version API has no 186 | parameters: https://api.bart.gov/docs/version/version.aspx 187 | """ 188 | return self._get_response_root(self.base_url) 189 | 190 | 191 | class BART(object): 192 | """Wrapper for the BART API.""" 193 | bsa = None 194 | etd = None 195 | route = None 196 | sched = None 197 | stn = None 198 | version = None 199 | 200 | def __init__(self, key=api_key, json_format=False): 201 | """Initialize the individual APIs with the API key.""" 202 | args = (key, json_format) 203 | self.bsa = AdvisoryAPI(*args) 204 | self.etd = EstimateAPI(*args) 205 | self.route = RouteAPI(*args) 206 | self.sched = ScheduleAPI(*args) 207 | self.stn = StationAPI(*args) 208 | self.version = VersionAPI(*args) 209 | -------------------------------------------------------------------------------- /pybart/draw.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from textwrap import TextWrapper 3 | 4 | 5 | class EstimateDrawer(object): 6 | """Class for drawing BART estimates on the terminal.""" 7 | bart = None 8 | stations = None 9 | window = None 10 | wrapper = None 11 | 12 | prev_lines = 0 13 | 14 | def __init__(self, bart, stations, window): 15 | self.bart = bart 16 | self.stations = stations 17 | self.window = window 18 | self.wrapper = TextWrapper() 19 | 20 | def _format_minutes(self, minutes): 21 | """Return the minutes estimate formatted with its color.""" 22 | color = None 23 | 24 | try: 25 | minutes = int(minutes) 26 | if minutes <= 5: 27 | color = 'RED' 28 | elif minutes <= 10: 29 | color = 'YELLOW' 30 | minutes = str(minutes) + ' min' 31 | except ValueError: 32 | color = 'RED' 33 | 34 | return (minutes + ' ', color) 35 | 36 | def _format_length(self, length): 37 | """Return the train length formatted with its color.""" 38 | color = None 39 | length = int(length) 40 | 41 | if length < 6: 42 | color = 'YELLOW' 43 | elif length >= 8: 44 | color = 'GREEN' 45 | 46 | return ('({length} car)'.format(length=length), color) 47 | 48 | def draw(self): 49 | """Draw the information on the terminal.""" 50 | y = 0 51 | 52 | # Display the current time 53 | self.window.center(y, 'BART departures as of {time}'.format( 54 | time=datetime.now().strftime('%I:%M:%S %p'))) 55 | 56 | # Display advisories (if any) 57 | for advisory in self.bart.bsa.bsa().iterfind('bsa'): 58 | # Ignore advisories that state there aren't any delays 59 | try: 60 | text = '{type} ({posted}) - {sms_text}'.format( 61 | posted=advisory.find('posted').text, 62 | type=advisory.find('type').text, 63 | sms_text=advisory.find('sms_text').text, 64 | ) 65 | except AttributeError: 66 | break 67 | 68 | self.window.clear_lines(y + 1) 69 | y += 1 70 | 71 | self.wrapper.width = self.window.width 72 | for line in self.wrapper.wrap(text): 73 | y += 1 74 | self.window.fill_line(y, line, color_name='RED', bold=True) 75 | 76 | # Display stations 77 | for station_abbr in self.stations: 78 | self.window.clear_lines(y + 1) 79 | y += 2 80 | station = self.bart.etd.etd(station_abbr).find('station') 81 | self.window.fill_line(y, station.find('name').text, bold=True) 82 | 83 | # Display all destinations for a station 84 | for departure in station.iterfind('etd'): 85 | y += 1 86 | destination = departure.find('destination').text 87 | self.window.addstr(y, 0, destination + ' ' * ( 88 | self.window.spacing - len(destination))) 89 | x = self.window.spacing 90 | 91 | # Display all estimates for a destination on the same line 92 | for i, estimate in enumerate( 93 | departure.iterfind('estimate'), start=1): 94 | self.window.addstr( 95 | y, x, '# ', color_name=estimate.find('color').text) 96 | x += 2 97 | 98 | minutes, color = self._format_minutes( 99 | estimate.find('minutes').text) 100 | self.window.addstr( 101 | y, x, minutes, color_name=color, bold=True) 102 | x += len(minutes) 103 | 104 | length, color = self._format_length( 105 | estimate.find('length').text) 106 | self.window.addstr(y, x, length, color_name=color) 107 | x += len(length) 108 | 109 | # Clear the space between estimates 110 | space = (i + 1) * self.window.spacing - x 111 | if space > 0: 112 | self.window.addstr(y, x, ' ' * space) 113 | x += space 114 | 115 | # Clear the rest of the line 116 | remaining = self.window.width - x 117 | if remaining > 0: 118 | self.window.addstr(y, x, ' ' * remaining) 119 | 120 | # Display help text at the bottom 121 | self.window.clear_lines(y + 1) 122 | self.window.fill_line(y + 2, 'Press \'q\' to quit.') 123 | 124 | # Clear the bottom lines that contained text from the previous draw 125 | y = y + 3 126 | self.window.clear_lines(y, lines=self.prev_lines - y) 127 | 128 | # Save the number of lines drawn, excluding cleared lines at the bottom 129 | self.prev_lines = y 130 | -------------------------------------------------------------------------------- /pybart/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import subprocess 3 | import sys 4 | import webbrowser 5 | 6 | import pybart 7 | from pybart import settings 8 | from pybart.api import BART 9 | from pybart.draw import EstimateDrawer 10 | from pybart.utils import Window 11 | 12 | 13 | def main(): 14 | """Parse arguments and run the appropriate function. 15 | 16 | Serves as the entry point for the program. 17 | """ 18 | # Initialize argument parser 19 | parser = argparse.ArgumentParser( 20 | description='Display real time BART estimates.', 21 | epilog=( 22 | 'examples:\n' 23 | ' bart get estimates for $BART_STATIONS\n' 24 | ' bart map open station map\n' 25 | ' bart list list all stations\n' 26 | ' bart est mcar get estimates for MacArthur station\n' 27 | ' bart est embr cols get estimates for Embarcadero and ' 28 | 'Coliseum stations\n' 29 | ' bart fare conc sfia get fare for a trip between Concord and ' 30 | 'SFO stations' 31 | ), 32 | formatter_class=argparse.RawDescriptionHelpFormatter, 33 | ) 34 | parser.add_argument( 35 | '-v', '--version', action='version', version=pybart.__version__) 36 | 37 | # Add parsers for sub-commands 38 | subparsers = parser.add_subparsers(title='commands') 39 | 40 | map_parser = subparsers.add_parser( 41 | 'map', help='open station map in web browser') 42 | map_parser.set_defaults(func=open_map) 43 | 44 | list_parser = subparsers.add_parser( 45 | 'list', help='show list of stations and their abbreviations') 46 | list_parser.set_defaults(func=list_stations) 47 | 48 | estimate_parser = subparsers.add_parser( 49 | 'est', help='display estimates for specified stations') 50 | estimate_parser.add_argument('stations', nargs='*', metavar='STN') 51 | estimate_parser.set_defaults(func=display_estimates) 52 | 53 | fare_parser = subparsers.add_parser( 54 | 'fare', help='show fare for a trip between two stations') 55 | fare_parser.add_argument('stations', nargs=2, metavar='STN') 56 | fare_parser.set_defaults(func=show_fare) 57 | 58 | # If arguments were supplied parse them and run the appropriate function, 59 | # otherwise default to displaying estimates 60 | if len(sys.argv) > 1: 61 | args = parser.parse_args() 62 | args.func(args, parser) 63 | else: 64 | # Note: the argv check and this explicit call are only necessary for 65 | # Python 2 because a default function can't be set. In Python 3 all 66 | # that's needed is: parser.set_defaults(func=display_estimates) 67 | display_estimates(None, parser) 68 | 69 | 70 | def catch_errors_and_exit(function): 71 | """Run the function and return its output, catching errors and exiting if 72 | one occurs. 73 | """ 74 | try: 75 | return function() 76 | except RuntimeError as error: 77 | print(error) 78 | exit(1) 79 | 80 | 81 | def open_map(args, parser): 82 | webbrowser.open_new_tab(settings.BART_MAP_URL) 83 | 84 | 85 | def show_fare(args, parser): 86 | root = catch_errors_and_exit(lambda: BART().sched.fare(*args.stations)) 87 | print('$' + root.find('trip').find('fare').text) 88 | 89 | 90 | def list_stations(args, parser): 91 | root = catch_errors_and_exit(lambda: BART().stn.stns()) 92 | 93 | # Format stations with their abbreviations, and find the max station width 94 | stations = [] 95 | max_station_width = 0 96 | for station in root.find('stations').iterfind('station'): 97 | station_name = '{name} ({abbr})'.format( 98 | name=station.find('name').text, abbr=station.find('abbr').text) 99 | stations.append(station_name) 100 | max_station_width = max(max_station_width, len(station_name)) 101 | 102 | station_count = len(stations) 103 | 104 | # At least two spaces need to be between every station in a row 105 | max_station_width += 2 106 | 107 | # Note: in Python 3.3+ this can be replaced with shutil.get_terminal_size() 108 | terminal_width = int(subprocess.check_output(('tput', 'cols'))) 109 | 110 | # Calculate columns and rows assuming all stations take up the max width 111 | columns = max(terminal_width // max_station_width, 1) 112 | rows = station_count // columns + (station_count % columns > 0) 113 | 114 | row_stations = [[] for _ in range(rows)] 115 | for column in range(0, station_count, rows): 116 | # Find the max width for the whole column 117 | column_stations = stations[column:column + rows] 118 | column_width = max(len(station) for station in column_stations) + 2 119 | 120 | # Add each station in the column to their appropriate row with spacing 121 | for row, station in enumerate(column_stations): 122 | row_stations[row].append( 123 | station + ' ' * (column_width - len(station))) 124 | 125 | # Combine the stations in each row and print them 126 | for row in row_stations: 127 | print(''.join(row)) 128 | 129 | 130 | def display_estimates(args, parser): 131 | # Use the default stations if no arguments were passed in 132 | try: 133 | stations = args.stations 134 | except AttributeError: 135 | stations = settings.BART_STATIONS 136 | 137 | # Show help text if no stations were specified 138 | if not stations: 139 | parser.print_help() 140 | exit(1) 141 | 142 | with Window(settings.REFRESH_INTERVAL, settings.TOTAL_COLUMNS) as window: 143 | drawer = EstimateDrawer(BART(), stations, window) 144 | char = '' 145 | 146 | # Keep running until 'q' is pressed to exit or an error occurs 147 | while char != 'q': 148 | try: 149 | drawer.draw() 150 | except RuntimeWarning: 151 | pass 152 | 153 | char = window.getch() 154 | -------------------------------------------------------------------------------- /pybart/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | DEFAULT_API_KEY = 'MW9S-E7SL-26DU-VV8V' 5 | 6 | REFRESH_INTERVAL = 100 # Milliseconds 7 | TOTAL_COLUMNS = 4 8 | 9 | BART_API_KEY = os.environ.get('BART_API_KEY') 10 | 11 | try: 12 | BART_STATIONS = os.environ['BART_STATIONS'].split(',') 13 | except KeyError: 14 | BART_STATIONS = [] 15 | 16 | BART_MAP_URL = 'https://www.bart.gov/stations' 17 | -------------------------------------------------------------------------------- /pybart/utils.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | 4 | class Window(object): 5 | """Wrapper for the curses module.""" 6 | COLOR_ORANGE = 0 7 | 8 | spacing = 0 9 | total_columns = 0 10 | width = 0 11 | window = None 12 | 13 | def __init__(self, refresh_interval, total_columns): 14 | """Initialize the window with various settings.""" 15 | self.total_columns = total_columns 16 | self.window = curses.initscr() 17 | 18 | # Initialize colors with red, green, yellow, blue, and white 19 | curses.start_color() 20 | curses.use_default_colors() 21 | for i in range(1, 5): 22 | curses.init_pair(i, i, -1) 23 | curses.init_pair(7, 7, -1) 24 | 25 | # Use the orange color if the terminal supports it, and magenta 26 | # otherwise 27 | if curses.COLORS == 256: 28 | self.COLOR_ORANGE = 208 29 | else: 30 | self.COLOR_ORANGE = curses.COLOR_MAGENTA 31 | curses.init_pair(self.COLOR_ORANGE, self.COLOR_ORANGE, -1) 32 | 33 | # Disable typing echo and hide the cursor 34 | curses.noecho() 35 | curses.curs_set(0) 36 | 37 | # Set the refresh interval 38 | curses.halfdelay(refresh_interval) 39 | 40 | self._update_dimensions() 41 | 42 | def __enter__(self): 43 | return self 44 | 45 | def __exit__(self, exception_type, exception_value, traceback): 46 | """Properly de-initialize curses when exiting the runtime context.""" 47 | self.endwin() 48 | 49 | # If an exception occurred (ignoring interrupt keys like Control-C), 50 | # print it and exit the program 51 | if exception_type and exception_type != KeyboardInterrupt: 52 | print(exception_value) 53 | exit(1) 54 | 55 | return True 56 | 57 | def _get_color(self, color_name): 58 | """Get the color to use based on the name given.""" 59 | if not color_name: 60 | return 0 61 | 62 | if color_name == 'ORANGE': 63 | color = self.COLOR_ORANGE 64 | else: 65 | color = getattr(curses, 'COLOR_' + color_name) 66 | return curses.color_pair(color) 67 | 68 | def _update_dimensions(self): 69 | """Update the width of the window and spacing needed for columns.""" 70 | _, self.width = self.window.getmaxyx() 71 | self.spacing = self.width // self.total_columns 72 | 73 | def addstr(self, y, x, string, color_name='', bold=False): 74 | """Add a string with optional color and boldness.""" 75 | color = self._get_color(color_name) 76 | if bold: 77 | color |= curses.A_BOLD 78 | 79 | try: 80 | self.window.addstr(y, x, string, color) 81 | except curses.error: 82 | raise RuntimeError('Terminal too small.') 83 | 84 | def center(self, y, text): 85 | """Center the text in bold at a specified location.""" 86 | text_length = len(text) 87 | center = (self.width - text_length) // 2 88 | text_end = center + text_length 89 | 90 | centered_text = ' ' * center + text + ' ' * (self.width - text_end) 91 | self.addstr(y, 0, centered_text, bold=True) 92 | 93 | def fill_line(self, y, text, *args, **kwargs): 94 | """Add the text and fill the rest of the line with whitespace.""" 95 | self.addstr( 96 | y, 0, text + ' ' * (self.width - len(text)), *args, **kwargs) 97 | 98 | def clear_lines(self, y, lines=1): 99 | """Clear the specified lines.""" 100 | for i in range(lines): 101 | self.addstr(y + i, 0, ' ' * self.width) 102 | 103 | def getch(self): 104 | """Get the character input as an ASCII string. 105 | 106 | Also update the dimensions of the window if the terminal was resized. 107 | """ 108 | char = self.window.getch() 109 | if char == curses.KEY_RESIZE: 110 | self._update_dimensions() 111 | 112 | try: 113 | return chr(char) 114 | except ValueError: 115 | return '' 116 | 117 | def endwin(self): 118 | """End the window.""" 119 | curses.endwin() 120 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdwang/pybart/87985306ab1e068927ef89b44dd968440026ab01/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | with open('README.rst') as readme: 5 | long_description = readme.read() 6 | 7 | setup( 8 | name='pybart', 9 | version=__import__('pybart').__version__, 10 | description=( 11 | 'Real time BART (Bay Area Rapid Transit) information in your ' 12 | 'terminal!'), 13 | long_description=long_description, 14 | author='Eric Wang', 15 | author_email='gnawrice@gmail.com', 16 | url='https://github.com/ericdwang/pybart', 17 | packages=['pybart'], 18 | entry_points={'console_scripts': ['bart = pybart.main:main']}, 19 | classifiers=[ 20 | 'Environment :: Console :: Curses', 21 | 'Intended Audience :: End Users/Desktop', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Natural Language :: English', 24 | 'Operating System :: MacOS :: MacOS X', 25 | 'Operating System :: POSIX :: Linux', 26 | 'Programming Language :: Python :: 2', 27 | 'Programming Language :: Python :: 3', 28 | ], 29 | license='BSD 3-Clause', 30 | keywords=['bart', 'bay', 'area', 'rapid', 'transit'], 31 | ) 32 | --------------------------------------------------------------------------------