Skip to content

Commit dbfa14e

Browse files
committed
add rich and update README
1 parent cca8db6 commit dbfa14e

File tree

4 files changed

+323
-60
lines changed

4 files changed

+323
-60
lines changed

‎README.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
`ghtop` provides a number of views of all current public activity from all users across the entire GitHub platform. (Note that GitHub delays all events by five minutes.)
66

7-
<img width="850" src="https://user-images.githubusercontent.com/56260/101270865-3f033780-3732-11eb-8dcc-97caf7cc58e6.png" style="max-width: 850px">
7+
<img width="650" src="https://user-images.githubusercontent.com/1483922/105071141-0c3ea580-5a39-11eb-8808-34952c0bf26d.gif" style="max-width: 650px">
88

99
## Install
1010

@@ -42,25 +42,25 @@ There are 4 views you can choose: `ghtop simple`, `ghtop tail`, `ghtop quad`, or
4242

4343
A simple dump to your console of all events as they happen.
4444

45-
<img src="https://user-images.githubusercontent.com/346999/101861674-79e7df80-3b25-11eb-92d3-f888843f4aa2.png" width="500" style="max-width: 500px">
45+
<img src="https://user-images.githubusercontent.com/1483922/105074536-56298a80-5a3d-11eb-8e8c-32ba33e09405.png" width="650" style="max-width: 650px">
4646

4747
### ghtop tail
4848

49-
Like `simple`, but removes most bots, and only includes releases, issues and PRs (open, close, and comment events). A summary of the frequency of push events is also shown at the bottom of the screen.
49+
Like `simple`, but only includes releases, issues and PRs (open, close, and comment events). A summary of the frequency of different kind of events along with sparklines are shown at the top of the screen.
5050

51-
<img src="https://user-images.githubusercontent.com/346999/101861658-69376980-3b25-11eb-96ef-9d68f075abf7.png" width="700" style="max-width: 700px">
51+
<img src="https://user-images.githubusercontent.com/1483922/105074448-398d5280-5a3d-11eb-9603-3def521d87e5.png" width="650" style="max-width: 650px">
5252

5353
### ghtop quad
5454

55-
The same information as `tail`, but in a split window showing separately PRs, issues, pushes, and releases. This view does not remove bot activity.
55+
The same information as `tail`, but in a split window showing separately PRs, issues, pushes, and releases.
5656

57-
<img src="https://user-images.githubusercontent.com/346999/101861560-2ecdcc80-3b25-11eb-9fba-25382b2df65f.png" width="900" style="max-width: 900px">
57+
<img src="https://user-images.githubusercontent.com/1483922/105074862-cb955b00-5a3d-11eb-99c5-3125bb98910b.png" width="650" style="max-width: 650px">
5858

5959
### ghtop users
6060

6161
A summary of activity for the most active current users.
6262

63-
<img src="https://user-images.githubusercontent.com/346999/101861612-4b6a0480-3b25-11eb-8124-19bb2434c27e.png" width="500" style="max-width: 500px">
63+
<img src="https://user-images.githubusercontent.com/1483922/105075363-8887b780-5a3e-11eb-9f7a-627268ac465f.png" width="650" style="max-width: 650px">
6464

6565
----
6666

‎ghtop/all_rich.py‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from rich import print as pr
2+
from rich.align import *
3+
from rich.bar import *
4+
from rich.color import *
5+
from rich.columns import *
6+
from rich.console import *
7+
from rich.emoji import *
8+
from rich.highlighter import *
9+
from rich.live import *
10+
from rich.logging import *
11+
from rich.markdown import *
12+
from rich.markup import *
13+
from rich.measure import *
14+
from rich.padding import *
15+
from rich.panel import *
16+
from rich.pretty import *
17+
from rich.progress_bar import *
18+
from rich.progress import *
19+
from rich.prompt import *
20+
from rich.protocol import *
21+
from rich.rule import *
22+
from rich.segment import *
23+
from rich.spinner import *
24+
from rich.status import *
25+
from rich.style import *
26+
from rich.styled import *
27+
from rich.syntax import *
28+
from rich.table import *
29+
from rich.text import *
30+
from rich.theme import *
31+
from rich.traceback import *
32+
from fastcore.all import *
33+
34+
@delegates(Style)
35+
def text(s, maxlen=None, **kwargs):
36+
"Create a styled `Text` object"
37+
if maxlen: s = truncstr(s, maxlen=maxlen)
38+
return Text(s, style=Style(**kwargs))
39+
40+
@delegates(Style)
41+
def segment(s, maxlen=None, space=' ', **kwargs):
42+
"Create a styled `Segment` object"
43+
if maxlen: s = truncstr(s, maxlen=maxlen, space=space)
44+
return Segment(s, style=Style(**kwargs))
45+
46+
class Segments(list):
47+
def __init__(self, options): self.w = options.max_width
48+
49+
@property
50+
def chars(self): return sum(o.cell_length for o in self)
51+
def txtlen(self, pct): return min((self.w-self.chars)*pct, 999)
52+
53+
@delegates(segment)
54+
def add(self, x, maxlen=None, pct=None, **kwargs):
55+
if pct: maxlen = math.ceil(self.txtlen(pct))
56+
self.append(segment(x, maxlen=maxlen, **kwargs))
57+
58+
@delegates(Table)
59+
def _grid(box=None, padding=0, collapse_padding=True, pad_edge=False, expand=False, show_header=False, show_edge=False, **kwargs):
60+
return Table(padding=padding, pad_edge=pad_edge, expand=expand, collapse_padding=collapse_padding,
61+
box=box, show_header=show_header, show_edge=show_edge, **kwargs)
62+
63+
@delegates(_grid)
64+
def grid(items, expand=True, no_wrap=True, **kwargs):
65+
g = _grid(expand=expand, **kwargs)
66+
for c in items[0]: g.add_column(no_wrap=no_wrap, justify='center')
67+
for i in items: g.add_row(*i)
68+
return g
69+
70+
Color = str_enum('Color', *ANSI_COLOR_NAMES)
71+
72+
class Deque(deque):
73+
def __rich__(self): return RenderGroup(*(filter(None, self)))
74+
75+
@delegates()
76+
class FixedPanel(Panel, GetAttr):
77+
_default='renderable'
78+
def __init__(self, height, **kwargs):
79+
super().__init__(Deque([' ']*height, maxlen=height), **kwargs)
80+
81+
@delegates(Style)
82+
def add(self, s:str, **kwargs):
83+
"Add styled `s` to panel"
84+
self.append(text(s, **kwargs))

‎ghtop/ghtop.py‎

Lines changed: 101 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22

3-
__all__ = ['term', 'logfile', 'github_auth_device', 'limit_cb', 'api', 'Events', 'print_event', 'tail_events',
4-
'watch_users', 'quad_logs', 'simple', 'main']
3+
__all__ = ['get_sparklines', 'ETYPES', 'term', 'tdim', 'github_auth_device', 'limit_cb', 'Events', 'print_event',
4+
'pct_comp', 'tail_events', 'watch_users', 'quad_logs', 'simple', 'main']
55

66

77
import sys, signal, shutil, os, json, emoji, enlighten
@@ -14,10 +14,29 @@
1414
from fastcore.foundation import *
1515
from fastcore.script import *
1616
from ghapi.all import *
17+
from .richext import *
18+
from .all_rich import (Console, Color, FixedPanel, box, Segments, Live,
19+
grid, ConsoleOptions, Progress, BarColumn, Spinner, Table)
20+
21+
22+
23+
ETYPES=PushEvent,PullRequestEvent,IssuesEvent,ReleaseEvent
24+
25+
def get_sparklines():
26+
s1 = ESpark('Push', 'magenta', [PushEvent], mx=30)
27+
s2 = ESpark('PR', 'yellow', [PullRequestEvent, PullRequestReviewCommentEvent, PullRequestReviewEvent], mx=8)
28+
s3 = ESpark('Issues', 'green', [IssueCommentEvent,IssuesEvent], mx=6)
29+
s4 = ESpark('Releases', 'blue', [ReleaseEvent], mx=0.4)
30+
s5 = ESpark('All Events', 'orange', mx=45)
31+
32+
return Stats([s1,s2,s3,s4,s5], store=5, span=5, spn_lbl='5/s', show_freq=True)
1733

1834

1935
term = Terminal()
20-
logfile = Path("log.txt")
36+
37+
tdim = L(os.popen('stty size', 'r').read().split())
38+
if not tdim: theight,twidth = 15,15
39+
else: theight,twidth = tdim.map(lambda x: max(int(x)-3, 15))
2140

2241

2342
def github_auth_device(wb='', n_polls=9999):
@@ -46,18 +65,6 @@ def limit_cb(rem,quota):
4665
if rem < 1000: print(f"{w}\nRemaining calls: {rem} out of {quota}\n{w}", file=sys.stderr)
4766

4867

49-
def _get_api():
50-
path = Path.home()/".ghtop_token"
51-
if path.is_file():
52-
try: token = path.read_text().strip()
53-
except: _exit("Error reading token")
54-
else: token = github_auth_device()
55-
path.write_text(token)
56-
return GhApi(limit_cb=limit_cb, token=token)
57-
58-
api = _get_api()
59-
60-
6168
Events = dict(
6269
IssuesEvent_closed=('⭐', 'closed', noop),
6370
IssuesEvent_opened=('📫', 'opened', noop),
@@ -78,79 +85,120 @@ def _to_log(e):
7885
elif e.type == "ReleaseEvent": return f'🚀 {login} released {e.payload.release.tag_name} of {repo}'
7986

8087

81-
def print_event(e, commits_counter):
88+
def print_event(e, counter):
8289
res = _to_log(e)
8390
if res: print(res)
84-
elif e.type == "PushEvent": [commits_counter.update() for c in e.payload.commits]
91+
elif counter and e.type == "PushEvent": [counter.update() for c in e.payload.commits]
8592
elif e.type == "SecurityAdvisoryEvent": print(term.blink("SECURITY ADVISORY"))
8693

8794

88-
def tail_events(evt):
89-
"Print events from `fetch_events` along with a counter of push events"
90-
manager = enlighten.get_manager()
91-
commits = manager.counter(desc='Commits', unit='commits', color='green')
92-
for ev in evt: print_event(ev, commits)
95+
def pct_comp(api): return int(((5000-int(api.limit_rem)) / 5000) * 100)
9396

9497

95-
def _pr_row(*its): print(f"{its[0]: <30} {its[1]: <6} {its[2]: <5} {its[3]: <6} {its[4]: <7}")
96-
def watch_users(evts):
98+
def tail_events(evt, api):
99+
"Print events from `fetch_events` along with a counter of push events"
100+
p = FixedPanel(theight, box=box.HORIZONTALS, title='ghtop')
101+
s = get_sparklines()
102+
g = grid([[s], [p]])
103+
with Live(g):
104+
for e in evt:
105+
s.add_events(e)
106+
s.update_prog(pct_comp(api))
107+
p.append(e)
108+
g = grid([[s], [p]])
109+
110+
111+
def _user_grid():
112+
g = Table.grid(expand=True)
113+
g.add_column(justify="left")
114+
for i in range(4): g.add_column(justify="center")
115+
g.add_row("", "", "", "", "")
116+
g.add_row("User", "Events", "PRs", "Issues", "Pushes")
117+
return g
118+
119+
120+
def watch_users(evts, api):
97121
"Print a table of the users with the most events"
98122
users,users_events = defaultdict(int),defaultdict(lambda: defaultdict(int))
99-
while True:
100-
for x in islice(evts, 10):
101-
users[x.actor.login] += 1
102-
users_events[x.actor.login][x.type] += 1
103123

104-
print(term.clear())
105-
_pr_row("User", "Events", "PRs", "Issues", "Pushes")
106-
sorted_users = sorted(users.items(), key=lambda o: (o[1],o[0]), reverse=True)
107-
for u in sorted_users[:20]:
108-
_pr_row(*u, *itemgetter('PullRequestEvent','IssuesEvent','PushEvent')(users_events[u[0]]))
124+
with Live() as live:
125+
s = get_sparklines()
126+
while True:
127+
for x in islice(evts, 10):
128+
users[x.actor.login] += 1
129+
users_events[x.actor.login][x.type] += 1
130+
s.add_events(x)
109131

132+
ig = _user_grid()
133+
sorted_users = sorted(users.items(), key=lambda o: (o[1],o[0]), reverse=True)
134+
for u in sorted_users[:theight]:
135+
data = (*u, *itemgetter('PullRequestEvent','IssuesEvent','PushEvent')(users_events[u[0]]))
136+
ig.add_row(*L(data).map(str))
110137

111-
def _push_to_log(e): return f"{e.actor.login} pushed {len(e.payload.commits)} commits to repo {e.repo.name}"
112-
def _logwin(title,color): return Log(title=title,border_color=2,color=color)
113-
114-
def quad_logs(evts):
115-
"Print 4 panels, showing most recent issues, commits, PRs, and releases"
116-
term.enter_fullscreen()
117-
ui = HSplit(VSplit(_logwin('Issues', color=7), _logwin('Commits' , color=3)),
118-
VSplit(_logwin('Pull Requests', color=4), _logwin('Releases', color=5)))
138+
s.update_prog(pct_comp(api))
139+
g = grid([[s], [ig]])
140+
live.update(g)
119141

120-
issues,commits,prs,releases = all_items = ui.items[0].items+ui.items[1].items
121-
for o in all_items: o.append(" ")
122142

123-
d = dict(PushEvent=commits, IssuesEvent=issues, IssueCommentEvent=issues, PullRequestEvent=prs, ReleaseEvent=releases)
124-
while True:
125-
for x in islice(evts, 10):
126-
f = [_to_log,_push_to_log][x.type == 'PushEvent']
127-
if x.type in d: d[x.type].append(f(x)[:95])
128-
ui.display()
143+
def _panelDict2Grid(pd):
144+
ispush,ispr,isiss,isrel = pd.values()
145+
return grid([[ispush,ispr],[isiss,isrel]], width=twidth)
129146

130147

131-
def simple(evts):
148+
def quad_logs(evts, api):
149+
"Print 4 panels, showing most recent issues, commits, PRs, and releases"
150+
pd = {o:FixedPanel(height=(theight//2),
151+
width=(twidth//2)-1,
152+
box=box.HORIZONTALS,
153+
title=camel2words(remove_suffix(o.__name__,'Event'))) for o in ETYPES}
154+
p = _panelDict2Grid(pd)
155+
s = get_sparklines()
156+
g = grid([[s], [p]])
157+
with Live(g):
158+
for e in evts:
159+
s.add_events(e)
160+
s.update_prog(pct_comp(api))
161+
typ = type(e)
162+
if typ in pd: pd[typ].append(e)
163+
p = _panelDict2Grid(pd)
164+
g = grid([[s], [p]])
165+
166+
167+
def simple(evts, api):
132168
for ev in evts: print(f"{ev.actor.login} {ev.type} {ev.repo.name}")
133169

134170

171+
def _get_token():
172+
path = Path.home()/".ghtop_token"
173+
if path.is_file():
174+
try: return path.read_text().strip()
175+
except: _exit("Error reading token")
176+
else: token = github_auth_device()
177+
path.write_text(token)
178+
return token
179+
180+
135181
def _signal_handler(sig, frame):
136182
if sig != signal.SIGINT: return
137183
print(term.exit_fullscreen(),term.clear(),term.normal)
138184
sys.exit(0)
139185

140186
_funcs = dict(tail=tail_events, quad=quad_logs, users=watch_users, simple=simple)
141-
_filts = str_enum('_filts', 'user', 'repo', 'org')
187+
_filts = str_enum('_filts', 'users', 'repo', 'org')
142188
_OpModes = str_enum('_OpModes', *_funcs)
143189

144190
@call_parse
145191
def main(mode: Param("Operation mode to run", _OpModes),
146192
include_bots: Param("Include bots (there's a lot of them!)", store_true)=False,
147193
types: Param("Comma-separated types of event to include (e.g PushEvent)", str)='',
194+
pause: Param("Number of seconds to pause between requests to the GitHub api", float)=0.4,
148195
filt: Param("Filtering method", _filts)=None,
149196
filtval: Param("Value to filter by (for `repo` use format `owner/repo`)", str)=None):
150197
signal.signal(signal.SIGINT, _signal_handler)
151198
types = types.split(',') if types else None
152199
if filt and not filtval: _exit("Must pass `filter_value` if passing `filter_type`")
153200
if filtval and not filt: _exit("Must pass `filter_type` if passing `filter_value`")
154201
kwargs = {filt:filtval} if filt else {}
155-
evts = api.fetch_events(types=types, incl_bot=include_bots, **kwargs)
156-
_funcs[mode](evts)
202+
api = GhApi(limit_cb=limit_cb, token=_get_token())
203+
evts = api.fetch_events(types=types, incl_bot=include_bots, pause=float(pause), **kwargs)
204+
_funcs[mode](evts, api)

0 commit comments

Comments
 (0)