gridhour 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gridhour/__init__.py +3 -0
- gridhour/__main__.py +3 -0
- gridhour/app.py +224 -0
- gridhour/canvas.py +108 -0
- gridhour/cli.py +126 -0
- gridhour/compose.py +561 -0
- gridhour/grid.py +417 -0
- gridhour/keys.py +143 -0
- gridhour/output.py +131 -0
- gridhour/plan.py +184 -0
- gridhour/safe.py +67 -0
- gridhour/state.py +104 -0
- gridhour/themes.py +111 -0
- gridhour-0.1.0.dist-info/METADATA +189 -0
- gridhour-0.1.0.dist-info/RECORD +19 -0
- gridhour-0.1.0.dist-info/WHEEL +5 -0
- gridhour-0.1.0.dist-info/entry_points.txt +2 -0
- gridhour-0.1.0.dist-info/licenses/LICENSE +21 -0
- gridhour-0.1.0.dist-info/top_level.txt +1 -0
gridhour/__init__.py
ADDED
gridhour/__main__.py
ADDED
gridhour/app.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""The interactive session.
|
|
2
|
+
|
|
3
|
+
Everything that can happen arrives as an event on one queue: key presses from a reader thread,
|
|
4
|
+
"resize" from the SIGWINCH handler, "data" when a background fetch finishes, and a "tick" when
|
|
5
|
+
the queue stays quiet until the next whole second. The main loop takes one event, updates the
|
|
6
|
+
state, draws a frame and writes only the rows that differ from the previous frame.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import queue
|
|
10
|
+
import shutil
|
|
11
|
+
import signal
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
|
|
17
|
+
from .compose import compose
|
|
18
|
+
from .grid import load
|
|
19
|
+
from .keys import handle_key
|
|
20
|
+
from .themes import C
|
|
21
|
+
|
|
22
|
+
RELOAD = 5 * 60 # rebuild the window from cache this often; the network is hit only when it is stale
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def utcnow():
|
|
26
|
+
return datetime.now(UTC)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------- background data
|
|
30
|
+
|
|
31
|
+
_fetch_lock = threading.Lock()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fetch(st, force=False, notify=None):
|
|
35
|
+
"""Load in a thread. Routine refreshes are skipped while one is running; a forced one (new
|
|
36
|
+
postcode, R) always starts, and the newest request wins."""
|
|
37
|
+
with _fetch_lock:
|
|
38
|
+
if st.loading and not force:
|
|
39
|
+
return
|
|
40
|
+
st.gen += 1
|
|
41
|
+
gen = st.gen
|
|
42
|
+
st.loading = True
|
|
43
|
+
|
|
44
|
+
def work():
|
|
45
|
+
fc, failure = None, None
|
|
46
|
+
try:
|
|
47
|
+
fc = load(utcnow(), st.postcode, st.region_id, st.gsp, st.prices, force=force)
|
|
48
|
+
except Exception as e: # noqa: BLE001 - a background failure must reach the status line, not stderr
|
|
49
|
+
failure = "update failed (%s)" % type(e).__name__
|
|
50
|
+
with _fetch_lock:
|
|
51
|
+
if gen == st.gen:
|
|
52
|
+
if fc is not None:
|
|
53
|
+
st.fc = fc
|
|
54
|
+
if fc.errors:
|
|
55
|
+
st.say(fc.errors[0], C.AMBER, 6)
|
|
56
|
+
elif force:
|
|
57
|
+
st.say("forecast updated", C.GREEN)
|
|
58
|
+
else:
|
|
59
|
+
st.say(failure, C.RED, 8)
|
|
60
|
+
st.loading = False
|
|
61
|
+
if notify:
|
|
62
|
+
notify("data")
|
|
63
|
+
|
|
64
|
+
threading.Thread(target=work, name="gridhour-fetch", daemon=True).start()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------- keyboard
|
|
68
|
+
|
|
69
|
+
class KeyDecoder:
|
|
70
|
+
"""Cuts a byte stream into keys: plain characters, CSI sequences (ESC [ ... final) and
|
|
71
|
+
SS3 sequences (ESC O x). A sequence split across two reads is held until it is complete;
|
|
72
|
+
an ESC with nothing after it in the same read is the Escape key itself."""
|
|
73
|
+
|
|
74
|
+
def __init__(self):
|
|
75
|
+
self.pending = ""
|
|
76
|
+
|
|
77
|
+
def feed(self, text):
|
|
78
|
+
self.pending += text
|
|
79
|
+
keys = []
|
|
80
|
+
while self.pending:
|
|
81
|
+
key, used = self._next(self.pending)
|
|
82
|
+
if used == 0:
|
|
83
|
+
break # an unfinished sequence, wait for more bytes
|
|
84
|
+
keys.append(key)
|
|
85
|
+
self.pending = self.pending[used:]
|
|
86
|
+
return keys
|
|
87
|
+
|
|
88
|
+
def flush(self):
|
|
89
|
+
"""Whatever is still pending is taken literally (a lone ESC, say)."""
|
|
90
|
+
keys = list(self.pending)
|
|
91
|
+
self.pending = ""
|
|
92
|
+
return keys
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _next(s):
|
|
96
|
+
if s[0] != "\x1b":
|
|
97
|
+
return s[0], 1
|
|
98
|
+
if len(s) == 1:
|
|
99
|
+
return None, 0
|
|
100
|
+
if s[1] == "O":
|
|
101
|
+
return (s[:3], 3) if len(s) >= 3 else (None, 0)
|
|
102
|
+
if s[1] != "[":
|
|
103
|
+
return "\x1b", 1 # ESC followed by an ordinary key: two keys
|
|
104
|
+
for j in range(2, len(s)):
|
|
105
|
+
if "\x40" <= s[j] <= "\x7e":
|
|
106
|
+
return s[:j + 1], j + 1
|
|
107
|
+
return None, 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def read_keys(fd, events):
|
|
111
|
+
"""Reader thread: decoded keys go onto the event queue."""
|
|
112
|
+
dec = KeyDecoder()
|
|
113
|
+
while True:
|
|
114
|
+
try:
|
|
115
|
+
data = os.read(fd, 1024)
|
|
116
|
+
except OSError:
|
|
117
|
+
return
|
|
118
|
+
if not data:
|
|
119
|
+
return
|
|
120
|
+
for k in dec.feed(data.decode("utf-8", "ignore")):
|
|
121
|
+
events.put(("key", k))
|
|
122
|
+
if dec.pending == "\x1b": # nothing followed the ESC in this read
|
|
123
|
+
for k in dec.flush():
|
|
124
|
+
events.put(("key", k))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------- terminal
|
|
128
|
+
|
|
129
|
+
class Terminal:
|
|
130
|
+
"""Cbreak mode on the alternate screen with the cursor hidden; all of it undone on exit."""
|
|
131
|
+
|
|
132
|
+
# autowrap off (?7l): if a row is ever wider than the terminal it is cut, not wrapped and scrolled
|
|
133
|
+
ENTER = "\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[2J"
|
|
134
|
+
LEAVE = "\x1b[0m\x1b[?7h\x1b[?25h\x1b[?1049l"
|
|
135
|
+
|
|
136
|
+
def __init__(self, fd, out):
|
|
137
|
+
self.fd, self.out, self.saved = fd, out, None
|
|
138
|
+
|
|
139
|
+
def __enter__(self):
|
|
140
|
+
import termios
|
|
141
|
+
import tty
|
|
142
|
+
self.saved = termios.tcgetattr(self.fd)
|
|
143
|
+
tty.setcbreak(self.fd)
|
|
144
|
+
self.out.write(self.ENTER)
|
|
145
|
+
self.out.flush()
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def __exit__(self, *exc):
|
|
149
|
+
import termios
|
|
150
|
+
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.saved)
|
|
151
|
+
self.out.write(self.LEAVE)
|
|
152
|
+
self.out.flush()
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Screen:
|
|
157
|
+
"""Remembers what is on screen and sends only the rows that changed, in one write."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, out):
|
|
160
|
+
self.out = out
|
|
161
|
+
self.shown = []
|
|
162
|
+
self.size = None
|
|
163
|
+
|
|
164
|
+
def show(self, lines, size):
|
|
165
|
+
parts = []
|
|
166
|
+
if size != self.size:
|
|
167
|
+
parts.append("\x1b[2J")
|
|
168
|
+
self.shown, self.size = [], size
|
|
169
|
+
for y, line in enumerate(lines):
|
|
170
|
+
if y >= len(self.shown) or self.shown[y] != line:
|
|
171
|
+
parts.append("\x1b[%d;1H" % (y + 1) + line)
|
|
172
|
+
if parts:
|
|
173
|
+
self.out.write("".join(parts))
|
|
174
|
+
self.out.flush()
|
|
175
|
+
self.shown = lines
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------- loop
|
|
179
|
+
|
|
180
|
+
def run(st):
|
|
181
|
+
fd, out = sys.stdin.fileno(), sys.stdout
|
|
182
|
+
# SimpleQueue.put is reentrant, so the SIGWINCH handler may call it while the main thread is
|
|
183
|
+
# inside get(); a plain Queue would deadlock on its own lock there.
|
|
184
|
+
events = queue.SimpleQueue()
|
|
185
|
+
previous = signal.signal(signal.SIGWINCH, lambda *a: events.put(("resize", None)))
|
|
186
|
+
try:
|
|
187
|
+
with Terminal(fd, out):
|
|
188
|
+
threading.Thread(target=read_keys, args=(fd, events), name="gridhour-keys", daemon=True).start()
|
|
189
|
+
_loop(st, events, Screen(out))
|
|
190
|
+
finally:
|
|
191
|
+
signal.signal(signal.SIGWINCH, previous if previous is not None else signal.SIG_DFL)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _loop(st, events, screen):
|
|
195
|
+
notify = lambda kind: events.put((kind, None)) # noqa: E731
|
|
196
|
+
fetch(st, notify=notify)
|
|
197
|
+
reloaded, hour = time.monotonic(), utcnow().hour
|
|
198
|
+
while True:
|
|
199
|
+
live = utcnow()
|
|
200
|
+
if time.monotonic() - reloaded > RELOAD or live.hour != hour:
|
|
201
|
+
reloaded, hour = time.monotonic(), live.hour
|
|
202
|
+
fetch(st, notify=notify)
|
|
203
|
+
W, H = shutil.get_terminal_size((100, 30))
|
|
204
|
+
now = st.frozen or live.replace(microsecond=0)
|
|
205
|
+
screen.show(compose(W, H, st, now, live.replace(microsecond=0)).lines(), (W, H))
|
|
206
|
+
try:
|
|
207
|
+
kind, value = events.get(timeout=max(0.05, 1.0 - live.microsecond / 1e6))
|
|
208
|
+
except queue.Empty:
|
|
209
|
+
continue # the clock ticked, draw again
|
|
210
|
+
batch = [(kind, value)]
|
|
211
|
+
while True: # take everything already waiting, draw once
|
|
212
|
+
try:
|
|
213
|
+
batch.append(events.get_nowait())
|
|
214
|
+
except queue.Empty:
|
|
215
|
+
break
|
|
216
|
+
for kind, value in batch:
|
|
217
|
+
if kind != "key":
|
|
218
|
+
continue
|
|
219
|
+
action = handle_key(st, value, utcnow())
|
|
220
|
+
if action == "quit":
|
|
221
|
+
return
|
|
222
|
+
if action in ("fetch", "refetch"):
|
|
223
|
+
reloaded = time.monotonic()
|
|
224
|
+
fetch(st, force=action == "refetch", notify=notify)
|
gridhour/canvas.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""An off-screen frame: a grid of cells that turns into ANSI text.
|
|
2
|
+
|
|
3
|
+
Each cell holds a character, a foreground, a background and a bold flag. When the frame is
|
|
4
|
+
turned into text, an emitter tracks the terminal's current attributes and writes only the ones
|
|
5
|
+
that change from one cell to the next, so a run of same-coloured cells costs one escape code.
|
|
6
|
+
"""
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from .themes import C
|
|
11
|
+
|
|
12
|
+
# C0 and C1 control characters, DEL included. Text from the network or the config file goes through
|
|
13
|
+
# put(), so an ESC in a region name or an error message can never reach the terminal as a sequence.
|
|
14
|
+
UNSAFE = re.compile(r"[\x00-\x1f\x7f-\x9f]")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def clean(text):
|
|
18
|
+
return UNSAFE.sub("?", str(text))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Cell:
|
|
23
|
+
char: str = " "
|
|
24
|
+
fg: tuple = None
|
|
25
|
+
bg: tuple = None
|
|
26
|
+
bold: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Canvas:
|
|
30
|
+
def __init__(self, w, h):
|
|
31
|
+
self.w, self.h = w, h
|
|
32
|
+
self.rows = [[Cell(bg=C.BG) for _ in range(w)] for _ in range(h)]
|
|
33
|
+
|
|
34
|
+
def cell(self, x, y):
|
|
35
|
+
"""The cell at (x, y), or None off the edge."""
|
|
36
|
+
if 0 <= y < self.h and 0 <= x < self.w:
|
|
37
|
+
return self.rows[y][x]
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def bg_at(self, x, y):
|
|
41
|
+
c = self.cell(x, y)
|
|
42
|
+
return c.bg if c else C.BG
|
|
43
|
+
|
|
44
|
+
def put(self, x, y, text, fg=None, bg=None, bold=False):
|
|
45
|
+
"""Write ``text`` from (x, y) rightwards. ``bg=None`` keeps each cell's own background."""
|
|
46
|
+
for i, ch in enumerate(clean(text)):
|
|
47
|
+
c = self.cell(x + i, y)
|
|
48
|
+
if c is None:
|
|
49
|
+
continue
|
|
50
|
+
c.char, c.fg, c.bold = ch, fg, bold
|
|
51
|
+
if bg is not None:
|
|
52
|
+
c.bg = bg
|
|
53
|
+
|
|
54
|
+
def fill(self, x, y, w, bg):
|
|
55
|
+
"""Blank ``w`` cells and give them background ``bg``."""
|
|
56
|
+
for i in range(w):
|
|
57
|
+
c = self.cell(x + i, y)
|
|
58
|
+
if c is not None:
|
|
59
|
+
c.char, c.fg, c.bg, c.bold = " ", None, bg, False
|
|
60
|
+
|
|
61
|
+
def tint(self, x, y, bg):
|
|
62
|
+
"""Change only the background of one cell."""
|
|
63
|
+
c = self.cell(x, y)
|
|
64
|
+
if c is not None:
|
|
65
|
+
c.bg = bg
|
|
66
|
+
|
|
67
|
+
def text(self):
|
|
68
|
+
"""The characters alone, one string per row (for tests)."""
|
|
69
|
+
return ["".join(c.char for c in row) for row in self.rows]
|
|
70
|
+
|
|
71
|
+
def lines(self):
|
|
72
|
+
"""One ANSI string per row. Every row starts and ends with attributes reset."""
|
|
73
|
+
return [_Emitter().row(row) for row in self.rows]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _Emitter:
|
|
77
|
+
"""Writes cells while remembering what the terminal is already set to."""
|
|
78
|
+
|
|
79
|
+
def __init__(self):
|
|
80
|
+
self.fg = self.bg = None # None is the terminal default, which is what reset leaves
|
|
81
|
+
self.bold = False
|
|
82
|
+
self.out = ["\x1b[0m"]
|
|
83
|
+
|
|
84
|
+
def row(self, cells):
|
|
85
|
+
for c in cells:
|
|
86
|
+
self.style(c)
|
|
87
|
+
self.out.append(c.char)
|
|
88
|
+
self.out.append("\x1b[0m")
|
|
89
|
+
return "".join(self.out)
|
|
90
|
+
|
|
91
|
+
def style(self, c):
|
|
92
|
+
codes = []
|
|
93
|
+
if c.bold != self.bold:
|
|
94
|
+
# there is no portable "bold off" that leaves colours alone, so a change resets
|
|
95
|
+
if self.bold:
|
|
96
|
+
codes.append("0")
|
|
97
|
+
self.fg = self.bg = None
|
|
98
|
+
else:
|
|
99
|
+
codes.append("1")
|
|
100
|
+
self.bold = c.bold
|
|
101
|
+
if c.fg != self.fg:
|
|
102
|
+
codes.append("39" if c.fg is None else "38;2;%d;%d;%d" % c.fg)
|
|
103
|
+
self.fg = c.fg
|
|
104
|
+
if c.bg != self.bg:
|
|
105
|
+
codes.append("49" if c.bg is None else "48;2;%d;%d;%d" % c.bg)
|
|
106
|
+
self.bg = c.bg
|
|
107
|
+
if codes:
|
|
108
|
+
self.out.append("\x1b[" + ";".join(codes) + "m")
|
gridhour/cli.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Command line entry point."""
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .app import run, utcnow
|
|
10
|
+
from .compose import MAX_H, MAX_W, compose
|
|
11
|
+
from .grid import cache_dir, load, normalize_postcode, parse_time, region_from_arg
|
|
12
|
+
from .output import json_output, line_output, watch
|
|
13
|
+
from .plan import MODES, parse_job
|
|
14
|
+
from .state import config_path, state_from_config
|
|
15
|
+
from .themes import THEME_ORDER, apply_theme
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_size(text):
|
|
19
|
+
m = re.fullmatch(r"([0-9]{1,4})x([0-9]{1,4})", text.strip().lower())
|
|
20
|
+
if not m:
|
|
21
|
+
raise ValueError("--size looks like 120x40")
|
|
22
|
+
W, H = int(m.group(1)), int(m.group(2))
|
|
23
|
+
if not (40 <= W <= MAX_W and 8 <= H <= MAX_H):
|
|
24
|
+
raise ValueError("--size must be between 40x8 and %dx%d" % (MAX_W, MAX_H))
|
|
25
|
+
return W, H
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def reset():
|
|
29
|
+
"""Delete the config and the cache (cache file names contain the postcode)."""
|
|
30
|
+
gone = 0
|
|
31
|
+
paths = [config_path()]
|
|
32
|
+
try:
|
|
33
|
+
paths += [os.path.join(cache_dir(), n) for n in os.listdir(cache_dir())
|
|
34
|
+
if n.endswith(".json") and (n.startswith(("carbon-", "prices-", "agile-", ".tmp-")))]
|
|
35
|
+
except OSError:
|
|
36
|
+
pass
|
|
37
|
+
for p in paths:
|
|
38
|
+
try:
|
|
39
|
+
os.remove(p)
|
|
40
|
+
gone += 1
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
pass
|
|
43
|
+
except OSError as e:
|
|
44
|
+
print("gridhour: could not remove %s: %s" % (p, e.strerror), file=sys.stderr)
|
|
45
|
+
print("gridhour: saved settings and cache removed" if gone else "gridhour: nothing to reset")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main(argv=None):
|
|
49
|
+
ap = argparse.ArgumentParser(prog="gridhour",
|
|
50
|
+
description="When British electricity is green and cheap: a 48 hour carbon and "
|
|
51
|
+
"Agile price timeline for the terminal.")
|
|
52
|
+
ap.add_argument("postcode", nargs="*", help="your postcode, full or the first half (remembered)")
|
|
53
|
+
ap.add_argument("--region", help="a region instead of a postcode: 1-18, an Agile letter A-P, or a name")
|
|
54
|
+
ap.add_argument("--no-prices", action="store_true", help="carbon only, skip Octopus Agile prices")
|
|
55
|
+
ap.add_argument("--mode", choices=MODES, help="rank windows by carbon, price or both")
|
|
56
|
+
ap.add_argument("--at", help="freeze the clock at this time (ISO 8601, UTC unless it has an offset)")
|
|
57
|
+
ap.add_argument("--line", action="store_true", help="print one line for a status bar and exit")
|
|
58
|
+
ap.add_argument("--best", metavar="DURATION", help="with --line/--watch: the best start for a job this long")
|
|
59
|
+
ap.add_argument("--tmux", action="store_true", help="like --line with tmux colour codes")
|
|
60
|
+
ap.add_argument("--watch", action="store_true", help="the one-liner, updating in place")
|
|
61
|
+
ap.add_argument("--json", action="store_true", help="print JSON and exit")
|
|
62
|
+
ap.add_argument("--once", action="store_true", help="print one frame of the full UI and exit")
|
|
63
|
+
ap.add_argument("--size", help="WxH for --once (default: terminal size)")
|
|
64
|
+
ap.add_argument("--theme", choices=THEME_ORDER, help="colour theme")
|
|
65
|
+
ap.add_argument("--12h", dest="h12", action="store_true", help="12-hour clock")
|
|
66
|
+
ap.add_argument("--compact", action="store_true", help="small-pane layout without the chart")
|
|
67
|
+
ap.add_argument("--reset", action="store_true", help="forget saved settings and jobs")
|
|
68
|
+
ap.add_argument("--version", action="version", version="gridhour " + __version__)
|
|
69
|
+
args = ap.parse_args(argv)
|
|
70
|
+
if args.reset:
|
|
71
|
+
reset()
|
|
72
|
+
return
|
|
73
|
+
st = state_from_config()
|
|
74
|
+
try:
|
|
75
|
+
if args.postcode:
|
|
76
|
+
st.postcode = normalize_postcode(" ".join(args.postcode))
|
|
77
|
+
st.region_id = None
|
|
78
|
+
st.save()
|
|
79
|
+
if args.region:
|
|
80
|
+
st.region_id = region_from_arg(args.region)
|
|
81
|
+
st.postcode = None
|
|
82
|
+
st.persist = False
|
|
83
|
+
best = parse_job("job " + args.best) if args.best else None
|
|
84
|
+
at = parse_time(args.at) if args.at else None
|
|
85
|
+
if at and not 2000 <= at.year <= 2100:
|
|
86
|
+
raise ValueError("--at must be between the years 2000 and 2100")
|
|
87
|
+
size = parse_size(args.size) if args.size else None
|
|
88
|
+
except ValueError as e:
|
|
89
|
+
sys.exit("gridhour: %s" % e)
|
|
90
|
+
if args.no_prices:
|
|
91
|
+
st.prices = False
|
|
92
|
+
if args.mode:
|
|
93
|
+
st.mode = args.mode
|
|
94
|
+
if args.theme:
|
|
95
|
+
apply_theme(args.theme)
|
|
96
|
+
if args.h12:
|
|
97
|
+
st.h12 = True
|
|
98
|
+
if args.compact:
|
|
99
|
+
st.compact = "on"
|
|
100
|
+
if args.watch:
|
|
101
|
+
watch(st, utcnow, "ansi" if sys.stdout.isatty() else "plain", best)
|
|
102
|
+
return
|
|
103
|
+
if args.line or args.tmux or args.json or args.once:
|
|
104
|
+
now = at or utcnow().replace(microsecond=0)
|
|
105
|
+
st.fc = load(now, st.postcode, st.region_id, st.gsp, st.prices)
|
|
106
|
+
if args.json:
|
|
107
|
+
print(json_output(st, now))
|
|
108
|
+
elif args.once:
|
|
109
|
+
W, H = size or shutil.get_terminal_size((120, 40))
|
|
110
|
+
print("\n".join(compose(W, H, st, now, now).lines()))
|
|
111
|
+
else:
|
|
112
|
+
color = "tmux" if args.tmux else ("ansi" if sys.stdout.isatty() else "plain")
|
|
113
|
+
print(line_output(st, now, color, best))
|
|
114
|
+
return
|
|
115
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
116
|
+
sys.exit("gridhour: needs an interactive terminal (use --once, --line or --json otherwise)")
|
|
117
|
+
if at:
|
|
118
|
+
st.frozen = at
|
|
119
|
+
try:
|
|
120
|
+
run(st)
|
|
121
|
+
except KeyboardInterrupt:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
main()
|