-
Notifications
You must be signed in to change notification settings - Fork 34
/
blinds
executable file
·342 lines (289 loc) · 12.7 KB
/
blinds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
#!/usr/bin/env python3
import itertools as it, operator as op, functools as ft
import os, sys, logging, re, textwrap
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
class LogMessage:
def __init__(self, fmt, a, k): self.fmt, self.a, self.k = fmt, a, k
def __str__(self): return self.fmt.format(*self.a, **self.k) if self.a or self.k else self.fmt
class LogStyleAdapter(logging.LoggerAdapter):
def __init__(self, logger, extra=None):
super().__init__(logger, extra or {})
def log(self, level, msg, *args, **kws):
if not self.isEnabledFor(level): return
log_kws = {} if 'exc_info' not in kws else dict(exc_info=kws.pop('exc_info'))
msg, kws = self.process(msg, kws)
self.logger.log(level, LogMessage(msg, args, kws), **log_kws)
get_logger = lambda name: LogStyleAdapter(logging.getLogger(name))
log = get_logger('blinds')
it_adjacent = lambda seq, n: it.zip_longest(*([iter(seq)] * n))
dedent = lambda text: textwrap.dedent(text).strip('\n') + '\n'
def color_rgba(c, opacity=255):
cs = c.strip().lstrip('#')
if len(cs) in [3,4]: cs = ''.join(c*2 for c in cs)
if len(cs) == 6:
if not isinstance(opacity, str):
if isinstance(opacity, float): opacity = int(opacity * 255)
opacity = '{:x}'.format(max(0, min(255, opacity)))
cs += opacity
elif len(cs) != 8: raise ValueError(c)
r,g,b,a = (int(''.join(v), 16) for v in it_adjacent(cs, 2))
return 'rgba({},{},{},{})'.format(r,g,b,a / 255.0)
class adict(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__dict__ = self
class BlindsConf:
app_id = 'net.fraggod.blinds'
no_session = False
win_title = 'blinds'
win_role = 'blinds-main'
win_icon = None
win_default_size = 700, 500
win_w = win_h = None
win_x = win_y = None
wm_hints = dict( decorated=False,
skip_taskbar=True, skip_pager=True, accept_focus=False )
wm_hints_all = (
' focus_on_map modal resizable hide_titlebar_when_maximized'
' stick maximize fullscreen keep_above keep_below decorated'
' deletable skip_taskbar skip_pager urgency accept_focus'
' auto_startup_notification mnemonics_visible focus_visible' ).split()
wm_type_hints = Gdk.WindowTypeHint.NORMAL
wm_type_hints_all = dict(
(e.value_nick, v) for v, e in Gdk.WindowTypeHint.__enum_values__.items() if v )
win_css = ''
win_css_opts_tpl = '#blinds, #blinds * {{ background: {win_color}; }}\n'
quit_keys = 'q', 'control q', 'control w', 'escape'
def __init__(self, **kws):
for k, v in kws.items():
if not hasattr(self, k): raise AttributeError(k)
setattr(self, k, v)
class BlindsWindow(Gtk.ApplicationWindow):
def __init__(self, app, conf):
super().__init__(name='blinds', application=app)
self.app, self.conf = app, conf
self.log = get_logger('blinds.win')
self.set_title(self.conf.win_title)
self.set_role(self.conf.win_role)
if self.conf.win_icon:
self.log.debug('Using icon: {}', self.conf.win_icon)
self.set_icon_name(self.conf.win_icon)
self.init_widgets()
def init_widgets(self):
css = Gtk.CssProvider()
css.load_from_data(self.conf.win_css.encode())
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(), css,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION )
self.draw = Gtk.DrawingArea()
self.add(self.draw)
hints = dict.fromkeys(self.conf.wm_hints_all)
hints.update(self.conf.wm_hints or dict())
for k in list(hints):
setter = getattr(self, f'set_{k}', None)
if not setter: setter = getattr(self, f'set_{k}_hint', None)
if not setter: setter = getattr(self, k, None)
if not setter: continue
v = hints.pop(k)
if v is None: continue
self.log.debug('Setting WM hint: {} = {}', k, v)
if not setter.get_arguments(): # e.g. w.fullscreen()
if v: setter()
continue
setter(v)
assert not hints, ['Unrecognized wm-hints:', hints]
self.set_type_hint(self.conf.wm_type_hints)
self.connect('composited-changed', self._set_visual)
self.connect('screen-changed', self._set_visual)
self._set_visual(self)
self.ev_discard = set()
self.set_default_size(*self.conf.win_default_size)
self.connect( 'configure-event',
ft.partial(self._place_window, ev_done='configure-event') )
self._place_window(self)
self.connect('key-press-event', self._window_key)
def _set_visual(self, w, *ev_data):
visual = w.get_screen().get_rgba_visual()
if visual: w.set_visual(visual)
def _place_window(self, w, *ev_data, ev_done=None):
if ev_done:
if ev_done in self.ev_discard: return
self.ev_discard.add(ev_done)
dsp, sg = w.get_screen().get_display(), adict(x=0, y=0, w=0, h=0)
geom = dict(S=sg)
for n in range(dsp.get_n_monitors()):
rct = dsp.get_monitor(n).get_geometry()
mg = geom[f'M{n+1}'] = adict(x=rct.x, y=rct.y, w=rct.width, h=rct.height)
sg.w, sg.h = max(sg.w, mg.x + mg.w), max(sg.h, mg.y + mg.h)
ww = wh = None
if self.conf.win_w and self.conf.win_h:
get_val = lambda v,k: int(v) if v.isdigit() else geom[v][k]
ww, wh = get_val(self.conf.win_w, 'w'), get_val(self.conf.win_h, 'h')
w.resize(ww, wh)
self.log.debug('win-resize: {} {}', ww, wh)
if self.conf.win_x or self.conf.win_y:
if not (ww or wh): ww, wh = w.get_size()
wx, wy = w.get_position()
get_pos = lambda v,k,wv: (
(int(v[1:]) if v[0] != '-' else (sg[k] - wv - int(v[1:])))
if v[0] in '+-' else geom[v][k] )
if self.conf.win_x: wx = get_pos(self.conf.win_x, 'x', ww)
if self.conf.win_y: wy = get_pos(self.conf.win_y, 'y', wh)
self.log.debug('win-move: {} {}', wx, wy)
w.move(wx, wy)
def _window_key(self, w, ev, _masks=dict()):
if not _masks:
for st, mod in Gdk.ModifierType.__flags_values__.items():
if ( len(mod.value_names) != 1
or not mod.first_value_nick.endswith('-mask') ): continue
assert st not in _masks, [mod.first_value_nick, _masks[st]]
mod = mod.first_value_nick[:-5]
if mod.startswith('modifier-reserved-'): mod = 'res-{}'.format(mod[18:])
_masks[st] = mod
chk, keyval = ev.get_keyval()
if not chk: return
key_sum, key_name = list(), Gdk.keyval_name(keyval)
for st, mod in _masks.items():
if ev.state & st == st: key_sum.append(mod)
key_sum = ' '.join(sorted(key_sum) + [key_name]).lower()
self.log.debug('key-press-event: {!r}', key_sum)
if key_sum in self.conf.quit_keys: self.app.quit()
class BlindsApp(Gtk.Application):
def __init__(self, conf):
self.conf = conf
super().__init__()
if self.conf.app_id: self.set_application_id(self.conf.app_id)
if self.conf.no_session: self.set_property('register-session', False)
def do_activate(self):
win = BlindsWindow(self, self.conf)
win.connect('delete-event', lambda w,*data: self.quit())
win.show_all()
def main(args=None, conf=None):
if not conf: conf = BlindsConf()
import argparse
class SmartHelpFormatter(argparse.HelpFormatter):
def __init__(self, *args, **kws):
return super().__init__(*args, **kws, width=100)
def _fill_text(self, text, width, indent):
if '\n' not in text: return super()._fill_text(text, width, indent)
return ''.join(indent + line for line in text.splitlines(keepends=True))
def _split_lines(self, text, width):
return super()._split_lines(text, width)\
if '\n' not in text else dedent(text).replace('\t', ' ').splitlines()
parser = argparse.ArgumentParser(
formatter_class=SmartHelpFormatter,
description='Create semi-transparent window just to cover stuff.')
group = parser.add_argument_group('Controls')
group.add_argument('-q', '--key-quit',
metavar='key', action='append',
help='''
Keys or combos to make app exit, when focused.
Will override defaults if specified. Override to e.g. non-existent "xxx" key to disable.
See -d/--debug output for list of pressed keys, as the app sees them.
Can be used more than once. Default: {}.'''.format(', '.join(conf.quit_keys)))
group = parser.add_argument_group('Appearance')
group.add_argument('-c', '--color',
metavar='hex', default='#022710',
help='''
Hex color for window background, example: #6ad468.
Can have alpha component at the end, e.g. #6ad46870.
Can use shorter 3/4-char forms (e.g. #fff), leading sharp character is optional.
Default: %(default)s.''')
group.add_argument('-o', '--opacity',
type=float, metavar='0-1.0', default=0.5,
help='''
Opacity of the window - float value in 0-1.0 range,
with 0 being fully-transparent and 1.0 fully opaque.
Only used if -c/--color does not have alpha component in it.
Should only have any effect with compositing Window Manager.
Default: %(default)s.''')
group = parser.add_argument_group('Window')
group.add_argument('-p', '--pos', metavar='(WxH)(+X)(+Y)',
help='''
Set window size and/or position hints for WM (usually followed).
W/H values can be special "S" to use screen size,
e.g. "SxS" (or just "S") is "fullscreen".
X/Y offsets must be specified in that order, if at all, with positive
values (prefixed with "+") meaning offset from top-left corner
of the screen, and negative - bottom-right.
Special values like "M1" (or M2, M3, etc) can
be used to specify e.g. monitor-1 width/heigth/offsets,
and if size is just "M1" or "M2", then x/y offsets default to that monitor too.
If not specified (default), all are left for Window Manager to decide/remember.
Examples: 800x600, -0+0 (move to top-right corner),
S (full screen), 200xS+0, M2 (full monitor 2), M2+M1, M2x500+M1+524.
"slop" tool - https://github.com/naelstrof/slop - can be used
used to get this value interactively via mouse selection (e.g. "-p $(slop)").''')
group.add_argument('-x', '--wm-hints', metavar='(+|-)hint(,...)',
help='''
Comma or space-separated list of WM hints to set/unset for the window.
All of these can have boolean yes/no or unspecified/default values.
Specifying hint name in the list will have it explicity set (i.e. "yes/true" value),
and preceding name with "-" will have it explicitly unset instead ("no/false").
List of recognized hints:
{}.
Example: keep_top -decorated skip_taskbar skip_pager -accept_focus.'''\
.format('\n\t\t\t\t'.join(textwrap.wrap(', '.join(conf.wm_hints_all), 75))))
group.add_argument('-t', '--wm-type-hints', metavar='hint(,...)',
help='''
Comma or space-separated list of window type hints for WM.
Similar to --wm-hints in general, but are
combined separately to set window type hint value.
List of recognized type-hints (all unset by default):
{}.
Probably does not make sense to use multiple of these at once.'''\
.format('\n\t\t\t\t'.join(textwrap.wrap(', '.join(conf.wm_type_hints_all), 75))))
group.add_argument('-i', '--icon-name', metavar='icon',
help='''
Name of the XDG icon to use for the window.
Can be icon from a theme, one of the default gtk ones, and such.
See XDG standards for how this name gets resolved into actual file path.
Example: image-x-generic.''')
group = parser.add_argument_group('Misc / debug')
group.add_argument('-n', '--no-register-session', action='store_true',
help='''
Do not try register app with any session manager.
Can be used to get rid of Gtk-WARNING messages
about these and to avoid using dbus, but not sure how/if it actually works.''')
group.add_argument('--dump-css', action='store_true',
help='Print css that is used for windows by default and exit.')
group.add_argument('-d', '--debug', action='store_true', help='Verbose operation mode.')
opts = parser.parse_args(sys.argv[1:] if args is None else args)
logging.basicConfig(
datefmt='%Y-%m-%d %H:%M:%S',
format='%(asctime)s :: %(levelname)s :: %(message)s',
level=logging.DEBUG if opts.debug else logging.WARNING )
color = color_rgba(opts.color, opacity=opts.opacity)
conf.win_css += conf.win_css_opts_tpl.format(win_color=color)
if opts.dump_css: return print(conf.win_css.replace('\t', ' '), end='')
if opts.pos:
m = re.search(
r'^((?:M?\d+|S)(?:x(?:M?\d+|S))?)?'
r'([-+]M?\d+)?([-+]M?\d+)?$', opts.pos )
if not m: parser.error(f'Invalid size/position spec: {opts.pos!r}')
size, x, y = m.groups()
size_fs = size if 'x' not in size else None
if size:
if size_fs: size = f'{size}x{size}'
conf.win_w, conf.win_h = size.split('x', 1)
if x: conf.win_x = x
if y: conf.win_y = y
if size_fs and not (x or y): conf.win_x = conf.win_y = size_fs
if opts.wm_hints:
conf.wm_hints = dict(
(hint.lstrip('+-'), not hint.startswith('-'))
for hint in opts.wm_hints.replace(',', ' ').split() )
if opts.wm_type_hints:
for k in opts.wm_type_hints.replace(',', ' ').split():
conf.wm_type_hints |= conf.wm_type_hints_all[k]
if opts.icon_name: conf.win_icon = opts.icon_name
conf.no_session = opts.no_register_session
if opts.key_quit: conf.quit_keys = opts.key_quit
log.debug('Starting application...')
BlindsApp(conf).run()
if __name__ == '__main__':
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
sys.exit(main())