From 2b1789d8fc3468a89988ec718801dcf8833c51e8 Mon Sep 17 00:00:00 2001 From: Tobias Manske Date: Thu, 5 Apr 2018 05:30:27 +0200 Subject: [PATCH] first changes --- i3/config | 268 ++++++------ i3/scripts/autorun.sh | 18 + i3/scripts/dmenu/Dmenu.py | 19 + .../dmenu/__pycache__/Dmenu.cpython-36.pyc | Bin 0 -> 904 bytes i3/scripts/dmenu/database.sqlite | Bin 0 -> 90112 bytes i3/scripts/dmenu/dmenu.json | 30 ++ i3/scripts/dmenu/init.py | 120 ++++++ i3/scripts/dmenu/modules/Application.py | 128 ++++++ i3/scripts/dmenu/modules/MPC.py | 118 ++++++ i3/scripts/dmenu/modules/Module.py | 25 ++ i3/scripts/dmenu/modules/__init__.py | 2 + .../__pycache__/Application.cpython-36.pyc | Bin 0 -> 4182 bytes .../modules/__pycache__/MPC.cpython-36.pyc | Bin 0 -> 3779 bytes .../modules/__pycache__/Module.cpython-36.pyc | Bin 0 -> 1521 bytes .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 203 bytes tmux/plugins/tmux-battery | 1 + tmux/plugins/tmux-copycat | 1 + tmux/plugins/tmux-online-status | 1 + tmux/plugins/tmux-open | 1 + tmux/plugins/tmux-plugin-sysstat | 1 + tmux/plugins/tmux-prefix-highlight | 1 + tmux/plugins/tmux-sidebar | 1 + tmux/plugins/tpm | 1 + tmux/renew_env.sh | 12 + tmux/tmux.conf | 384 ++++++++++++++++++ tmux/tmux.remote.conf | 10 + tmux/yank.sh | 68 ++++ 27 files changed, 1094 insertions(+), 116 deletions(-) create mode 100755 i3/scripts/autorun.sh create mode 100644 i3/scripts/dmenu/Dmenu.py create mode 100644 i3/scripts/dmenu/__pycache__/Dmenu.cpython-36.pyc create mode 100644 i3/scripts/dmenu/database.sqlite create mode 100644 i3/scripts/dmenu/dmenu.json create mode 100755 i3/scripts/dmenu/init.py create mode 100644 i3/scripts/dmenu/modules/Application.py create mode 100644 i3/scripts/dmenu/modules/MPC.py create mode 100644 i3/scripts/dmenu/modules/Module.py create mode 100644 i3/scripts/dmenu/modules/__init__.py create mode 100644 i3/scripts/dmenu/modules/__pycache__/Application.cpython-36.pyc create mode 100644 i3/scripts/dmenu/modules/__pycache__/MPC.cpython-36.pyc create mode 100644 i3/scripts/dmenu/modules/__pycache__/Module.cpython-36.pyc create mode 100644 i3/scripts/dmenu/modules/__pycache__/__init__.cpython-36.pyc create mode 160000 tmux/plugins/tmux-battery create mode 160000 tmux/plugins/tmux-copycat create mode 160000 tmux/plugins/tmux-online-status create mode 160000 tmux/plugins/tmux-open create mode 160000 tmux/plugins/tmux-plugin-sysstat create mode 160000 tmux/plugins/tmux-prefix-highlight create mode 160000 tmux/plugins/tmux-sidebar create mode 160000 tmux/plugins/tpm create mode 100755 tmux/renew_env.sh create mode 100644 tmux/tmux.conf create mode 100644 tmux/tmux.remote.conf create mode 100755 tmux/yank.sh diff --git a/i3/config b/i3/config index 9da4d8f..77d44f3 100644 --- a/i3/config +++ b/i3/config @@ -9,11 +9,30 @@ # # Please see https://i3wm.org/docs/userguide.html for a complete reference! -set $mod Mod4 +# {{{ VARIABLES }}} -# Font for window titles. Will also be used by the bar unless a different font -# is used in the bar {} block below. -font pango:monospace 8 + set $mod Mod4 + set $TERMINAL urxvt + + # gaps + set $gap_outer 25 + set $gap_inner 15 + + # Define names for default workspaces for which we configure key bindings later on. + # We use variables to avoid repeating the names in multiple places. + set $ws1 "1" + set $ws2 "2" + set $ws3 "3" + set $ws4 "4" + set $ws5 "5" + set $ws6 "6" + set $ws7 "7" + set $ws8 "8" + set $ws9 "9" + set $ws10 "10" + +# {{{ FONT }}} +font pango:Hack 8 # This font is widely installed, provides lots of unicode glyphs, right-to-left # text rendering and scalability on retina/hidpi displays (thanks to pango). @@ -29,142 +48,159 @@ font pango:monospace 8 # Use Mouse+$mod to drag floating windows to their wanted position floating_modifier $mod -# start a terminal -bindsym $mod+Return exec i3-sensible-terminal +# {{{ KEYBINDINGS }}} -# kill focused window -bindsym $mod+Shift+q kill + # start a terminal + bindsym $mod+Return exec $TERMINAL -# start dmenu (a program launcher) -bindsym $mod+d exec dmenu_run -# There also is the (new) i3-dmenu-desktop which only displays applications -# shipping a .desktop file. It is a wrapper around dmenu, so you need that -# installed. -# bindsym $mod+d exec --no-startup-id i3-dmenu-desktop + # kill focused window + bindsym $mod+Shift+q kill -# change focus -bindsym $mod+j focus left -bindsym $mod+k focus down -bindsym $mod+l focus up -bindsym $mod+semicolon focus right + # Program launcher + bindsym Mod1+space exec ~/.config/i3/scripts/dmenu/init.py -# alternatively, you can use the cursor keys: -bindsym $mod+Left focus left -bindsym $mod+Down focus down -bindsym $mod+Up focus up -bindsym $mod+Right focus right + # change focus + bindsym $mod+h focus left + bindsym $mod+j focus down + bindsym $mod+k focus up + bindsym $mod+l focus right -# move focused window -bindsym $mod+Shift+j move left -bindsym $mod+Shift+k move down -bindsym $mod+Shift+l move up -bindsym $mod+Shift+semicolon move right + # move focused window + bindsym $mod+Shift+h move left + bindsym $mod+Shift+j move down + bindsym $mod+Shift+k move up + bindsym $mod+Shift+l move right -# alternatively, you can use the cursor keys: -bindsym $mod+Shift+Left move left -bindsym $mod+Shift+Down move down -bindsym $mod+Shift+Up move up -bindsym $mod+Shift+Right move right + # Change split orientation + bindsym $mod+v split v + bindsym $mod+Shift+v split h -# split in horizontal orientation -bindsym $mod+h split h + # enter fullscreen mode for the focused container + bindsym $mod+f fullscreen toggle -# split in vertical orientation -bindsym $mod+v split v + # change container layout (stacked, tabbed, toggle split) + bindsym $mod+s layout stacking + bindsym $mod+w layout tabbed + bindsym $mod+e layout toggle split -# enter fullscreen mode for the focused container -bindsym $mod+f fullscreen toggle + # toggle tiling / floating + bindsym $mod+Ctrl+space floating toggle -# change container layout (stacked, tabbed, toggle split) -bindsym $mod+s layout stacking -bindsym $mod+w layout tabbed -bindsym $mod+e layout toggle split + # change focus between tiling / floating windows + bindsym $mod+space focus mode_toggle -# toggle tiling / floating -bindsym $mod+Shift+space floating toggle + # focus the parent container + bindsym $mod+a focus parent -# change focus between tiling / floating windows -bindsym $mod+space focus mode_toggle + # focus the child container + bindsym $mod+Shift+a focus child -# focus the parent container -bindsym $mod+a focus parent -# focus the child container -#bindsym $mod+d focus child + # switch to workspace + bindsym $mod+1 workspace $ws1 + bindsym $mod+2 workspace $ws2 + bindsym $mod+3 workspace $ws3 + bindsym $mod+4 workspace $ws4 + bindsym $mod+5 workspace $ws5 + bindsym $mod+6 workspace $ws6 + bindsym $mod+7 workspace $ws7 + bindsym $mod+8 workspace $ws8 + bindsym $mod+9 workspace $ws9 + bindsym $mod+0 workspace $ws10 -# Define names for default workspaces for which we configure key bindings later on. -# We use variables to avoid repeating the names in multiple places. -set $ws1 "1" -set $ws2 "2" -set $ws3 "3" -set $ws4 "4" -set $ws5 "5" -set $ws6 "6" -set $ws7 "7" -set $ws8 "8" -set $ws9 "9" -set $ws10 "10" + # move focused container to workspace + bindsym $mod+Shift+1 move container to workspace $ws1 + bindsym $mod+Shift+2 move container to workspace $ws2 + bindsym $mod+Shift+3 move container to workspace $ws3 + bindsym $mod+Shift+4 move container to workspace $ws4 + bindsym $mod+Shift+5 move container to workspace $ws5 + bindsym $mod+Shift+6 move container to workspace $ws6 + bindsym $mod+Shift+7 move container to workspace $ws7 + bindsym $mod+Shift+8 move container to workspace $ws8 + bindsym $mod+Shift+9 move container to workspace $ws9 + bindsym $mod+Shift+0 move container to workspace $ws10 -# switch to workspace -bindsym $mod+1 workspace $ws1 -bindsym $mod+2 workspace $ws2 -bindsym $mod+3 workspace $ws3 -bindsym $mod+4 workspace $ws4 -bindsym $mod+5 workspace $ws5 -bindsym $mod+6 workspace $ws6 -bindsym $mod+7 workspace $ws7 -bindsym $mod+8 workspace $ws8 -bindsym $mod+9 workspace $ws9 -bindsym $mod+0 workspace $ws10 + # reload the configuration file + bindsym $mod+Shift+c reload -# move focused container to workspace -bindsym $mod+Shift+1 move container to workspace $ws1 -bindsym $mod+Shift+2 move container to workspace $ws2 -bindsym $mod+Shift+3 move container to workspace $ws3 -bindsym $mod+Shift+4 move container to workspace $ws4 -bindsym $mod+Shift+5 move container to workspace $ws5 -bindsym $mod+Shift+6 move container to workspace $ws6 -bindsym $mod+Shift+7 move container to workspace $ws7 -bindsym $mod+Shift+8 move container to workspace $ws8 -bindsym $mod+Shift+9 move container to workspace $ws9 -bindsym $mod+Shift+0 move container to workspace $ws10 + # restart i3 inplace (preserves your layout/session, can be used to upgrade i3) + bindsym $mod+Shift+r restart -# reload the configuration file -bindsym $mod+Shift+c reload -# restart i3 inplace (preserves your layout/session, can be used to upgrade i3) -bindsym $mod+Shift+r restart -# exit i3 (logs you out of your X session) -bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'" + # exit i3 (logs you out of your X session) + bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'" -# resize window (you can also use the mouse for that) -mode "resize" { - # These bindings trigger as soon as you enter the resize mode + # UNBIND F1 + bindsym F1 exec --no-startup-id echo > /dev/null - # Pressing left will shrink the window’s width. - # Pressing right will grow the window’s width. - # Pressing up will shrink the window’s height. - # Pressing down will grow the window’s height. - bindsym j resize shrink width 10 px or 10 ppt - bindsym k resize grow height 10 px or 10 ppt - bindsym l resize shrink height 10 px or 10 ppt - bindsym semicolon resize grow width 10 px or 10 ppt - # same bindings, but for the arrow keys - bindsym Left resize shrink width 10 px or 10 ppt - bindsym Down resize grow height 10 px or 10 ppt - bindsym Up resize shrink height 10 px or 10 ppt - bindsym Right resize grow width 10 px or 10 ppt + # MEDIA KEYS + bindsym XF86AudioRaiseVolume exec --no-startup-id amixer set Master 2%+ + bindsym XF86AudioLowerVolume exec --no-startup-id amixer set Master 2%- + bindsym XF86AudioMute exec --no-startup-id amixer set Master toggle + bindsym XF86AudioPlay exec --no-startup-id playerctl play-pause + bindsym XF86AudioNext exec --no-startup-id playerctl next + bindsym XF86AudioPrev exec --no-startup-id playerctl previous + bindsym XF86AudioStop exec --no-startup-id playerctl stop + + + # {{{ RESIZE MODE }}} + mode "resize" { + # These bindings trigger as soon as you enter the resize mode + + bindsym h resize grow width 10 px or 10 ppt + bindsym j resize grow height 10 px or 10 ppt + bindsym k resize shrink height 10 px or 10 ppt + bindsym l resize shrink width 10 px or 10 ppt + + # back to normal: Enter or Escape or $mod+r + bindsym Return mode "default" + bindsym Escape mode "default" + bindsym $mod+r mode "default" + } + bindsym $mod+r mode "resize" + + # {{{ GAPS MODE }}} + mode "gaps" { + bindsym h gaps outer all plus 5 + bindsym j gaps inner all plus 5 + bindsym k gaps inner all minus 5 + bindsym l gaps outer all minus 5 + bindsym i gaps inner all set $gap_inner + bindsym o gaps outer all set $gap_outer + bindsym u gaps inner all set 0 + bindsym p gaps outer all set 0 + + bindsym Shift+h gaps outer current plus 5 + bindsym Shift+j gaps inner current plus 5 + bindsym Shift+k gaps inner current minus 5 + bindsym Shift+l gaps outer current minus 5 + bindsym Shift+i gaps inner current set $gap_inner + bindsym Shift+o gaps outer current set $gap_outer + bindsym Shift+u gaps inner current set 0 + bindsym Shift+p gaps outer current set 0 - # back to normal: Enter or Escape or $mod+r bindsym Return mode "default" bindsym Escape mode "default" - bindsym $mod+r mode "default" -} + } + bindsym $mod+g mode "gaps" -bindsym $mod+r mode "resize" - -# Start i3bar to display a workspace bar (plus the system information i3status -# finds out, if available) +# {{{ STATUS BAR }}} bar { - status_command i3status + position top + + status_command bumblebee-status -m disk nic sensors battery caffeine amixer brightness datetime -p battery.device=BAT0,BAT1 brightness.device_path=/sys/class/backlight/acpi_video0 disk.format={left} nic.states=^down sensors.path=/sys/class/thermal/thermal_zone2/temp -t greyish-powerline } + +# {{{ GAPS }}} + gaps inner $gap_inner + gaps outer $gap_outer + +# {{{ NO BORDERS }}} + default_border none + default_floating_border normal + +# {{{ KEYBOARD MAP }}} + exec_always setxkbmap de + +# {{{ AUTOSTART }}} +exec ~/.config/i3/scripts/autorun.sh diff --git a/i3/scripts/autorun.sh b/i3/scripts/autorun.sh new file mode 100755 index 0000000..13ee4db --- /dev/null +++ b/i3/scripts/autorun.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +function run { + if ! pgrep $1 ; + then + $@& + fi +} +run compton +run kwalletd5 +run xscreensaver -nosplash +nitrogen --restore & + +# Programms +run nextcloud +#run telegram-desktop +#run discord +#run thunderbird diff --git a/i3/scripts/dmenu/Dmenu.py b/i3/scripts/dmenu/Dmenu.py new file mode 100644 index 0000000..79d91d7 --- /dev/null +++ b/i3/scripts/dmenu/Dmenu.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import json +import subprocess + + +class Dmenu(object): + def __init__(self, items: list): + with open(os.path.dirname(sys.argv[0]) + "/dmenu.json") as fp: + self._dmenu = json.load(fp)["dmenu"] + self._items = items + + def run(self): + """Returns (exitCode, stdout)""" + p1 = subprocess.run(self._dmenu, input="\n".join(self._items), encoding="utf-8", stdout=subprocess.PIPE) + return (p1.returncode, p1.stdout) diff --git a/i3/scripts/dmenu/__pycache__/Dmenu.cpython-36.pyc b/i3/scripts/dmenu/__pycache__/Dmenu.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68927eaead43b53e43a0740ae3c77311eb6843e5 GIT binary patch literal 904 zcmZuvyKdD$5Z#x39p@pSNJxZGM1>K=ijsl?p-2HCnkYa4a$`B(bz;lcHoI#?u5PNN zMf?Ciz=!Y?+fwlhRLty6q!g?*XUDTMXJ(JRx4Ifv)vs)s5%QbdS_1q7FtZOJiKH#b z7CX(Te`gu! zg*|f=0r&52de%=> zstdUtHKncxYZmjQUWlFH+(mg_ceTy)Wgsvi0HF`)7M-mvQf`h* zybdnZ4AZ<;Pr$LHfqOSh(uTt>P%DT9l7ABybWWQPY*H~)Ww!NR*->|9`?303+rwU} zr`yKLezb|B@hli^_3Sy^FQ~g=v@TLz*~_{+b>VeZckgy}UrWy7u)YXNs5r0)Zueg^+j!XDDu(&F;Kew2i$5q_OK zcX6IidpWk~ujKjXv1qSPL|YrH??Jt%yVU}jweN${#bCZqMArh1ktEasrbGyMtTzB$ b*q=12v_9p3Y%Y4S8095SZLm6`{^9=sYM9kr literal 0 HcmV?d00001 diff --git a/i3/scripts/dmenu/database.sqlite b/i3/scripts/dmenu/database.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..738d8b3385a6ce1e8f582a36f1edfc1058c3b8c2 GIT binary patch literal 90112 zcmeHw3yfUXdEQVIDK5pGku1xyEz7#5EK4$Zclp+1BumSEkSlVROYZWehpD?WcV{nm zX72FbncZ2cZbB|8+OCs0ZIGrxkQzn$KvSea(WXUVq(vMAO%S9>S`B^> zq(P7bMZfRdd+t5=H8Yo_6~`IEKH53=Jpcbc|M|~<{{KIVXHQ#gQ(Li}hSAmz7al2W z+g5l((+Y*{(}hCeWB9-Pw+(+hD1YF;{N3jt+8F!PPmh-VxUjwWy@lehmwpld8U7nW zUgTsPfobsR>oxaPHv z*{sBWJ~(-9dSYo>Tbek2dRmK>eZFZl%>AhO!j1N}hn{%y$!%M&i#E+>+p$d7|IY)F zHvG)z-CEgr>dZ~gFKOrJXV0FW-mkqk zkG|g1mZmQ);m1|iZU#SAt+pFBHGgJFn?Ha0v^F(8Gjaa(l6C;)tCq{TR{IodZ?^nC zv_vz{typ#QIkd(9dVRm3Kd|$Oi6^%etY+2h`oJ+dZCm`l;?MNTLI02KAhlg2w;lNZ zqotn|f0q8V^hc%tQ~JHq@05PC^j}N=vGngszf$_e($AOvRq0=r{#og#y<#jn{5OQa z5CTI83?VRtzz_mM2n-=GguoC2LkJ8ZFoeJm0^dFeJhX|ncU?m=Vj8DEnp!9c2KUVtT($`C0E!`^p)l#?QlJO^7JsPt{lzaAZx;VbalP0oUM*IOUo2iKo-3X%&J^D$ zzEV6=94kIw+*|xq@ngkDio1&sj{d*VKOX)4(cc^W?a}`>`kzMs{phcZ{+rRC8~x_! z&y4=`=sz6&iP0Y){gKhXKKixM4@SRt^!jLfv^lyux;*;M=!MaR(Kkn@M!zun(&(Yl z{iC15w1@wO5Ew#W2!SC4h7cG+U9uQ2TP>!x&9vEM+GsGX*O{)ZF}-?~sbw);U1eIUF2+)8%ERhQai`_n2O}!t{$@Wcu#AOy7Bj>DzBJed{fzmoGEDbcyN3i%c(E zV0!*M)1@V*i;GOponw0TEYpPrrf1GDou6kqH^=n!X{M)6F@5t*rn9q5Po89Y;sn!~ z8K%?IOsA%pPEImCew^vV1k*R(VEX#&Ouz62rmwxm^wn3Hmdi|Ed4=iAFEf4VC8jUF z$n@ATrbmx5J#vKU;loT19b$U$AkzZ}n2wJ#9UEh+>rD6WXS#16)6ajN=?gC~eg1i- z&ppTVbDv}S?6XWi`&p*XJi~PFUZ$V<3{y>G`t;LGKmBQ@Pd&x-Q=ek`>BA2* z-MyRX$OzM2yO=)o5YwGIneN!Z^uY(2Zr{%Ifd{s49~q$*U|Z=A3i#jf-w*;r2n-=G zguoC2LkJ8ZFoeJm0z(K4Auxo%5CTI8eD^@$;ceTW+~)fMygynzRCr*|vBI8XMY;kE z{|zB9guoC2LkJ8ZFoeJm0z(K4Auxo%5CSm>Y@NP7QkcD6*|zH$Yqr@o>vii*ZFXvU zLOWk?TZklPYSUG#ZQ0HD_pX{o)pYj0qV1hB-LYp|z2lEZH@)#_&vsaNwqiH?7iP&N(`i^u zqt4PRM%^`;x!5+GcBgf5ZnAE;Es>Q==dFql0F;M8oo zr1RsHxo+0&7Ux#RJjyIVr*3{5@3_vmyJ|S*xDmGDjz_y1t9nq~`=tkzp7Pb*_x`TJ z#G|`*J@)t?3r3#4Hn3ptc2Bf!epyEiO?50*-4{b31`Y*+0(4}Uo+>Z0O~to znzbDxg-_gknbW-JASQ|vPSpFIJ=G0Sz9&K@tiA(g`oWhT94S2WwYNFb(_DjR#2QQ! z8~mjg&i{hxxZG8-R~{TYz!|4**7Nq1<+iY;v0$);O9>!jb&1vn*}=W>mM`h&%&LCU zXzOGR__^3woYp68$JB!ob4Ii3SnI}VtJ&%L+4`CGs_BS*KaCYVd!7TCvl`}-KNv+6 z7+-Hz$LyBb^!S$T)W*zi+iVgj<*x2lR?UW~ucFtw>4?J0WXCEE%N6}fDNF%BJ3V1G zO~a6}!=b+=E~@THDqzPmP1sFdhxO zuvs;ns&(D0YGDD_V)J&} zS`jF2I~^gUUD8j?p3&#+nGSe0P%rb7!kYpXl}oerOZus)X??-5+jhmS3z?0pX8&rt z-P*r`|8JPfRb!n+J^BfFcYhlFf-qLKQ+L#yGxWIp&JpmH)*C+B7%x4TK)DH++FRlku`-i>sn- z`Zd@^KhEU$m?&8# zIo71n1d(f;kjrwX%*_qksd|)T*Ksr|H;krH!+*_gs}6=yMue!fvZ$~LOf(|Qp^2DI zR=wlKU@X{#WNBmswWi%L$L9Ug>^PM-O0J@aAuKy}yyJ4>zzSnkrL3Oha*G~pPRyT~ zn=YSNI#oW8@q#Cm$qUOf00Y>dm@Yd~Ih)nAtLAtWv{Y%^&SqKVbY1<$>pJ-f#T&EQ z>zfWSEB1Z)y46a_QW{LDBzuchl-c0+$&M;5%Z`<=CHt`G4Ct3dPZf|L5*2yFRhwkGFl}!Qb5eXWtda|L=lH+`7Dh^U;@IfTp#wv!I=7!mCmC%*@>0f@twWfg1WbD)V=01xrDNJ zMNv~egVWGNwMvoZxNWZuw4W)H47tD{n%|U)H!7PWg@ZynR+$jPSngC-wQv2*%1V>M zuw<^vw|6=R4y+s#-*aJAH@tZ+W1g(okME_7cJG%IAvKF%rqsLnSa+oG+SU%p`4t{z zLIT&;O=kuAdv&f^CGyRdLL$~6D<$`Sf1Sp#{~<%=)~DCOSMEFq@#We@Xdq0@XlS#q zr}33QR+03el;@~ptAEOD*jmlB>$Z34p(%1aiz{qr1}8*q)9yH0rQ( zB!vK#Aq9Eq`XNZP-e8~-G7ub?`};(_Zg05hbER@*+0@)sd!wmAw+xw4+&BsCM!$t3 zryEW(WD5hVm+b1dCOc3;-uV)ijeXj*a1iZD8%a4Id)B4!2FL=R#-JuU&0Ct-AeIWN>ZwOai2?0q_E)>~pP0Gkzenz8AJr53n_ zgMMOQlPl3f3`eM}Dlbzrt){AAe8n-#l?HAY%Izci!2MA&5DO5!Ea1M9>PwBYc%-tosE6u=7C66gx0)C-om z0T7EpL-1;M-2Ix>T!U1KOLR})22}GYGYtY8+HD%(GlGpu`dMx1ZQHId+g%m8iZro` z%lg(h)XT;=>==AvAq1BSQQA^w5@^BLt#>|52#i51 z* zo^~l^dF0>_?jEJ-BlKCvKr(U97QFNSPZo-w-P7Ft+q-^r=T~+d-1d_XGMwP>-**-S zZtrd3!s08ZpbB_<#=CJzTaEtlBGi+T{*vOET$X-IEEMC-7tLjV>G7ko>CNJ#K(5`y zj^}oq6;BBumtk&95O<9!#^8)zqSY07K9^jz=mG_kJb*}(sxScoZrjxr0$Cjf6xobaxL)#DE? z8OuH{%3{L==!MvzC>KJ6m-%P(PHi9=!$(QDe>|q_V9Fk8sMct)N;kfEZKUw>&F8R% z`1gtaKr$~D1WX>rBTRT^zIbJ za8P%;(Sg?A_DC+1n)|`kA7)570}xKE&aGk-CxtstLm_bmE8_Zw>8z!(x8S580V@l~ zAPbJSVbt4DKSE@5G+7ENpJiwrknM3T3Dssp_asYPLOUk(oI-GyXuwwzy(#1k2|CKc zfy|Zx|5RN9PK@Ch6XpeF@3NE=khfvSJGe3fi_+yE>`W9(Ed~1o8egywCQ3!8FVv@& z1}ksDkkdPQMy4ja?|B*5j~Id$bCE-$)~(sb?!w_amq8FWwlA*2##oqLLGuNf~v;FrMr$xF^n~^#1V&T*Jn@? zz!I(b@vHRFAGyIG#CQ{(D^kJXu|vnk4hbMc&c_~Ul5y}`R6P=sZ7P}fqZ4H&acJeD zoB`U|+EE`VJbn9NG8fEcACA6_-bWe9U4qgv`Vjdz6PudZH5y`4DLir8#^!lta?V)G zt5cL%5p`o2nRIN|)nVt?78-!%j#aO^VzXmG$;m4X^s3i7miFw5iV4volti5tb5l`q zNuQ%P0dPIx)I9qg=?{>>ZnBZVPBL+*%D9TGHQv?5=qo(34kXY8lg)*-M6IpMS4Rqm zZ+{O~&-X0aEA0)qBxpv*UHR5GaS51eHI>+Wh2R5&} zes?MgGYk1VRxU0Dbb1#Y#;kNaYbL-xbrGauJrheAI9HJegu#UZ8|`?fY2j?)Pi7$8 zWzI4eF8j8^^Z!HJ_7z6|#~$OMFYJ8(!N2;z@8jq2UlIaaQ`Sh~+1tDkG0(P-U=MMv z8f;{H3L=tYsTD}fv9q=~Pp=rz#>!S%NI7$R+1iQovo1|!HMXY^cx0=Aa!3qJzt%p& zX5@m1=c}Zuv;{sROA1O&T^4y1!cjJOwhcvl==H<__LCIka5$k{Eh) z@1C4mT%e@a6NRZ$m3Wmc#L#upfNcdXm+N&6UBD9B^Q2Os%nFk2tybL@iiizq1{OgJ z1}`hb6_ol(;!)<^yPk3)Jjs_QsNTH*r#X8d{uB%XFs4`EKxgTArLF5V9Hu|ul6w18 z9ZFMYFsaNhOBlu$(zMmoyh8#cG8~IwZ`NpN*E zw&9wqjNQFQ4|6P<}-Jlp)nmJO?|HiYN!s*a^MNB1)r}(;AOiX$+ zaAL|atd}2)B!&23*u+5S_Z?AYV2Usax6*d`gl~*lJ~S2lc|llT2R{>jEl?~ZomhO- z3C1CrnleluO?PLSiWMv@MhCG?Rk7K6Fs?|<{0$IV^z9v z#9LH9j%7BnZow-jQn#ybv}%zYs-`QjlCKK_O?+zLbkkX`ThK2!WpO!)oqEMFO?vo5 zN>}QJW4TlVxsm)X%rI&uPe4%%`Brr$R?dZ0xR@=8OYU`X3g@>N`}GMya$c5D80#c# zhT#`PMJL^{HTFQmjs4)X@D!<#D7|wuZ(3b?{&ZXv5bH^>(oB8@-A1ajkIH*J(k?I3 zFUlyx8U&bclusM5KxzwPJh>>f6O!iUtO|~mklVF|0Q6zr1%XsLHej14ZB(jG*{;^g z?s~0Epsu@Bu_k756A$B)g~0fQn!2Nejz1ygormQjn zBX~D88lXJ*Igv{ma9>(99b7iJU^igVO<4D}U6=%HAVZp~9k*%=R;3Lkb9n{-ekr9Kl!+A+ z0(`VTKR2Vi6ZPwAt+BXMPn3p`Xf||0Ez-P~jpJ)PHwh3FMdpiQh!!GhK<10fq1~eg zUe+t{RgBM~tBX%?*>;j21*8u|2H<6-Ou}ug8I?#;uyf|^qiNZ(BZAe+B@t{rwTh#! z!_3e#70uYy)x{ND0HxVV27ckaHL5ziJd3B&Bgdhz5S$#aGJT+ zS+d%#$KInw>BEpJ)}&2P(^$7^v?=Jh>XHIp2DBxP%&x7A_yM|JH#$u?s)PnJG0i{B zWpu@KDf7^fipVG%l+uD{G)~)bn0D(+Hg18S!xTgmy%Y^>#=RQ4%19CdML+W*$(jvQ(1N4$>UwG^rHpBRRLHog2x7Cr1;Z{HjNPHx0vi9hfF(m8AOf6ly%`*!DzC3C>kJ)Qw4FYgV-iGhwxfTinoI`o>>}kPB?p8}c;Bn#~zLtPJ-HI=8R*~<1}Ozeg>x~2~!+p5Jd(D#aG71xh;&Zctc{fZ4nhOnU!+yM zQF{k!?3>d(SmQ*e`A52N_)2DNN=`O!T#FjZ-xP@A^H5#8Oez8hspQ$!+?(aEGsT26n zCn?VVg-;hAo_Of3oqzMezqb9KKk!T2erMZGWZ~Zp^D;p&K@ccO_1?vi!e>N4IWQ?)BG?GW z1^ZgLZo|Aj6BX*a-qQ?0@!?MK>IEG9Z=N9z18rP5!yqerf*q$mzQ`?Qb_&`LhCUVU zm%Ugq#S|#}*ye{j*|X;v2a#s>CTxkJYm%7cuk0zhQlXo>oU zMEexzq|#265+-?0m5Xpc_k%r^LmQIC3hpc_W|_8i zP^Ui`yCQc9WJI>KDr8rjqIYODuXV(MRJkN)clUlGo%S1!UWof+F3bPgxZGxcbN6;~ zB@hX{5C7!Kp6|;t|H`Xp5eM!EFJqaXKQVt^o2Az{{n@P+J*yZj+BZW>Tl78dyE7K3 zB(&ar5vVb;I}UqzT3sYaPJD1Q+*R^G=f`)NLBLa(N34MQ5S+wtmc`kJ&B`Nq4WXkE zW#uJ*y((7R@(W8#7Q{UM9zf+97+)w>N#|g%XF9C2E%hUeC=pO~Fn;J6YkL1qZm z>H?IWqg|q)qTvHH#tsQ5nnWoP=Jz-r5|EMqRUl<5s8!{$XYsS#X!*83wZkh$SN?6S zgcd;-E}ErzP^%(~;8F%!E?PwBMspL+?_n94zihQr^Fasc6+AEcupwBx$d28b75v?< z8NuJ(SSMXCN^G?g%nLU_m2fr{puvmz8OavVt1X9iP$C1-hJ%j064t7p0TwD7z1lKs zL9vz_jSPygO)v~~td?vE;U0p*7L0U*9|-@&7y|xpSo|bhjcEZS;jz(>e_uu5OD~rL zIdtH_f#7F|t1bc}xxQCsKOvkm&Be;p3OxVsd~mr?oY?c|?u}hPwd4Qd$MD|}0z(K4 zA#h(Hu=U6S4FK`H%rmAiNzU&<`sarS@y7;`aub#k)e?PF(fy8g<$u#s64PQ1EsZ|*6e_7z7N#91^G z!^lN147_)tCL%l$A&TXS=3VFe_6-s#p-Dy)k<2}(YPOt^ta6KdiobAbgAvKE##a#f zOMd;`r$zX4_7^o5=;K2zXFCBm5f?VuZGB;tw|y`l_8+zI)Ya-laNJmj8;|f%6@l5x zo{Z&Tz>D;dY&R{8oktt&`xj_mN0nT)n%6sU{03j_x%M*XTrSDXmOa;Sv`&;NYZ4dB&_py5 zzn7NdV1#MI3M%hs{5XqoMzPa$R^am2gnbkhXhI`{UvTlO!TFu=VOGgjc?8QvWrvZme3`h51Q@T&3~5?#Hw0Nhyl{#P@!aM3U89 zj5MY~dpE6#*a+HNCJc_@POBShe2)kwra*W&DnVKj!GV~uyxtVfAE}7Tpk_%3O)ut- z2puFSB)Y=AJziQ8a`f3ak8v>;?#GHb@?7{-EAafkW82pYyB>LHddH7$e+_>Q|NS{a zVC(a5^0f3iMe|cjW*wx(wS+jWJr_|1fl4Z0gJu^kf!(pCqcL;Y^mC5NeG-PNUVF*N zcn%!*tdz&cT-ST?>XHs0JGi&6Qj%P=Y!{x+=FxGlxpABn%Q! zOkyy|wPG^r-g;{m!S2313S>2oBlnsFX>u-MRc>I_N?4uq zQZ~Uw?q_5NuC|V6T7!BlCVA1a90xNc{)0>%!S1VV5rZfXql@t~M1%5p+IjzNrx}cC z32}A%1Vv26s62|ubKKpA#0uufi62!upAWj7LMx&v&jF9((|_raTswt{iT zDsMf160bAd)KFUD(LgJ8A zCH78Sf6JKrX~Fl!VA-Ig&wGfw9|%Sn+^!#*{7H%^T)G%oE<|Nlzj5*ePBS+)AOi_| zcjdl3YO$bvAWS466%ztTnewgDe$!EEi8{F^lU4Z#8Z)q3`0>{{mD9>yHlH#~qq%#s zT97mm9}HoWf#WDA@~z$#unuU5pw#GP@lI6Sojro9m`td$JB{8`%np>|q3J=qo-~%{ zs1mje>c{jj1P#~hR^YP|Zx(R{(m&9w8etE-W0R}f;$_z0$uMQe2#tWC1PDo+)wK6{ ztYN^Le!9s^$m&?ki%tM0M~8udMrNO~MBi>D0Q^xRk|Rv|619XONX1=`OcJ4zmRG#+ z!wNhiMdDSGu!iOtyuW22Y9L+}TsN?rME&Imo4Sp7_xeHokbXGQd@SPBCdOs6?c z4$8(zE6B6mD-HKbavF@W7N@VSoA+3gT4V}9F$?rz^9Ue*c{4?%3c_oFd)aYAi(hVMG*pp)xDiU;{Xw% z!!36qtK$FNdvd%X=m^yh-+YNZ-5Ct#0u&~%W15@t}0EA8sOeD+DL3QM?Z=fXLxz|Iuu{w7c6haK7&@Q z;~9GSPdy_X2Sa3=P_eq&acE8P`kx!xgRv(msd2M~9szHQ`#~*iC}LSain5-_PKBpofFD9c%Q(QHJ7<8#xfqvLb8FDUc_Nc%0>SRo#y|{StermLn&Bbs& zm_|EnCPSkQr5hX0gGNaaf6f|hj64@Mmz4^)o<5Ff#dz5e?>(OAFxqp(DRH0`nNlx? zA~*>7@!`e$g`xt2M!Jad))d!>sy!@6q3IE~oSKp+fiCg{#?nK-WOB>s8Y5%$)N|6S z5Qk##Dt~}C@4T8Eg{&l7(HHoJ9~HO3K%vg{qJQc$&;aVQ-g~IJqC`6|c2LAQQ}s!b zAak*A(We(nKNo4F)Z3byK+p;ih5UVGcdV=;Mp*Bl6_THJ{S6MvYbO!ZUHzE3{v_{D z^dFbPLRQ(qf{cXnt1hY5pl8|2D0))UT{D?UaaWHs8whH zeb6a5e7qzV4*Rrm^NVl5D&*E2R{TX_?$mRQE5sSQZxnT7@b4K=j%6iD1eN7#PQO)= zv>&mg84O`P9&YJ}1TnZ=Adl3zRegP=@Yt6P0C@3&HJH&2$`L%VKIZ#upH)ui)VK!?F-|1;G5D*VHWg;K?5bO{{qWoRiw{I1(B>k`Ww9pqcT%dTZbj6qDdAE^O!xd=@ufAiWG;G}%( zZHUn#z`D1`_FT-8jnan5D;`_F;XchG%ZQb_ZrRR$gpSz6H5vks!qGQs_R|ItU~5m< z{IriSC~2J90%Q-ghGFXy@LjfaSEBDVWrVpvomP?O|A)3+DHLDXb949Ccm48?@$KKh z&*8uO1A$v#cn#Mnch)G3zGXJyFQPpU4?YJM#l6j+C1J-ak!oT*6>kD)3-IBAC6m#F z1S3c^d{aZ*(zyt&YmP%5fDw$l9Xx`ndq1G_W_ig{`xZ`NxWN*TJ72O9`P`=8r+h*+ zokK^t?nD@+D0opb5K%aEMWs1`@IH}8UX{p zXnA&_>`JMKUH0RF-ZXV$IoASBskgP~RRqSl{TUF@+t6XR+IDZdyQFlJ!I&h0`HgZ~ zF}k{hXA5v??xwg50_Oe)0p3do4+>1+{-S0}2RFH0@{6*E`~BgbvrF?l$j|O(v!de> z$%+VKBq_09IZ14Oxk1q47tMu|Se-kcEW>jC`<@zsQtuRQ#XHUdcJaY(V~j zm-H1VaiEkA-Y-+_cf!VEraP+ScS z=n_aVNtUrHw*NP;b{aCO_Zad+kG@oA$(X-l;6!;hPLWP+IT%q-r%1&CQo0$^q~C_= zXG8Hv#GSA12+vr*S?Pu7P22y)yD#5|GUy!v|2*r_D6@rpBHkmE;m@-@a0T_GMY8M1 z8)@0URyD^I+RC*cd(_{Wcm?vKc%Y{ujIgvz2-etBejJ!x$|d1xfsJK^j3)@>x`N=` z(u3UWBY}EQ3HHq>4kX{qA8}H$^uUTt%%8(l7)aC%+OwF()g!H;MR9#X_3zsSNw(OT-O7p=9?00L^`kZ0a^#z|3-d{u}!-=x9bBJ>RU__rPHXATl zXyA-y3A@Auc7qdV+Iom0ciyuUmb#Vu98Q!YY_ull z5Yx$pZ9+Vy?$*(lczI$0;}=f|eXU$1JrOP^7_AbUU)uA!yuCKGv!bWE1;uPzqAe<@ z2*n}WlS@Cykc&RZ$PlrjsP`1BRL=U&I&z`Q=??Z3?;t8yF9t!sRV#*{3xkmwTaz!+ z7i&9L>f~vty?df714K#wEAc9tB=}=eDISl2k|xU2CdViD?bBwbrpGkIiZr#u2lT43 zskI%vxgjKciLs!v#OXVG#bJeyw~BA??DgM;lt_u_>7oc#@3PQ$x-v-e2A^pt?|fV} z+lU>8Fp7FGs(u)YRmp|BjJo3fzwm>F;=_A3cHiFhZ+HISgFo@WuOj2m>))+ckMYF( zE?&7`gZ;ZnAsBYA;*GJM6qf2jmrY;swqqnE>_tF6RJqgDMq7t@4k%BvSG1riu326G zg^O0?b_e$CV&Hs8Pt?|Re@{u$LA)#nBI#9rV_qsnW-O~Hp~_8%t*DZ@YB`6f8`T>1 z^KH1!)b(~PY1@xHws8L#ker!9^8;Q#Tz*TauzF%t?jT-KPBONI^ii03 zS{T-oDED%PsxN?~A1QdC%7y!Am0RV*BZU|5Tm-UfQ^z&mgHWiM5GNY}cn89LYRwqE zrDaRh5n zbq>FuFHB1|fx&vA*V$_p_m2!Oekr6*EA?CSITZMci5iCox-N?JoQrHyYi=Drv>R9Z z;b}>-}+{21^9CP71(phXKdFJTnc93!T{SiaX%vW2+^CjydyovSjDs-8_#z@7v_?z z>Gptw#?)%D-LWRqtCJSgO`sx^AxkRD&`2c%>M^ zrho~r2({!3hU;z!EFWXw0EP%cT!1DN7$|naTs5sm)4~&4P_5%O80!=HY*5mWy!_D{ zFc#*4%_RJIsZVw2E8KIKumXa0WPO zO!?;)LcqP7fZZlbh0z)$&sNJQ(Q@LSq09_|^KhPiP#*TuZ}A0_hBFD_mJ8uo zmBsyk;b@_Fws3UMZ}0xuUHf5iZ3bNK)2`U~LKKlC=v=$EJLwGN{?K_W82=_;$B z{hkC9P)16{7=g^;5pEZONAaqi7W9LK8@_X=UfnX>BCdjz2#(cttK3Ca_3C=3EbfnF zE9C{pmVmFdrj_(ebk0ocUdC6bGz9bbXw`?i_-o1}>vWuWC z905buXyhL6RI8i#uUK(%KtXBcyc{}6NA(S%rqlP&^aA?~^4v5ho3(!>UnyR`p3JMI zV8o@zYb&Iqhz+=OLq}m;(C8=h7k-l0jp~);@^$rBP5g>BA%+t5_@ggfTW3YJ zsD<}dTr4u!??>{nvxCyrk{og}ua=U>epu2QI&Vk0TJ|B}Romig(XnPl@vCtsNI$pZ zw<4BWbT}2nYK`YyPvpFq$cg%tdSFSm#r`YH(e^kM+8c`U9y|{ zDFacPAUhk+2FfQpNNP zIAyWYK`xE@fl>@87xW@EZkP6B+yAl)fOX_48G1rop73zpCm@)FUs^>1O;JSR%p^jQ zi$+833wEBME3JX&<7HEj+cAs{qYfcNg+GiQ@U}Azi{P*(tfzJV@T}sm*?@rr?B;M3 z$6E5Nf%!cZX_-`Z_8FToO^C-KCG`)-#9_wv_nMvkRD)LFCEc||&ZH`u4hY7;)+fZ&i{ULCm{Wz76 z84s7a$R)Mr&DGE2LDE}%x$er@wl;y|lZzmqgW2t=6=F2PMd4~FAmsq>!3QGO>>7AM z1W{NqTZuUy+eE)At|FPKOecf4L~#)hBWYK}u`E2>^aFV?j$GhQ)Y{trJV1SW5+aIC za+DhUffl4%AqHwV;wU1RSaZTNmdg+RuGOTHK)85X7=rp1uv{UjAM-$ez$Ky@2}o66 zs@T*IJ&6J5LQ`gK&;I`x3Zo|a*$8r$Vg8%yS;^jDC)Pu1{UUnpkREyu=1j%Tn^UK>H{ma+y$si(x>ecK z7`}0!JvzI33|_qps+wEjxhOALpupvT5KB{7+e*?DA}udkuC6!6SwVkUhcj{WYISX# z=6lgvsl=8+{F0Zj(V$H+m&k=(d<+*OkKpER862K>c>8DG-RAu!oVqpcrjJTSs0f)>J`=3~T%w%TsD@Fq7S zM%GuM8hz#1;e+F>BH>)=w1iZg;1qYofP=Lg2SAJ(C(kb|Y8OomwT-}6hId!g4~-mZ zE|jSiZtZ+#r0_28({SjR$9`0SyHHh{GU|))dRoPfX1PK{cVPsIy{+d8jAAWhmSUe~ zdoqht&meY*x27Br%SspyI?5Pm^Hht;0B4*k9PHx2VEd9ajDV>Z}+Cmy0OXq@G!skLw{2ODHjbSYHb;N5hnNpK5V-gV~Ufw*tF||U3sTh z!R7f^&Wp=)vZ8g{LBubk({n}3g(tYTtikyx7`@;( z6$%GQB6aHK|z14}LMk-Tc&cnpz1St>uIZjh;tEE(clR3yBAI$#bk z7k`ONP1f9c{b}lgd0{95x}1g_(t!qiV2sELF@#K1@uHt&H1T=_FYTZLwxNVSqAG|Z zdUw2RCHj?Mm$GF%KcPY5ML*&Z*KxC0SsjzbBenotJ;>%Lzr7e`5B;_YIh@=mOslxH zN*3SNYS>VnVQixsV+QmLY26$^BRa&F33DdxsLA>v4t*b_sGXm&z-wLs&M zP=T;kKYYhJ%!Tuex?4|t3h_qp2nZDWSbwMUG0>pAm~$9JB%OcuWuI(CE-=X~nD^~s zn|HH72f>pZdoBdUD{VdgM0<0zJezoehL;+^bKqrFtmZ0+z*8q`k5|=?{U=@&q zM?@tM^>{wfsap00hAkP0TZW_XZ6pTbm5Ewg+dm0s!P}1kFP9-AfK&ElkQp>4(c-lb z!^vr*1Kk}W?(ij0uY`D&$q$krv6q8|9=yU3@G@|aWMit7An!*c`jjpsT*_D3(7au0 zK8RehR=3Q*{A(Jvudc}WJqW+|<7D0#a;=YOm95Wxf_#ex7vx)0rpq|hy-^`yu9W9k zV-ka~jiLTFXq1JNy~gW{ivlJ&(2$@x#c&`Z7lP2lu-JlJ3}@RgzPgG#BYMjOUsZrA zt$9x|ic*5cRlJ7U>~!C;=)#GK?t>snjxQIQ;+1&*-&y#hLh;U?J3Ibh$5Vwr+J5XW z)Uofb27BwZC&0W#kb(D4nP8Bvdug&KpOC~ElujW{;bsKit?&{iMw+D34ivJRIxo?2 zSHfsgj3_`x;=PlN@cf}sFarA?I&iS!_vyJY(Zp@hSTV&X@z|tf9BU@tka68EUMyN+ z$BHvU@QL}7(j%GvWsG9275SWpu#+-pl+@)d#i9(U2ioVjve%eDltTmUBO*YQ1jX1X ziQRCO3=&oy)2uPL=zYj;_`FV(uIj0=$A$E;9Sy_Z(rN{B~<^uk$q&}0mj%q3-{Z#qEiSF zt_UTHVWhIo-0}3PHx@qzFU6ZXfM$2z+|U*r#!2%0g`y9;sb5s^DlePi{9!eqk9Wlu z)Nd|IMOiJ^gnHEfzDFNNfczhMpR+oxJn=T`jV_H1-uGAD6i8%9U{RhSEtC`lz(6T- zD;*jq;grgjW#sr2Is?!hDxrn`aH1)=*8R`oFA z(CC?_ygTNzE@4Zb*iz&H3wMmOXY_e{rsGDUEvQBXi^QQofcIDQ=`nRWscg?axV?7OK26rt&MeN1cbGBT}qB`CMT*YA` z!Z9*GbU89_A=~hjz#9a@8b&UWeo~2OH!{Fn1dv_*=K5oBExUCEGRZ|8*e@J8eMr1# zrTK@2G@)b~+ArbtB6(6;#Tf44)MIZ1FJ%ek)Da;rz>tDZqes*O*Vd2J53vC$-3A$X zMB}I+6H|(*6elA5mJ#o68Sz-l0Kq`V9Wka{Scuf%`G3dGQ-#u}ikEl)?ygVZ&*8rz z1cne8LSP7iAq4V3VC&QDcK#R}D5U<+lzVpmIbAr=WSG?ui4jONUD0(37C7NkF5l~j=*J9AVHu1hyjR9izYq8MpBIubM z*$ZqI^TqoJ3-wN|7LP#aePXaeEW9u}5KtFZ z=kvA8Am}R>;T1+lfwi6-Dub+&lr1qBG)o}Gm>F&r#d|JcJPW?K01tW*qEesWyX(+P zprXR+O>)a`f;wr6br_mZ~ zRg8FC9cID*LRr_iC3Ty&9WQ9L7%wb(aD_D;oD}#1eLP$cXuDmHwdxpE@W@!Ei4DMM zndVw7f8BCBMjg$Fs0^`>$Y#(Hd)XB}RI#r|X+s~&Yny8@zGzxq-B~#r`zGF(0g_x- zM+^Y)kSH+t%hX#29Jbo{3KNA`0Y7z$Y5D%wX z_I*I;(@Wauw;%)B$hH0wwQjtzd!+FA4I9ht{W+t(8s3-i7Y8DVQMszX9SJO4#Mgeq zRBGvv2oNz73izBaQuJx#hB1OE+&suB(D5yt0)J&sVcGwhj3(sYqeuvR2h35N{|moW zc<68K{FUwhdHW~v=Xc$|TZcaa!Ss#`<>KYJ4sD_ChV4rx@3o*}zaiRWPEX&Ki*r)4 zm_u|QFXC^*Y#S9A1hBI7fw?%B>w@L9v9TPtyMh9IlUb#P z7kF0GVu^kON1Ow;w`_NF4L(`zcIy%7RKGgQ37o-Bi=e6)RXE(VMG!Ek02iqzpKK8* zts~AlCJl*E;HYB)u5gpX$%ch81TMKzghTglX%#Ui{KkgVBbEQfauJ^y#}=^%W{Eak zeLaA_z77;b_TR);25ICro?Xgbk8a_SCTzUd%h4)MZzK*lPb+2$xV9S)2Q`itjT|f| z5Xcy!SF*Mg0K-Dejfw)nc~ddH@KEh?mHO*b3`UVY8GaS<(=^d?1X+m1(^(M#4@B65 z>x_Azor8}Acmh7Te#CrKA1LHOIP`lY`5Pxoa5%ZKi5>eI4K@H-5d|sSxz$|4Nz#@g z3P1?izys)>5lp|;s?_sq<(fiv^{-cpU_alWiP1GOd($(4&@PIh^;o-@p`11+m|Uvz z3`ooer&CEq$pNvEjD;B`GT%l67oSydcig482va(kkp}BPUiRwrveNJnb9Y7>h^msb zfJiaUV7Y9gjI0WHQ8B^>GAutt;|=f4M)g$Czj*eLq#cL{oWv-ipVO^G*nO;At!^*l z-Eeh~4HpNBY9xylVVjW1M@U*cL9SNr>Z=>kBcRh>@m?3_A8JuH$`%oceFc`dk#AY~ ztW&q!9d9fkrbsP*?QMx6+Vw~fh6XTb!T1I5NVMZmHz?5G2BW75OQK_Gos!wHEYP=O zZ=`F<;I%w3RV|!|;XOCzk&T{$ZxJLHR)h?U*^Vt91XntC;E;zu?y3YH7!@%}D<0+8parePFual0T8=x`s*8vyk$jRt%cw_H1_-C_ zSfuP2prEy|o0~drW1}m(i-$gG9}`^&(q=|jrqE(W{1NFLkGHyF8nIkr!(Y2(wo>{} za(#Y{2~rm^4%j97tdj+l4LojRL*^q)XrMi6ty&eAZz0y9SV5eq{hDJEnD|3dK5UT@ z$S}p+=Azhx?%mpEi5&T~=)XK;bjwq6$`qud2o8EY&a-YPE F{|~ivxMTnT literal 0 HcmV?d00001 diff --git a/i3/scripts/dmenu/dmenu.json b/i3/scripts/dmenu/dmenu.json new file mode 100644 index 0000000..60ef1ef --- /dev/null +++ b/i3/scripts/dmenu/dmenu.json @@ -0,0 +1,30 @@ +{ + "dmenu": [ + "dmenu", + "-i", + "-l", + "6", + "-x", + "550", + "-y", + "400", + "-w", + "500", + "-h", + "20", + "-dim", + "0.2", + "-p", + "Do", + "-fn", + "Inconsolata-14:normal", + "-nb", + "#3F3F3F", + "-nf", + "#DCDCCC", + "-sb", + "#1E2320", + "-sf", + "#F0DFAF" + ] +} diff --git a/i3/scripts/dmenu/init.py b/i3/scripts/dmenu/init.py new file mode 100755 index 0000000..3ada4fd --- /dev/null +++ b/i3/scripts/dmenu/init.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sqlite3 +import json +import argparse +import sys +import os +from modules import Application, MPC +from Dmenu import Dmenu + +del sys.path[0] +sys.path.insert(0, os.path.dirname(sys.argv[0])) + + +# CONFIG +loadModules = [Application(), MPC()] + +con = sqlite3.connect(os.path.dirname(sys.argv[0]) + "/database.sqlite") + +# ARGUMENTS PARSER + +parser = argparse.ArgumentParser(description='Smart Dropdown Launcher for AwesomeWM') +parser.add_argument('--create', dest='FLAG_CREATE', action='store_const', const=True, default=False, help='Force TABLE \ + CREATION') +parser.add_argument('--build', dest='FLAG_BUILD', action='store_const', const=True, default=False, help='Force Build \ +the Database') +parser.add_argument('--truncate', dest='FLAG_TRUNCATE', action='store_const', const=True, default=False, help='Truncate\ + Database during build') +args = parser.parse_args() + + +# Helper functions + +def create_db(): + command = "CREATE TABLE entries(`ID` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `Name` TEXT, `json` TEXT, `hits` INTEGER NOT NULL DEFAULT 0, `disabled` INTEGER NOT NULL DEFAULT 0, `type` TEXT NOT NULL DEFAULT 'file', `file` TEXT);" + with con: + cur = con.cursor() + cur.execute(command) + con.commit() + + +def retrieve_db(): + rows = [] + with con: + con.row_factory = sqlite3.Row + cur = con.cursor() + cur.execute("SELECT * FROM entries;") + ro = cur.fetchall() + for r in ro: + r = dict(r) + r['json'] = json.loads(r['json']) + rows.append(r) + return rows + + +def build_db(): + entries = {} + for mod in loadModules: + entries.update(mod.build_db()) + for k, v in entries.items(): + with con: + con.cursor().execute("INSERT INTO entries(Name, json, file, type, disabled) VALUES(?, ?, ?, ?, ?)", + (k, json.dumps(v['json']), v['file'], v['type'], v['disabled'])) + + +def build_menu(): + entries = [] + db = retrieve_db() + for mod in loadModules: + entries += mod.build_menu([x for x in db if x['type'] == mod.db_type]) + + # sort by hits + return sorted(entries, key=lambda i: i[2], reverse=True) + + +def run_program(data): + with con: + cur = con.cursor() + cur.execute("UPDATE entries SET hits=? WHERE ID=?", (data[2] + 1, data[0])) + cur.execute("SELECT type FROM entries WHERE ID=?", (data[0],)) + con.commit() + typ = dict(cur.fetchone())['type'] + for mod in loadModules: + if mod.db_type == typ: + mod.call(data[1]) + + +if (args.FLAG_CREATE): + with con: + con.cursor().execute("DROP TABLE IF EXISTS entries;") + create_db() + # set BUILD flag so the database gets rebuilt too + args.FLAG_BUILD = True + +if (args.FLAG_BUILD): + if(args.FLAG_TRUNCATE): + with con: + con.cursor().execute("DELETE FROM entries") + con.cursor().execute("DELETE FROM sqlite_sequence WHERE name='entries'") + con.commit() + build_db() + +entries = build_menu() +p1 = Dmenu([x[1] for x in entries]).run() + +if (p1[0] != 0): + # error on escape + exit(1) + +selected = list(filter(lambda x: p1[1].strip().startswith(x[1]), entries)) +if selected == []: + # -> terminal module? None-Type modules? + pass +else: + # use actual user input + selected = (selected[0][0], p1[1].rstrip(), selected[0][2]) + run_program(selected) + +con.close() diff --git a/i3/scripts/dmenu/modules/Application.py b/i3/scripts/dmenu/modules/Application.py new file mode 100644 index 0000000..2c2c113 --- /dev/null +++ b/i3/scripts/dmenu/modules/Application.py @@ -0,0 +1,128 @@ +from .Module import Module as _Module +import locale as _locale +import subprocess as _subprocess +import shlex as _shlex +from pathlib import Path as _Path +from os import listdir as _listdir +from os.path import isfile as _isfile, join as _join + + +class Application(_Module): + + def __init__(self): + self.db_type = "Application" + self._home = str(_Path.home()) + self._lang = _locale.getlocale()[0].split("_")[0].strip() + self._dirs = ["/usr/share/applications", self._home + "/.local/share/applications"] + self._references = {} + + def build_db(self): + """Function which adds entries to the database. + returns dict with: + Name, json, disabled, type, "file" """ + db = {} + for d in self._dirs: + for fi in _listdir(d): + if _isfile(_join(d, fi)): + fc = self._scan_file(_join(d, fi)) + for k, v in fc.items(): + db.update({k: { + "Name": k, + "json": v, + "disabled": (("NoDisplay" in v and v["NoDisplay"] == "true") or + ("Hidden" in v and v["Hidden"] == "true")), + "file": v['file'], + "type": self.db_type + }}) + return db + + def update_db(self, database: dict): + """Function which updates entries in the database. + returns dict with modified/deleted/added database entries""" + entries = self.build_db() + dbremove = [] + # dbKey == id, dbValue dict of fileds + for dbKey, dbValue in database: + if dbValue["type"] == self.db_type: + if dbValue["Name"] not in entries: + dbremove.append(dbKey) + else: + # update all values managed by this module + for k, v in entries[dbValue["Name"]]: + dbValue[k] = v + # remove no more existing entries + for i in dbremove: + database.pop(i) + return database + + def build_menu(self, items: dict): + """Builds the menu entries. + return list of tuples (id, name, hits)""" + # we only get entries of our app type so no problem here + # json is already unjsonified + NAME_PREFIX="run " + NAME_POSTFIX=" (%PATH%)" + entries = [] + for r in items: + identifier=(NAME_PREFIX + r['Name'] + NAME_POSTFIX).replace("%PATH%", r['json']['Exec']) + entries.append((r['ID'], identifier, r['hits'])) + if ("Terminal" not in r["json"]): + r['json']["Terminal"] = "false" + self._references[identifier] = (r['json']["Exec"], r['json']["Terminal"], r['file'], r['Name']) + + return entries + + def call(self, cmd: str): + """Handles the selected entry""" + # find the entry + reference = list(filter(lambda x: cmd.startswith(x), self._references.keys())) + entry = reference[0] + cmd = cmd.replace(entry, "").lstrip() + # Just call the program + _subprocess.Popen(_shlex.split(self._expand_fieldcodes(self._references[entry][0], cmd, reference)), + encoding="utf-8", + shell=(self._references[entry][1] == "true")) + + def _scan_file(self, fi): + entries = {} + with open(fi, encoding="utf-8", mode="r") as f: + data = {} + for line in f: + line = line.strip() + if line.startswith("["): # and line.rstrip() != "[Desktop Entry]": + data["header"] = line.strip("[]") + if "Name" in data and "Exec" in data: + data['file'] = fi + entries[data["Name"]] = data + data = {} + if "=" in line: + s = line.split("=") + if "[" in s[0] and "]" in s[0]: + if "[" + self._lang + "]" in s[0]: + data[s[0].replace("[" + self._lang + "]", "").strip()] = s[1] + elif s[0] != "Name" or "Name" not in data.keys(): + data[s[0]] = s[1] + + if "Name" in data: + data['file'] = fi + entries[data["Name"]] = data + return entries + + def _expand_fieldcodes(self, entry: str, cmd: str, reference): + fieldcodes = ['%f', '%F', '%u', '%U', '%i', '%c', '%k'] # todo implement %i as expand icon-key + for fc in range(0, len(fieldcodes)): + fieldcodes[fc] = fieldcodes[fc] in entry + if True in fieldcodes: + if '%k' in entry: + entry = entry.replace('%k', '"' + reference[2] + "'") + if '%c' in entry: + entry = entry.replace('%c', '"' + reference[3] + "'") + if '%i' in entry: + entry = entry.replace(' %i', "") + if '%f' in entry or '%F' in entry or '%u' in entry or '%U' in entry: + entry = entry.replace('%f', cmd) + entry = entry.replace('%F', cmd) + entry = entry.replace('%u', cmd) + entry = entry.replace('%U', cmd) + cmd = "" + return (entry + " " + cmd).rstrip() diff --git a/i3/scripts/dmenu/modules/MPC.py b/i3/scripts/dmenu/modules/MPC.py new file mode 100644 index 0000000..2f6070a --- /dev/null +++ b/i3/scripts/dmenu/modules/MPC.py @@ -0,0 +1,118 @@ +from .Module import Module as _Module +import locale as _locale +from pathlib import Path as _Path +from Dmenu import Dmenu +import subprocess as _subprocess + + +class MPC(_Module): + + def __init__(self): + self.db_type = "MPC" + self._home = str(_Path.home()) + self._lang = _locale.getlocale()[0].split("_")[0].strip() + self._dirs = ["/usr/share/applications", self._home + "/.local/share/applications"] + self.replace = True + + def build_db(self): + """Function which adds entries to the database. + returns dict with: + Name, json, disabled, type, "file" """ + + # we only add an generic Submenu entry. + db = {"Media Player Control [MPC]": { + "Name": "Media Player Control [MPC]", + "json": "", + "disabled": False, + "file": "none", + "type": self.db_type + }} + return db + + def update_db(self, database: dict): + """Function which updates entries in the database. + returns dict with modified/deleted/added database entries""" + return self.build_db() + + def build_menu(self, items: dict): + """Builds the menu entries. + return list of tuples (id, name, hits)""" + # we only get entries of our app type so no problem here + # json is already unjsonified + entries = [] + for r in items: + entries.append((r['ID'], r['Name'], r['hits'])) + + return entries + + def gatherInfo(self): + out = {} + infoprocess = _subprocess.run("mpc", stdout=_subprocess.PIPE, shell=True) + info = infoprocess.stdout.decode("utf8").split("\n") + if len(info) == 4: + status = [x.split(":") for x in info[2].split(" ")] + + out["song"] = info[0] + out["playing"] = "[playing]" in info[1] + + else: + status = [x.split(":") for x in info[0].split(" ")] + + out["replace"] = self.replace + status = [x for x in status if len(x) == 2] + + for i in status: + i[0] = i[0].strip() + i[1] = i[1].strip() + if i[1] == "off": + out[i[0]] = False + elif i[1] == "on": + out[i[0]] = True + else: + out[i[0]] = i[1] + return out + + def buildStatusMenu(self): + info = self.gatherInfo() + if info["replace"]: + playlists = ['MODE: REPLACE'] + else: + playlists = ['MODE: APPEND'] + + if info["random"]: + playlists += ['RANDOM: on'] + else: + playlists += ['RANDOM: off'] + + return (len(playlists), playlists) + + def handleStatusMenu(self, text: str): + if text.startswith("MODE: "): + self.replace = not self.replace + elif text.startswith("RANDOM: "): + _subprocess.run("mpc random", shell=True) + + def call(self, cmd: str): + """Handles the selected entry""" + # our menu entry was selected, time to create and show the submenu + while True: + infolen, playlists = self.buildStatusMenu() + + playprocess = _subprocess.run("mpc lsplaylists", stdout=_subprocess.PIPE, shell=True) + playlists += playprocess.stdout.decode("utf8").split("\n") + menu = Dmenu(playlists) + ex = menu.run() + + if ex[0] == 0: + if ex[1].rstrip() in playlists[0:infolen]: + self.handleStatusMenu(ex[1].rstrip()) + continue + # user selected an entry + # mpc clear(?) -> mpc load ex[1] -> mpc play + command = "mpc load '" + ex[1].rstrip() + "'" + if self.replace: + command = "mpc clear; " + command + "; mpc play" + _subprocess.run(command, shell=True) + break + else: + break diff --git a/i3/scripts/dmenu/modules/Module.py b/i3/scripts/dmenu/modules/Module.py new file mode 100644 index 0000000..619c805 --- /dev/null +++ b/i3/scripts/dmenu/modules/Module.py @@ -0,0 +1,25 @@ +class Module(object): + """Abstract class, implement: build_db, update_db, build_menu, call""" + + def __init__(self, obj): + pass + + def build_db(): + """Function which adds entries to the database. + returns dict with: + Name, json, disabled, type, "file" """ + raise NotImplementedError("Should have implemented this") + + def update_db(database: dict): + """Function which updates entries in the database. + returns dict with modified/deleted/added database entries""" + raise NotImplementedError("Should have implemented this") + + def build_menu(items: dict): + """Builds the menu entries. + return list of tuples (id, name, hits)""" + raise NotImplementedError("Should have implemented this") + + def call(entry: str): + """Handles the selected entry""" + raise NotImplementedError("Should have implemented this") diff --git a/i3/scripts/dmenu/modules/__init__.py b/i3/scripts/dmenu/modules/__init__.py new file mode 100644 index 0000000..03e78ad --- /dev/null +++ b/i3/scripts/dmenu/modules/__init__.py @@ -0,0 +1,2 @@ +from .Application import Application +from .MPC import MPC diff --git a/i3/scripts/dmenu/modules/__pycache__/Application.cpython-36.pyc b/i3/scripts/dmenu/modules/__pycache__/Application.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d31706fea50c3513bbd96bade3f232f5f6dbd33 GIT binary patch literal 4182 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 lQ&Z+&BI~btx-c%j%EF{c7}`lGo1P%e%@JN zfqjEhSl7zfU2@NV2` z8>J3PUFf0Yi4Gi~)Q15|T{y&7j`G&W6tY;X&tA^7Q9)=}7zbL9m|QGlu@H$Fvsor% z;4mApYzcu8#ddRu2HA*(K^%vT(py?>oUd_e0tyzsWKX_Z!$$6E5zlicon0!jD?Y!* zev~eRuL5`m!OA~@)XZfpv@cJ69V)pr+6Q{cUlh)2f8+GYa+Q0W%S0N^=_OtGV6P8L zeWm+@OR_0^JHU-W*01d^CFkqa+bjunl0q|+G7|}g3?!7FK^@ve3fEX0Ui>NFXlg$`!1fDtpRC7vD=)MK`OauKCj3@i#hi<*1^ z_@>m8p`AaNq~=2v6ajCPN>xqbX4gbWxmU%jy`{w6Qc~&gLa#y+Qn+Z)`Zs#aEqXTT zP%|Wx|6qt=Fy&kd@Il1F;EHS`-pZ?r_kS{@2@mgH+&L?d>wLemf>EwTnkvXB+EY=`5X3 zhCQljqKEUm%Qg9CI~rxhz`*brh~a<<$Z`PUVi_Qj!jQt4!;s4m#lQ$+GvzSmGDa~1+04NV znk+9Ffl4$PZ*e;o6y#(kCzfR9=K;CQz5&jDnoLC?lZu#u#7c%D77!am{Ib!{$j?pH zFG@@?NlC2K*GtJSNz2ShE!NL8)-O&j$}A`;)=$Yz%`4T<%}*%>NyNuz=4F<|$LkeT S-r}% /dev/null 2>&1" \ + "run 'tmux set -g default-command \"exec $(tmux show -gv default-shell) 2>/dev/null & reattach-to-user-namespace -l $(tmux show -gv default-shell)\"'" + +yank="~/.tmux/yank.sh" + +# Copy selected text +bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "$yank" +bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "$yank" +bind -T copy-mode-vi Y send-keys -X copy-line \;\ + run "tmux save-buffer - | $yank" +bind-key -T copy-mode-vi D send-keys -X copy-end-of-line \;\ + run "tmux save-buffer - | $yank" +bind -T copy-mode-vi C-j send-keys -X copy-pipe-and-cancel "$yank" +bind-key -T copy-mode-vi A send-keys -X append-selection-and-cancel \;\ + run "tmux save-buffer - | $yank" + +# Copy selection on drag end event, but do not cancel copy mode and do not clear selection +# clear select on subsequence mouse click +bind -T copy-mode-vi MouseDragEnd1Pane \ + send-keys -X copy-pipe "$yank" +bind -T copy-mode-vi MouseDown1Pane select-pane \;\ + send-keys -X clear-selection + +# iTerm2 works with clipboard out of the box, set-clipboard already set to "external" +# tmux show-options -g -s set-clipboard +# set-clipboard on|external + +# ===================================== +# === Theme === +# ===================================== + +# Feel free to NOT use this variables at all (remove, rename) +# this are named colors, just for convenience +color_orange="colour166" # 208, 166 +color_purple="colour134" # 135, 134 +color_green="colour076" # 070 +color_blue="colour39" +color_yellow="colour220" +color_red="colour160" +color_black="colour232" +color_white="white" # 015 + +# This is a theme CONTRACT, you are required to define variables below +# Change values, but not remove/rename variables itself +color_dark="$color_black" +color_light="$color_white" +color_session_text="$color_blue" +color_status_text="colour245" +color_main="$color_orange" +color_secondary="$color_purple" +color_level_ok="$color_green" +color_level_warn="$color_yellow" +color_level_stress="$color_red" +color_window_off_indicator="colour088" +color_window_off_status_bg="colour238" +color_window_off_status_current_bg="colour254" + +# ===================================== +# === Appearence and status bar === +# ====================================== + +set -g mode-style "fg=default,bg=$color_main" + +# command line style +set -g message-style "fg=$color_main,bg=$color_dark" + +# status line style +set -g status-style "fg=$color_status_text,bg=$color_dark" + +# window segments in status line +set -g window-status-separator "" +separator_powerline_left="" +separator_powerline_right="" + +# setw -g window-status-style "fg=$color_status_text,bg=$color_dark" +setw -g window-status-format " #I:#W " +setw -g window-status-current-style "fg=$color_light,bold,bg=$color_main" +setw -g window-status-current-format "#[fg=$color_dark,bg=$color_main]$separator_powerline_right#[default] #I:#W# #[fg=$color_main,bg=$color_dark]$separator_powerline_right#[default]" + +# when window has monitoring notification +setw -g window-status-activity-style "fg=$color_main" + +# outline for active pane +setw -g pane-active-border-style "fg=$color_main" + +# general status bar settings +set -g status on +set -g status-interval 5 +set -g status-position top +set -g status-justify left +set -g status-right-length 100 + +# define widgets we're going to use in status bar +# note, that this is not the complete list, some of them are loaded from plugins +wg_session="#[fg=$color_session_text] #S #[default]" +wg_battery="#{battery_status_fg} #{battery_icon} #{battery_percentage}" +wg_date="#[fg=$color_secondary]%h %d %H:%M#[default]" +wg_user_host="#[fg=$color_secondary]#(whoami)#[default]@#H" +wg_is_zoomed="#[fg=$color_dark,bg=$color_secondary]#{?window_zoomed_flag,[Z],}#[default]" +# TODO: highlighted for nested local session as well +wg_is_keys_off="#[fg=$color_light,bg=$color_window_off_indicator]#([ $(tmux show-option -qv key-table) = 'off' ] && echo 'OFF')#[default]" + +set -g status-left "$wg_session" +set -g status-right "#{prefix_highlight} $wg_is_keys_off $wg_is_zoomed | $wg_user_host | $wg_date $wg_battery #{online_status}" + +# online and offline icon for tmux-online-status +set -g @online_icon "#[fg=$color_level_ok]●#[default]" +set -g @offline_icon "#[fg=$color_level_stress]●#[default]" + +# Configure view templates for tmux-plugin-sysstat "MEM" and "CPU" widget +set -g @sysstat_mem_view_tmpl 'MEM:#[fg=#{mem.color}]#{mem.pused}#[default] #{mem.used}' + +# Configure colors for tmux-plugin-sysstat "MEM" and "CPU" widget +set -g @sysstat_cpu_color_low "$color_level_ok" +set -g @sysstat_cpu_color_medium "$color_level_warn" +set -g @sysstat_cpu_color_stress "$color_level_stress" + +set -g @sysstat_mem_color_low "$color_level_ok" +set -g @sysstat_mem_color_medium "$color_level_warn" +set -g @sysstat_mem_color_stress "$color_level_stress" + +set -g @sysstat_swap_color_low "$color_level_ok" +set -g @sysstat_swap_color_medium "$color_level_warn" +set -g @sysstat_swap_color_stress "$color_level_stress" + + +# Configure tmux-battery widget colors +set -g @batt_color_full_charge "#[fg=$color_level_ok]" +set -g @batt_color_high_charge "#[fg=$color_level_ok]" +set -g @batt_color_medium_charge "#[fg=$color_level_warn]" +set -g @batt_color_low_charge "#[fg=$color_level_stress]" + +# Configure tmux-prefix-highlight colors +set -g @prefix_highlight_output_prefix '[' +set -g @prefix_highlight_output_suffix ']' +set -g @prefix_highlight_fg "$color_dark" +set -g @prefix_highlight_bg "$color_secondary" +set -g @prefix_highlight_show_copy_mode 'on' +set -g @prefix_highlight_copy_mode_attr "fg=$color_dark,bg=$color_secondary" + + +# ===================================== +# === Renew environment === +# ===================================== +set -g update-environment \ + "DISPLAY\ + SSH_ASKPASS\ + SSH_AUTH_SOCK\ + SSH_AGENT_PID\ + SSH_CONNECTION\ + SSH_TTY\ + WINDOWID\ + XAUTHORITY" + +bind '$' run "~/.tmux/renew_env.sh" + + +# ============================ +# === Plugins === +# ============================ +set -g @plugin 'tmux-plugins/tpm' +# set -g @plugin 'tmux-plugins/tmux-battery' +set -g @plugin 'tmux-plugins/tmux-prefix-highlight' +# set -g @plugin 'tmux-plugins/tmux-online-status' +set -g @plugin 'tmux-plugins/tmux-sidebar' +set -g @plugin 'tmux-plugins/tmux-copycat' +set -g @plugin 'tmux-plugins/tmux-open' +# set -g @plugin 'samoshkin/tmux-plugin-sysstat' + +# Plugin properties +set -g @sidebar-tree 't' +set -g @sidebar-tree-focus 'T' +set -g @sidebar-tree-command 'tree -C' + +# set -g @open-S 'https://www.google.com/search?q=' + + +# ============================================== +# === Nesting local and remote sessions === +# ============================================== + +# Session is considered to be remote when we ssh into host +if-shell 'test -n "$SSH_CLIENT"' \ + 'source-file ~/.tmux/tmux.remote.conf' + +# We want to have single prefix key "C-a", usable both for local and remote session +# we don't want to "C-a" + "a" approach either +# Idea is to turn off all key bindings and prefix handling on local session, +# so that all keystrokes are passed to inner/remote session + +# see: toggle on/off all keybindings · Issue #237 · tmux/tmux - https://github.com/tmux/tmux/issues/237 + +# Also, change some visual styles when window keys are off +bind -T root F12 \ + set prefix None \;\ + set key-table off \;\ + set status-style "fg=$color_status_text,bg=$color_window_off_status_bg" \;\ + set window-status-current-format "#[fg=$color_window_off_status_bg,bg=$color_window_off_status_current_bg]$separator_powerline_right#[default] #I:#W# #[fg=$color_window_off_status_current_bg,bg=$color_window_off_status_bg]$separator_powerline_right#[default]" \;\ + set window-status-current-style "fg=$color_dark,bold,bg=$color_window_off_status_current_bg" \;\ + if -F '#{pane_in_mode}' 'send-keys -X cancel' \;\ + refresh-client -S \;\ + +bind -T off F12 \ + set -u prefix \;\ + set -u key-table \;\ + set -u status-style \;\ + set -u window-status-current-style \;\ + set -u window-status-current-format \;\ + refresh-client -S + +# Run all plugins' scripts +run '~/.tmux/plugins/tpm/tpm' diff --git a/tmux/tmux.remote.conf b/tmux/tmux.remote.conf new file mode 100644 index 0000000..ea385bc --- /dev/null +++ b/tmux/tmux.remote.conf @@ -0,0 +1,10 @@ +# show status bar at bottom for remote session, +# so it do not stack together with local session's one +set -g status-position bottom + +# Set port of SSH remote tunnel, where tmux will pipe buffers to transfer on local machine for copy +set -g @copy_backend_remote_tunnel_port 11988 + +# In remote mode we don't show "clock" and "battery status" widgets +set -g status-left "$wg_session" +set -g status-right "#{prefix_highlight} $wg_is_keys_off $wg_is_zoomed #{sysstat_cpu} | #{sysstat_mem} | #{sysstat_loadavg} | $wg_user_host | #{online_status}" diff --git a/tmux/yank.sh b/tmux/yank.sh new file mode 100755 index 0000000..83686c6 --- /dev/null +++ b/tmux/yank.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +set -eu + +is_app_installed() { + type "$1" &>/dev/null +} + +# get data either form stdin or from file +buf=$(cat "$@") + +copy_backend_remote_tunnel_port=$(tmux show-option -gvq "@copy_backend_remote_tunnel_port") +copy_use_osc52_fallback=$(tmux show-option -gvq "@copy_use_osc52_fallback") + +# Resolve copy backend: pbcopy (OSX), reattach-to-user-namespace (OSX), xclip/xsel (Linux) +copy_backend="" +if is_app_installed pbcopy; then + copy_backend="pbcopy" +elif is_app_installed reattach-to-user-namespace; then + copy_backend="reattach-to-user-namespace pbcopy" +elif [ -n "${DISPLAY-}" ] && is_app_installed xsel; then + copy_backend="xsel -i --clipboard" +elif [ -n "${DISPLAY-}" ] && is_app_installed xclip; then + copy_backend="xclip -i -f -selection primary | xclip -i -selection clipboard" +elif [ -n "${copy_backend_remote_tunnel_port-}" ] \ + && (netstat -f inet -nl 2>/dev/null || netstat -4 -nl 2>/dev/null) \ + | grep -q "[.:]$copy_backend_remote_tunnel_port"; then + copy_backend="nc localhost $copy_backend_remote_tunnel_port" +fi + +# if copy backend is resolved, copy and exit +if [ -n "$copy_backend" ]; then + printf "%s" "$buf" | eval "$copy_backend" + exit; +fi + + +# If no copy backends were eligible, decide to fallback to OSC 52 escape sequences +# Note, most terminals do not handle OSC +if [ "$copy_use_osc52_fallback" == "off" ]; then + exit; +fi + +# Copy via OSC 52 ANSI escape sequence to controlling terminal +buflen=$( printf %s "$buf" | wc -c ) + +# https://sunaku.github.io/tmux-yank-osc52.html +# The maximum length of an OSC 52 escape sequence is 100_000 bytes, of which +# 7 bytes are occupied by a "\033]52;c;" header, 1 byte by a "\a" footer, and +# 99_992 bytes by the base64-encoded result of 74_994 bytes of copyable text +maxlen=74994 + +# warn if exceeds maxlen +if [ "$buflen" -gt "$maxlen" ]; then + printf "input is %d bytes too long" "$(( buflen - maxlen ))" >&2 +fi + +# build up OSC 52 ANSI escape sequence +esc="\033]52;c;$( printf %s "$buf" | head -c $maxlen | base64 | tr -d '\r\n' )\a" +esc="\033Ptmux;\033$esc\033\\" + +# resolve target terminal to send escape sequence +# if we are on remote machine, send directly to SSH_TTY to transport escape sequence +# to terminal on local machine, so data lands in clipboard on our local machine +pane_active_tty=$(tmux list-panes -F "#{pane_active} #{pane_tty}" | awk '$1=="1" { print $2 }') +target_tty="${SSH_TTY:-$pane_active_tty}" + +printf "$esc" > "$target_tty"