Skip to content

Commit dccd407

Browse files
committed
Lots and lots of changes, mostly for sleuth tv
1 parent 5a160e0 commit dccd407

11 files changed

+328
-16
lines changed

requirements.in

+8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ watchdog
99
pyautogui
1010
cairosvg
1111

12+
elgato
13+
14+
# presentation deps
15+
pyyaml
16+
#pytz
17+
#python-dateutil
18+
python-slugify
19+
1220
pytest
1321
black
1422
reorder-python-imports

requirements.txt

+35-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44
#
55
# pip-compile
66
#
7+
aiohttp==3.8.1
8+
# via elgato
9+
aiosignal==1.2.0
10+
# via aiohttp
711
aspy-refactor-imports==2.2.0
812
# via reorder-python-imports
913
async-generator==1.10
1014
# via
1115
# trio
1216
# trio-websocket
17+
async-timeout==4.0.2
18+
# via aiohttp
1319
attrs==21.4.0
1420
# via
21+
# aiohttp
1522
# outcome
1623
# pytest
1724
# trio
@@ -29,6 +36,8 @@ cffi==1.15.0
2936
# via
3037
# cairocffi
3138
# cryptography
39+
charset-normalizer==2.1.1
40+
# via aiohttp
3241
click==8.0.3
3342
# via black
3443
cryptography==36.0.1
@@ -39,16 +48,27 @@ cssselect2==0.4.1
3948
# via cairosvg
4049
defusedxml==0.7.1
4150
# via cairosvg
51+
elgato==3.0.0
52+
# via -r requirements.in
53+
frozenlist==1.3.1
54+
# via
55+
# aiohttp
56+
# aiosignal
4257
h11==0.12.0
4358
# via wsproto
4459
idna==3.3
4560
# via
4661
# trio
4762
# urllib3
63+
# yarl
4864
iniconfig==1.1.1
4965
# via pytest
5066
mouseinfo==0.1.3
5167
# via pyautogui
68+
multidict==6.0.2
69+
# via
70+
# aiohttp
71+
# yarl
5272
mypy-extensions==0.4.3
5373
# via black
5474
numpy==1.22.1
@@ -79,6 +99,8 @@ pyautogui==0.9.53
7999
# via -r requirements.in
80100
pycparser==2.21
81101
# via cffi
102+
pydantic==1.9.2
103+
# via elgato
82104
pydub==0.25.1
83105
# via -r requirements.in
84106
pygetwindow==0.0.9
@@ -97,12 +119,16 @@ pyscreeze==0.1.28
97119
# via pyautogui
98120
pytest==6.2.5
99121
# via -r requirements.in
122+
python-slugify==6.1.2
123+
# via -r requirements.in
100124
python3-xlib==0.15
101125
# via
102126
# mouseinfo
103127
# pyautogui
104128
pytweening==1.0.4
105129
# via pyautogui
130+
pyyaml==6.0
131+
# via -r requirements.in
106132
reorder-python-imports==2.6.0
107133
# via -r requirements.in
108134
selenium==4.1.0
@@ -117,6 +143,8 @@ sortedcontainers==2.4.0
117143
# via trio
118144
streamdeck==0.9.0
119145
# via -r requirements.in
146+
text-unidecode==1.3
147+
# via python-slugify
120148
tinycss2==1.1.1
121149
# via
122150
# cairosvg
@@ -132,7 +160,9 @@ trio==0.19.0
132160
trio-websocket==0.9.2
133161
# via selenium
134162
typing-extensions==4.0.1
135-
# via black
163+
# via
164+
# black
165+
# pydantic
136166
urllib3[secure]==1.26.8
137167
# via selenium
138168
watchdog==1.0.2
@@ -145,3 +175,7 @@ websocket-client==1.2.3
145175
# via obs-websocket-py
146176
wsproto==1.0.0
147177
# via trio-websocket
178+
yarl==1.8.1
179+
# via
180+
# aiohttp
181+
# elgato

sleuth-tv-live-s01e01.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
title: "DORA Metrics 101"
2+
sections:
3+
- title: Welcome!
4+
byline: Let's make this interactive - chat welcome!
5+
- title: "DORA Metrics 101"
6+
byline: "The 'why' and 'what'"
7+
- title: "Q: How do the best teams work?"
8+
byline: "To understand the answer, you need to know the question"
9+
- title: "(Short) History of DORA"
10+
byline: How DORA and the State of DevOps intertwine
11+
- title: "What are the DORA metrics?"
12+
byline: Hard to do remotely, but possible
13+
- title: "1. Deployment frequency"
14+
byline: How often you change production
15+
- title: "2. Change lead time"
16+
byline: How quickly a change gets to production
17+
- title: "3. Change failure rate"
18+
byline: How often changes fail
19+
- title: "4. Mean time to recover (MTTR)"
20+
byline: How long failure lasts
21+
- title: "How to measure?"
22+
byline: Well, Sleuth of course! ... but how does that work?
23+
- title: "Final thoughts"
24+
byline: "Elephant in the room: How can/should you use this?"

sleuth-tv-live-s01e02.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
title: "What are DORA metrics good for?"
2+
guest:
3+
name: Dylan
4+
title: CEO/Co-founder
5+
sections:
6+
- title: Welcome!
7+
byline: "Question of the day - How often do you release?"
8+
- title: "What are DORA metrics good for?"
9+
byline: "... and it isn't 'absolutely nothing' (!)"
10+
- title: "Quick recap: What are DORA metrics?"
11+
byline: "Key metrics that correlate with high performing teams"
12+
- title: "What can they be used for?"
13+
byline: "Which of these apply to your team?"
14+
- title: "1. Assessment"
15+
byline: Where does your team fit in the industry?
16+
- title: "2. Measure progress on an initiative"
17+
byline: Baseline, make a change, measure again
18+
- title: "3. Track the impact of an unrelated change"
19+
byline: Verify delivery performance is unaffected or even improved
20+
- title: "4. Track impact of team scale events"
21+
byline: As you add people, how is your delivery performance impacted?
22+
- title: "Are DORA metrics just for 'bad' teams?"
23+
byline: "Surprise answer: no :)"
24+
- title: "Final thoughts"
25+
byline: "Change happens, so know the impact of that change"
26+
- title: "Second opinion"
27+
byline: "Let's hear from Dylan Etkin, my co-founder"

sleuth-tv-live.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
title: "Does failure happen if no one is around to measure it?"
2+
guest:
3+
name: Dylan
4+
title: CEO/Co-founder
5+
sections:
6+
- title: Welcome!
7+
byline: "Fun surprise at the end (!)"
8+
- title: "Does failure happen if no one is around to measure it?"
9+
byline: "... uh, yes?"
10+
- title: "DevOps World 2022 Preview"
11+
byline: ""
12+
- title: "Up next: Metrics and dev productivity"
13+
byline: "How and how NOT To do it"

src/sleuthdeck/actions.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from sleuthdeck.deck import KeyScene
1515
from sleuthdeck.deck import Scene
1616
from sleuthdeck.keys import IconKey
17-
from sleuthdeck.windows import get_window, By
17+
from sleuthdeck.windows import get_window, By, get_windows, get_focused_window
1818

1919

2020
class Sequential(Action):
@@ -143,16 +143,18 @@ def __init__(self, title: Union[str, By], *hotkey: str):
143143

144144
def __call__(self, scene: KeyScene, key: Key, click: ClickType):
145145
print("sending key")
146+
focused_window = get_focused_window()
147+
146148
window = get_window(self.title, attempts=5 * 10)
147149
if not window:
148150
print(f"No window found for {self.title}")
149151
return
150152
print(f"got window {self.title}")
151153
window.focus()
152-
sleep(.1)
153154
from pyautogui import hotkey
154155
hotkey(*self.hotkey)
155156
print("sent")
157+
focused_window.focus()
156158

157159

158160
class DeckBrightness(Action):

src/sleuthdeck/plugins/obs/actions.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def change_scene(self, name: str):
3333
def close(self):
3434
return Close(self)
3535

36-
def toggle_source(self, name: str, show: bool = True, scene: Optional[str] = None):
36+
def toggle_source(self, name: str, show: bool = None, scene: Optional[str] = None):
3737
return ToggleSource(self, name, show, scene=scene)
3838

3939
def _ensure_connected(self):
@@ -91,14 +91,21 @@ def __call__(self, scene: KeyScene, key: OBSKey, click: ClickType):
9191

9292

9393
class ToggleSource(Action):
94-
def __init__(self, obs: OBS, name: str, show: bool = True, scene = None):
94+
def __init__(self, obs: OBS, name: str, show: bool = None, scene = None):
9595
self.name = name
9696
self.obs = obs
9797
self.scene = scene
9898
self._show = show
9999

100100
def __call__(self, scene: KeyScene, key: OBSKey, click: ClickType):
101-
self.obs.obs(requests.SetSceneItemRender(self.name, self._show, scene_name=self.scene))
101+
if self._show is None:
102+
visible = not self.obs.obs(requests.GetSceneItemProperties(self.name)).getVisible()
103+
else:
104+
visible = self._show
105+
106+
print(f"Making {visible}")
107+
108+
self.obs.obs(requests.SetSceneItemRender(self.name, visible, scene_name=self.scene))
102109

103110

104111
class StopVirtualCam(Baserequests):

src/sleuthdeck/plugins/presentation/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from time import sleep
5+
from typing import Optional, List
6+
7+
import yaml
8+
from obswebsocket import requests
9+
from slugify import slugify
10+
11+
from sleuthdeck.deck import Action, KeyScene, Key, ClickType
12+
from sleuthdeck.plugins.obs import OBS
13+
14+
15+
@dataclass
16+
class Event:
17+
title: str
18+
sections: List[Section]
19+
id: Optional[str] = None
20+
21+
@property
22+
def slug(self):
23+
return slugify(self.title)
24+
25+
26+
@dataclass
27+
class Section:
28+
title: str
29+
byline: str
30+
31+
32+
class Presentation:
33+
def __init__(self, obs: OBS, path: str, title_scene_item="Section title",
34+
byline_scene_item="Section byline",
35+
title_scene="Title", overlay_scene="Overlay",
36+
guest_name_item="Guest 1",
37+
guest_title_item="Guest 2"):
38+
with open(path, "r") as stream:
39+
try:
40+
data = yaml.safe_load(stream)
41+
except yaml.YAMLError as exc:
42+
print(exc)
43+
self.event = Event("Missing", [])
44+
return
45+
46+
sections = []
47+
if "sections" in data:
48+
sections = [Section(title=s["title"], byline=s["byline"]) for s in data["sections"]]
49+
50+
if "guest" in data:
51+
obs.obs(requests.SetTextFreetype2Properties(guest_name_item, text=data["guest"]["name"]))
52+
obs.obs(requests.SetTextFreetype2Properties(guest_title_item, text=data["guest"]["title"]))
53+
54+
self.event = Event(
55+
title=data["title"],
56+
sections=sections,
57+
)
58+
self.obs = obs
59+
self.current_section_idx = 0
60+
self.title_scene_item = title_scene_item
61+
self.byline_scene_item = byline_scene_item
62+
self.title_scene = title_scene
63+
self.overlay_scene = overlay_scene
64+
self.reset()
65+
66+
def next_section(self) -> Action:
67+
def action(scene: KeyScene, key: Key, click: ClickType):
68+
self._update_labels(self._next_section())
69+
70+
return action
71+
72+
def reset(self) -> Action:
73+
def action(scene: KeyScene, key: Key, click: ClickType):
74+
self.current_section_idx = 0
75+
self._update_labels(self.event.sections[0], new_scene=False)
76+
77+
return action
78+
79+
def previous_section(self) -> Action:
80+
def action(scene: KeyScene, key: Key, click: ClickType):
81+
self._update_labels(self._previous_section())
82+
83+
return action
84+
85+
def _update_labels(self, section, new_scene=True):
86+
self.obs.obs(requests.SetSceneItemRender(self.title_scene_item, False, self.overlay_scene))
87+
self.obs.obs(requests.SetSceneItemRender(self.byline_scene_item, False, self.overlay_scene))
88+
self.obs.obs(requests.SetTextFreetype2Properties(self.title_scene_item, text=section.title))
89+
self.obs.obs(requests.SetTextFreetype2Properties(self.byline_scene_item, text=section.byline))
90+
if new_scene:
91+
self.obs.obs(requests.SetCurrentScene(self.title_scene))
92+
sleep(.3)
93+
self.obs.obs(requests.SetSceneItemRender(self.title_scene_item, True, self.overlay_scene))
94+
self.obs.obs(requests.SetSceneItemRender(self.byline_scene_item, True, self.overlay_scene))
95+
96+
def _next_section(self) -> Section:
97+
if len(self.event.sections) == self.current_section_idx:
98+
self.current_section_idx = 0
99+
else:
100+
self.current_section_idx += 1
101+
102+
return self.event.sections[self.current_section_idx]
103+
104+
def _previous_section(self) -> Section:
105+
if self.current_section_idx == 0:
106+
self.current_section_idx = len(self.event.sections) - 1
107+
else:
108+
self.current_section_idx -= 1
109+
110+
return self.event.sections[self.current_section_idx]

src/sleuthdeck/windows.py

+11
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def move(self, x: int, y: int, width: int, height: int):
5353

5454
def focus(self):
5555
shell.run("wmctrl", "-ia", self.window_id)
56+
for _ in range(10):
57+
focused = get_focused_window()
58+
if focused.window_id == self.window_id:
59+
return
60+
else:
61+
print("Unable to focus window")
5662

5763
def __repr__(self):
5864
return f"Window (id='{self.window_id}', class='{self.window_class}', title='{self.title}')"
@@ -87,6 +93,11 @@ def _parse_window_output(output):
8793
return result
8894

8995

96+
def get_focused_window() -> Window:
97+
window_name = shell.run("xdotool", "getwindowfocus", "getwindowname").strip()
98+
return get_window(By.title(window_name), attempts=1)
99+
100+
90101
def get_window(selector: Union[str, By], attempts: int = 2) -> Optional[Window]:
91102
windows = []
92103
if isinstance(selector, str):

0 commit comments

Comments
 (0)