guest@dotshare [~/groups/wms/i3] $ ls i3-cfg-with-custom-ion3-like-i3ipc-based-stuff/ | cat

i3 cfg with custom ion3-like i3ipc-based stuff (scrot)

Neg Jan 06, 2020 (wms/i3)

i3conf(raw, dl)

SCROT

  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
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
#---------------------------------------
# ░▀█▀░▀▀█░░░█░█▀▀░█▀█░█▀█░█▀▀░▀█▀░█▀▀ #
# ░░█░░░▀▄░▄▀░░█░░░█░█░█░█░█▀▀░░█░░█░█ #
# ░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀░▀░▀░░░▀▀▀░▀▀▀ #
#---------------------------------------

# ░█▄█░▀█▀░█▀▀░█▀▀
# ░█░█░░█░░▀▀█░█░░
# ░▀░▀░▀▀▀░▀▀▀░▀▀▀
workspace_layout tabbed
floating_modifier $m4

set $m4  Mod4
set $alt Mod1
set $c   Control
set $S   Shift

set $i3 ${XDG_CONFIG_HOME}/i3

set $circle exec --no-startup-id ${XDG_CONFIG_HOME}/i3/send circle
set $bscratch exec --no-startup-id ${XDG_CONFIG_HOME}/i3/send bscratch
set $win_history exec --no-startup-id ${XDG_CONFIG_HOME}/i3/send win_history
set $menu exec ${XDG_CONFIG_HOME}/i3/send menu
set $win_action exec --no-startup-id ${XDG_CONFIG_HOME}/i3/send win_action
set $volume exec --no-startup-id ${XDG_CONFIG_HOME}/i3/send vol
set $executor exec --no-startup-id ${XDG_CONFIG_HOME}/i3/send executor

set $scratchpad_dialog move scratchpad, move position 180 20, resize set 1556 620

# ░█▀▀░█▀█░█░░░█▀█░█▀▄░█▀▀ #
# ░█░░░█░█░█░░░█░█░█▀▄░▀▀█ #
# ░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀ #
# 1 :: border
# 2 :: background active
# 3 :: foreground inactive
# 4 :: background inactive
# 5 :: indicator
client.focused                 #222233  #000000  #ddddee  #112211 #0C0C0D
client.focused_inactive        #000000  #000000  #005fff  #000000 #222233
client.unfocused               #000000  #000000  #315c70  #000000 #222233
client.urgent                  #000000  #2E2457  #4C407C  #32275E #32275E
client.placeholder             #000000  #0c0c0c  #ffffff  #000000 #0c0c0c
client.background              #000000

# ░█▀▀░█▀█░█▀█░▀█▀
# ░█▀▀░█░█░█░█░░█░
# ░▀░░░▀▀▀░▀░▀░░▀░
set $myfont Iosevka Term Heavy 8
font pango: $myfont

# ░█▄█░█▀█░█░█░█▀▀░█▀▀░░░█░█▀▀░█▀█░█▀▀░█░█░█▀▀
# ░█░█░█░█░█░█░▀▀█░█▀▀░▄▀░░█▀▀░█░█░█░░░█░█░▀▀█
# ░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░░░▀░░░▀▀▀░▀▀▀░▀▀▀░▀▀▀
focus_follows_mouse no
force_display_urgency_hint 0 ms
focus_on_window_activation urgent
# warp the mouse to the middle of the window when changing to a different screen
mouse_warping none
focus_wrapping yes

# ░█░█░▀█▀░█▀█░█▀▀
# ░█▄█░░█░░█░█░▀▀█
# ░▀░▀░▀▀▀░▀░▀░▀▀▀
show_marks yes
smart_borders on
hide_edge_borders both
gaps inner 0
gaps outer 0
popup_during_fullscreen smart

# ░█░█░█▄█░█▀▀
# ░█▄█░█░█░▀▀█
# ░▀░▀░▀░▀░▀▀▀
set $term "1 ::  α:term"
set $web "2 ::  β:web"
set $doc "3 ::  γ:doc"
set $dev "4 ::  δ:dev"
set $gfx "5 ::  ε:gfx"
set $draw "6 ::  ζ:draw"
set $sys "7 ::  η:sys"
set $ide "8 ::  θ:ide"
set $steam "9 ::  ι:steam"
set $torrent "10 ::  κ:torrent"
set $vm "11 ::  λ:vm"
set $wine "12 ::  μ:wine"
set $spotify "13 ::  ν:spotify"
set $pic "14 ::  ξ:pic"
set $remote "15 ::  ο:remote"
set $sound "16 ::  ο:sound"

# ░█░█░█▄█░░░░░█▀▀░█░░░█▀█░█▀▀░█▀▀░░░░░█▀▀░█▀▄░█▀█░█░█░█▀█░█▀▀ #
# ░█▄█░█░█░░░░░█░░░█░░░█▀█░▀▀█░▀▀█░░░░░█░█░█▀▄░█░█░█░█░█▀▀░▀▀█ #
# ░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░▀▀▀░▀░░░▀▀▀ #
set $browsers [class="^(firefox|Nightly|Waterfox|Chromium|Yandex-browser-beta|Tor Browser)$"]
set $sclient [class="^(Steam|steam)$"]
set $games [class="^(steam_app.*|PillarsOfEternityII|lutris|Lutris)$"]
set $wine_apps [class="^(Wine|wine|Crossover)$"]
set $windows_exe_by_title [title="^.*.exe$"]
set $windows_exe_by_class [class="^.*.exe$"]
set $pdf [class="Zathura"]
set $fb2 [class="Cr3" instance="cr3"]
set $mplayer [class="^(MPlayer|mpv|vaapi|vdpau)$"]
set $webcam [class="^(cheese|obs)$"]
set $vim [instance="nwim"]
set $vms [class="(?i)^(VirtualBox|vmware|looking-glass-client|[Qq]emu.*|spic).*$"]
set $daw [class="Bitwig Studio" instance="^(airwave-host-32.exe|Bitwig Studio)$"]

title_align left
for_window [class=".*"] title_format "<span foreground='#395573'> >_ </span> %title"
# ░█░█░█▄█░░░░░█▀▄░█░█░█░░░█▀▀░█▀▀
# ░█▄█░█░█░░░░░█▀▄░█░█░█░░░█▀▀░▀▀█
# ░▀░▀░▀░▀░▀▀▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀
for_window $browsers move workspace $web, focus
for_window [class="guncharmap"] move workspace $doc
for_window $vms move workspace $vm, focus
for_window [{class,instance}="^term$"] move workspace $term, focus
for_window $mplayer move workspace $gfx, focus
for_window [class="Nicotine.*" instance="nicotine"] move workspace $gfx, focus
for_window [class="^lollypop$"] move workspace $gfx, focus
for_window [class="spotify"] move workspace $spotify, focus
for_window [class="Sxiv"] move workspace $pic, focus
for_window [instance="^(gpartedbin|recoll|gnome-disks)$"] move workspace $sys, floating enable, focus
for_window [instance="^(xfreerdp|remmina|org.remmina.Remmina)$"] move workspace $remote, focus
for_window [title="^Java iKVM Viewer.*$"] move workspace $remote, focus
for_window [class="albert"] move workspace current, focus
for_window $daw move workspace $sound, focus

# ░█▀▄░█▀▀░█▀█░█▀▄░█▀▀░█▀▄░█▀▀
# ░█▀▄░█▀▀░█▀█░█░█░█▀▀░█▀▄░▀▀█
# ░▀░▀░▀▀▀░▀░▀░▀▀░░▀▀▀░▀░▀░▀▀▀
for_window {$fb2,$pdf}, move workspace $doc, focus

# ░█░█░█▀█░█▀▄░░░░░█▀▀░█░░░█▀█░█▀█░▀█▀░▀█▀░█▀█░█▀▀
# ░▀▄▀░█▀█░█▀▄░░░░░█▀▀░█░░░█░█░█▀█░░█░░░█░░█░█░█░█
# ░░▀░░▀░▀░▀░▀░▀▀▀░▀░░░▀▀▀░▀▀▀░▀░▀░░▀░░▀▀▀░▀░▀░▀▀▀
for_window [class="^(Lxappearance|Conky|Xmessage|XFontSel|gcolor2|Gcolor3|rdesktop|Arandr)$"] floating enable

# ░█▀▀░█▀▄░█▀█░█▀█░█░█░▀█▀░█▀▀░█▀▀
# ░█░█░█▀▄░█▀█░█▀▀░█▀█░░█░░█░░░▀▀█
# ░▀▀▀░▀░▀░▀░▀░▀░░░▀░▀░▀▀▀░▀▀▀░▀▀▀
for_window [class="^(draw|darktable|inkscape|gimp)$"] move workspace $draw

# ░█▀▀░█▀▄░▀█▀░▀█▀░█▀█░█▀▄░█▀▀
# ░█▀▀░█░█░░█░░░█░░█░█░█▀▄░▀▀█
# ░▀▀▀░▀▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀
for_window $vim move workspace $dev, focus

# ░█▀▀░█▀█░█▄█░█▀▀░█▀▀
# ░█░█░█▀█░█░█░█▀▀░▀▀█
# ░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀
for_window {$sclient,$games} move workspace $steam, focus

#░█▀▀░█▀▀░█▀▄░█▀█░▀█▀░█▀▀░█░█░█▀█░█▀█░█▀▄░█▀▀
#░▀▀█░█░░░█▀▄░█▀█░░█░░█░░░█▀█░█▀▀░█▀█░█░█░▀▀█
#░▀▀▀░▀▀▀░▀░▀░▀░▀░░▀░░▀▀▀░▀░▀░▀░░░▀░▀░▀▀░░▀▀▀
for_window [class="zoom"] move scratchpad, move absolute position 1372 127, resize set 548 1037
for_window [class="^([Tt]elegram|[Ss]kype).*$"] move scratchpad, move absolute position 1372 127, resize set 548 1037
for_window [instance="ncmpcpp"] move scratchpad, move absolute position 276 326, resize set 1389 696
for_window [class="cool-retro-term"] move scratchpad, move absolute position 276 326, resize set 1389 696
for_window [instance="mutt"] move scratchpad, move absolute position 52 0, resize set 1835 1114
for_window [instance="ranger"] move scratchpad, move absolute position 3 1, resize set 1916 816
for_window [instance="teardrop"] move scratchpad, move absolute position 39 4, resize set 1844 704
for_window [class="Pavucontrol"] move scratchpad, move absolute position 1023 824, resize set 895 314
for_window [instance="youtube-get"] move scratchpad, move absolute position 247 13, resize set 1339 866
for_window [instance="webcam"] move scratchpad, move absolute position 667 363, resize set 1234 771
for_window [class="discord"] move scratchpad, move absolute position 974 127, resize set 944 1036

#░█▀▄░▀█▀░█▀█░█░░░█▀█░█▀▀░█▀▀
#░█░█░░█░░█▀█░█░░░█░█░█░█░▀▀█
#░▀▀░░▀▀▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀
for_window [window_role="^(GtkFileChooserDialog|Organizer|Manager)$"] $scratchpad_dialog
for_window [class="Places"] $scratchpad_dialog

#---------------------------------------------
# ░█░█░█▀▀░█░█░█▀▄░▀█▀░█▀█░█▀▄░▀█▀░█▀█░█▀▀░█▀▀
# ░█▀▄░█▀▀░░█░░█▀▄░░█░░█░█░█░█░░█░░█░█░█░█░▀▀█
# ░▀░▀░▀▀▀░░▀░░▀▀░░▀▀▀░▀░▀░▀▀░░▀▀▀░▀░▀░▀▀▀░▀▀▀
set $exit mode "default"
bindsym $alt+e mode ""               # special mode
bindsym $m4+r mode ""                # resize mode
bindsym $m4+minus mode ""            # window-manager / split / tiling mode
bindsym $m4+$S+$c+BackSpace mode "" # blocking mode
bindsym XF86Audio{Lower,Raise}Volume $volume {d,u}
bindsym $m4+p exec ~/bin/scripts/tmux_clipboard
mode "" {
    bindsym $m4+$S+BackSpace mode "default"
    bindsym $m4+$c+$S+BackSpace mode "default"
}

bindsym $m4+$c+q kill
#----------------------------------------------
bindsym Print exec --no-startup-id ~/bin/scripts/screenshot
bindsym $m4+$S+d exec --no-startup-id "zsh -c '~/bin/scripts/dw s'"
bindsym $m4+$S+y exec --no-startup-id "~/bin/clip youtube-dw-list"
bindsym $m4+$S+0 exec --no-startup-id splatmoji type
bindsym $m4+$S+l exec --no-startup-id "~/bin/scripts/rofi_lutris"
bindsym $m4+$C+5 $circle next remote
bindsym $m4+$C+b $circle next bitwig
bindsym $S+Print exec --no-startup-id ~/bin/scripts/screenshot -c
bindsym {$c+Print,$m4+$S+3} exec --no-startup-id ~/bin/scripts/screenshot -r
bindsym $m4+$S+4 exec --no-startup-id flameshot gui
bindsym $m4+$S+t exec --no-startup-id ~/bin/clip translate
#----------------------------------------------
# ░█▀█░█▀█░█▄█░█▀▀░█▀▄░░░█▀▀░█▀▀░█▀▄░█▀█░▀█▀░█▀▀░█░█░█▀█░█▀█░█▀▄
# ░█░█░█▀█░█░█░█▀▀░█░█░░░▀▀█░█░░░█▀▄░█▀█░░█░░█░░░█▀█░█▀▀░█▀█░█░█
# ░▀░▀░▀░▀░▀░▀░▀▀▀░▀▀░░░░▀▀▀░▀▀▀░▀░▀░▀░▀░░▀░░▀▀▀░▀░▀░▀░░░▀░▀░▀▀░
bindsym $m4+{f,e,d,a} $bscratch toggle {ncmpcpp,im,teardrop,youtube}
bindsym $m4+$S+p $bscratch toggle volcontrol
bindsym $m4+v $bscratch toggle discord

bindsym $m4+$c+$S+{R,D,S} $bscratch {geom_restore,geom_dump,geom_autosave_mode}
bindsym $m4+3 $bscratch next
bindsym $m4+s $bscratch hide_current

# ░█▀▀░▀█▀░█▀▄░█▀▀░█░░░█▀▀
# ░█░░░░█░░█▀▄░█░░░█░░░█▀▀
# ░▀▀▀░▀▀▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
bindsym $m4+$c+c $circle next sxiv
bindsym $m4+$S+c $circle subtag sxiv wallpaper
bindsym $m4+{x,1} $circle next {term,nwim}
bindsym $m4+$c+v $circle next vm
bindsym $m4+$c+e $circle next lutris
bindsym $m4+$S+e $circle next steam
bindsym $m4+$c+f $circle next looking_glass
bindsym $m4+{w,b,o} $circle next {web,vid,doc}
bindsym $m4+$S+o $circle next obs

# ░█░█░▀█▀░█▀█░░░░░█▀█░█▀▀░▀█▀░▀█▀░█▀█░█▀█
# ░█▄█░░█░░█░█░░░░░█▀█░█░░░░█░░░█░░█░█░█░█
# ░▀░▀░▀▀▀░▀░▀░▀▀▀░▀░▀░▀▀▀░░▀░░▀▀▀░▀▀▀░▀░▀
bindsym $m4+grave $win_history focus_next_visible
bindsym $m4+$S+grave $win_history focus_prev_visible

bindsym $m4+{h,l,j,k} focus {left,right,down,up}

# resize window (you can also use the mouse for that)
# ░█▄█░█▀█░█▀▄░█▀▀░░░░░░░█▀▄░█▀▀░█▀▀░▀█▀░▀▀█░█▀▀
# ░█░█░█░█░█░█░█▀▀░░▀░░░░█▀▄░█▀▀░▀▀█░░█░░▄▀░░█▀▀
# ░▀░▀░▀▀▀░▀▀░░▀▀▀░░▀░░░░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀
mode "" {
    bindsym {h,$S+h} $win_action resize left {4,-4}
    bindsym {j,$S+j} $win_action resize bottom {4,-4}
    bindsym {k,$S+k} $win_action resize top {4,-4}
    bindsym {l,$S+l} $win_action resize right {4,-4}

    bindsym {a,$S+a} $win_action resize left {4,-4}
    bindsym {s,$S+s} $win_action resize bottom {4,-4}
    bindsym {w,$S+w} $win_action resize top {4,-4}
    bindsym {d,$S+d} $win_action resize right {4,-4}

    #-------------------------------------------------------
    bindsym {semicolon,$S+colon} resize {shrink,grow} right 4
    bindsym {Return,Escape,space,$c+C,$c+G} $exit
}

bindsym $m4+q fullscreen toggle
# ░█▄█░█▀▀░█▀█░█░█
# ░█░█░█▀▀░█░█░█░█
# ░▀░▀░▀▀▀░▀░▀░▀▀▀
bindsym $alt+g $menu goto_win
bindsym $m4+m $menu xprop, $exit
bindsym $m4+$s+i exec ~/bin/scripts/rofi_nmcli, $exit
bindsym $m4+$S+a $menu attach
bindsym $m4+g $menu ws
bindsym $m4+$c+g $menu movews
bindsym $m4+$c+grave $menu cmd_menu
bindsym $m4+$S+s $menu autoprop
#-------------------------------------------------------
bindsym $alt+Tab $win_history switch
bindsym $m4+slash $win_history switch
#-------------------------------------------------------
# ░█▄█░█▀█░█▀▄░█▀▀░░░░░░░█▀█░█▀█░█▀▀░█▀▀
# ░█░█░█░█░█░█░█▀▀░░▀░░░░█▀▀░█▀█░▀▀█░▀▀█
# ░▀░▀░▀▀▀░▀▀░░▀▀▀░░▀░░░░▀░░░▀░▀░▀▀▀░▀▀▀
mode "pass " {
    bindsym {c,p} exec ~/bin/scripts/lpwd {,type}, $exit
    bindsym {Return,Escape,$c+C,$c+G}, $exit
}
# ░█▄█░█▀█░█▀▄░█▀▀░░░░░░░█░█░▀█▀░█▀█░░░░░█▄█░█▀█░█▀█░█▀█░█▀▀░█▀▀
# ░█░█░█░█░█░█░█▀▀░░▀░░░░█▄█░░█░░█░█░░░░░█░█░█▀█░█░█░█▀█░█░█░█▀▀
# ░▀░▀░▀▀▀░▀▀░░▀▀▀░░▀░░░░▀░▀░▀▀▀░▀░▀░▀▀▀░▀░▀░▀░▀░▀░▀░▀░▀░▀▀▀░▀▀▀
mode "" {
	bindsym {grave,t,minus,backslash} layout {default,tabbed,splith,splitv}; $exit
	bindsym {j,k,h,l} split {vertical,vertical,horizontal,horizontal}; $exit
	bindsym {w,a,s,d} move {up,left,down,right}

    # ░█░█░█▄█░▀▀█
    # ░█▄█░█░█░░▀▄
    # ░▀░▀░▀░▀░▀▀░
	bindsym m $win_action maximize
    bindsym $S+m $win_action revert_maximize
    bindsym {x,y} $win_action {maxhor,maxvert}
    bindsym $S+x $win_action revert_maximize
    bindsym $S+y $win_action revert_maximize
    bindsym {1,2,3,4} $win_action quad {1,2,3,4}
    bindsym $S+{w,a,s,d} $win_action x2 {hup,vleft,hdown,vright}
    bindsym $S+{plus,minus} $win_action {grow,shrink}
    bindsym c $win_action center none
    bindsym $S+c $win_action center resize
    #-------------------------------------------------------
    bindsym g mode ""
    #-------------------------------------------------------
	bindsym $c+{a,3} layout toggle all
	bindsym $c+s     layout toggle split
	bindsym $c+t     layout toggle
    #-------------------------------------------------------
    bindsym {Return,Escape,$c+C,$c+G} $exit
}
# ░█▄█░█▀█░█▀▄░█▀▀░░░░░░░█▀▀░█▀█░█▀▀░█▀▀░▀█▀░█▀█░█░░
# ░█░█░█░█░█░█░█▀▀░░▀░░░░▀▀█░█▀▀░█▀▀░█░░░░█░░█▀█░█░░
# ░▀░▀░▀▀▀░▀▀░░▀▀▀░░▀░░░░▀▀▀░▀░░░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀
mode "" {
    #-------------------------------------------------------
    bindsym c mode "pass "
    bindsym e mode "default", [urgent=latest] focus
    bindsym a mode "default", $bscratch dialog

    bindsym 5 mode "default", $circle subtag web tor
    bindsym y mode "default", $circle subtag web yandex
    bindsym f mode "default", $circle subtag web firefox

    bindsym $S+t mode "default", $menu gtk_theme
    bindsym $S+i mode "default", $menu icon_theme

	bindsym $S+d floating toggle; $exit
	bindsym $S+l exec sh -c 'sudo gllock'; $exit
	bindsym v exec ~/bin/qemu/vm_menu; $exit
	bindsym $S+v exec ~/bin/qemu/vm_menu start_win10; $exit
	bindsym o $menu pulse_output; $exit
	bindsym i $menu pulse_input; $exit
    bindsym $S+s exec ~/bin/pls -switch, $exit
    bindsym {i,o,$S+O} mode "default", exec ~/bin/pls -{output,sink,vol}
    #-------------------------------------------------------
    bindsym {$m4,$alt,}+s $bscratch subtag im skype, mode "default"
    bindsym {$m4+t,$alt+t,t} $bscratch subtag im tel, mode "default"
    bindsym m $bscratch toggle mutt, mode "default"
    bindsym w $bscratch toggle webcam, mode "default"
    bindsym $S+r $bscratch toggle ranger, mode "default"
    #-------------------------------------------------------
    bindsym {Return,Escape,$c+C,$c+G} $exit
}

# ░█▄█░█▀█░█▀▄░█▀▀░░░░░░░█▀▀░█▀█░█▀█░█▀▀
# ░█░█░█░█░█░█░█▀▀░░▀░░░░█░█░█▀█░█▀▀░▀▀█
# ░▀░▀░▀▀▀░▀▀░░▀▀▀░░▀░░░░▀▀▀░▀░▀░▀░░░▀▀▀
mode "" {
	bindsym {o,i} mode -{outer,inner}
    bindsym {Return,Escape,$c+C,$c+G} $exit
}
# ░█▄█░█▀█░█▀▄░█▀▀░░░░░░░█▀▀░█▀█░█▀█░█▀▀░░░░░█▀█░█░█░▀█▀░█▀▀░█▀▄
# ░█░█░█░█░█░█░█▀▀░░▀░░░░█░█░█▀█░█▀▀░▀▀█░▄▄▄░█░█░█░█░░█░░█▀▀░█▀▄
# ░▀░▀░▀▀▀░▀▀░░▀▀▀░░▀░░░░▀▀▀░▀░▀░▀░░░▀▀▀░░░░░▀▀▀░▀▀▀░░▀░░▀▀▀░▀░▀
mode "-outer" {
	bindsym {plus,minus}     gaps outer current {plus,minus} 5
	bindsym 0                gaps outer current set 0

	bindsym $S+{plus,minus}  gaps outer all {plus,minus} 5
	bindsym $S+0             gaps outer all set 0

    bindsym {Return,Escape,$c+C,$c+G} $exit
}
# ░█▄█░█▀█░█▀▄░█▀▀░░░░░░░█▀▀░█▀█░█▀█░█▀▀░░░░░▀█▀░█▀█░█▀█░█▀▀░█▀▄
# ░█░█░█░█░█░█░█▀▀░░▀░░░░█░█░█▀█░█▀▀░▀▀█░▄▄▄░░█░░█░█░█░█░█▀▀░█▀▄
# ░▀░▀░▀▀▀░▀▀░░▀▀▀░░▀░░░░▀▀▀░▀░▀░▀░░░▀▀▀░░░░░▀▀▀░▀░▀░▀░▀░▀▀▀░▀░▀
mode "-inner" {
	bindsym {plus,minus}     gaps inner current {plus,minus} 5
	bindsym 0                gaps inner current set 0

	bindsym $S+{plus,minus}  gaps inner all {plus,minus} 5
	bindsym $S+0             gaps inner all set 0

    bindsym {Return,Escape,$c+C,$c+G} $exit
}

for_window [title="Desktop — Plasma"] floating enable; border none
for_window [class="plasmashell"] floating enable
for_window [class="Plasma"] floating enable
for_window [class="krunner"] floating enable
for_window [class="Kmix"] floating enable
for_window [class="Klipper"] floating enable
for_window [class="Plasmoidviewer"] floating enable

for_window [window_role="pop-up"] floating enable
for_window [window_role="bubble"] floating enable
for_window [window_role="task_dialog"] floating enable
for_window [window_role="Preferences"] floating enable
for_window [window_role="About"] floating enable 
for_window [window_type="dialog"] floating enable
for_window [window_type="menu"] floating enable

for_window [class="^.*"] border pixel 2

# ░█▀▀░▀█▀░█▀█░█▀▄░▀█▀
# ░▀▀█░░█░░█▀█░█▀▄░░█░
# ░▀▀▀░░▀░░▀░▀░▀░▀░░▀░
exec_always ~/.config/i3/i3_prepare &
exec_always ~/bin/scripts/gnome_settings &
exec /usr/lib/gsd-xsettings &
exec_always ~/bin/scripts/panel_run.sh hard
exec /usr/sbin/gpaste-client daemon
# vim:filetype=i3

CLICK TO VIEW

x

all_menu_lib_dir(raw, dl)

  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
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
==> menu_mods/gnome.py <==
""" Change gtk / icons themes and another gnome settings
"""
import os
import configparser
import subprocess
import glob
import path

from misc import Misc


class gnome():
    """
        Change gtk / icons themes and another gnome settings using
        gsd-xsettings.
    """

    def __init__(self, menu):
        self.menu = menu
        self.gtk_config = configparser.ConfigParser()
        self.gnome_settings_script = os.path.expanduser(
            '~/bin/scripts/gnome_settings'
        )

    def menu_params(self, length, prompt):
        """ Set menu params """
        return {
            'cnum': length / 2,
            'lnum': 2,
            'width': int(self.menu.screen_width * 0.55),
            'prompt':
                f'{self.menu.wrap_str(prompt)} {self.menu.conf("prompt")}'
        }

    def apply_settings(self, selection, *cmd_opts):
        """ Apply selected gnome settings """
        ret = ""
        if selection is not None:
            ret = selection.decode('UTF-8').strip()

            if ret is not None and ret != '':
                try:
                    subprocess.call([
                        self.gnome_settings_script, *cmd_opts, ret
                    ], check=True)
                except subprocess.CalledProcessError as proc_err:
                    Misc.print_run_exception_info(proc_err)

    def change_icon_theme(self):
        """ Changes icon theme with help of gsd-xsettings """
        icon_dirs = []
        icons_path = path.Path('~/.icons').expanduser()
        for icon in glob.glob(icons_path + '/*'):
            if icon:
                icon_dirs += [path.Path(icon).name]

        menu_params = self.menu_params(len(icon_dirs), 'icon theme')

        try:
            selection = subprocess.run(
                self.menu.args(menu_params),
                stdout=subprocess.PIPE,
                input=bytes('\n'.join(icon_dirs), 'UTF-8'),
                check=True
            ).stdout
        except subprocess.CalledProcessError as proc_err:
            Misc.print_run_exception_info(proc_err)

        self.apply_settings(selection, '-i')

    def change_gtk_theme(self):
        """ Changes gtk theme with help of gsd-xsettings """
        theme_dirs = []
        gtk_theme_path = path.Path('~/.themes').expanduser()
        for theme in glob.glob(gtk_theme_path + '/*/*/gtk.css'):
            if theme:
                theme_dirs += [path.Path(theme).dirname().dirname().name]

        menu_params = self.menu_params(len(theme_dirs), 'gtk theme')
        try:
            selection = subprocess.run(
                self.menu.args(menu_params),
                stdout=subprocess.PIPE,
                input=bytes('\n'.join(theme_dirs), 'UTF-8'),
                check=True
            ).stdout
        except subprocess.CalledProcessError as proc_err:
            Misc.print_run_exception_info(proc_err)

        self.apply_settings(selection, '-a')


==> menu_mods/i3menu.py <==
import sys
import json
import re
import subprocess

from typing import List


class i3menu():
    def __init__(self, menu):
        self.menu = menu

    def i3_cmds(self) -> List[str]:
        """ Return the list of i3 commands with magic_pie hack autocompletion.
        """
        try:
            out = subprocess.run(
                [self.menu.i3cmd, 'magic_pie'],
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                check=False
            ).stdout
        except Exception:
            return []

        lst = [
            t.replace("'", '')
            for t in re.split('\\s*,\\s*', json.loads(
                out.decode('UTF-8')
            )[0]['error'])[2:]
        ]

        lst.remove('nop')
        lst.extend(['splitv', 'splith'])
        lst.sort()

        return lst

    def i3_cmd_args(self, cmd: str) -> List[str]:
        try:
            out = subprocess.run(
                [self.menu.i3cmd, cmd],
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                check=False
            ).stdout
            if out is not None:
                ret = [
                    t.replace("'", '') for t in
                    re.split('\\s*, \\s*', json.loads(
                        out.decode('UTF-8')
                    )[0]['error'])[1:]
                ]
            return ret
        except Exception:
            return [""]

    def cmd_menu(self) -> int:
        """ Menu for i3 commands with hackish autocompletion.
        """
        # set default menu args for supported menus
        cmd = ''

        try:
            menu = subprocess.run(
                self.menu.args({}),
                stdout=subprocess.PIPE,
                input=bytes('\n'.join(self.i3_cmds()), 'UTF-8'),
                check=True
            ).stdout
            if menu is not None and menu:
                cmd = menu.decode('UTF-8').strip()
        except subprocess.CalledProcessError as call_e:
            sys.exit(call_e.returncode)

        if not cmd:
            # nothing to do
            return 0

        debug, ok, notify_msg = False, False, ""
        args, prev_args = None, None
        menu_params = {
            'prompt': f"{self.menu.wrap_str('i3cmd')} \
                {self.menu.conf('prompt')} " + cmd,
        }
        while not (ok or args == ['<end>'] or args == []):
            if debug:
                print(f"evaluated cmd=[{cmd}] args=[{self.i3_cmd_args(cmd)}]")
            out = subprocess.run(
                (f"{self.menu.i3cmd} " + cmd).split(),
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                check=False
            ).stdout
            if out is not None and out:
                ret = json.loads(out.decode('UTF-8').strip())[0]
                result, err = ret.get('success', ''), ret.get('error', '')
                ok = True
                if not result:
                    ok = False
                    notify_msg = ['notify-send', 'i3-cmd error', err]
                    try:
                        args = self.i3_cmd_args(cmd)
                        if args == prev_args:
                            return 0
                        cmd_rerun = subprocess.run(
                            self.menu.args(menu_params),
                            stdout=subprocess.PIPE,
                            input=bytes('\n'.join(args), 'UTF-8'),
                            check=False
                        ).stdout
                        cmd += ' ' + cmd_rerun.decode('UTF-8').strip()
                        prev_args = args
                    except subprocess.CalledProcessError as call_e:
                        return call_e.returncode

        if not ok:
            subprocess.run(notify_msg, check=False)


==> menu_mods/props.py <==
import subprocess
import re
import socket
from typing import List


class props():
    def __init__(self, menu):
        self.menu = menu

        # Magic delimiter used by add_prop / del_prop routines.
        self.delim = "@"

        # default echo server host
        self.host = self.menu.conf("host")

        # default echo server port
        self.port = int(self.menu.conf("port"))

        # create echo server socket
        self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)

        # negi3mods which allows add / delete property.
        # For example this feature can be used to move / delete window
        # to / from named scratchpad.
        self.possible_mods = ['ns', 'circle']

        # Window properties used by i3 to match windows.
        self.i3rules_xprop = set(self.menu.conf("rules_xprop"))

    def tag_name(self, mod: str, lst: List[str]) -> str:
        """ Returns tag name, selected by menu.

        Args:
            mod (str): module name string.
            lst (List[str]): list of menu input.
        """
        menu_params = {
            'cnum': len(lst),
            'width': int(self.menu.screen_width * 0.75),
            'prompt': f'{self.menu.wrap_str(mod)} {self.menu.conf("prompt")}',
        }
        menu_tag = subprocess.run(
            self.menu.args(menu_params),
            stdout=subprocess.PIPE,
            input=bytes('\n'.join(lst), 'UTF-8'),
            check=False
        ).stdout

        if menu_tag is not None and menu_tag:
            return menu_tag.decode('UTF-8').strip()

        return ""

    def autoprop(self) -> None:
        """ Start autoprop menu to move current module to smth.
        """
        mod = self.get_mod()
        if mod is None or not mod:
            return

        aprop_str = self.get_autoprop_as_str(with_title=False)

        lst = self.mod_data_list(mod)
        tag_name = self.tag_name(mod, lst)

        if tag_name is not None and tag_name:
            for mod in self.possible_mods:
                cmdl = [
                    f'{self.menu.i3_path}send',
                    f'{mod}', 'add_prop',
                    f'{tag_name}', f'{aprop_str}'
                ]
                subprocess.run(cmdl, check=False)
        else:
            print(f'No tag name specified for props [{aprop_str}]')

    def get_mod(self) -> str:
        """ Select negi3mod for add_prop by menu.
        """
        menu_params = {
            'cnum': len(self.possible_mods),
            'lnum': 1,
            'width': int(self.menu.screen_width * 0.75),
            'prompt': f'{self.menu.wrap_str("selmod")} \
            {self.menu.conf("prompt")}'
        }
        mod = subprocess.run(
            self.menu.args(menu_params),
            stdout=subprocess.PIPE,
            input=bytes('\n'.join(self.possible_mods), 'UTF-8'),
            check=False
        ).stdout

        if mod is not None and mod:
            return mod.decode('UTF-8').strip()

        return ""

    def show_props(self) -> None:
        """ Send notify-osd message about current properties.
        """
        aprop_str = self.get_autoprop_as_str(with_title=False)
        notify_msg = ['notify-send', 'X11 prop', aprop_str]
        subprocess.run(notify_msg, check=False)

    def get_autoprop_as_str(self, with_title: bool = False,
                            with_role: bool = False) -> str:
        """ Convert xprops list to i3 commands format.

        Args:
            with_title (bool): add WM_NAME attribute, to the list, optional.
            with_role (bool): add WM_WINDOW_ROLE attribute to the list,
            optional.
        """
        xprops = []
        win = self.menu.i3ipc.get_tree().find_focused()
        xprop = subprocess.run(
            ['xprop', '-id', str(win.window)] + self.menu.xprops_list,
            stdout=subprocess.PIPE,
            check=False
        ).stdout
        if xprop is not None:
            xprop = xprop.decode('UTF-8').split('\n')
        ret = []
        for attr in self.i3rules_xprop:
            for xattr in xprop:
                xprops.append(xattr)
                if attr in xattr and 'not found' not in xattr:
                    founded_attr = re.search("[A-Z]+(.*) = ", xattr).group(0)
                    xattr = re.sub("[A-Z]+(.*) = ", '', xattr).split(', ')
                    if "WM_CLASS" in founded_attr:
                        if xattr[0] is not None and xattr[0]:
                            ret.append(f'instance={xattr[0]}{self.delim}')
                        if xattr[1] is not None and xattr[1]:
                            ret.append(f'class={xattr[1]}{self.delim}')
                    if with_role and "WM_WINDOW_ROLE" in founded_attr:
                        ret.append(f'window_role={xattr[0]}{self.delim}')
                    if with_title and "WM_NAME" in founded_attr:
                        ret.append(f'title={xattr[0]}{self.delim}')
        return "[" + ''.join(sorted(ret)) + "]"

    def mod_data_list(self, mod: str) -> List[str]:
        """ Extract list of module tags. Used by add_prop menus.

        Args:
            mod (str): negi3mod name.
        """
        self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
        self.sock.connect((self.host, self.port))
        self.sock.send(bytes(f'{mod}_list\n', 'UTF-8'))

        out = self.sock.recv(1024)

        self.sock.shutdown(1)
        self.sock.close()

        lst = []
        if out is not None:
            lst = out.decode('UTF-8').strip()[1:-1].split(', ')
            lst = [t.replace("'", '') for t in lst]

        return lst


==> menu_mods/pulse_menu.py <==
import subprocess
import pulsectl


class pulse_menu():
    def __init__(self, menu):
        self.menu = menu
        self.pulse = pulsectl.Pulse('neg-pulse-selector')
        self.pulse_data = {}

    def pulseaudio_output(self):
        self.pulse_data = {
            "app_list": [],
            "sink_output_list": [],
            "app_props": {},
            "pulse_sink_list": self.pulse.sink_list(),
            "pulse_app_list": self.pulse.sink_input_list(),
        }

        for app in self.pulse_data["pulse_app_list"]:
            app_name = app.proplist["media.name"] + ' -- ' + \
                app.proplist["application.name"]
            self.pulse_data["app_list"] += [app_name]
            self.pulse_data["app_props"][app_name] = app

        if self.pulse_data["app_list"]:
            app_ret = self.pulseaudio_select_app()
            if self.pulse_data["sink_output_list"]:
                self.pulseaudio_select_output(app_ret)

    def pulseaudio_input(self):
        pass

    def pulseaudio_select_app(self):
        menu_params = {
            'cnum': 1,
            'lnum': len(self.pulse_data["app_list"]),
            'auto_selection': '-auto-select',
            'width': int(self.menu.screen_width * 0.55),
            'prompt': f'{self.menu.wrap_str("pulse app")} \
            {self.menu.conf("prompt")}',
        }
        menu_app_sel = subprocess.run(
            self.menu.args(menu_params),
            stdout=subprocess.PIPE,
            input=bytes('\n'.join(self.pulse_data["app_list"]), 'UTF-8'),
            check=False
        ).stdout

        if menu_app_sel is not None:
            app_ret = menu_app_sel.decode('UTF-8').strip()

        exclude_device_name = ""
        sel_app_props = \
            self.pulse_data["app_props"][app_ret].proplist
        for stream in self.pulse.stream_restore_list():
            if stream is not None:
                if stream.device is not None:
                    if stream.name == sel_app_props['module-stream-restore.id']:
                        exclude_device_name = stream.device

        for _, sink in enumerate(self.pulse_data["pulse_sink_list"]):
            if sink.proplist.get('udev.id', ''):
                if sink.proplist['udev.id'].split('.')[0] == \
                        exclude_device_name.split('.')[1]:
                    continue
            if sink.proplist.get('device.profile.name', ''):
                if sink.proplist['device.profile.name'] == \
                        exclude_device_name.split('.')[-1]:
                    continue
            self.pulse_data["sink_output_list"] += \
                [str(sink.index) + ' -- ' + sink.description]

        return app_ret

    def pulseaudio_select_output(self, app_ret) -> None:
        """ Create params for pulseaudio selector """
        menu_params = {
            'cnum': 1,
            'lnum': len(self.pulse_data["sink_output_list"]),
            'auto_selection': '-auto-select',
            'width': int(self.menu.screen_width * 0.55),
            'prompt':
                f'{self.menu.wrap_str("pulse output")} \
                {self.menu.conf("prompt")}'
        }
        menu_output_sel = subprocess.run(
            self.menu.args(menu_params),
            stdout=subprocess.PIPE,
            input=bytes(
                '\n'.join(self.pulse_data["sink_output_list"]),
                'UTF-8'
            ),
            check=False
        ).stdout

        if menu_output_sel is not None:
            out_ret = menu_output_sel.decode('UTF-8').strip()

        target_idx = out_ret.split('--')[0].strip()
        if int(self.pulse_data["app_props"][app_ret].index) is not None \
                and int(target_idx) is not None:
            self.pulse.sink_input_move(
                int(self.pulse_data["app_props"][app_ret].index),
                int(target_idx),
            )


==> menu_mods/winact.py <==
import subprocess
from functools import partial
from typing import Callable


class winact():
    def __init__(self, menu):
        self.menu = menu
        self.workspaces = menu.conf("workspaces")

    def win_act_simple(self, cmd: str, prompt: str) -> None:
        """ Run simple and fast selection dialog for window with given action.
            Args:
                cmd (str): action for window to run.
                prompt (str): custom prompt for menu.
        """
        leaves = self.menu.i3ipc.get_tree().leaves()
        winlist = [win.name for win in leaves]
        winlist_len = len(winlist)
        menu_params = {
            'cnum': winlist_len,
            'width': int(self.menu.screen_width * 0.75),
            'prompt': f"{prompt} {self.menu.conf('prompt')}"
        }
        if winlist and winlist_len > 1:
            win_name = subprocess.run(
                self.menu.args(menu_params),
                stdout=subprocess.PIPE,
                input=bytes('\n'.join(winlist), 'UTF-8'),
                check=False
            ).stdout
        elif winlist_len:
            win_name = winlist[0].encode()

        if win_name is not None and win_name:
            win_name = win_name.decode('UTF-8').strip()
            for win in leaves:
                if win.name == win_name:
                    win.command(cmd)

    def goto_win(self) -> None:
        """ Run menu goto selection dialog
        """
        self.win_act_simple('focus', self.menu.wrap_str('go'))

    def attach_win(self) -> None:
        """ Attach window to the current workspace.
        """
        self.win_act_simple(
            'move window to workspace current', self.menu.wrap_str('attach')
        )

    def select_ws(self, use_wslist: bool) -> str:
        """ Apply target function to workspace.
        """
        if use_wslist:
            wslist = self.workspaces
        else:
            wslist = [ws.name for ws in self.menu.i3ipc.get_workspaces()] + \
                ["[empty]"]
        menu_params = {
            'cnum': len(wslist),
            'width': int(self.menu.screen_width * 0.66),
            'prompt': f'{self.menu.wrap_str("ws")} {self.menu.conf("prompt")}',
        }
        workspace_name = subprocess.run(
            self.menu.args(menu_params),
            stdout=subprocess.PIPE,
            input=bytes('\n'.join(wslist), 'UTF-8'),
            check=False
        ).stdout

        selected_ws = workspace_name.decode('UTF-8').strip()
        return str(wslist.index(selected_ws) + 1) + ' :: ' + selected_ws

    @staticmethod
    def apply_to_ws(ws_func: Callable) -> None:
        """ Partial apply function to workspace.
        """
        ws_func()

    def goto_ws(self, use_wslist: bool = True) -> None:
        """ Go to workspace menu.
        """
        workspace_name = self.select_ws(use_wslist)
        if workspace_name is not None and workspace_name:
            self.apply_to_ws(
                partial(self.menu.i3ipc.command, f'workspace {workspace_name}')
            )

    def move_to_ws(self, use_wslist: bool = True) -> None:
        """ Move current window to the selected workspace
        """
        workspace_name = self.select_ws(use_wslist)
        if workspace_name is not None and workspace_name:
            self.apply_to_ws(
                partial(self.menu.i3ipc.command,
                        f'[con_id=__focused__] \
                        move to workspace {workspace_name}')
            )


==> menu_mods/xprop.py <==
import subprocess
from misc import Misc


class xprop():
    """ Setup screen resolution """
    def __init__(self, menu):
        self.menu = menu

    def xprop(self) -> None:
        """ Menu to show X11 atom attributes for current window.
        """
        xprops = []
        target_win = self.menu.i3ipc.get_tree().find_focused()
        try:
            xprop_ret = subprocess.run(
                ['xprop', '-id', str(target_win.window)] +
                self.menu.xprops_list,
                stdout=subprocess.PIPE,
                check=True
            ).stdout
            if xprop_ret is not None:
                xprop_ret = xprop_ret.decode().split('\n')
                for line in xprop_ret:
                    if 'not found' not in line:
                        xprops.append(line)
        except subprocess.CalledProcessError as proc_err:
            Misc.print_run_exception_info(proc_err)

        menu_params = {
            'cnum': 1,
            'lnum': len(xprops),
            'width': int(self.menu.screen_width * 0.75),
            'prompt':
                f'{self.menu.wrap_str("xprop")} {self.menu.conf("prompt")}'
        }

        ret = ""

        try:
            xprop_sel = subprocess.run(
                self.menu.args(menu_params),
                stdout=subprocess.PIPE,
                input=bytes('\n'.join(xprops), 'UTF-8'),
                check=True
            ).stdout

            if xprop_sel is not None:
                ret = xprop_sel.decode('UTF-8').strip()
        except subprocess.CalledProcessError as proc_err:
            Misc.print_run_exception_info(proc_err)

        # Copy to the clipboard
        if ret is not None and ret != '':
            try:
                subprocess.run(
                    ['xsel', '-i'],
                    input=bytes(ret.strip(), 'UTF-8'),
                    check=True
                )
            except subprocess.CalledProcessError as proc_err:
                Misc.print_run_exception_info(proc_err)

==> menu_mods/xrandr.py <==
import subprocess


class xrandr():
    def __init__(self, menu):
        self.menu = menu

    def change_resolution_xrandr(self):
        from display import Display
        xrandr_data = Display.xrandr_resolution_list()
        menu_params = {
            'cnum': 8,
            'width': int(Display.get_screen_resolution()["width"] * 0.55),
            'prompt': f'{self.menu.wrap_str("gtk_theme")} \
            {self.menu.conf("prompt")}',
        }
        resolution_sel = subprocess.run(
            self.menu.args(menu_params),
            stdout=subprocess.PIPE,
            input=bytes('\n'.join(xrandr_data), 'UTF-8'),
            check=False
        ).stdout

        if resolution_sel is not None:
            ret = resolution_sel.decode('UTF-8').strip()

        ret_list = []
        if ret and 'x' in ret:
            size_pair = ret.split(':')
            size_id = size_pair[0]
            res_str = size_pair[1:][0].strip()
            ret_list = res_str.split('x')

        width, height = ret_list[0].strip(), ret_list[1].strip()

        print(f'Set size to {width}x{height}')
        Display.set_screen_size(size_id)
 

x

all_cfg_dir(raw, dl)

  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
==> bscratch.cfg <==
[im]
class = [ "ViberPC", "VK", "zoom", "IGdm"]
class_r = [ "[Tt]elegram.*", "[Ss]kype.*",]
geom = "548x1165+1368+3"

[ncmpcpp]
instance = [ "ncmpcpp", "Tauon Music Box"]
geom = "1381x673+276+326"
spawn = "ncmpcpp"

[ncmpcpp_fun]
class = [ "cool-retro-term",]
geom = "1389x696+276+326"
prog = "cool-retro-term --program ncmpcpp"

[mutt]
class = ["mutterfox"]
instance = [ "mutt", "mutterfox"]
geom = "1835x1114+52+0"
spawn = "mutt"

[ranger]
instance = [ "ranger",]
geom = "1916x816+3+1"
spawn = "ranger"

[teardrop]
instance = [ "teardrop",]
geom = "1844x704+39+4"
spawn = "teardrop"

[volcontrol]
class = [ "Pavucontrol",]
geom = "895x314+1023+824"
prog = "pavucontrol"

[youtube]
instance = [ "youtube-get",]
geom = "1339x866+247+13"
spawn = "youtube-get"

[webcam]
instance = [ "webcam",]
geom = "1234x771+667+363"
prog = "~/bin/webcam"

[im.tel]
prog = "proxychains telegram-desktop"
class = [ "TelegramDesktop", "Telegram-desktop", "telegram-desktop",]

[im.skype]
prog = "skypeforlinux"
class = [ "skype", "Skype",]

[discord]
prog = "discord"
class = ['discord',]
geom = "944x1036+974+127"

[transients]
match_all = [ "True" ]
geom = "1844x704+39+4"

==> circle.cfg <==
[web]
class = [ "firefox", "Waterfox", "Tor Browser", "Chromium"]
priority = "firefox"
prog = "firefox"

[web.firefox]
class = [ "Firefox", "Nightly", "Navigator"]
prog = "firefox"

[web.tor]
class = [ "Tor Browser",]
prog = "tor-browser rutracker.org"

[web.yandex]
class = [ "Yandex-browser-beta",]
prog = "yandex-browser-beta"

[vid]
class = [ "mpv", "mplayer", "mplayer2", "Vlc",]
prog = "~/bin/pl rofi 1st_level ~/vid/new"

[lutris]
class = [ "Wine", "Lutris",]
prog = "lutris"

[steam]
class = [ "Steam" ]
prog = "steam"

[doc]
class = [ "Zathura", "cr3", ]

[vm]
class = [ "vmware", "spicy",]
class_r = [ "^[Qq]emu-.*$", ]

[term]
class = [ "term",]
instance = [ "term",]
spawn = "term"

[nwim]
class = [ "nwim"]
instance = [ "nwim"]
spawn = "nwim"

[obs]
class = [ "obs", ]
prog = "obs"

[remote]
class = [ "xfreerdp", "reminna", "org.remmina.Remmina", ]

[sxiv]
class = [ "Sxiv",]
prog = "dash -c 'exec find ~/dw/ ~/tmp/shots/ -maxdepth 1 -type d -print0 |xargs -0 ~/bin/sx'"

[sxiv.wallpaper]
class = [ "Sxiv",]
prog = "~/bin/wl --show"

[looking_glass]
class = [ "looking-glass-client",]
prog = "pgrep qemu && looking-glass-client -F -o opengl:vsync=0 -k"

[bitwig]
class = [ "Bitwig Studio",]
prog = "bitwig-studio"

==> executor.cfg <==
[term]
class="term"
font="Iosevka"
font_size=18
colorscheme='dark3'

[ranger]
class="ranger"
font="Iosevka"
font_size=18
postfix='-n ranger ranger\; set status off'
colorscheme='dark3'

[teardrop]
class="teardrop"
font="Iosevka Term"
font_size=18
postfix='-n mixer ncpamixer \; neww -n atop atop \; neww -n stig stig \; neww -n tasksh tasksh \; select-window -t 2'
colorscheme='dark3'

[ncmpcpp]
class="ncmpcpp"
font="Iosevka"
font_size=18
run_tmux=0
prog='ncmpcpp'
x_padding=4
y_padding=4
colorscheme='dark3'

[mutt]
class="mutt"
font="Iosevka"
font_size=18
run_tmux=0
prog_detach='neomutt'
colorscheme='dark3'

[nwim]
class="nwim"
font="Iosevka"
font_size=15.5
font_style_normal="Medium"
run_tmux=0
prog='NVIM_LISTEN_ADDRESS=/tmp/nvimsocket nvim'
colorscheme='dark3'

[youtube-get]
class="youtube-get"
font="Iosevka"
font_size=15.5
font_style_normal="Medium"
run_tmux=0
prog='/home/neg/bin/scripts/yr'
colorscheme='dark3'

==> fs.cfg <==
panel_classes = ["polybar"]
ws_fullscreen = ["gfx", "pic", "steam"]
classes_to_hide_panel = ["mpv", "Sxiv", "Steam", "vaapi", "vdpau", "gl"]

==> menu.cfg <==
modules = ['i3menu', 'winact', 'pulse_menu', 'xprop', 'props', 'gnome', 'xrandr',]

i3cmd = "i3-msg"
host = "::"
port = 31888

workspaces = [" α:term", " β:web", " γ:doc", " δ:dev", " ε:gfx", " ζ:draw", " η:sys", " θ:ide", " ι:steam", " κ:torrent", " λ:vm", " μ:wine", " ν:spotify", " ξ:pic", " ο:remote", " ο:sound"]

# Window properties shown by xprop menu.
xprops_list = [
    "WM_CLASS",
    "WM_NAME",
    "WM_WINDOW_ROLE",
    "WM_TRANSIENT_FOR",
    "_NET_WM_WINDOW_TYPE",
    "_NET_WM_STATE",
    "_NET_WM_PID"
]

rules_xprop = [
    "WM_CLASS",
    "WM_WINDOW_ROLE",
    "WM_NAME",
    "_NET_WM_NAME"
]

font = "Iosevka Medium"
font_size = "15"
location = "south"
anchor = "south"
matching = "fuzzy"

prompt = "〉〉"
left_bracket = '⟬'
right_bracket = '⟭'
gap = '38'
use_default_width = '1920'

==> negi3mods.cfg <==
notification_color_field="\\*.color4"
prefix="〉〉"
module_list=["fs", "executor", "win_action", "circle", "win_history", "menu", "bscratch", "vol"]
port='15555'

==> polybar_vol.cfg <==
mpdaddr = '::1'
mpdport = "6600"
bufsize = 1024

delims = [" || ", "%{{O12}} ⃦%{{O8}}", " ║ "]
delimiter = "%{T3}%{F#005f87} ╲ %{F-}%{T-}"
show_volume = 'y'
vol_prefix = "Vol:"
vol_suffix = ""

bracket_color_field = '\\*.color4'
bright_color_field = 'polybar.light'
foreground_color_field = 'polybar.light'

==> polybar_ws.cfg <==
ws_color_field = "polybar.ws_color"
binding_color_field = "polybar.mode_color"

==> vol.cfg <==
mpd_inc = 1
mpd_addr = "::1"
mpd_port = "6600"
mpv_socket = "/tmp/mpv.socket"
use_mpv09 = 1
mpd_buf_size = 1024

==> win_action.cfg <==
cache_list_size = 10
quad_use_gaps = 1
x2_use_gaps = 1
grow_coeff = 1.01
shrink_coeff = 0.99

[useless_gaps]
w = 12
a = 12
s = 12
d = 12


==> win_history.cfg <==
autoback = ["pic", "gfx", "vm", ]
 

x

all_proc_dir(raw, dl)

  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
==> polybar_vol.py <==
#!/usr/bin/pypy3 -u

""" Volume printing daemon.

This daemon prints current MPD volume like `tail -f` echo server, so there is
no need to use busy waiting to extract information from it.

Usage:
    ./polybar_vol.py

Suppoused to be used inside polybar.

Config example:

[module/volume]
type = custom/script
interval = 0
exec = ~/.config/i3/proc/polybar_vol.py
exec-if = sleep 1
tail = true

Also you need to use unbuffered output for polybar, otherwise you will see no
output at all. I've considered that pypy3 is better choise here, because of
this application run pretty long time to get advantages of JIT compilation.

Created by :: Neg
email :: <serg.zorg@gmail.com>
github :: https://github.com/neg-serg?tab=repositories
year :: 2020

"""

import asyncio
import sys

from lib.standalone_cfg import modconfig
from lib.misc import Misc


class polybar_vol(modconfig):
    def __init__(self):
        self.loop = asyncio.get_event_loop()

        # Initialize modcfg.
        modconfig.__init__(self, self.loop)

        # default MPD address
        self.addr = self.conf("mpdaddr")

        # default MPD port
        self.port = self.conf("mpdport")

        # buffer size
        self.buf_size = self.conf("bufsize")

        # output string
        self.volume = ""

        # command to wait for mixer or player events from MPD
        self.idle_mixer = "idle mixer player\n"

        # command to get status from MPD
        self.status_cmd_str = "status\n"

        # various MPD // Volume printer delimiters
        self.delimiter = self.conf("delimiter")

        # volume prefix and suffix
        self.vol_prefix = self.conf("vol_prefix")
        self.vol_suffix = self.conf("vol_suffix")

        # xrdb-colors: use blue by default for brackets
        self.bracket_color_field = self.conf("bracket_color_field")
        self.bright_color_field = self.conf("bright_color_field")
        self.foreground_color_field = self.conf("foreground_color_field")

        self.bracket_color = Misc.extract_xrdb_value(self.bracket_color_field)
        self.bright_color = Misc.extract_xrdb_value(self.bright_color_field)
        self.foreground_color = Misc.extract_xrdb_value(
            self.foreground_color_field
        )

        self.right_bracket = ""

        # set string for the empty output
        if self.conf('show_volume').startswith('y'):
            self.empty_str = f"%{{F{self.bracket_color}}}{self.delimiter}" + \
                f"%{{F{self.bright_color}}}" + \
                f"{self.vol_prefix}%{{F{self.foreground_color}}}n/a%{{F-}}" + \
                f" %{{F{self.bracket_color}}}{self.right_bracket}%{{F-}}"
        else:
            self.empty_str = f" %{{F{self.bracket_color}}}" + \
                f"{self.right_bracket}%{{F-}}"

        # run mainloop
        self.main()

    def main(self):
        """ Mainloop starting here.
        """
        asyncio.set_event_loop(self.loop)
        try:
            self.loop.run_until_complete(
                self.update_mpd_volume(self.loop)
            )
        except ConnectionError:
            self.empty_output()
        finally:
            self.loop.close()

    def print_volume(self):
        """ Create nice and shiny output for polybar.
        """
        return f'%{{F{self.bracket_color}}}{self.delimiter}%{{F-}}' + \
            f'%{{F{self.foreground_color}}}' + \
            f'{self.vol_prefix}{self.volume}{self.vol_suffix}%{{F-}}' + \
            f'%{{F{self.bracket_color}}}{self.right_bracket}%{{F-}}'

    def empty_output(self):
        """ This output will be used if no information about volume.
        """
        sys.stdout.write(f'{self.empty_str}\n')

    async def initial_mpd_volume(self, reader, writer):
        """ Load MPD volume state when script started.
        """
        mpd_stopped = None

        data = await reader.read(self.buf_size)
        writer.write(self.status_cmd_str.encode(encoding='utf-8'))
        stat_data = await reader.read(self.buf_size)
        parsed = stat_data.decode('utf-8').split('\n')
        if 'volume' in parsed[0]:
            self.volume = parsed[0][8:]
            if int(self.volume) >= 0:
                self.volume = self.print_volume()
                sys.stdout.write(f"{self.volume}\n")
            else:
                sys.stdout.write(f" \n")
        else:
            for token in parsed:
                if token == 'state: stop':
                    mpd_stopped = True
                    break
            if mpd_stopped:
                print()
            else:
                print(self.empty_str)
        return data.startswith(b'OK')

    async def update_mpd_volume(self, loop):
        """ Update MPD volume here and print it.
        """
        prev_volume = ''
        reader, writer = await asyncio.open_connection(
            host=self.addr, port=self.port, loop=loop
        )
        if await self.initial_mpd_volume(reader, writer):
            while True:
                writer.write(self.idle_mixer.encode(encoding='utf-8'))
                data = await reader.read(self.buf_size)
                if data.decode('utf-8'):
                    writer.write(self.status_cmd_str.encode(encoding='utf-8'))
                    stat_data = await reader.read(self.buf_size)
                    parsed = stat_data.decode('utf-8').split('\n')
                    if 'state: play' in parsed and 'volume' in parsed[0]:
                        self.volume = parsed[0][8:]
                        if int(self.volume) >= 0:
                            if prev_volume != self.volume:
                                self.volume = self.print_volume()
                                sys.stdout.write(f"{self.volume}\n")
                            prev_volume = parsed[0][8:]
                    else:
                        prev_volume = ''
                        writer.close()
                        self.empty_output()
                        return
                else:
                    prev_volume = ''
                    writer.close()
                    self.empty_output()
                    return


if __name__ == '__main__':
    polybar_vol()

==> polybar_ws.py <==
#!/usr/bin/python3

""" Current workspace printing daemon.

This daemon prints current i3 workspace. You need this because of bugs inside
of polybar's i3 current workspace implementation: you will get race condition
leading to i3 wm dead-lock.

Also it print current keybinding mode.

Usage:
    ./polybar_ws.py

Suppoused to be used inside polybar.

Config example:

[module/ws]
type = custom/script
exec = ~/.config/i3/proc/polybar_ws.py
exec-if = sleep 1
format = <label>
tail = true

Also you need to use unbuffered output for polybar, otherwise you will see no
output at all. I've considered that pypy3 is better choise here, because of
this application run pretty long time to get advantages of JIT compilation.

Created by :: Neg
email :: <serg.zorg@gmail.com>
github :: https://github.com/neg-serg?tab=repositories
year :: 2020

"""

import sys
import re
import asyncio
import i3ipc
from i3ipc.aio import Connection

from lib.standalone_cfg import modconfig
from lib.misc import Misc


class polybar_ws(modconfig):
    def __init__(self):
        # initialize asyncio loop
        self.loop = asyncio.get_event_loop()

        # Initialize modcfg.
        modconfig.__init__(self, self.loop)

        self.conn = None

        self.ws_name = ""
        self.binding_mode = ""

        # regexes used to extract current binding mode.
        self.mode_regex = re.compile('.*mode ')
        self.split_by = re.compile('[;,]')

        self.ws_color_field = self.conf("ws_color_field")
        self.binding_color_field = self.conf("binding_color_field")
        self.ws_color = Misc.extract_xrdb_value(self.ws_color_field)
        self.binding_color = Misc.extract_xrdb_value(self.binding_color_field)
        self.ws_name = ""

    async def on_ws_focus(self, _, event):
        """ Get workspace name and throw event.
        """
        ws_name = event.current.name
        self.ws_name = ws_name.split(' :: ')[1:][0]
        await self.update_status()

    @staticmethod
    def colorize(s, color):
        return f"%{{T4}}%{{F{color}}}{s}%{{F-}}%{{T-}}"

    async def on_event(self, _, event):
        bind_cmd = event.binding.command
        for t in re.split(self.split_by, bind_cmd):
            if 'mode' in t:
                ret = re.sub(self.mode_regex, '', t)
                if ret[0] == ret[-1] and ret[0] in {'"', "'"}:
                    ret = ret[1:-1]
                    if ret == "default":
                        self.binding_mode = ''
                    else:
                        self.binding_mode = \
                            polybar_ws.colorize(
                                ret, color=self.binding_color
                            ) + ' '
                    await self.update_status()

    async def special_reload(self):
        """ Reload mainloop here.
        """
        asyncio.get_event_loop().close()
        self.loop = asyncio.get_event_loop()
        await self.main()

    async def main(self):
        """ Mainloop starting here.
        """
        asyncio.set_event_loop(self.loop)

        self.conn = await Connection(auto_reconnect=True).connect()
        self.conn.on(i3ipc.Event.WORKSPACE_FOCUS, self.on_ws_focus)
        self.conn.on(i3ipc.Event.BINDING, self.on_event)

        workspaces = await self.conn.get_workspaces()
        for ws in workspaces:
            if ws.focused:
                ws_name = ws.name
                self.ws_name = ws_name.split(' :: ')[1:][0]
                break

        await self.update_status()
        await self.conn.main()

    async def update_status(self):
        """ Print workspace information here. Event-based.
        """
        workspace = self.ws_name
        if not workspace[0].isalpha():
            workspace = polybar_ws.colorize(
                workspace[0], color=self.ws_color
            ) + workspace[1:]
        sys.stdout.write(f"{self.binding_mode + workspace}\n")
        await asyncio.sleep(0)


async def main():
    """ Start polybar_ws from here """
    await polybar_ws().main()


if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(main())
 

x

negi3mods_runner(raw, dl)

  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
#!/usr/bin/python3

""" i3 negi3mods daemon script.

This module loads all negi3mods an start it via main's manager
mailoop. Inotify-based watchers for all negi3mods TOML-based configuration
spawned here, to use it just start it from any place without parameters. Also
there is i3 config watcher to convert it from ppi3 format to plain i3
automatically. Moreover it contains pid-lock which prevents running several
times.

Usage:
    ./negi3mods.py [--debug|--tracemalloc|--start]

Options:
    --debug         disables signal handlers for debug.
    --tracemalloc   calculates and shows memory tracing with help of
                    tracemalloc.
    --start         make actions for the start, not reloading

Created by :: Neg
email :: <serg.zorg@gmail.com>
github :: https://github.com/neg-serg?tab=repositories
year :: 2020

"""

import os
import timeit
import atexit
import sys
import subprocess
import signal
import functools
import importlib
import shutil
from threading import Thread

for m in ["inotipy", "i3ipc", "docopt", "pulsectl",
          "toml", "Xlib", "yaml", "yamlloader", "ewmh"]:
    if not importlib.util.find_spec(m):
        print("Cannot import [{m}], please install")

import asyncio
import inotipy

import i3ipc
from docopt import docopt

from lib.locker import get_lock
from lib.msgbroker import MsgBroker
from lib.misc import Misc
from lib.standalone_cfg import modconfig


class negi3mods(modconfig):
    def __init__(self, cmd_args):
        """ Init function

            Using of self.intern for better performance, create i3ipc
            connection, connects to the asyncio eventloop.
        """

        # i3 path used to get i3 config path for "send" binary, _config needed
        # by ppi3 path and another configs.
        self.i3_path = Misc.i3path()
        loop = asyncio.new_event_loop()

        self.tracemalloc_enabled = False

        if cmd_args["--tracemalloc"]:
            self.tracemalloc_enabled = True
            import tracemalloc

        if self.tracemalloc_enabled:
            tracemalloc.start()

        self.first_run = False
        if cmd_args["--start"]:
            self.first_run = True

        if not (cmd_args['--debug'] or self.tracemalloc_enabled):
            def loop_exit(signame):
                print(f"Got signal {signame}: exit")
                loop.stop()
                os._exit(0)

            for signame in {'SIGINT', 'SIGTERM'}:
                loop.add_signal_handler(
                    getattr(signal, signame),
                    functools.partial(loop_exit, signame))
            loop.set_exception_handler(None)

        modconfig.__init__(self, loop)

        self.loop = loop

        self.mods = {}
        for mod in self.conf("module_list"):
            self.mods[sys.intern(mod)] = None

        self.prepare_notification_text()

        # i3 path used to get "send" binary path
        self.i3_cfg_path = self.i3_path + '/cfg/'

        # test config to check ppi3 conversion result
        self.test_cfg_path = os.path.realpath(
            os.path.expandvars('$HOME/tmp/config_test')
        )

        self.port = int(self.conf('port'))

        self.echo = Misc.echo_off
        self.notify = Misc.notify_off

        # main i3ipc connection created here and can be bypassed to the most of
        # modules here.
        self.i3 = i3ipc.Connection()

    def prepare_notification_text(self):
        """ stuff for startup notifications """
        self.notification_text = "Starting negi3mods\n\n"
        notification_color_field = self.conf("notification_color_field")
        notification_color = Misc.extract_xrdb_value(notification_color_field)
        prefix = self.conf("prefix")
        self.msg_prefix = f"<span weight='normal' \
            color='{notification_color}'> {prefix} </span>"

    def load_modules(self):
        """ Load modules.

            This function init MsgBroker, use importlib to load all the
            stuff, then add_ipc and update notification with startup
            benchmarks.
        """
        mod_startup_times = []
        self.echo('Loading modules')
        for mod in self.mods:
            start_time = timeit.default_timer()
            i3mod = importlib.import_module('lib.' + mod)
            self.mods[mod] = getattr(i3mod, mod)(self.i3, loop=self.loop)
            mod_startup_times.append(timeit.default_timer() - start_time)
            time_elapsed = f'{mod_startup_times[-1]:4f}s'
            mod_loaded_info = f'{mod:<10s} ~ {time_elapsed:>10s}'
            self.notification_text += self.msg_prefix + mod_loaded_info + '\n'
            self.echo(mod_loaded_info, flush=True)
        loading_time_msg = f'Loading time = {sum(mod_startup_times):6f}s'
        self.notification_text += loading_time_msg
        self.echo(loading_time_msg)

    def mods_cfg_watcher(self):
        """ cfg watcher to update modules config in realtime.
        """
        watcher = inotipy.Watcher.create()
        watcher.watch(self.i3_cfg_path, inotipy.IN.MODIFY)
        return watcher

    def autostart(self):
        """ Autostart auto negi3mods initialization """
        if self.first_run:
            try:
                subprocess.run(
                    [self.i3_path + 'send', 'circle', 'next', 'term'],
                    check=True
                )
            except subprocess.CalledProcessError as proc_err:
                Misc.print_run_exception_info(proc_err)

    def i3_config_watcher(self):
        """ i3 config watcher to run ppi3 on write.
        """
        watcher = inotipy.Watcher.create()
        watcher.watch(self.i3_path, inotipy.IN.CLOSE_WRITE)
        return watcher

    async def mods_cfg_worker(self, watcher, reload_one=True):
        """ Reloading configs on change. Reload only appropriate config by
            default.

            Args:
                watcher: watcher for cfg.
        """
        while True:
            event = await watcher.get()
            print(event)
            changed_mod = event.pathname[:-4]
            if changed_mod in self.mods:
                if reload_one:
                    try:
                        subprocess.run(
                            [self.i3_path + 'send', changed_mod, 'reload'],
                            check=True
                        )
                        self.notify(f'[Reloaded {changed_mod}]')
                    except subprocess.CalledProcessError as proc_err:
                        Misc.print_run_exception_info(proc_err)
                else:
                    for mod in self.mods:
                        try:
                            subprocess.run(
                                [self.i3_path + 'send', mod, 'reload'],
                                check=True
                            )
                        except subprocess.CalledProcessError as proc_err:
                            Misc.print_run_exception_info(proc_err)
                    self.notify(
                        '[Reloaded {' + ','.join(self.mods.keys()) + '}]'
                    )
        watcher.close()

    async def i3_config_worker(self, watcher):
        """ Run ppi3 when config is changed

            Args:
                watcher: watcher for i3 config.
        """
        while True:
            event = await watcher.get()
            if event.pathname == '_config':
                with open(self.test_cfg_path, "w") as fconf:
                    try:
                        subprocess.run(
                            ['ppi3', self.i3_path + '_config'],
                            stdout=fconf,
                            check=True
                        )
                        config_is_valid = self.validate_i3_config()
                    except subprocess.CalledProcessError as proc_err:
                        Misc.print_run_exception_info(proc_err)
                if config_is_valid:
                    self.echo("i3 config is valid!")
                    shutil.move(self.test_cfg_path, self.i3_path + 'config')

    def validate_i3_config(self):
        """ Checks that i3 config is ok.
        """
        try:
            check_config = subprocess.run(
                ['i3', '-c', self.test_cfg_path, '-C'],
                stdout=subprocess.PIPE,
                check=True
            ).stdout.decode('utf-8')
        except subprocess.CalledProcessError as proc_err:
            Misc.print_run_exception_info(proc_err)
        if check_config:
            error_data = check_config.encode('utf-8')
            self.echo(error_data)
            self.notify(error_data, "Error >")

            # remove invalid config
            os.remove(self.test_cfg_path)

            return False
        return True

    def run_config_watchers(self):
        """ Start all watchers here via ensure_future to run it in background.
        """
        asyncio.ensure_future(self.mods_cfg_worker(self.mods_cfg_watcher()))
        asyncio.ensure_future(self.i3_config_worker(self.i3_config_watcher()))

    def run(self):
        """ Run negi3mods here.
        """
        def start(func, args=None):
            """ Helper for pretty-printing of loading process.

                Args:
                    func (callable): callable routine to run.
                    name: routine name.
                    args: routine args, optional.
            """
            if args is None:
                func()
            elif args is not None:
                func(*args)

        start(self.load_modules)
        start(self.run_config_watchers)

        # Start modules mainloop.
        mainloop = Thread(
            target=MsgBroker.mainloop,
            args=(self.loop, self.mods, self.port,),
            daemon=True
        )
        start((mainloop).start)

        self.echo('... everything loaded ...')
        self.notify(self.notification_text)
        try:
            self.autostart()
            self.i3.main()
        except KeyboardInterrupt:
            self.i3.main_quit()
        self.echo('... exit ...')


def main():
    """ Run negi3mods from here """
    get_lock(os.path.basename(__file__))

    # We need it because of thread_wait on Ctrl-C.
    atexit.register(lambda: os._exit(0))

    cmd_args = docopt(__doc__, version='0.8')

    negi3mods_instance = negi3mods(cmd_args)
    negi3mods_instance.run()

    if negi3mods_instance.tracemalloc_enabled:
        snapshot = tracemalloc.take_snapshot()
        top_stats = snapshot.statistics('lineno')

        print("[ Top 10 ]")
        for stat in top_stats[:10]:
            print(stat)


if __name__ == '__main__':
    main()
 

x

ipc_to_socket_echo(raw, dl)

 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
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <bsd/string.h>

#define PORT 15555

int main(int argc, char const *argv[]) {
    struct sockaddr_in serv_addr;

    int sock = 0;

    char *cmd = calloc(1024, 1);
    for (int i = 1; i < argc; i++) {
        strlcat(cmd, argv[i], 128);
        strlcat(cmd, " ", 128);
    }
    strlcat(cmd, "\n", 128);

    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        puts("\n Socket creation error \n");
        free(cmd);
        return -1;
    }

    memset(&serv_addr, '0', sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) {
        printf("\nInvalid address/ Address not supported \n");
        free(cmd);
        return -1;
    }


    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        free(cmd);
        return -1;
    }

    if (send(sock, cmd, strnlen(cmd, 1024), 0) <= 0) {
        printf("Send failed");
    }

    free(cmd);
    return 0;
}
 

x

i3_prepare(raw, dl)

1
2
3
4
5
#!/bin/zsh
${XDG_CONFIG_HOME}/i3/negi3mods.py --start >> ${HOME}/tmp/negi3mods.log 2>&1 &
make -C ${XDG_CONFIG_HOME}/i3/ &
pkill -f 'mpc idle'
pkill sxhkd; sxhkd &
 

x

main_lib_dir_pt_1(raw, dl)

   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
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
==> bscratch.py <==
""" Named scratchpad i3 module

This is a module about ion3/notion-like named scratchpad implementation.
You can think about it as floating "tabs" for windows, which can be
shown/hidden by request, with next "tab" navigation.

The foundation of it is a i3 mark function, you can create a mark with
tag+'-'+uuid format. And then this imformation used to performs all
actions.

Also I've hacked fullscreen behaviour for it, so you can always get
your scratchpad from fullscreen and also restore fullsreen state of the
window when needed.
"""

import uuid
from typing import List, Callable, Set, Optional

import geom
from cfg import cfg
from matcher import Matcher
from misc import Misc
from negewmh import NegEWMH
from negi3mod import negi3mod


class bscratch(negi3mod, cfg, Matcher):
    """ Named scratchpad class

    Parents:
        cfg: configuration manager to autosave/autoload
             TOML-configutation with inotify
        Matcher: class to check that window can be tagged with given tag by
                 WM_CLASS, WM_INSTANCE, regexes, etc
    """

    def __init__(self, i3, loop=None) -> None:
        """ Init function

        Args:
            i3: i3ipc connection
            loop: asyncio loop. It's need to be given as parameter because of
                  you need to bypass asyncio-loop to the thread
        """
        # Initialize superclasses.
        cfg.__init__(self, i3, convert_me=True)
        Matcher.__init__(self)

        # Initialization

        # winlist is used to reduce calling i3.get_tree() too many times.
        self.win = None

        # fullscreen_list is used to perform fullscreen hacks
        self.fullscreen_list = []

        # nsgeom used to respect current screen resolution in the geometry
        # settings and scale it
        self.nsgeom = geom.geom(self.cfg)

        # marked used to get the list of current tagged windows
        # with the given tag
        self.marked = {l: [] for l in self.cfg}

        # Mark all tags from the start
        self.mark_all_tags(hide=True)

        # Do not autosave geometry by default
        self.auto_save_geom(False)

        # focus_win_flag is a helper to perform attach/detach window to the
        # named scratchpad with add_prop/del_prop routines
        self.focus_win_flag = [False, ""]

        # i3ipc connection, bypassed by negi3mods runner
        self.i3ipc = i3

        self.bindings = {
            "show": self.show_scratchpad,
            "hide": self.hide_scratchpad_all_but_current,
            "next": self.next_win_on_curr_tag,
            "toggle": self.toggle,
            "hide_current": self.hide_current,
            "geom_restore": self.geom_restore_current,
            "geom_dump": self.geom_dump_current,
            "geom_save": self.geom_save_current,
            "geom_autosave_mode": self.autosave_toggle,
            "subtag": self.run_subtag,
            "add_prop": self.add_prop,
            "del_prop": self.del_prop,
            "reload": self.reload_config,
            "dialog": self.dialog_toggle,
        }

        i3.on('window::new', self.mark_tag)
        i3.on('window::close', self.unmark_tag)

    def taglist(self) -> List:
        """ Returns list of tags without transients windows.
        """
        tag_list = list(self.cfg.keys())
        tag_list.remove('transients')
        return tag_list

    @staticmethod
    def mark_uuid_tag(tag: str) -> str:
        """ Generate unique mark for the given [tag]

            Args:
                tag: tag string
        """
        return f'mark {tag}-{str(str(uuid.uuid4().fields[-1]))}'

    def show_scratchpad(self, tag: str, hide: bool = True) -> None:
        """ Show given [tag]

            Args:
                tag: tag string
                hide: optional predicate to hide all windows except current.
                      Should be used in the most cases because of better
                      performance and visual neatness
        """
        win_to_focus = None
        for win in self.marked[tag]:
            win.command('move window to workspace current')
            win_to_focus = win
        if hide and tag != 'transients':
            self.hide_scratchpad_all_but_current(tag, win_to_focus)
        if win_to_focus is not None:
            win_to_focus.command('focus')

    def hide_scratchpad(self, tag: str) -> None:
        """ Hide given [tag]

            Args:
                tag (str): scratchpad name to hide
        """
        if self.geom_auto_save:
            self.geom_save(tag)
        for win in self.marked[tag]:
            win.command('move scratchpad')
        self.restore_fullscreens()

    def hide_scratchpad_all_but_current(self, tag: str, current_win) -> None:
        """ Hide all tagged windows except current.

            Args:
                tag: tag string
        """
        if len(self.marked[tag]) > 1 and current_win is not None:
            for win in self.marked[tag]:
                if win.id != current_win.id:
                    win.command('move scratchpad')
                else:
                    win.command('move window to workspace current')

    def find_visible_windows(
            self, focused: Optional[bool] = None) -> List:
        """ Find windows on the current workspace, which is enough for
        scratchpads.

            Args:
                focused: denotes that [focused] window should be extracted from
                         i3.get_tree() or not
        """
        if focused is None:
            focused = self.i3ipc.get_tree().find_focused()
        return NegEWMH.find_visible_windows(
            focused.workspace().leaves()
        )

    def dialog_toggle(self) -> None:
        """ Show dialog windows
        """
        self.show_scratchpad('transients', hide=False)

    def toggle_fs(self, win) -> None:
        """ Toggles fullscreen on/off and show/hide requested scratchpad after.

            Args:
                w : window that fullscreen state should be on/off.
        """
        if win.fullscreen_mode:
            win.command('fullscreen toggle')
            self.fullscreen_list.append(win)

    def toggle(self, tag: str) -> None:
        """ Toggle scratchpad with given [tag].

            Args:
                tag (str): denotes the target tag.
        """
        if not self.marked.get(tag, []):
            prog_str = self.extract_prog_str(self.conf(tag))
            if prog_str:
                self.i3ipc.command(f'exec {prog_str}')
            else:
                spawn_str = self.extract_prog_str(
                    self.conf(tag), "spawn", exe_file=False
                )
                if spawn_str:
                    self.i3ipc.command(
                        f'exec ~/.config/i3/send executor run {spawn_str}'
                    )

        if self.visible_window_with_tag(tag):
            self.hide_scratchpad(tag)
            return

        # We need to hide scratchpad it is visible,
        # regardless it focused or not
        focused = self.i3ipc.get_tree().find_focused()

        if self.marked.get(tag, []):
            self.toggle_fs(focused)
            self.show_scratchpad(tag)

    def focus_sub_tag(self, tag: str, subtag_classes_set: Set) -> None:
        """ Cycle over the subtag windows.

            Args:
                tag (str): denotes the target tag.
                subtag_classes_set (set): subset of classes of target [tag]
                                          which distinguish one subtag from
                                          another.
        """
        focused = self.i3ipc.get_tree().find_focused()
        self.toggle_fs(focused)

        if focused.window_class in subtag_classes_set:
            return

        self.show_scratchpad(tag)

        for _ in self.marked[tag]:
            if focused.window_class not in subtag_classes_set:
                self.next_win_on_curr_tag()
                focused = self.i3ipc.get_tree().find_focused()

    def run_subtag(self, tag: str, subtag: str) -> None:
        """ Run-or-focus the application for subtag

            Args:
                tag (str): denotes the target tag.
                subtag (str): denotes the target subtag.
        """
        if subtag in self.conf(tag):
            class_list = [win.window_class for win in self.marked[tag]]
            subtag_classes_set = self.conf(tag, subtag, "class")
            subtag_classes_matched = [
                w for w in class_list if w in subtag_classes_set
            ]
            if not subtag_classes_matched:
                prog_str = self.extract_prog_str(self.conf(tag, subtag))
                self.i3ipc.command(f'exec {prog_str}')
                self.focus_win_flag = [True, tag]
            else:
                self.focus_sub_tag(tag, subtag_classes_set)
        else:
            self.toggle(tag)

    def restore_fullscreens(self) -> None:
        """ Restore all fullscreen windows
        """
        for win in self.fullscreen_list:
            win.command('fullscreen toggle')
        self.fullscreen_list = []

    def visible_window_with_tag(self, tag: str) -> bool:
        """ Counts visible windows for given tag

            Args:
                tag (str): denotes the target tag.
        """
        for win in self.find_visible_windows():
            for i in self.marked[tag]:
                if win.id == i.id:
                    return True
        return False

    def get_current_tag(self, focused) -> str:
        """ Get the current tag

            This function use focused window to determine the current tag.

            Args:
                focused : focused window.
        """
        for tag in self.cfg:
            for i in self.marked[tag]:
                if focused.id == i.id:
                    return tag

        return ''

    def apply_to_current_tag(self, func: Callable) -> bool:
        """ Apply function [func] to the current tag

            This is the generic function used in next_win_on_curr_tag,
            hide_current and another to perform actions on the currently
            selected tag.

            Args:
                func(Callable) : function to apply.
        """
        curr_tag = self.get_current_tag(self.i3ipc.get_tree().find_focused())
        if curr_tag:
            func(curr_tag)
        return bool(curr_tag)

    def next_win_on_curr_tag(self, hide: bool = True) -> None:
        """ Show the next window for the currently selected tag.

            Args:
                hide (bool): hide window or not. Primarly used to cleanup
                             "garbage" that can appear after i3 (re)start, etc.
                             Because of I've think that is't better to make
                             screen clear after (re)start.
        """
        def next_win(tag: str) -> None:
            self.show_scratchpad(tag, hide_)
            for idx, win in enumerate(self.marked[tag]):
                if focused_win.id != win.id:
                    self.marked[tag][idx].command(
                        'move window to workspace current'
                    )
                    self.marked[tag].insert(
                        len(self.marked[tag]),
                        self.marked[tag].pop(idx)
                    )
                    win.command('move scratchpad')
            self.show_scratchpad(tag, hide_)

        hide_ = hide
        focused_win = self.i3ipc.get_tree().find_focused()
        self.apply_to_current_tag(next_win)

    def hide_current(self) -> None:
        """ Hide the currently selected tag.
        """
        self.apply_to_current_tag(self.hide_scratchpad)

    def geom_restore(self, tag: str) -> None:
        """ Restore default window geometry

            Args:
                tag(str) : hide another windows for the current tag or not.
        """
        for idx, win in enumerate(self.marked[tag]):
            # delete previous mark
            del self.marked[tag][idx]

            # then make a new mark and move scratchpad
            win_cmd = f"{bscratch.mark_uuid_tag(tag)}, \
                move scratchpad, {self.nsgeom.get_geom(tag)}"
            win.command(win_cmd)
            self.marked[tag].append(win)

    def geom_restore_current(self) -> None:
        """ Restore geometry for the current selected tag.
        """
        self.apply_to_current_tag(self.geom_restore)

    def geom_dump(self, tag: str) -> None:
        """ Dump geometry for the given tag

            Args:
                tag(str) : denotes target tag.
        """
        focused = self.i3ipc.get_tree().find_focused()
        for win in self.marked[tag]:
            if win.id == focused.id:
                self.conf[tag]["geom"] = f"{focused.rect.width}x" + \
                    f"{focused.rect.height}+{focused.rect.x}+{focused.rect.y}"
                self.dump_config()
                break

    def geom_save(self, tag: str) -> None:
        """ Save geometry for the given tag

            Args:
                tag(str) : denotes target tag.
        """
        focused = self.i3ipc.get_tree().find_focused()
        for win in self.marked[tag]:
            if win.id == focused.id:
                self.conf[tag]["geom"] = f"{focused.rect.width}x\
                    {focused.rect.height}+{focused.rect.x}+{focused.rect.y}"
                if win.rect.x != focused.rect.x \
                    or win.rect.y != focused.rect.y \
                    or win.rect.width != focused.rect.width \
                        or win.rect.height != focused.rect.height:
                    self.nsgeom = geom.geom(self.cfg)
                    win.rect.x = focused.rect.x
                    win.rect.y = focused.rect.y
                    win.rect.width = focused.rect.width
                    win.rect.height = focused.rect.height
                break

    def auto_save_geom(self, save: bool = True,
                       with_notification: bool = False) -> None:
        """ Set geometry autosave option with optional notification.

            Args:
                save(bool): predicate that shows that want to enable/disable
                             autosave mode.
                with_notification(bool): to create notify-osd-based
                                         notification or not.
        """
        self.geom_auto_save = save
        if with_notification:
            Misc.notify_msg(f"geometry autosave={save}")

    def autosave_toggle(self) -> None:
        """ Toggle autosave mode.
        """
        if self.geom_auto_save:
            self.auto_save_geom(False, with_notification=True)
        else:
            self.auto_save_geom(True, with_notification=True)

    def geom_dump_current(self) -> None:
        """ Dump geometry for the current selected tag.
        """
        self.apply_to_current_tag(self.geom_dump)

    def geom_save_current(self) -> None:
        """ Save geometry for the current selected tag.
        """
        self.apply_to_current_tag(self.geom_save)

    def add_prop(self, tag_to_add: str, prop_str: str) -> None:
        """ Add property via [prop_str] to the target [tag].

            Args:
                tag_to_add (str): denotes the target tag.
                prop_str (str): string in i3-match format used to add/delete
                                target window in/from scratchpad.
        """
        if tag_to_add in self.cfg:
            self.add_props(tag_to_add, prop_str)

        for tag in self.cfg:
            if tag != tag_to_add:
                self.del_props(tag, prop_str)
                if self.marked[tag] != []:
                    for win in self.marked[tag]:
                        win.command('unmark')

        self.initialize(self.i3ipc)

    def del_prop(self, tag: str, prop_str: str) -> None:
        """ Delete property via [prop_str] to the target [tag].

            Args:
                tag (str): denotes the target tag.
                prop_str (str): string in i3-match format used to add/delete
                                target window in/from scratchpad.
        """
        self.del_props(tag, prop_str)

    def mark_tag(self, _, event) -> None:
        """ Add unique mark to the new window.

            Args:
                _: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        win = event.container
        is_dialog_win = NegEWMH.is_dialog_win(win)

        self.win = win
        for tag in self.cfg:
            if not is_dialog_win and tag != "transients":
                if self.match(win, tag):
                    # scratch_move
                    win.command(
                        f"{bscratch.mark_uuid_tag(tag)}, move scratchpad, \
                        {self.nsgeom.get_geom(tag)}")
                    self.marked[tag].append(win)
            elif is_dialog_win and tag == "transients":
                win.command(
                    f"{bscratch.mark_uuid_tag('transients')}, \
                    move scratchpad")
                self.marked["transients"].append(win)

        # Special hack to invalidate windows after subtag start
        if self.focus_win_flag[0]:
            special_tag = self.focus_win_flag[1]
            if special_tag in self.cfg:
                self.show_scratchpad(special_tag, hide=True)
            self.focus_win_flag[0] = False
            self.focus_win_flag[1] = ""

        self.dialog_toggle()

    def unmark_tag(self, _, event) -> None:
        """ Delete unique mark from the closed window.

            Args:
                _: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        win_ev = event.container
        self.win = win_ev
        for tag in self.taglist():
            for win in self.marked[tag]:
                if win.id == win_ev.id:
                    self.marked[tag].remove(win)
                    self.show_scratchpad(tag)
                    break

        if win_ev.fullscreen_mode:
            self.apply_to_current_tag(self.hide_scratchpad)

        for transient in self.marked["transients"]:
            if transient.id == win_ev.id:
                self.marked["transients"].remove(transient)

    def mark_all_tags(self, hide: bool = True) -> None:
        """ Add marks to the all tags.

            Args:
                hide (bool): hide window or not. Primarly used to cleanup
                             "garbage" that can appear after i3 (re)start, etc.
                             Because of I've think that is't better to make
                             screen clear after (re)start.
        """
        winlist = self.i3ipc.get_tree().leaves()
        hide_cmd = ''

        for win in winlist:
            is_dialog_win = NegEWMH.is_dialog_win(win)
            for tag in self.cfg:
                if not is_dialog_win and tag != "transients":
                    if self.match(win, tag):
                        if hide:
                            hide_cmd = '[con_id=__focused__] scratchpad show'
                        win_cmd = f"{bscratch.mark_uuid_tag(tag)}, \
                            move scratchpad, \
                            {self.nsgeom.get_geom(tag)}, {hide_cmd}"
                        win.command(win_cmd)
                        self.marked[tag].append(win)
                if is_dialog_win:
                    win_cmd = f"{bscratch.mark_uuid_tag('transients')}, \
                        move scratchpad"
                    win.command(win_cmd)
                    self.marked["transients"].append(win)
            self.win = win

==> cfg.py <==
""" Dynamic TOML-based config for negi3mods.

This is a superclass for negi3mods which want to store configuration via TOML
files. It supports inotify-based updating of self.cfg dynamically and has
pretty simple API. I've considered that inheritance here is good idea.
"""

import re
import os
import sys
from typing import Set, Callable
import traceback

import toml
from misc import Misc


class cfg(object):
    def __init__(self, i3, convert_me: bool = False, loop=None) -> None:
        # detect current negi3mod
        self.mod = self.__class__.__name__

        # negi3mod config path
        self.i3_cfg_mod_path = Misc.i3path() + '/cfg/' + self.mod + '.cfg'

        # convert config values or not
        self.convert_me = convert_me

        # load current config
        self.load_config()

        # used for props add / del hacks
        self.win_attrs = {}

        # bind numbers to cfg names
        self.conv_props = {
            'class': 'class',
            'instance': 'instance',
            'window_role': 'window_role',
            'title': 'name',
        }

        self.i3ipc = i3
        self.loop = None
        if loop is not None:
            self.loop = loop

    def conf(self, *conf_path):
        """ Helper to extract config for current tag.

        Args:
            conf_path: path of config from where extract.
        """
        ret = {}
        for part in conf_path:
            if not ret:
                ret = self.cfg.get(part)
            else:
                ret = ret.get(part)
        return ret

    @staticmethod
    def extract_prog_str(conf_part: str,
                         prog_field: str = "prog", exe_file: bool = True):
        """ Helper to extract prog(by default) string from config

        Args:
            conf_part (str): part of config from where you want to extract it.
            prog_field (str): string name to extract.
        """
        if conf_part is None:
            return ""

        if exe_file:
            return re.sub(
                "~",
                os.path.realpath(os.path.expandvars("$HOME")),
                conf_part.get(prog_field, "")
            )

        return conf_part.get(prog_field, "")

    @staticmethod
    def cfg_regex_props() -> Set[str]:
        """ Props with regexes """
        # regex cfg properties
        return {"class_r", "instance_r", "name_r", "role_r"}

    def win_all_props(self):
        """ All window props """
        # basic + regex props
        return self.cfg_props() | self.cfg_regex_props()

    @staticmethod
    def possible_props() -> Set[str]:
        """ Possible window props """
        # windows properties used for props add / del
        return {'class', 'instance', 'window_role', 'title'}

    @staticmethod
    def cfg_props() -> Set[str]:
        """ basic window props """
        # basic cfg properties, without regexes
        return {'class', 'instance', 'name', 'role'}

    @staticmethod
    def subtag_attr_list() -> Set[str]:
        """ Helper to create subtag attr list. """
        return cfg.possible_props()

    def reload_config(self, *arg) -> None:
        """ Reload config for current selected module.
            Call load_config, print debug messages and reinit all stuff.
        """
        prev_conf = self.cfg
        try:
            self.load_config()
            if self.loop is None:
                self.__init__(self.i3ipc)
            else:
                self.__init__(self.i3ipc, loop=self.loop)
            print(f"[{self.mod}] config reloaded")
        except Exception:
            print(f"[{self.mod}] config reload failed")
            traceback.print_exc(file=sys.stdout)
            self.cfg = prev_conf
            self.__init__()

    def dict_converse(self) -> None:
        """ Convert list attributes to set for the better performance.
        """
        self.dict_apply(lambda key: set(key), cfg.convert_subtag)

    def dict_deconverse(self) -> None:
        """ Convert set attributes to list, because of set cannot be saved
            / restored to / from TOML-files corretly.
        """
        self.dict_apply(lambda key: list(key), cfg.deconvert_subtag)

    @staticmethod
    def convert_subtag(subtag: str) -> None:
        """ Convert subtag attributes to set for the better performance.

            Args:
                subtag (str): target subtag.
        """
        cfg.subtag_apply(subtag, lambda key: set(key))

    @staticmethod
    def deconvert_subtag(subtag: str) -> None:
        """ Convert set attributes to list, because of set cannot be saved
        / restored to / from TOML-files corretly.

            Args:
                subtag (str): target subtag.
        """
        cfg.subtag_apply(subtag, lambda key: list(key))

    def dict_apply(self, field_conv: Callable, subtag_conv: Callable) -> None:
        """ Convert list attributes to set for the better performance.

            Args:
                field_conv (Callable): function to convert dict field.
                subtag_conv (Callable): function to convert subtag inside dict.
        """
        for string in self.cfg.values():
            for key in string:
                if key in self.win_all_props():
                    string[sys.intern(key)] = field_conv(
                        string[sys.intern(key)]
                    )
                elif key == "subtag":
                    subtag_conv(string[sys.intern(key)])

    @staticmethod
    def subtag_apply(subtag: str, field_conv: Callable) -> None:
        """ Convert subtag attributes to set for the better performance.

            Args:
                subtag (str): target subtag name.
                field_conv (Callable): function to convert dict field.
        """
        for val in subtag.values():
            for key in val:
                if key in cfg.subtag_attr_list():
                    val[sys.intern(key)] = field_conv(val[sys.intern(key)])

    def load_config(self) -> None:
        """ Reload config itself and convert lists in it to sets for the better
            performance.
        """
        with open(self.i3_cfg_mod_path, "r") as negi3modcfg:
            self.cfg = toml.load(negi3modcfg)
        if self.convert_me:
            self.dict_converse()

    def dump_config(self) -> None:
        """ Dump current config, can be used for debugging.
        """
        with open(self.i3_cfg_mod_path, "r+") as negi3modcfg:
            if self.convert_me:
                self.dict_deconverse()
            toml.dump(self.cfg, negi3modcfg)
            self.cfg = toml.load(negi3modcfg)
        if self.convert_me:
            self.dict_converse()

    def property_to_winattrib(self, prop_str: str) -> None:
        """ Parse property string to create win_attrs dict.
            Args:
                prop_str (str): property string in special format.
        """
        self.win_attrs = {}
        prop_str = prop_str[1:-1]
        for token in prop_str.split('@'):
            if token:
                toks = token.split('=')
                attr = toks[0]
                value = toks[1]
                if value[0] == value[-1] and value[0] in {'"', "'"}:
                    value = value[1:-1]
                if attr in cfg.subtag_attr_list():
                    self.win_attrs[self.conv_props.get(attr, {})] = value

    def add_props(self, tag: str, prop_str: str) -> None:
        """ Move window to some tag.
            Args:
                tag (str): target tag
                prop_str (str): property string in special format.
        """
        self.property_to_winattrib(prop_str)
        ftors = self.cfg_props() & set(self.win_attrs.keys())
        if tag in self.cfg:
            for tok in ftors:
                if self.win_attrs[tok] not in \
                        self.cfg.get(tag, {}).get(tok, {}):
                    if tok in self.cfg[tag]:
                        if isinstance(self.cfg[tag][tok], str):
                            self.cfg[tag][tok] = {self.win_attrs[tok]}
                        elif isinstance(self.cfg[tag][tok], set):
                            self.cfg[tag][tok].add(self.win_attrs[tok])
                    else:
                        self.cfg[tag].update({tok: self.win_attrs[tok]})
                    # special fix for the case where attr
                    # is just attr not {attr}
                    if isinstance(self.conf(tag, tok), str):
                        self.cfg[tag][tok] = {self.win_attrs[tok]}

    def del_direct_props(self, target_tag: str) -> None:
        """ Remove basic(non-regex) properties of window from target tag.
            Args:
                tag (str): target tag
        """
        # Delete 'direct' props:
        for prop in self.cfg[target_tag].copy():
            if prop in self.cfg_props():
                if isinstance(self.conf(target_tag, prop), str):
                    del self.cfg[target_tag][prop]
                elif isinstance(self.conf(target_tag, prop), set):
                    for tok in self.cfg[target_tag][prop].copy():
                        if self.win_attrs[prop] == tok:
                            self.cfg[target_tag][prop].remove(tok)

    def del_regex_props(self, target_tag: str) -> None:
        """ Remove regex properties of window from target tag.
            Args:
                target_tag (str): target tag
        """
        def check_for_win_attrs(win, prop):
            class_r_check = \
                (prop == "class_r" and winattr == win.window_class)
            instance_r_check = \
                (prop == "instance_r" and winattr == win.window_instance)
            role_r_check = \
                (prop == "role_r" and winattr == win.window_role)
            if class_r_check or instance_r_check or role_r_check:
                self.cfg[target_tag][prop].remove(target_tag)

        # Delete appropriate regexes
        for prop in self.cfg[target_tag].copy():
            if prop in self.cfg_regex_props():
                for reg in self.cfg[target_tag][prop].copy():
                    if prop == "class_r":
                        lst_by_reg = self.i3ipc.get_tree().find_classed(reg)
                    if prop == "instance_r":
                        lst_by_reg = self.i3ipc.get_tree().find_instanced(reg)
                    if prop == "role_r":
                        lst_by_reg = self.i3ipc.get_tree().find_by_role(reg)
                    winattr = self.win_attrs[prop[:-2]]
                    for win in lst_by_reg:
                        check_for_win_attrs(win, prop)

    def del_props(self, tag: str, prop_str: str) -> None:
        """ Remove window from some tag.
            Args:
                tag (str): target tag
                prop_str (str): property string in special format.
        """
        self.property_to_winattrib(prop_str)
        self.del_direct_props(tag)
        self.del_regex_props(tag)

        # Cleanup
        for prop in self.cfg_regex_props() | self.cfg_props():
            if prop in self.conf(tag) and self.conf(tag, prop) == set():
                del self.cfg[tag][prop]

==> circle.py <==
""" Circle over windows module.

This is a module about better run-or-raise features like in ion3, stumpwm and
others. As the result user can get not only the usual run the appropriate
application if it is not started, but also create a list of application, which
I call "tag" and then switch to the next of it, instead of just simple focus.

The foundation of it is pretty complicated go_next function, which use counters
with incrementing of the current "position" of the window in the tag list over
the finite field. As the result you get circle over all tagged windows.

Also I've hacked fullscreen behaviour for it, so you can always switch to the
window with the correct fullscreen state, where normal i3 behaviour has a lot
of issues here in detection of existing/visible windows, etc.
"""

from negi3mod import negi3mod
from matcher import Matcher
from cfg import cfg


class circle(negi3mod, cfg, Matcher):
    """ Circle over windows class

    Parents:
        cfg: configuration manager to autosave/autoload
             TOML-configutation with inotify
        Matcher: class to check that window can be tagged with given tag by
                 WM_CLASS, WM_INSTANCE, regexes, etc

    """

    def __init__(self, i3, loop=None) -> None:
        """ Init function

        Main part is in self.initialize, which performs initialization itself.

        Args:
            i3: i3ipc connection
            loop: asyncio loop. It's need to be given as parameter because of
                  you need to bypass asyncio-loop to the thread
        """
        # Initialize superclasses.
        cfg.__init__(self, i3, convert_me=True)
        Matcher.__init__(self)

        # i3ipc connection, bypassed by negi3mods runner.
        self.i3ipc = i3

        # map of tag to the tagged windows.
        self.tagged = {}

        # current_position for the tag [tag]
        self.current_position = {}

        # list of windows which fullscreen state need to be restored.
        self.restore_fullscreen = []

        # is the current action caused by user actions or not? It's needed for
        # corrent fullscreen on/off behaviour.
        self.interactive = True

        # how many attempts taken to find window with priority
        self.repeats = 0

        # win cache for the fast matching
        self.win = None

        # used for subtag info caching
        self.subtag_info = {}

        # Should the special fullscreen-related actions to be performed or not.
        self.need_handle_fullscreen = True

        # Initialize
        i3tree = self.i3ipc.get_tree()

        # prepare for prefullscreen
        self.fullscreened = i3tree.find_fullscreen()

        # store the current window here to cache get_tree().find_focused value.
        self.current_win = i3tree.find_focused()

        # winlist is used to reduce calling i3.get_tree() too many times.
        self.winlist = i3tree.leaves()
        for tag in self.cfg:
            self.tagged[tag] = []
            self.current_position[tag] = 0

        # tag all windows after start
        self.tag_windows(invalidate_winlist=False)

        self.bindings = {
            "next": self.go_next,
            "subtag": self.go_subtag,
            "add_prop": self.add_prop,
            "del_prop": self.del_prop,
            "reload": self.reload_config,
        }

        self.i3ipc.on('window::new', self.add_wins)
        self.i3ipc.on('window::close', self.del_wins)
        self.i3ipc.on("window::focus", self.set_curr_win)
        self.i3ipc.on("window::fullscreen_mode", self.handle_fullscreen)

    def run_prog(self, tag: str, subtag: str = '') -> None:
        """ Run the appropriate application for the current tag/subtag.

        Args:
            tag (str): denotes target [tag]
            subtag (str): denotes the target [subtag], optional.
        """
        if tag is not None and self.cfg.get(tag) is not None:
            if not subtag:
                prog_str = self.extract_prog_str(self.conf(tag))
            else:
                prog_str = self.extract_prog_str(
                    self.conf(tag, subtag)
                )
            if prog_str:
                self.i3ipc.command(f'exec {prog_str}')
            else:
                spawn_str = self.extract_prog_str(
                    self.conf(tag), "spawn", exe_file=False
                )
                if spawn_str:
                    self.i3ipc.command(
                        f'exec ~/.config/i3/send executor run {spawn_str}'
                    )

    def find_next_not_the_same_win(self, tag: str) -> None:
        """ It was used as the guard to infinite loop in the past.
        Args:
            tag (str): denotes target [tag]
        """
        if len(self.tagged[tag]) > 1:
            self.current_position[tag] += 1
            self.go_next(tag)

    def prefullscreen(self, tag: str) -> None:
        """ Prepare to go fullscreen.
        """
        for win in self.fullscreened:
            if self.current_win.window_class in set(self.conf(tag, "class")) \
                    and self.current_win.id == win.id:
                self.need_handle_fullscreen = False
                win.command('fullscreen disable')

    def postfullscreen(self, tag: str, idx: int) -> None:
        """ Exit from fullscreen.
        """
        now_focused = self.twin(tag, idx).id
        for win_id in self.restore_fullscreen:
            if win_id == now_focused:
                self.need_handle_fullscreen = False
                self.i3ipc.command(
                    f'[con_id={now_focused}] fullscreen enable'
                )

    def focus_next(self, tag: str, idx: int,
                   inc_counter: bool = True,
                   fullscreen_handler: bool = True,
                   subtagged: bool = False) -> None:
        """ Focus next window. Used by go_next function.
        Tag list is a list of windows by some factor, which determined by
        config settings.

        Args:
            tag (str): target tag.
            idx (int): index inside tag list.
            inc_counter (bool): increase counter or not.
            fullscreen_handler (bool): for manual set / unset fullscreen,
                                        because of i3 is not perfect in it.
                                        For example you need it for different
                                        workspaces.
            subtagged (bool): this flag denotes to subtag using.
        """
        if fullscreen_handler:
            self.prefullscreen(tag)

        self.twin(tag, idx, subtagged).command('focus')

        if inc_counter:
            self.current_position[tag] += 1

        if fullscreen_handler:
            self.postfullscreen(tag, idx)

        self.need_handle_fullscreen = True

    def twin(self, tag: str, idx: int, with_subtag: bool = False):
        """ Detect target window.
            Args:
                tag (str): selected tag.
                idx (int): index in tag list.
                with_subtag (bool): contains subtag, special behaviour then.
        """
        if not with_subtag:
            return self.tagged[tag][idx]

        subtag_win_classes = self.subtag_info.get("class", {})
        for subidx, win in enumerate(self.tagged[tag]):
            if win.window_class in subtag_win_classes:
                return self.tagged[tag][subidx]

        return self.tagged[tag][0]

    def need_priority_check(self, tag):
        """ Checks that priority string is defined, then thecks that currrent
        window not in class set.

            Args:
                tag(str): target tag name

        """
        return "priority" in self.conf(tag) and \
            self.current_win.window_class not in set(self.conf(tag, "class"))

    def not_priority_win_class(self, tag, win):
        """ Window class is not priority class for the given tag

            Args:
                tag(str): target tag name
                win: window
        """
        return win.window_class in self.conf(tag, "class") and \
            win.window_class != self.conf(tag, "priority")

    def no_prioritized_wins(self, tag):
        """ Checks all tagged windows for the priority win.

            Args:
                tag(str): target tag name
        """
        return not [
            win for win in self.tagged[tag]
            if win.window_class == self.conf(tag, "priority")
        ]

    def go_next(self, tag: str) -> None:
        """ Circle over windows. Function "called" from the user-side.

        Args:
            tag (str): denotes target [tag]
        """
        self.sort_by_parent(tag)
        if not self.tagged[tag]:
            self.run_prog(tag)
        elif len(self.tagged[tag]) == 1:
            idx = 0
            self.focus_next(tag, idx, fullscreen_handler=False)
        else:
            idx = self.current_position[tag] % len(self.tagged[tag])
            if self.need_priority_check(tag):
                for win in self.tagged[tag]:
                    if self.no_prioritized_wins(tag):
                        self.run_prog(tag)
                        return
                for idx, win in enumerate(self.tagged[tag]):
                    if win.window_class == self.conf(tag, "priority"):
                        self.focus_next(tag, idx, inc_counter=False)
            elif self.current_win.id == self.twin(tag, idx).id:
                self.find_next_not_the_same_win(tag)
            else:
                self.focus_next(tag, idx)

    def go_subtag(self, tag: str, subtag: str) -> None:
        """ Circle over subtag windows. Function "called" from the user-side.

        Args:
            tag (str): denotes target [tag]
            subtag (str): denotes the target [subtag].
        """
        self.subtag_info = self.conf(tag, subtag)
        self.tag_windows()

        if self.subtag_info:
            subtagged_class_set = set(self.subtag_info.get("class", {}))
            tagged_win_classes = {
                w.window_class for w in self.tagged.get(tag, {})
            }
            if not tagged_win_classes & subtagged_class_set:
                self.run_prog(tag, subtag)
            else:
                idx = 0
                self.focus_next(tag, idx, subtagged=True)

    def add_prop(self, tag_to_add: str, prop_str: str) -> None:
        """ Add property via [prop_str] to the target [tag].

        Args:
            tag (str): denotes the target tag.
            prop_str (str): string in i3-match format used to add/delete
                            target window in/from scratchpad.
        """
        if tag_to_add in self.cfg:
            self.add_props(tag_to_add, prop_str)

        for tag in self.cfg:
            if tag != tag_to_add:
                self.del_props(tag, prop_str)

        self.initialize(self.i3ipc)

    def del_prop(self, tag: str, prop_str: str) -> None:
        """ Delete property via [prop_str] to the target [tag].

            Args:
                tag (str): denotes the target tag.
                prop_str (str): string in i3-match format used to add/delete
                                target window in/from scratchpad.
        """
        self.del_props(tag, prop_str)

    def find_acceptable_windows(self, tag: str) -> None:
        """ Wrapper over Matcher.match to find acceptable windows and add it to
            tagged[tag] list.

            Args:
                tag (str): denotes the target tag.
        """
        for win in self.winlist:
            if self.match(win, tag):
                self.tagged.get(tag, {}).append(win)

    def tag_windows(self, invalidate_winlist=True) -> None:
        """ Find acceptable windows for the all tags and add it to the
            tagged[tag] list.

            Args:
                tag (str): denotes the target tag.
        """
        if invalidate_winlist:
            self.winlist = self.i3ipc.get_tree().leaves()
        self.tagged = {}

        for tag in self.cfg:
            self.tagged[tag] = []
            self.find_acceptable_windows(tag)

    def sort_by_parent(self, tag: str) -> None:
        """
            Sort windows by some infernal logic: At first sort by parent
            container order, than in any order.

            Args:
                tag (str): target tag to sort.
        """
        i = 0

        try:
            for tagged_win in self.tagged[tag]:
                for container_win in tagged_win.parent:
                    if container_win in self.tagged[tag]:
                        oldidx = self.tagged[tag].index(container_win)
                        self.tagged[tag].insert(
                            i, self.tagged[tag].pop(oldidx)
                        )
                        i += 1
        except TypeError:
            pass

    def add_wins(self, _, event) -> None:
        """ Tag window if it is match defined rules.

            Args:
                _: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        win = event.container
        for tag in self.cfg:
            if self.match(win, tag):
                self.tagged[tag].append(win)
        self.win = win

    def del_wins(self, _, event) -> None:
        """ Delete tag from window if it's closed.

            Args:
                _: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        win_con = event.container
        for tag in self.cfg:
            if self.match(win_con, tag):
                for win in self.tagged[tag]:
                    if win.id in self.restore_fullscreen:
                        self.restore_fullscreen.remove(win.id)

        for tag in self.cfg:
            for win in self.tagged[tag]:
                if win.id == win_con.id:
                    self.tagged[tag].remove(win)
        self.subtag_info = {}

    def set_curr_win(self, _, event) -> None:
        """ Cache the current window.

            Args:
                _: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        self.current_win = event.container

    def handle_fullscreen(self, _, event) -> None:
        """ Performs actions over the restore_fullscreen list.

            This function memorize the current state of the fullscreen property
            of windows for the future reuse it in functions which need to
            set/unset fullscreen state of the window correctly.

            Args:
                _: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        win = event.container
        self.fullscreened = self.i3ipc.get_tree().find_fullscreen()
        if self.need_handle_fullscreen:
            if win.fullscreen_mode:
                if win.id not in self.restore_fullscreen:
                    self.restore_fullscreen.append(win.id)
                    return
            if not win.fullscreen_mode:
                if win.id in self.restore_fullscreen:
                    self.restore_fullscreen.remove(win.id)
                    return
 

x

main_lib_dir_pt_2(raw, dl)

   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
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
==> display.py <==
""" Handle X11 screen tasks with randr extension
"""

import subprocess
from Xlib import display
from Xlib.ext import randr

from misc import Misc


class Display():
    d = display.Display()
    s = d.screen()
    window = s.root.create_window(0, 0, 1, 1, 1, s.root_depth)
    xrandr_cache = randr.get_screen_info(window)._data
    resolution_list = []

    @classmethod
    def get_screen_resolution(cls) -> dict:
        size_id = cls.xrandr_cache['size_id']
        resolution = cls.xrandr_cache['sizes'][size_id]
        return {
            'width': int(resolution['width_in_pixels']),
            'height': int(resolution['height_in_pixels'])
        }

    @classmethod
    def get_screen_resolution_data(cls) -> dict:
        return cls.xrandr_cache['sizes']

    @classmethod
    def xrandr_resolution_list(cls) -> dict:
        if not cls.resolution_list:
            delimiter = 'x'
            resolution_data = cls.get_screen_resolution_data()
            for size_id, res in enumerate(resolution_data):
                if res is not None and res:
                    cls.resolution_list.append(
                        str(size_id) + ': ' +
                        str(res['width_in_pixels']) +
                        delimiter +
                        str(res['height_in_pixels'])
                    )

        return cls.resolution_list

    @classmethod
    def set_screen_size(cls, size_id=0) -> None:
        try:
            subprocess.run(['xrandr', '-s', str(size_id)], check=True)
        except subprocess.CalledProcessError as proc_err:
            Misc.print_run_exception_info(proc_err)

==> executor.py <==
""" Tmux Manager.

Give simple and consistent way for user to create tmux sessions on dedicated
sockets. Also it can run simply run applications without Tmux. The main
advantage is dynamic config reloading and simplicity of adding or modifing of
various parameters, also it works is faster then dedicated scripts, because
there is no parsing / translation phase here in runtime.
"""
import subprocess
import os
import errno
import shlex
import shutil
import threading
import multiprocessing
import yaml
import yamlloader

from negi3mod import negi3mod
from typing import List
from os.path import expanduser
from cfg import cfg
from misc import Misc


class env():
    """ Environment class. It is a helper for tmux manager to store info about
        currently selected application. This class rules over parameters and
        settings of application, like used terminal enumator, fonts, all path
        settings, etc.
    Parents:
        config: configuration manager to autosave/autoload
                TOML-configutation with inotify
    """

    def __init__(self, name: str, config: dict) -> None:
        self.name = name
        self.tmux_socket_dir = expanduser('/dev/shm/tmux_sockets')
        self.alacritty_cfg_dir = expanduser('/dev/shm/alacritty_cfg')
        self.sockpath = expanduser(f'{self.tmux_socket_dir}/{name}.socket')
        self.default_alacritty_cfg_path = "~/.config/alacritty/alacritty.yml"
        Misc.create_dir(self.tmux_socket_dir)
        Misc.create_dir(self.alacritty_cfg_dir)
        try:
            os.makedirs(self.tmux_socket_dir)
        except OSError as dir_not_created:
            if dir_not_created.errno != errno.EEXIST:
                raise

        try:
            os.makedirs(self.alacritty_cfg_dir)
        except OSError as dir_not_created:
            if dir_not_created.errno != errno.EEXIST:
                raise

        # get terminal from config, use Alacritty by default
        self.term = config.get(name, {}).get("term", "alacritty").lower()

        self.wclass = config.get(name, {}).get("class", self.term)
        self.title = config.get(name, {}).get("title", self.wclass)

        self.font = config.get("default_font", "")
        if not self.font:
            self.font = config.get(name, {}).get("font", "Iosevka Term")
        self.font_size = config.get("default_font_size", "")
        if not self.font_size:
            self.font_size = config.get(name, {}).get("font_size", "18")

        use_one_fontstyle = config.get("use_one_fontstyle", False)
        self.font_style = config.get("default_font_style", "")
        if not self.font_style:
            self.font_style = config.get(name, {}).get("font_style", "Regular")

        if use_one_fontstyle:
            self.font_style_normal = config.get(name, {})\
                .get("font_style_normal", self.font_style)

            self.font_style_bold = config.get(name, {})\
                .get("font_style_bold", self.font_style)

            self.font_style_italic = config.get(name, {})\
                .get("font_style_italic", self.font_style)
        else:
            self.font_style_normal = config.get(name, {})\
                .get("font_style_normal", 'Regular')

            self.font_style_bold = config.get(name, {})\
                .get("font_style_bold", 'Bold')

            self.font_style_italic = config.get(name, {})\
                .get("font_style_italic", 'Italic')

        self.tmux_session_attach = \
            f"tmux -S {self.sockpath} a -t {name}"
        self.tmux_new_session = \
            f"tmux -S {self.sockpath} new-session -s {name}"
        colorscheme = config.get("colorscheme", "")
        if not colorscheme:
            colorscheme = config.get(name, {}).get("colorscheme", 'dark3')
        self.set_colorscheme = \
            f"{expanduser('~/bin/dynamic-colors')} switch {colorscheme};"
        self.postfix = config.get(name, {}).get("postfix", '')
        if self.postfix and self.postfix[0] != '-':
            self.postfix = '\\; ' + self.postfix
        self.run_tmux = int(config.get(name, {}).get("run_tmux", 1))
        if not self.run_tmux:
            prog_to_dtach = config.get(name, {}).get('prog_detach', '')
            if prog_to_dtach:
                self.prog = \
                    f'dtach -A ~/1st_level/{name}.session {prog_to_dtach}'
            else:
                self.prog = config.get(name, {}).get('prog', 'true')
        self.set_wm_class = config.get(name, {}).get('set_wm_class', '')
        self.set_instance = config.get(name, {}).get('set_instance', '')

        self.x_pad = config.get(name, {}).get('x_padding', '2')
        self.y_pad = config.get(name, {}).get('y_padding', '2')

        self.create_term_params(config, name)

        def join_processes():
            for prc in multiprocessing.active_children():
                prc.join()

        threading.Thread(target=join_processes, args=(), daemon=True).start()

    @staticmethod
    def generate_alacritty_config(
            alacritty_cfg_dir, config: dict, name: str) -> str:
        """ Config generator for alacritty.
            We need it because of alacritty cannot bypass most of user
            parameters with command line now.

            Args:
                alacritty_cfg_dir: alacritty config dir
                config: config dirtionary
                name(str): name of config to generate

            Return:
               cfgname(str): configname
        """
        app_name = config.get(name, {}).get('app_name', {})
        if not app_name:
            app_name = config.get(name, {}).get('class')

        app_name = expanduser(app_name + '.yml')
        cfgname = expanduser(f'{alacritty_cfg_dir}/{app_name}')

        if not os.path.exists(cfgname):
            shutil.copyfile(
                expanduser("~/.config/alacritty/alacritty.yml"),
                cfgname
            )
        return cfgname

    def yaml_config_create(self, custom_config: str) -> None:
        """ Create config for alacritty

            Args:
                custom_config(str): config name to create
        """
        with open(custom_config, "r") as cfg_file:
            try:
                conf = yaml.load(
                    cfg_file, Loader=yamlloader.ordereddict.CSafeLoader)
                if conf is not None:
                    conf["font"]["normal"]["family"] = self.font
                    conf["font"]["bold"]["family"] = self.font
                    conf["font"]["italic"]["family"] = self.font
                    conf["font"]["normal"]["style"] = self.font_style_normal
                    conf["font"]["bold"]["style"] = self.font_style_bold
                    conf["font"]["italic"]["style"] = self.font_style_italic
                    conf["font"]["size"] = self.font_size
                    conf["window"]["padding"]['x'] = int(self.x_pad)
                    conf["window"]["padding"]['y'] = int(self.y_pad)
            except yaml.YAMLError as yamlerror:
                print(yamlerror)

        with open(custom_config, 'w', encoding='utf8') as outfile:
            try:
                yaml.dump(
                    conf,
                    outfile,
                    default_flow_style=False,
                    allow_unicode=True,
                    canonical=False,
                    explicit_start=True,
                    Dumper=yamlloader.ordereddict.CDumper
                )
            except yaml.YAMLError as yamlerror:
                print(yamlerror)

    def create_term_params(self, config: dict, name: str) -> None:
        """ This function fill self.term_opts for settings.abs

            Args:
                config(dict): config dictionary which should be adopted to
                commandline options or settings.
        """
        if self.term == "alacritty":
            custom_config = self.generate_alacritty_config(
                self.alacritty_cfg_dir, config, name
            )
            multiprocessing.Process(
                target=self.yaml_config_create, args=(custom_config,),
                daemon=True
            ).start()
            self.term_opts = [
                "alacritty", '-qq', "--live-config-reload", "--config-file",
                expanduser(custom_config)
            ] + [
                "--class", self.wclass,
                "-t", self.title,
                "-e", "dash", "-c"
            ]
        elif self.term == "st":
            self.term_opts = ["st"] + [
                "-c", self.wclass,
                "-f", self.font + ":size=" + str(self.font_size),
                "-e", "dash", "-c",
            ]
        elif self.term == "urxvt":
            self.term_opts = ["urxvt"] + [
                "-name", self.wclass,
                "-fn", "xft:" + self.font + ":size=" + str(self.font_size),
                "-e", "dash", "-c",
            ]
        elif self.term == "xterm":
            self.term_opts = ["xterm"] + [
                '-class', self.wclass,
                '-fa', "xft:" + self.font + ":size=" + str(self.font_size),
                "-e", "dash", "-c",
            ]
        elif self.term == "cool-retro-term":
            self.term_opts = ["cool-retro-term"] + [
                "-e", "dash", "-c",
            ]


class executor(negi3mod, cfg):
    """ Tmux Manager class. Easy and consistent way to create tmux sessions on
        dedicated sockets. Also it can run simply run applications without
        Tmux. The main advantage is dynamic config reloading and simplicity of
        adding or modifing of various parameters.

    Parents:
        cfg: configuration manager to autosave/autoload
                  TOML-configutation with inotify
    """
    def __init__(self, i3, loop=None) -> None:
        """ Init function.

        Arguments for this constructor used only for compatibility.

        Args:
            i3: i3ipc connection(not used).
            loop: asyncio loop. It's need to be given as parameter because of
                  you need to bypass asyncio-loop to the thread(not used).
        """
        cfg.__init__(self, i3, convert_me=False)
        self.envs = {}
        for app in self.cfg:
            self.envs[app] = env(app, self.cfg)

        self.bindings = {
            "run": self.run,
            "reload": self.reload_config,
        }

    def __exit__(self, exc_type, exc_value, traceback) -> None:
        self.envs.clear()

    def run_app(self, args: List) -> None:
        """ Wrapper to run selected application in background.
            Args:
                args (List): arguments list.
        """
        if not self.env.set_wm_class:
            subprocess.Popen(args)
        else:
            if not self.env.set_instance:
                self.env.set_instance = self.env.set_wm_class
            subprocess.Popen(
                [
                    './wm_class',
                    '--run',
                    self.env.set_wm_class,
                    self.env.set_instance,
                ] + args,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )

    @staticmethod
    def detect_session_bind(sockpath, name) -> str:
        """ Find target session for given socket.
        """
        session_list = subprocess.run(
            shlex.split(f"tmux -S {sockpath} list-sessions"),
            stdout=subprocess.PIPE,
            check=False
        ).stdout
        return subprocess.run(
            shlex.split(f"awk -F ':' '/{name}/ {{print $1}}'"),
            stdout=subprocess.PIPE,
            input=(session_list),
            check=False
        ).stdout.decode()

    def attach_to_session(self) -> None:
        """ Run tmux to attach to given socket.
        """
        self.run_app(
            self.env.term_opts +
            [f"{self.env.set_colorscheme} {self.env.tmux_session_attach}"]
        )

    def search_classname(self) -> bytes:
        """ Search for selected window class.
        """
        return subprocess.run(
            shlex.split(f"xdotool search --classname {self.env.wclass}"),
            stdout=subprocess.PIPE,
            check=False
        ).stdout

    def create_new_session(self) -> None:
        """ Run tmux to create the new session on given socket.
        """
        self.run_app(
            self.env.term_opts +
            [f"{self.env.set_colorscheme} \
            {self.env.tmux_new_session} {self.env.postfix} && \
                {self.env.tmux_session_attach}"]
        )

    def run(self, name: str) -> None:
        """ Entry point, run application with Tmux on dedicated socket(in most
            cases), or without tmux, if config value run_tmux=0.
            Args:
                name (str): target application name, with configuration taken
                            from TOML.
        """
        self.env = self.envs[name]
        if self.env.run_tmux:
            if self.env.name in self.detect_session_bind(
                    self.env.sockpath, self.env.name):
                wid = self.search_classname()
                try:
                    if int(wid.decode()):
                        pass
                except ValueError:
                    self.attach_to_session()
            else:
                self.create_new_session()
        else:
            self.run_app(
                self.env.term_opts + [
                    self.env.set_colorscheme + self.env.prog
                ]
            )

==> fs.py <==
#!/usr/bin/pypy3
""" Module to set / unset dpms while fullscreen is toggled on.

I am simply use xset here. There is better solution possible,
for example wayland-friendly.
"""

import subprocess
from negi3mod import negi3mod
from cfg import cfg


class fs(negi3mod, cfg):
    def __init__(self, i3conn, loop=None):
        # i3ipc connection, bypassed by negi3mods runner
        self.i3ipc = i3conn
        self.panel_should_be_restored = False

        # Initialize modcfg.
        cfg.__init__(self, i3conn, convert_me=False)

        # default panel classes
        self.panel_classes = self.cfg.get("panel_classes", [])

        # fullscreened workspaces
        self.ws_fullscreen = self.cfg.get("ws_fullscreen", [])

        # for which windows we shoudn't show panel
        self.classes_to_hide_panel = self.cfg.get(
            "classes_to_hide_panel", []
        )

        self.show_panel_on_close = False

        self.bindings = {
            "reload": self.reload_config,
            "fullscreen": self.hide,
        }

        self.i3ipc.on('window::close', self.on_window_close)
        self.i3ipc.on('workspace::focus', self.on_workspace_focus)

    def on_workspace_focus(self, _, event):
        """ Hide panel if it is fullscreen workspace, show panel otherwise """
        for tgt_ws in self.ws_fullscreen:
            if event.current.name.endswith(tgt_ws):
                self.panel_action('hide', restore=False)
                return

        self.panel_action('show', restore=False)

    def panel_action(self, action: str, restore: bool):
        """ Helper to do show/hide with panel or another action

            Args:
                action (str): action to do.
                restore(bool): shows should the panel state be restored or not.
        """
        # should be empty
        ret = subprocess.Popen(
            ['xdo', action, '-N', 'Polybar'], stdout=subprocess.PIPE
        ).communicate()[0]

        if not ret and restore is not None:
            self.panel_should_be_restored = restore

    def on_fullscreen_mode(self, _, event):
        """ Disable panel if it was in fullscreen mode and then goes to
        windowed mode.

            Args:
                _: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        if event.container.window_class in self.panel_classes:
            return

        self.hide()

    def hide(self):
        """ Hide panel for this workspace """
        i3_tree = self.i3ipc.get_tree()
        fullscreens = i3_tree.find_fullscreen()
        focused_ws = i3_tree.find_focused().workspace().name

        if not fullscreens:
            return

        for win in fullscreens:
            for tgt_class in self.classes_to_hide_panel:
                if win.window_class == tgt_class:
                    for tgt_ws in self.ws_fullscreen:
                        if focused_ws.endswith(tgt_ws):
                            self.panel_action('hide', restore=False)
                            break

    def on_window_close(self, i3conn, event):
        """ If there are no fullscreen windows then show panel closing window.

            Args:
                i3: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        if event.container.window_class in self.panel_classes:
            return

        if self.show_panel_on_close:
            if not i3conn.get_tree().find_fullscreen():
                self.panel_action('show', restore=True)

==> geom.py <==
""" Module to convert from 16:10 1080p geometry to target screen geometry.

    This module contains geometry converter and also i3-rules generator. Also
    in this module geometry is parsed from config X11 internal format to the i3
    commands.
"""

import re
from typing import List
from display import Display


class geom():
    def __init__(self, cfg: dict) -> None:
        """ Init function

        Args:
            cfg: config bypassed from target module, nsd for example.
        """
        # generated command list for i3 config
        self.cmd_list = []

        # geometry in the i3-commands format.
        self.parsed_geom = {}

        # set current screen resolution
        self.current_resolution = Display.get_screen_resolution()

        # external config
        self.cfg = cfg

        # fill self.parsed_geom with self.parse_geom function.
        for tag in self.cfg:
            self.parsed_geom[tag] = self.parse_geom(tag)

    @staticmethod
    def scratchpad_hide_cmd(hide: bool) -> str:
        """ Returns cmd needed to hide scratchpad.

            Args:
                hide (bool): to hide target or not.
        """
        ret = ""
        if hide:
            ret = ", [con_id=__focused__] scratchpad show"
        return ret

    @staticmethod
    def ch(lst: List, ch: str) -> str:
        """ Return char is list is not empty to prevent stupid commands.
        """
        ret = ''
        if len(lst) > 1:
            ret = ch
        return ret

    def ret_info(self, tag: str, attr: str, target_attr: str,
                 dprefix: str, hide: str) -> str:
        """ Create rule in i3 commands format

        Args:
            tag (str): target tag.
            attr (str): tag attrubutes.
            target_attr (str): attribute to fill.
            dprefix (str): rule prefix.
            hide (str): to hide target or not.
        """
        if target_attr in attr:
            lst = [item for item in self.cfg[tag][target_attr] if item != '']
            if lst != []:
                pref = dprefix+"[" + '{}="'.format(attr) + \
                    self.ch(self.cfg[tag][attr], '^')
                for_win_cmd = pref + self.parse_attr(self.cfg[tag][attr]) + \
                    "move scratchpad, " + self.get_geom(tag) \
                                        + self.scratchpad_hide_cmd(hide)
                return for_win_cmd
        return ''

    @staticmethod
    def parse_attr(attrib_list: List) -> str:
        """ Create attribute matching string.
            Args:
                tag (str): target tag.
                attr (str): target attrubute.
        """
        ret = ''
        if len(attrib_list) > 1:
            ret += '('
        for iter, item in enumerate(attrib_list):
            ret += item
            if iter+1 < len(attrib_list):
                ret += '|'
        if len(attrib_list) > 1:
            ret += ')$'
        ret += '"] '

        return ret

    def create_i3_match_rules(self, hide: bool = True,
                              dprefix: str = "for_window ") -> None:
        """ Create i3 match rules for all tags.

        Args:
            hide (bool): to hide target or not, optional.
            dprefix (str): i3-cmd prefix is "for_window " by default, optional.
        """
        cmd_list = []
        for tag in self.cfg:
            for attr in self.cfg[tag]:
                cmd_list.append(self.ret_info(
                    tag, attr, 'class', dprefix, hide)
                )
                cmd_list.append(self.ret_info(
                    tag, attr, 'instance', dprefix, hide)
                )
        self.cmd_list = filter(lambda str: str != '', cmd_list)

    # nsd need this function
    def get_geom(self, tag: str) -> str:
        """ External function used by nsd
        """
        return self.parsed_geom[tag]

    def parse_geom(self, tag: str) -> str:
        """ Convert geometry from self.cfg format to i3 commands.

        Args:
            tag (str): target self.cfg tag
        """
        rd = {'width': 1920, 'height': 1200}  # resolution_default
        cr = self.current_resolution          # current resolution

        g = re.split(r'[x+]', self.cfg[tag]["geom"])
        cg = []  # converted_geom

        cg.append(int(int(g[0])*cr['width'] / rd['width']))
        cg.append(int(int(g[1])*cr['height'] / rd['height']))
        cg.append(int(int(g[2])*cr['width'] / rd['width']))
        cg.append(int(int(g[3])*cr['height'] / rd['height']))

        return "move absolute position {2} {3}, resize set {0} {1}".format(*cg)

==> __init__.py <==
import os
import sys
sys.path.append(os.getenv("XDG_CONFIG_HOME") + "/i3/lib")

==> locker.py <==
""" Create a pid lock with abstract socket.

    Taken from [https://stackoverflow.com/questions/788411/check-to-see-if-python-script-is-running]
"""

import sys
import socket


def get_lock(process_name: str) -> None:
    """
    Without holding a reference to our socket somewhere it gets garbage
    collected when the function exits

    Args:
        process_name (str): process name to bind.
    """
    get_lock._lock_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)

    try:
        get_lock._lock_socket.bind('\0' + process_name)
        print('locking successful')
    except socket.error:
        print('lock exists')
        sys.exit()

==> matcher.py <==
""" Matcher module

In this class to check that window can be tagged with given tag by
WM_CLASS, WM_INSTANCE, regexes, etc. It can be used by named scrachpad,
circle run-or-raise, etc.
"""

import sys
import re
from typing import List, Iterator


class Matcher():
    """ Generic matcher class

    Used by several classes. It can match windows by several criteria, which
    I am calling "factor", including:
        - by class, by class regex
        - by instance, by instance regex
        - by role, by role regex
        - by name regex

    Of course this list can by expanded. It uses sys.intern hack for better
    performance and simple caching. One of the most resource intensive part of
    negi3mods.

    """

    factors = [
        sys.intern("class"),
        sys.intern("instance"),
        sys.intern("role"),
        sys.intern("class_r"),
        sys.intern("instance_r"),
        sys.intern("name_r"),
        sys.intern("role_r"),
        sys.intern('match_all')
    ]

    def __init__(self):
        self.matched_list = []

        self.match_dict = {
            sys.intern("class"): lambda: self.win.window_class in self.matched_list,
            sys.intern("instance"): lambda: self.win.window_instance in self.matched_list,
            sys.intern("role"): lambda: self.win.window_role in self.matched_list,
            sys.intern("class_r"): self.class_r,
            sys.intern("instance_r"): self.instance_r,
            sys.intern("role_r"): self.role_r,
            sys.intern("name_r"): self.name_r,
            sys.intern("match_all"): Matcher.match_all
        }

    @staticmethod
    def find_classed(win: List, pattern: str) -> Iterator:
        """ Returns iterator to find by window class """
        return (c for c in win
                if c.window_class and re.search(pattern, c.window_class))

    @staticmethod
    def find_instanced(win: List, pattern: str) -> Iterator:
        """ Returns iterator to find by window instance """
        return (c for c in win
                if c.window_instance and re.search(pattern, c.window_instance))

    @staticmethod
    def find_by_role(win: List, pattern: str) -> Iterator:
        """ Returns iterator to find by window role """
        return (c for c in win
                if c.window_role and re.search(pattern, c.window_role))

    @staticmethod
    def find_named(win: List, pattern: str) -> Iterator:
        """ Returns iterator to find by window name """
        return (c for c in win if c.name and re.search(pattern, c.name))

    def class_r(self) -> bool:
        """ Check window by class with regex """
        for pattern in self.matched_list:
            cls_by_regex = Matcher.find_classed([self.win], pattern)
            if cls_by_regex:
                for class_regex in cls_by_regex:
                    if self.win.window_class == class_regex.window_class:
                        return True
        return False

    def instance_r(self) -> bool:
        """ Check window by instance with regex """
        for pattern in self.matched_list:
            inst_by_regex = Matcher.find_instanced([self.win], pattern)
            if inst_by_regex:
                for inst_regex in inst_by_regex:
                    if self.win.window_instance == inst_regex.window_instance:
                        return True
        return False

    def role_r(self) -> bool:
        """ Check window by role with regex """
        for pattern in self.matched_list:
            role_by_regex = Matcher.find_by_role([self.win], pattern)
            if role_by_regex:
                for role_regex in role_by_regex:
                    if self.win.window_role == role_regex.window_role:
                        return True
        return False

    def name_r(self) -> bool:
        """ Check window by name with regex """
        for pattern in self.matched_list:
            name_by_regex = Matcher.find_named([self.win], pattern)
            if name_by_regex:
                for name_regex in name_by_regex:
                    if self.win.name == name_regex.name:
                        return True
        return False

    @staticmethod
    def match_all() -> bool:
        """ Match every possible window """
        return True

    def match(self, win, tag: str) -> bool:
        """ Check that window matches to the config rules """
        self.win = win

        for f in Matcher.factors:
            self.matched_list = self.cfg.get(tag, {}).get(f, {})
            if self.matched_list and self.match_dict[f]():
                return True
        return False

==> menu.py <==
""" Menu manager module.

    This module is about creating various menu.

    For now it contains following menus:
        - Goto workspace.
        - Attach to workspace.
        - Add window to named scratchpad or circle group.
        - xprop menu to get X11-atom parameters of selected window.
        - i3-cmd menu with autocompletion.
"""

import importlib
from typing import List

from cfg import cfg
from misc import Misc
from negi3mod import negi3mod


class menu(negi3mod, cfg):
    """ Base class for menu module """

    def __init__(self, i3ipc, loop=None) -> None:
        # Initialize cfg.
        cfg.__init__(self, i3ipc)

        # i3ipc connection, bypassed by negi3mods runner
        self.i3ipc = i3ipc

        # i3 path used to get "send" binary path
        self.i3_path = Misc.i3path()

        # i3-msg application name
        self.i3cmd = self.conf("i3cmd")

        # Window properties shown by xprop menu.
        self.xprops_list = self.conf("xprops_list")

        # cache screen width
        if not self.conf("use_default_width"):
            from display import Display
            self.screen_width = Display.get_screen_resolution()["width"]
        else:
            self.screen_width = int(self.conf('use_default_width'))

        for mod in self.cfg['modules']:
            module = importlib.import_module('menu_mods.' + mod)
            setattr(self, mod, getattr(module, mod)(self))

        self.bindings = {
            "cmd_menu": self.i3menu.cmd_menu,

            "xprop": self.xprop.xprop,

            "autoprop": self.props.autoprop,
            "show_props": self.props.show_props,

            "pulse_output": self.pulse_menu.pulseaudio_output,
            "pulse_input": self.pulse_menu.pulseaudio_input,

            "ws": self.winact.goto_ws,
            "goto_win": self.winact.goto_win,
            "attach": self.winact.attach_win,
            "movews": self.winact.move_to_ws,

            "gtk_theme": self.gnome.change_gtk_theme,
            "icon_theme": self.gnome.change_icon_theme,

            "xrandr_resolution": self.xrandr.change_resolution_xrandr,

            "reload": self.reload_config,
        }

    def args(self, params: dict) -> List[str]:
        """ Create run parameters to spawn rofi process from dict

            Args:
                params(dict): parameters for rofi
            Return:
                List(str) to do rofi subprocessing
        """
        prompt = self.conf("prompt")

        params['width'] = params.get('width', int(self.screen_width * 0.85))
        params['prompt'] = params.get('prompt', prompt)
        params['cnum'] = params.get('cnum', 16)
        params['lnum'] = params.get('lnum', 2)
        params['markup_rows'] = params.get('markup_rows', '-no-markup-rows')
        params['auto_selection'] = \
            params.get('auto_selection', "-no-auto-selection")

        launcher_font = self.conf("font") + " " + \
            str(self.conf("font_size"))
        location = self.conf("location")
        anchor = self.conf("anchor")
        matching = self.conf("matching")
        gap = self.conf("gap")

        return [
            'rofi', '-show', '-dmenu',
            '-columns', str(params['cnum']),
            '-lines', str(params['lnum']),
            '-disable-history',
            params['auto_selection'],
            params['markup_rows'],
            '-p', params['prompt'],
            '-i',
            '-matching', f'{matching}',
            '-theme-str',
            f'* {{ font: "{launcher_font}"; }}',
            '-theme-str',
            f'#window {{ width:{params["width"]}; y-offset: -{gap}; \
            location: {location}; \
            anchor: {anchor}; }}',
        ]

    def wrap_str(self, string: str) -> str:
        """ String wrapper to make it beautiful """
        return self.conf('left_bracket') + string + self.conf('right_bracket')

==> misc.py <==
""" Various helper functions

    Class for this is created for the more well defined namespacing and more
    simple import.
"""
import os
import subprocess
import errno


class Misc():
    """ Implements various helper functions
    """

    @staticmethod
    def create_dir(dirname):
        """ Helper function to create directory

            Args:
                dirname(str): directory name to create
        """
        try:
            os.makedirs(dirname)
        except OSError as oserr:
            if oserr.errno != errno.EEXIST:
                raise

    @staticmethod
    def i3path() -> str:
        """ Easy way to return i3 config path.
        """
        return os.environ.get("XDG_CONFIG_HOME") + "/i3/"

    @staticmethod
    def extract_xrdb_value(field: str) -> str:
        """ Extracts field from xrdb executable.
        """
        try:
            out = subprocess.run(
                f"xrescat '{field}'",
                shell=True,
                stdout=subprocess.PIPE,
                check=True
            ).stdout
            if out is not None and out:
                ret = out.decode('UTF-8').split()[0]
                return ret
        except subprocess.CalledProcessError as proc_err:
            Misc.print_run_exception_info(proc_err)

        return ""

    @classmethod
    def notify_msg(cls, msg: str, prefix: str = " "):
        """ Send messages via notify-osd based notifications.

            Args:
                msg: message string.
                prefix: optional prefix for message string.
        """

        def get_pids(process):
            try:
                pidlist = map(
                    int, subprocess.check_output(["pidof", process]).split()
                )
            except subprocess.CalledProcessError:
                pidlist = []
            return pidlist

        if get_pids('dunst'):
            foreground_color = cls.extract_xrdb_value('\\*.foreground')
            notify_msg = [
                'dunstify', '',
                f"<span weight='normal' color='{foreground_color}'>" +
                prefix + msg +
                "</span>"
            ]
            subprocess.Popen(notify_msg)

    @classmethod
    def notify_off(cls, _dummy_msg: str, _dummy_prefix: str = " "):
        """ Do nothing """
        return

    @staticmethod
    def echo_on(*args, **kwargs):
        """ print info """
        print(*args, **kwargs)

    @staticmethod
    def echo_off(*_dummy_args, **_dummy_kwargs):
        """ do not print info """
        return

    @staticmethod
    def print_run_exception_info(proc_err):
        print(f'returncode={proc_err.returncode}, \
                cmd={proc_err.cmd}, \
                output={proc_err.output}')
 

x

main_lib_dir_pt_2(raw, dl)

   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
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
==> msgbroker.py <==
""" Module contains routines used by several another modules.

Daemon manager and mod daemon:
    Mod daemon creates appropriate files in the /dev/shm directory.

    Daemon manager handles all requests to this named pipe based API with help
    of asyncio.
"""

import asyncio


class MsgBroker():
    """ This is asyncio message broker for negi3mods.
        Every module has indivisual main loop with indivisual neg-ipc-file.
    """
    lock = asyncio.Lock()
    @classmethod
    def mainloop(cls, loop, mods, port) -> None:
        """ Mainloop by loop create task """
        cls.mods = mods
        loop.create_task(asyncio.start_server(
            cls.handle_client, 'localhost', port))
        loop.run_forever()

    @classmethod
    async def handle_client(cls, reader, _) -> None:
        """ Proceed client message here """
        async with cls.lock:
            while True:
                response = (await reader.readline()).decode('utf8').split()
                if not response:
                    return
                name = response[0]
                cls.mods[name].send_msg(response[1:])

==> negewmh.py <==
"""
In this module we have EWMH routines to detect dialog windows, visible windows,
etc using python-xlib and python-ewmh.
"""
from typing import List
from contextlib import contextmanager

import Xlib
import Xlib.display
from ewmh import EWMH


class NegEWMH():
    """ Custom EWMH support functions """
    disp = Xlib.display.Display()
    ewmh = EWMH()

    @staticmethod
    @contextmanager
    def window_obj(disp, win_id):
        """Simplify dealing with BadWindow (make it either valid or None)"""
        window_obj = None
        if win_id:
            try:
                window_obj = disp.create_resource_object('window', win_id)
            except Xlib.error.XError:
                pass
        yield window_obj

    @staticmethod
    def is_dialog_win(win) -> bool:
        """ Check that window [win] is not dialog window

            At first check typical window roles and classes, because of it more
            fast, then using python EWMH module to detect dialog window type or
            modal state of window.

            Args:
                win : target window to check
        """
        if win.window_instance == "Places" \
                or win.window_role in {
                        "GtkFileChooserDialog",
                        "confirmEx",
                        "gimp-file-open"} \
                or win.window_class == "Dialog":
            return True

        with NegEWMH.window_obj(NegEWMH.disp, win.window) as win_obj:
            win_type = NegEWMH.ewmh.getWmWindowType(win_obj, str=True)
            if '_NET_WM_WINDOW_TYPE_DIALOG' in win_type:
                return True

            win_state = NegEWMH.ewmh.getWmState(win_obj, str=True)
            if '_NET_WM_STATE_MODAL' in win_state:
                return True

            return False

    @staticmethod
    def find_visible_windows(windows_on_ws: List) -> List:
        """ Find windows visible on the screen now.

        Args:
            windows_on_ws: windows list which going to be filtered with this
                        function.
        """
        visible_windows = []
        for win in windows_on_ws:
            with NegEWMH.window_obj(NegEWMH.disp, win.window) as win_obj:
                win_state = NegEWMH.ewmh.getWmState(win_obj, str=True)
                if '_NET_WM_STATE_HIDDEN' not in win_state:
                    visible_windows.append(win)

        return visible_windows


==> negi3mod.py <==
from typing import List


class negi3mod():
    def __init__(self):
        self.bindings = {}

    def send_msg(self, args: List) -> None:
        """ Creates bindings from socket IPC to current module public function
            calls.

            This function defines bindings to the module methods that
            can be used by external users as i3-bindings, sxhkd, etc. Need the
            [send] binary which can send commands to the appropriate socket.

            Args:
                args (List): argument list for the selected function.
        """
        self.bindings[args[0]](*args[1:])

==> standalone_cfg.py <==
""" Dynamic TOML-based config for basic negi3mods.

It is the simplified version of cfg for modules like polybar_vol, etc.
There are no external dependecies like i3 or asyncio.

"""

import sys
import toml
import traceback
import asyncio
import inotipy
from misc import Misc


class modconfig():
    def __init__(self, loop):
        # set asyncio loop
        self.loop = loop

        # detect current negi3mod
        self.mod = self.__class__.__name__

        # config dir path
        self.i3_cfg_path = Misc.i3path() + '/cfg/'

        # negi3mod config path
        self.mod_cfg_path = self.i3_cfg_path + self.mod + '.cfg'

        # load current config
        self.load_config()

        # run inotify watcher to update config on change.
        self.run_inotify_watchers()

    def reload_config(self):
        """ Reload config.
            Call load_config and reinit all stuff.
        """
        prev_conf = self.cfg
        try:
            self.load_config()
            self.__init__()
            self.special_reload()
        except Exception:
            traceback.print_exc(file=sys.stdout)
            self.cfg = prev_conf
            self.__init__()

    def conf(self, *conf_path):
        """ Helper to extract config for current tag.

        Args:
            conf_path: path of config from where extract.
        """
        ret = {}
        for part in conf_path:
            if not ret:
                ret = self.cfg.get(part)
            else:
                ret = ret.get(part)
        return ret

    def load_config(self):
        """ Reload config itself and convert lists in it to sets for the better
            performance.
        """
        with open(self.mod_cfg_path, "r") as fp:
            self.cfg = toml.load(fp)

    def dump_config(self):
        """ Dump current config, can be used for debugging.
        """
        with open(self.mod_cfg_path, "r+") as fp:
            toml.dump(self.cfg, fp)
            self.cfg = toml.load(fp)

    def cfg_watcher(self):
        """ cfg watcher to update modules config in realtime.
        """
        watcher = inotipy.Watcher.create()
        watcher.watch(self.i3_cfg_path, inotipy.IN.MODIFY)
        return watcher

    async def cfg_worker(self, watcher):
        """ Reload target config

            Args:
                watcher: watcher for cfg.
        """
        while True:
            event = await watcher.get()
            if event.name == self.mod + '.cfg':
                self.reload_config()
                Misc.notify_msg(f'[Reloaded {self.mod}]')

    def run_inotify_watchers(self):
        """ Start all watchers here via ensure_future to run it in background.
        """
        asyncio.ensure_future(self.cfg_worker(self.cfg_watcher()))


==> vol.py <==
""" Volume-manager daemon module.

This is a volume manager. Smart tool which allow you control volume of mpd, mpv
or whatever, depending on the context. For example if mpd playing it set
up/down the mpd volume, if it is not then it handles mpv volume via mpvc if mpv
window is not focused or via sending 0, 9 keyboard commands if it is.

"""
import subprocess
import socket
import asyncio
from cfg import cfg
from negi3mod import negi3mod


class vol(negi3mod, cfg):
    def __init__(self, i3, loop) -> None:
        """ Init function

        Args:
            i3: i3ipc connection
            loop: asyncio loop. It's need to be given as parameter because of
                  you need to bypass asyncio-loop to the thread
        """
        # Initialize cfg.
        cfg.__init__(self, i3, loop=loop)

        # i3ipc connection, bypassed by negi3mods runner.
        self.i3ipc = i3

        # Bypass loop from negi3mods script here.
        self.loop = loop

        # Default increment step for mpd.
        self.inc = self.conf("mpd_inc")

        # Default mpd address.
        self.mpd_addr = self.conf("mpd_addr")

        # Default mpd port.
        self.mpd_port = self.conf("mpd_port")

        # Default mpd buffer size.
        self.mpd_buf_size = self.conf("mpd_buf_size")

        # Default mpv socket.
        self.mpv_socket = self.conf("mpv_socket")

        # Send 0, 9 keys to the mpv window or not.
        self.use_mpv09 = self.conf("use_mpv09")

        # Cache current window on focus.
        self.i3ipc.on("window::focus", self.set_curr_win)

        # Default mpd status is False
        self.mpd_playing = False

        # MPD idle command listens to the player events by default.
        self.idle_cmd_str = "idle player\n"

        # MPD status string, which we need send to extract most of information.
        self.status_cmd_str = "status\n"

        self.bindings = {
            "u": self.volume_up,
            "d": self.volume_down,
            "reload": self.reload_config,
        }

        # Initial state for the current_win
        self.current_win = self.i3ipc.get_tree().find_focused()

        # Setup asyncio, because of it is used in another thread.
        asyncio.set_event_loop(self.loop)
        asyncio.ensure_future(self.update_mpd_status(self.loop))

    def set_curr_win(self, i3, event) -> None:
        """ Cache the current window.

            Args:
                i3: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        self.current_win = event.container

    async def update_mpd_status(self, loop) -> None:
        """ Asynchronous function to get current MPD status.

            Args:
                loop: asyncio.loop
        """
        reader, writer = await asyncio.open_connection(
            host=self.mpd_addr, port=self.mpd_port, loop=loop
        )
        data = await reader.read(self.mpd_buf_size)
        if data.startswith(b'OK'):
            writer.write(self.status_cmd_str.encode(encoding='utf-8'))
            stat_data = await reader.read(self.mpd_buf_size)
            if 'state: play' in stat_data.decode('UTF-8').split('\n'):
                self.mpd_playing = True
            else:
                self.mpd_playing = False
            while True:
                writer.write(self.idle_cmd_str.encode(encoding='utf-8'))
                data = await reader.read(self.mpd_buf_size)
                if 'player' in data.decode('UTF-8').split('\n')[0]:
                    writer.write(self.status_cmd_str.encode(encoding='utf-8'))
                    stat_data = await reader.read(self.mpd_buf_size)
                    if 'state: play' in stat_data.decode('UTF-8').split('\n'):
                        self.mpd_playing = True
                    else:
                        self.mpd_playing = False
                else:
                    self.mpd_playing = False
                if writer.transport._conn_lost:
                    # TODO: add function to wait for MPD port here.
                    break

    def change_volume(self, val: int) -> None:
        """ Change volume here.

            This function using MPD state information, information about
            currently focused window from i3, etc to perform contextual volume
            changing.

            Args:
                val (int): volume step.
        """
        val_str = str(val)
        mpv_key = '9'
        mpv_cmd = '--decrease'
        if val > 0:
            val_str = "+" + str(val)
            mpv_key = '0'
            mpv_cmd = '--increase'
        if self.mpd_playing:
            self.mpd_socket = socket.socket(
                socket.AF_INET6, socket.SOCK_STREAM
            )
            try:
                self.mpd_socket.connect((self.mpd_addr, int(self.mpd_port)))
                self.mpd_socket.send(bytes(
                        f'volume {val_str}\nclose\n', 'UTF-8'
                ))
                self.mpd_socket.recv(self.mpd_buf_size)
            finally:
                self.mpd_socket.close()
        elif self.use_mpv09 and self.current_win.window_class == "mpv":
            subprocess.run([
                    'xdotool', 'type', '--clearmodifiers',
                    '--delay', '0', str(mpv_key) * abs(val)
                ],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                check=False
            )
        elif self.use_mpv09:
            subprocess.run([
                    'mpvc', 'set', 'volume', mpv_cmd, str(abs(val))
                ],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                check=False
            )
        else:
            return

    def volume_up(self, *args) -> None:
        """ Increase target volume level.

            Args:
                args (*args): used as multiplexer for volume changing because
                              of pipe-based nature of negi3mods IPC.
        """
        count = len(args)
        if count <= 0:
            count = 1
        self.change_volume(count)

    def volume_down(self, *args) -> None:
        """ Decrease target volume level.

            Args:
                args (*args): used as multiplexer for volume changing because
                              of pipe-based nature of negi3mods IPC.
        """
        count = len(args)
        if count <= 0:
            count = 1
        self.change_volume(-count)


==> win_action.py <==
""" 2bwm-like features module.

There are a lot of various actions over the floating windows in this module,
which may reminds you 2bwm, subtle, or another similar window managers.

You can change window geometry, move it to the half or quad size of the screen
space, etc.

Partially code is taken from https://github.com/miseran/i3-tools, thanks to
you, miseran(https://github.com/miseran)

"""

import collections
from typing import Mapping
from display import Display
from cfg import cfg
from negi3mod import negi3mod


class win_action(negi3mod, cfg):
    """ Named scratchpad class

    Parents:
        cfg: configuration manager to autosave/autoload
                  TOML-configutation with inotify

    """

    def __init__(self, i3, loop=None) -> None:
        """ Init function

        Main part is in self.initialize, which performs initialization itself.

        Attributes:
            i3: i3ipc connection
            loop: asyncio loop. It's need to be given as parameter because of
                  you need to bypass asyncio-loop to the thread
        """
        # Initialize cfg.
        cfg.__init__(self, i3)
        # i3ipc connection, bypassed by negi3mods runner.
        self.i3ipc = i3

        # cache list length
        maxlength = self.conf("cache_list_size")

        # create list with the finite number of elements by the [None] * N hack
        self.geom_list = collections.deque(
            [None] * maxlength,
            maxlen=maxlength
        )

        # we need to know current resolution for almost all operations here.
        self.current_resolution = Display.get_screen_resolution()

        # here we load information about useless gaps
        self.load_useless_gaps()

        # config about useless gaps for quad splitting, True by default
        self.quad_use_gaps = self.conf("quad_use_gaps")

        # config about useless gaps for half splitting, True by default
        self.x2_use_gaps = self.conf("x2_use_gaps")

        # coeff to grow window in all dimensions
        self.grow_coeff = self.conf("grow_coeff")

        # coeff to shrink window in all dimensions
        self.shrink_coeff = self.conf("shrink_coeff")

        self.bindings = {
            "reload": self.reload_config,
            "maximize": self.maximize,
            "maxhor": lambda: self.maximize(by='X'),
            "maxvert": lambda: self.maximize(by='Y'),
            "x2": self.x2,
            "x4": self.quad,
            "quad": self.quad,
            "grow": self.grow,
            "shrink": self.shrink,
            "center": self.move_center,
            "revert_maximize": self.revert_maximize,
            "resize": self.resize,
            "tab-focus": self.focus_tab,
            "tab-move": self.move_tab,
        }

    def load_useless_gaps(self) -> None:
        """ Load useless gaps settings.
        """
        try:
            self.useless_gaps = self.cfg.get("useless_gaps", {
                "w": 12, "a": 12, "s": 12, "d": 12
            })
            for field in ["w", "a", "s", "d"]:
                if self.useless_gaps[field] < 0:
                    self.useless_gaps[field] = abs(self.useless_gaps[field])
        except (KeyError, TypeError, AttributeError):
            self.useless_gaps = {"w": 0, "a": 0, "s": 0, "d": 0}

    def center_geom(self, win,
                    change_geom: bool = False, degrade_coeff: float = 0.82):
        """ Move window to the center with geometry optional changing.

        Args:
            win: target window.
            change_geom (bool): predicate to change geom to the [degrade_coeff]
                                of the screen space in both dimenhions.
            degrade_coeff (int): coefficient which denotes change geom of the
                                 target window.
        """
        geom = {}
        center = {}

        if degrade_coeff > 1.0:
            degrade_coeff = 1.0

        center['x'] = int(self.current_resolution['width'] / 2)
        center['y'] = int(self.current_resolution['height'] / 2)

        if not change_geom:
            geom['width'] = int(win.rect.width)
            geom['height'] = int(win.rect.height)
        else:
            geom['width'] = int(
                self.current_resolution['width'] * degrade_coeff
            )
            geom['height'] = int(
                self.current_resolution['height'] * degrade_coeff
            )

        geom['x'] = center['x'] - int(geom['width'] / 2)
        geom['y'] = center['y'] - int(geom['height'] / 2)

        return geom

    def move_center(self, resize: str) -> None:
        """ Move window to center.

        Args:
            resize (str): predicate which shows resize target window or not.

        """
        focused = self.i3ipc.get_tree().find_focused()
        if resize in {"default", "none"}:
            geom = self.center_geom(focused)
            win_action.set_geom(focused, geom)
        elif resize in {"resize", "on", "yes"}:
            geom = self.center_geom(focused, change_geom=True)
            win_action.set_geom(focused, geom)
        else:
            return

    def get_prev_geom(self):
        """ Get previous window geometry.
        """
        self.geom_list.append(
            {
                "id": self.current_win.id,
                "geom": self.save_geom()
            }
        )
        return self.geom_list[-1]["geom"]

    @staticmethod
    def multiple_geom(win, coeff: float) -> Mapping[str, int]:
        """ Generic function to shrink/grow floating window geometry.

            Args:
                win: target window.
                coeff (float): generic coefficient which denotes grow/shrink
                               geom of the target window.
        """
        return {
            'x': int(win.rect.x),
            'y': int(win.rect.y),
            'width': int(win.rect.width * coeff),
            'height': int(win.rect.height * coeff),
        }

    def grow(self) -> None:
        """ Grow floating window geometry by [self.grow_coeff].
        """
        focused = self.i3ipc.get_tree().find_focused()
        geom = win_action.multiple_geom(focused, self.grow_coeff)
        win_action.set_geom(focused, geom)

    def shrink(self) -> None:
        """ Shrink floating window geometry by [self.shrink_coeff].
        """
        focused = self.i3ipc.get_tree().find_focused()
        geom = win_action.multiple_geom(focused, self.shrink_coeff)
        win_action.set_geom(focused, geom)

    def x2(self, mode: str) -> None:
        """ Move window to the 1st or 2nd half of the screen space with the
            given orientation.

        Args:
            mode (h1,h2,v1,v2): defines h1,h2,v1 or v2 half of
                                screen space to move.
        """
        curr_scr = self.current_resolution
        self.current_win = self.i3ipc.get_tree().find_focused()

        if self.x2_use_gaps:
            gaps = self.useless_gaps
        else:
            gaps = {"w": 0, "a": 0, "s": 0, "d": 0}

        half_width = int(curr_scr['width'] / 2)
        half_height = int(curr_scr['height'] / 2)
        double_dgaps = int(gaps['d'] * 2)
        double_sgaps = int(gaps['s'] * 2)

        if mode in {'h1', 'hup'}:
            geom = {
                'x': gaps['a'],
                'y': gaps['w'],
                'width': curr_scr['width'] - double_dgaps,
                'height': half_height - double_sgaps,
            }
        elif mode in {'h2', 'hdown'}:
            geom = {
                'x': gaps['a'],
                'y': half_height + gaps['w'],
                'width': curr_scr['width'] - double_dgaps,
                'height': half_height - double_sgaps,
            }
        elif mode in {'v1', 'vleft'}:
            geom = {
                'x': gaps['a'],
                'y': gaps['w'],
                'width': half_width - double_dgaps,
                'height': curr_scr['height'] - double_sgaps,
            }
        elif mode in {'v2', 'vright'}:
            geom = {
                'x': gaps['a'] + half_width,
                'y': gaps['w'],
                'width': half_width - double_dgaps,
                'height': curr_scr['height'] - double_sgaps,
            }
        else:
            return

        if self.current_win is not None:
            if not self.geom_list[-1]:
                self.get_prev_geom()
            elif self.geom_list[-1]:
                prev = self.geom_list[-1].get('id', {})
                if prev != self.current_win.id:
                    geom = self.get_prev_geom()

            win_action.set_geom(self.current_win, geom)

    def quad(self, mode: int) -> None:
        """ Move window to the 1,2,3,4 quad of 2D screen space

        Args:
            mode (1,2,3,4): defines 1,2,3 or 4 quad of
                            screen space to move.
        """
        try:
            mode = int(mode)
        except TypeError:
            print("cannot convert mode={mode} to int")
            return

        curr_scr = self.current_resolution
        self.current_win = self.i3ipc.get_tree().find_focused()

        if self.quad_use_gaps:
            gaps = self.useless_gaps
        else:
            gaps = {"w": 0, "a": 0, "s": 0, "d": 0}

        half_width = int(curr_scr['width'] / 2)
        half_height = int(curr_scr['height'] / 2)
        double_dgaps = int(gaps['d'] * 2)
        double_sgaps = int(gaps['s'] * 2)

        if mode == 1:
            geom = {
                'x': gaps['a'],
                'y': gaps['w'],
                'width': half_width - double_dgaps,
                'height': half_height - double_sgaps,
            }
        elif mode == 2:
            geom = {
                'x': half_width + gaps['a'],
                'y': gaps['w'],
                'width': half_width - double_dgaps,
                'height': half_height - double_sgaps,
            }
        elif mode == 3:
            geom = {
                'x': gaps['a'],
                'y': gaps['w'] + half_height,
                'width': half_width - double_dgaps,
                'height': half_height - double_sgaps,
            }
        elif mode == 4:
            geom = {
                'x': gaps['a'] + half_width,
                'y': gaps['w'] + half_height,
                'width': half_width - double_dgaps,
                'height': half_height - double_sgaps,
            }
        else:
            return

        if self.current_win is not None:
            if not self.geom_list[-1]:
                self.get_prev_geom()
            elif self.geom_list[-1]:
                prev = self.geom_list[-1].get('id', {})
                if prev != self.current_win.id:
                    geom = self.get_prev_geom()

            win_action.set_geom(self.current_win, geom)

    def maximize(self, by: str = 'XY') -> None:
        """ Maximize window by attribute.

        Args:
            by (str): maximize by X, Y or XY.
        """
        geom = {}

        self.current_win = self.i3ipc.get_tree().find_focused()
        if self.current_win is not None:
            if not self.geom_list[-1]:
                geom = self.get_prev_geom()
            elif self.geom_list[-1]:
                prev = self.geom_list[-1].get('id', {})
                if prev != self.current_win.id:
                    geom = self.get_prev_geom()
                else:
                    # do nothing
                    return
            if by in {'XY', 'YX'}:
                max_geom = self.maximized_geom(
                    geom.copy(), gaps={}, byX=True, byY=True
                )
            elif by == 'X':
                max_geom = self.maximized_geom(
                    geom.copy(), gaps={}, byX=True, byY=False
                )
            elif by == 'Y':
                max_geom = self.maximized_geom(
                    geom.copy(), gaps={}, byX=False, byY=True
                )
            win_action.set_geom(self.current_win, max_geom)

    def revert_maximize(self) -> None:
        """ Revert changed window state.
        """
        try:
            focused = self.i3ipc.get_tree().find_focused()
            if self.geom_list[-1].get("geom", {}):
                win_action.set_geom(focused, self.geom_list[-1]["geom"])
            del self.geom_list[-1]
        except (KeyError, TypeError, AttributeError):
            pass

    def maximized_geom(self, geom: dict, gaps: dict,
                       byX: bool = False, byY: bool = False) -> dict:
        """ Return maximized geom.

        Args:
            geom (dict): var to return maximized geometry.
            gaps (dict): dict to define useless gaps.
            byX (bool): maximize by X.
            byY (bool): maximize by Y.
        """
        if gaps == {}:
            gaps = self.useless_gaps
        if byX:
            geom['x'] = 0 + gaps['a']
            geom['width'] = self.current_resolution['width'] - gaps['d'] * 2
        if byY:
            geom['y'] = 0 + gaps['w']
            geom['height'] = self.current_resolution['height'] - gaps['s'] * 2
        return geom

    @staticmethod
    def set_geom(win, geom: dict) -> dict:
        """ Generic function to set geometry.

        Args:
            win: target window to change windows.
            geom (dict): geometry.
        """
        win.command(f"move absolute position {geom['x']} {geom['y']}")
        win.command(f"resize set {geom['width']} {geom['height']} px")

    @staticmethod
    def set_resize_params_single(direction, amount):
        """ Set resize parameters for the single window """
        if direction == "natural":
            direction = "horizontal"
        elif direction == "orthogonal":
            direction = "vertical"

        if int(amount) < 0:
            mode = "plus"
            amount = -amount
        else:
            mode = "minus"

        return direction, mode, int(amount)

    @staticmethod
    def set_resize_params_multiple(direction, amount, vertical):
        """ Set resize parameters for the block of windows """
        mode = ""

        if direction == "horizontal":
            direction = "width"
        elif direction == "vertical":
            direction = "height"
        elif direction == "natural":
            direction = "height" if vertical else "width"
        elif direction == "orthogonal":
            direction = "width" if vertical else "height"
        elif direction == "top":
            direction = "up"
        elif direction == "bottom":
            direction = "down"

        if int(amount) < 0:
            mode = "shrink"
            amount = -int(amount)
        else:
            mode = "grow"

        return direction, mode, int(amount)

    def resize(self, direction, amount):
        """
            Resize the current container along to the given direction. If there
            is only a single container, resize by adjusting gaps. If the
            direction is "natural", resize vertically in a splitv container,
            else horizontally. If it is "orhtogonal", do the opposite.
        """
        if direction not in [
                "natural", "orthogonal", "horizontal", "vertical",
                "top", "bottom", "left", "right",
                ]:
            try:
                amount = int(amount)
            except ValueError:
                print("Bad resize amount given.")
                return

        node = self.i3ipc.get_tree().find_focused()
        single, vertical = True, False

        # Check if there is only a single leaf.
        # If not, check if the curent container is in a vertical split.
        while True:
            parent = node.parent
            if node.type == "workspace" or not parent:
                break
            elif parent.type == "floating_con":
                single = False
                break
            elif len(parent.nodes) > 1 and parent.layout == "splith":
                single = False
                break
            elif len(parent.nodes) > 1 and parent.layout == "splitv":
                single = False
                vertical = True
                break
            node = parent

        if single:
            direction, mode, amount = self.set_resize_params_single(
                direction, amount
            )
            self.i3ipc.command(f"gaps {direction} current {mode} {amount}")
        else:
            direction, mode, amount = self.set_resize_params_multiple(
                direction, amount, vertical
            )
            self.i3ipc.command(
                f"resize {mode} {direction} {amount} px or {amount//16} ppt"
            )

    @staticmethod
    def create_geom_from_rect(rect) -> dict:
        """ Create geometry from the given rectangle.

        Args:
            rect: rect to extract geometry from.

        """
        geom = {}
        geom['x'] = rect.x
        geom['y'] = rect.y
        geom['height'] = rect.height
        geom['width'] = rect.width

        return geom

    def save_geom(self, target_win=None) -> dict:
        """ Save geometry.

        Args:
            target_win: [optional] denotes target window.

        """
        if target_win is None:
            target_win = self.current_win
        return win_action.create_geom_from_rect(target_win.rect)

    @staticmethod
    def focused_order(node):
        """Iterate through the children of "node"
           in most recently focused order.
        """
        for focus_id in node.focus:
            return next(n for n in node.nodes if n.id == focus_id)

    @staticmethod
    def focused_child(node):
        """Return the most recently focused child of "node"."""
        return next(win_action.focused_order(node))

    @staticmethod
    def is_in_line(old, new, direction):
        """
        Return true if container "new" can reasonably be considered to be in
        direction "direction" of container "old".
        """
        if direction in {"up", "down"}:
            return new.rect.x <= old.rect.x + old.rect.width*0.9 \
                and new.rect.x + new.rect.width >= \
                old.rect.x + old.rect.width * 0.1

        if direction in {"left", "right"}:
            return new.rect.y <= old.rect.y + old.rect.height*0.9 \
                and new.rect.y + new.rect.height >= \
                old.rect.y + old.rect.height * 0.1

        return None

    def output_in_direction(self, output, window, direction):
        """
            Return the output in direction "direction" of window "window" on
            output "output".
        """
        tree = self.i3ipc.get_tree().find_focused()
        for new in self.focused_order(tree):
            if new.name == "__i3":
                continue
            if not self.is_in_line(window, new, direction):
                continue
            orct = output.rect
            nrct = new.rect
            if (direction == "left" and nrct.x + nrct.width == orct.x) \
                    or (direction == "right" and nrct.x == orct.x + orct.width) \
                    or (direction == "up" and nrct.y + nrct.height == orct.y) \
                    or (direction == "down" and nrct.y == orct.y + orct.height):
                return new

        return None

    def focus_tab(self, direction):
        """
            Cycle through the innermost stacked or tabbed ancestor container,
            or through floating containers.
        """
        if direction == "next":
            delta = 1
        elif direction == "prev":
            delta = -1
        else:
            return

        tree = self.i3ipc.get_tree()
        node = tree.find_focused()

        # Find innermost tabbed or stacked container, or detect floating.
        while True:
            parent = node.parent
            if not parent or node.type != "con":
                return
            if parent.layout in {"tabbed", "stacked"} \
                    or parent.type == "floating_con":
                break
            node = parent

        if parent.type == "floating_con":
            node = parent
            parent = node.parent
            # The order of floating_nodes is not useful, sort it somehow.
            parent_nodes = sorted(parent.floating_nodes, key=lambda n: n.id)
        else:
            parent_nodes = parent.nodes

        index = parent_nodes.index(node)
        node = parent_nodes[(index + delta) % len(parent_nodes)]

        # Find most recently focused leaf in new tab.
        while node.nodes:
            node = self.focused_child(node)

        self.i3ipc.command(f'[con_id="{node.id}"] focus')

    def move_tab(self, direction):
        """
            Move the innermost stacked or tabbed ancestor container.
        """
        if direction == "next":
            delta = 1
        elif direction == "prev":
            delta = -1
        else:
            return

        node = self.i3ipc.get_tree().find_focused()

        # Find innermost tabbed or stacked container.
        while True:
            parent = node.parent
            if not parent or node.type != "con":
                return
            if parent.layout in ["tabbed", "stacked"]:
                break
            node = parent

        index = parent.nodes.index(node)

        if 0 <= index + delta < len(parent.nodes):
            other = parent.nodes[index + delta]
            self.i3ipc.command(
                f'[con_id="{node.id}"] swap container with con_id {other.id}'
            )

==> win_history.py <==
""" Advanced alt-tab module.

This module allows you to focus previous window a-la "alt-tab" not by workspace
but by window itself. To achieve that I am using self.window_history to store
information about previous windows. We need this because previously selected
window may be closed, and then you cannot focus it.
"""

from typing import Iterator
from itertools import cycle

from cfg import cfg
from negewmh import NegEWMH
from negi3mod import negi3mod


class win_history(negi3mod, cfg):
    """ Advanced alt-tab class.
    """

    def __init__(self, i3, loop=None) -> None:
        """ Init function

        Args:
            i3: i3ipc connection
            loop: asyncio loop. It's need to be given as parameter because of
                  you need to bypass asyncio-loop to the thread
        """
        # Initialize cfg.
        cfg.__init__(self, i3)

        # i3ipc connection, bypassed by negi3mods runner
        self.i3ipc = i3

        # previous / current window list
        self.window_history = []

        # depth of history list
        self.max_win_history = 4

        # workspaces with auto alt-tab when close
        self.autoback = self.conf('autoback')

        self.bindings = {
            "switch": self.alt_tab,
            "reload": self.reload_config,
            "focus_next": self.focus_next,
            "focus_prev": self.focus_prev,
            "focus_next_visible": self.focus_next_visible,
            "focus_prev_visible": self.focus_prev_visible,
        }

        self.i3ipc.on('window::focus', self.on_window_focus)
        self.i3ipc.on('window::close', self.goto_nonempty_ws_on_close)

    def reload_config(self) -> None:
        """ Reloads config. Dummy.
        """
        self.__init__(self.i3ipc)

    def alt_tab(self) -> None:
        """ Focus previous window.
        """
        wids = set(w.id for w in self.i3ipc.get_tree().leaves())
        for wid in self.window_history[1:]:
            if wid not in wids:
                self.window_history.remove(wid)
            else:
                self.i3ipc.command(f'[con_id={wid}] focus')
                return

    def on_window_focus(self, _, event) -> None:
        """ Store information about current / previous windows.

            Args:
                i3: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        wid = event.container.id

        if wid in self.window_history:
            self.window_history.remove(wid)

        self.window_history.insert(0, wid)
        if len(self.window_history) > self.max_win_history:
            del self.window_history[self.max_win_history:]

    def get_windows_on_ws(self) -> Iterator:
        """ Get windows on the current workspace.
        """
        return filter(
            lambda x: x.window,
            self.i3ipc.get_tree().find_focused().workspace().leaves()
        )

    def goto_visible(self, reversed_order=False):
        """ Focus next visible window.

        Args:
            reversed_order(bool) : [optional] predicate to change order.

        """
        wins = NegEWMH.find_visible_windows(self.get_windows_on_ws())
        self.goto_win(wins, reversed_order)

    def goto_win(self, wins, reversed_order=False):
        if reversed_order:
            cycle_windows = cycle(reversed(wins))
        else:
            cycle_windows = cycle(wins)
        for window in cycle_windows:
            if window.focused:
                focus_to = next(cycle_windows)
                self.i3ipc.command('[id="%d"] focus' % focus_to.window)
                break

    def goto_any(self, reversed_order: bool = False) -> None:
        """ Focus any next window.

        Args:
            reversed_order(bool) : [optional] predicate to change order.
        """
        wins = self.i3ipc.get_tree().leaves()
        self.goto_win(wins, reversed_order)

    def focus_next(self) -> None:
        self.goto_any(reversed_order=False)

    def focus_prev(self) -> None:
        self.goto_any(reversed_order=True)

    def focus_next_visible(self) -> None:
        self.goto_visible(reversed_order=False)

    def focus_prev_visible(self) -> None:
        self.goto_visible(reversed_order=True)

    def goto_nonempty_ws_on_close(self, i3, _) -> None:
        """ Go back for temporary tags like pictures or media.

            This function make auto alt-tab for workspaces which should by
            temporary. This is good if you do not want to see empty workspace
            after switching to the media content workspace.

            Args:
                i3: i3ipc connection.
                event: i3ipc event. We can extract window from it using
                event.container.
        """
        workspace = i3.get_tree().find_focused().workspace()
        focused_ws_name = workspace.name
        if not workspace.leaves():
            for ws_substr in self.autoback:
                if focused_ws_name.endswith(ws_substr):
                    self.alt_tab()
                    return
 

x

Notes

I put stuff here in agregatted way. You can clone it from git repo if you like.

This stuff contains a lot of python scripts, which is not so easy to put here. I recommend you to get it from my github repo. For now it’s actively supported. Here is my current README.md taken from
https://github.com/neg-serg/negi3mods without screenshots and demo.

What is it?

For now this collection of modules for i3 includes:

main

negi3mods : application that run all modules and handle configuration of
ppi3+i3 and modules on python. Also handles TOML-configs updating.

modules

bscratch : named ion3-like scratchpads with a whistles and fakes.

win_history : alt-tab to the previous window, not the workspace.

circle : better run-or-raise, with jump in a circle, subtags, priorities
and more.

menu : menu module including i3-menu with hackish autocompletion, menu to
attach window to window group(circled) or target named scratchpad(nsd) and
more.

vol: contextual volume manager. Handles mpd by default. If mpd is
stopped then handles mpv with mpvc if the current window is mpv, or with
sending 0, 9 keys to the mpv window if not.

win_action: various stuff to emulate some 2bwm UX.

executor: module to create various terminal windows with custom config
and/or tmux session handling. Supports a lot of terminal emulators.

fs: fullscreen panel hacking. Works unstable, so disable it for now.

procs to run by negi3mods as another process

There are processes, not threads, separated from the main negi3mods event
loop to reach better performance or another goals.

For now there are no any processes started by negi3mods. I’ve considered that
this scheme of loading can cause various race condictions and another
stability issues.

procs to run from polybar

polybar_ws: async current i3 workspace printer for polybar.

polybar_vol : async MPD printer for polybar.

Dependencies:

Modern python3 with modules:

  • i3ipc – for i3 ipc interaction, installed from master
  • toml – to save/load human-readable configuration files.
  • inotipy – async inotify bindings
  • Xlib – xlib bindings to work with NET_WM_ parameters, etc.
  • ewmh – used to create EWMH helper.
  • yamlloader – module for the more fast yaml file loading.
  • pulsectl – used for menu to change pulseaudio input / output sinks
  • docopt – for cli options in negi3mods script

To install it you may use pip:


sudo pip install -r requirements.txt --upgrade

or


sudo pip install --upgrade --force-reinstall git+git://github.com/acrisci/i3ipc-python@master \
toml inotipy Xlib ewmh yamlloader pulsectl docopt

In case of pypy it may be something like


sudo pypy3 -m pip install --upgrade --force-reinstall git+git://github.com/acrisci/i3ipc-python@master \
toml inotipy Xlib ewmh yamlloader pulsectl docopt

or


sudo pypy3 -m pip install -r requirements.txt --upgrade

etc. Of course you are also need pip or conda, or smth to install dependencies.

Also you need ppi3 as i3 config preprocessor.

Run

To start daemon you need:


cd ${XDG_CONFIG_HOME}/i3
./negi3mods.py

but I recommend you to look at my config(_config). It can start / restart automatically,
because of i3 connection will be closed after i3 restart and then started by
exec_always.

Performance

Performance profiling

You can check measure startup performance with tools like pycallgraph.

Also you can try


kernprof -l -v ./negi3mods.py

for the more detail function/line-based profiling. As far as I know PyPy3 is
not supported yet.

For now negi3mods using cpython interpreter because of more fast startup.

Why

It is only my attempt to port the best UX parts from ion3/notion and also improve
it when possible. But if you try I may find that things, like better
scratchpad or navigation very useful.

Bugs

For now here can be dragons, so add bug report to github if you get
problems.