├── .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 |
--------------------------------------------------------------------------------