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
|
x
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
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
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
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
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
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
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
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.