/
xrandr.py
381 lines (338 loc) · 13.4 KB
/
xrandr.py
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# -*- coding: utf-8 -*-
"""
Control your screen(s) layout easily.
This modules allows you to handle your screens outputs directly from your bar!
- Detect and propose every possible screen combinations
- Switch between combinations using click events and mouse scroll
- Activate the screen or screen combination on a single click
- It will detect any newly connected or removed screen automatically
For convenience, this module also proposes some added features:
- Dynamic parameters for POSITION and WORKSPACES assignment (see below)
- Automatic fallback to a given screen or screen combination when no more
screen is available (handy for laptops)
- Automatically apply this screen combination on start: no need for xorg!
- Automatically move workspaces to screens when they are available
Configuration parameters:
- cache_timeout: how often to (re)detect the outputs
- fallback: when the current output layout is not available anymore,
fallback to this layout if available. This is very handy if you
have a laptop and switched to an external screen for presentation
and want to automatically fallback to your laptop screen when you
disconnect the external screen.
- force_on_start: switch to the given combination mode if available
when the module starts (saves you from having to configure xorg)
- format_clone: string used to display a 'clone' combination
- format_extend: string used to display a 'extend' combination
Dynamic configuration parameters:
- <OUTPUT>_pos: apply the given position to the OUTPUT
Example: DP1_pos = "-2560x0"
Example: DP1_pos = "above eDP1"
Example: DP1_pos = "below eDP1"
Example: DP1_pos = "left-of LVDS1"
Example: DP1_pos = "right-of eDP1"
- <OUTPUT>_workspaces: comma separated list of workspaces to move to
the given OUTPUT when it is activated
Example: DP1_workspaces = "1,2,3"
Example config:
xrandr {
force_on_start = "eDP1+DP1"
DP1_pos = "left-of eDP1"
VGA_workspaces = "7"
}
@author ultrabug
"""
import shlex
from collections import deque
from collections import OrderedDict
from itertools import combinations
from subprocess import call, Popen, PIPE
from syslog import syslog, LOG_INFO
from time import sleep, time
class Py3status:
"""
"""
# available configuration parameters
cache_timeout = 10
fallback = True
fixed_width = True
force_on_start = None
format_clone = '='
format_extend = '+'
def __init__(self):
"""
"""
self.active_comb = None
self.active_layout = None
self.active_mode = 'extend'
self.displayed = None
self.max_width = 0
def _get_layout(self):
"""
Get the outputs layout from xrandr and try to detect the
currently active layout as best as we can on start.
"""
connected = list()
active_layout = list()
disconnected = list()
layout = OrderedDict({
'connected': OrderedDict(),
'disconnected': OrderedDict()
})
current = Popen(['xrandr'], stdout=PIPE)
for line in current.stdout.readlines():
try:
# python3
line = line.decode()
except:
pass
try:
s = line.split(' ')
if s[1] == 'connected':
output, state = s[0], s[1]
if s[2][0] == '(':
mode, infos = None, ' '.join(s[2:]).strip('\n')
else:
mode, infos = s[2], ' '.join(s[3:]).strip('\n')
active_layout.append(output)
connected.append(output)
elif s[1] == 'disconnected':
output, state = s[0], s[1]
mode, infos = None, ' '.join(s[2:]).strip('\n')
disconnected.append(output)
else:
continue
except Exception as err:
syslog(LOG_INFO, 'xrandr error="{}"'.format(err))
else:
layout[state][output] = {
'infos': infos,
'mode': mode,
'state': state
}
# initialize the active layout
if self.active_layout is None:
self.active_comb = tuple(active_layout)
self.active_layout = self._get_string_and_set_width(
tuple(active_layout), self.active_mode)
return layout
def _set_available_combinations(self):
"""
Generate all connected outputs combinations and
set the max display width while iterating.
"""
available_combinations = set()
combinations_map = {}
self.max_width = 0
for output in range(len(self.layout['connected']) + 1):
for comb in combinations(self.layout['connected'], output):
if comb:
for mode in ['clone', 'extend']:
string = self._get_string_and_set_width(comb, mode)
if len(comb) == 1:
combinations_map[string] = (comb, None)
else:
combinations_map[string] = (comb, mode)
available_combinations.add(string)
self.available_combinations = deque(available_combinations)
self.combinations_map = combinations_map
def _get_string_and_set_width(self, combination, mode):
"""
Construct the string to be displayed and record the max width.
"""
show = '{}'.format(self._separator(mode)).join(combination)
show = show.rstrip('{}'.format(self._separator(mode)))
self.max_width = max([self.max_width, len(show)])
return show
def _choose_what_to_display(self, force_refresh=False):
"""
Choose what combination to display on the bar.
By default we try to display the active layout on the first run, else
we display the last selected combination.
"""
for _ in range(len(self.available_combinations)):
if (self.displayed is None and
self.available_combinations[0] == self.active_layout):
self.displayed = self.available_combinations[0]
break
else:
if self.displayed == self.available_combinations[0]:
break
else:
self.available_combinations.rotate(1)
else:
if force_refresh:
self.displayed = self.available_combinations[0]
else:
syslog(LOG_INFO,
'xrandr error="displayed combination is not available"')
def _center(self, s):
"""
Center the given string on the detected max width.
"""
fmt = '{:^%d}' % self.max_width
return fmt.format(s)
def _apply(self, force=False):
"""
Call xrandr and apply the selected (displayed) combination mode.
"""
if self.displayed == self.active_layout and not force:
# no change, do nothing
return
combination, mode = self.combinations_map.get(self.displayed,
(None, None))
if combination is None and mode is None:
# displayed combination cannot be activated, ignore
return
cmd = 'xrandr'
outputs = list(self.layout['connected'].keys())
outputs += list(self.layout['disconnected'].keys())
previous_output = None
for output in outputs:
cmd += ' --output {}'.format(output)
#
if output in combination:
pos = getattr(self, '{}_pos'.format(output), '0x0')
#
if mode == 'clone' and previous_output is not None:
cmd += ' --auto --same-as {}'.format(previous_output)
else:
if ('above' in pos or 'below' in pos or 'left-of' in pos or
'right-of' in pos):
cmd += ' --auto --{} --rotate normal'.format(pos)
else:
cmd += ' --auto --pos {} --rotate normal'.format(pos)
previous_output = output
else:
cmd += ' --off'
#
code = call(shlex.split(cmd))
if code == 0:
self.active_comb = combination
self.active_layout = self.displayed
self.active_mode = mode
syslog(LOG_INFO, 'command "{}" exit code {}'.format(cmd, code))
# move workspaces to outputs as configured
self._apply_workspaces(combination, mode)
def _apply_workspaces(self, combination, mode):
"""
Allows user to force move a comma separated list of workspaces to the
given output when it's activated.
Example:
- DP1_workspaces = "1,2,3"
"""
if len(combination) > 1 and mode == 'extend':
sleep(3)
for output in combination:
workspaces = getattr(self, '{}_workspaces'.format(output),
'').split(',')
for workspace in workspaces:
if not workspace:
continue
# switch to workspace
cmd = 'i3-msg workspace "{}"'.format(workspace)
call(shlex.split(cmd), stdout=PIPE, stderr=PIPE)
# move it to output
cmd = 'i3-msg move workspace to output "{}"'.format(output)
call(shlex.split(cmd), stdout=PIPE, stderr=PIPE)
# log this
syslog(LOG_INFO,
'moved workspace {} to output {}'.format(workspace,
output))
def _refresh_py3status(self):
"""
Send a SIGUSR1 signal to py3status to force a bar refresh.
"""
call(shlex.split('killall -s USR1 py3status'))
def _fallback_to_available_output(self):
"""
Fallback to the first available output when the active layout
was composed of only one output.
This allows us to avoid cases where you get stuck with a black sreen
on your laptop by switching back to the integrated screen
automatically !
"""
if len(self.active_comb) == 1:
self._choose_what_to_display(force_refresh=True)
self._apply()
self._refresh_py3status()
def _force_force_on_start(self):
"""
Force the user configured mode on start.
"""
if self.force_on_start in self.available_combinations:
self.displayed = self.force_on_start
self.force_on_start = None
self._choose_what_to_display(force_refresh=True)
self._apply(force=True)
self._refresh_py3status()
def _separator(self, mode):
"""
Return the separator for the given mode.
"""
if mode == 'extend':
return self.format_extend
if mode == 'clone':
return self.format_clone
def _switch_selection(self, direction):
self.available_combinations.rotate(direction)
self.displayed = self.available_combinations[0]
def on_click(self, i3s_output_list, i3s_config, event):
"""
Click events
- left click & scroll up/down: switch between modes
- right click: apply selected mode
- middle click: force refresh of available modes
"""
button = event['button']
if button == 4:
self._switch_selection(-1)
if button in [1, 5]:
self._switch_selection(1)
if button == 2:
self._choose_what_to_display(force_refresh=True)
if button == 3:
self._apply()
def xrandr(self, i3s_output_list, i3s_config):
"""
This is the main py3status method, it will orchestrate what's being
displayed on the bar.
"""
self.layout = self._get_layout()
self._set_available_combinations()
self._choose_what_to_display()
if self.fixed_width is True:
full_text = self._center(self.displayed)
else:
full_text = self.displayed
response = {
'cached_until': time() + self.cache_timeout,
'full_text': full_text
}
# coloration
if self.displayed == self.active_layout:
response['color'] = i3s_config['color_good']
elif self.displayed not in self.available_combinations:
response['color'] = i3s_config['color_bad']
# force default layout setup
if self.force_on_start is not None:
sleep(1)
self._force_force_on_start()
# fallback detection
if self.active_layout not in self.available_combinations:
response['color'] = i3s_config['color_degraded']
if self.fallback is True:
self._fallback_to_available_output()
return response
if __name__ == "__main__":
"""
Test this module by calling it directly.
"""
x = Py3status()
config = {
'color_bad': '#FF0000',
'color_degraded': '#FFFF00',
'color_good': '#00FF00'
}
while True:
print(x.xrandr([], config))
sleep(1)