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 0000000..68927ea Binary files /dev/null and b/i3/scripts/dmenu/__pycache__/Dmenu.cpython-36.pyc differ diff --git a/i3/scripts/dmenu/database.sqlite b/i3/scripts/dmenu/database.sqlite new file mode 100644 index 0000000..738d8b3 Binary files /dev/null and b/i3/scripts/dmenu/database.sqlite differ 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 0000000..1d31706 Binary files /dev/null and b/i3/scripts/dmenu/modules/__pycache__/Application.cpython-36.pyc differ diff --git a/i3/scripts/dmenu/modules/__pycache__/MPC.cpython-36.pyc b/i3/scripts/dmenu/modules/__pycache__/MPC.cpython-36.pyc new file mode 100644 index 0000000..e8e050f Binary files /dev/null and b/i3/scripts/dmenu/modules/__pycache__/MPC.cpython-36.pyc differ diff --git a/i3/scripts/dmenu/modules/__pycache__/Module.cpython-36.pyc b/i3/scripts/dmenu/modules/__pycache__/Module.cpython-36.pyc new file mode 100644 index 0000000..98af5de Binary files /dev/null and b/i3/scripts/dmenu/modules/__pycache__/Module.cpython-36.pyc differ diff --git a/i3/scripts/dmenu/modules/__pycache__/__init__.cpython-36.pyc b/i3/scripts/dmenu/modules/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..5e7a2f8 Binary files /dev/null and b/i3/scripts/dmenu/modules/__pycache__/__init__.cpython-36.pyc differ diff --git a/tmux/plugins/tmux-battery b/tmux/plugins/tmux-battery new file mode 160000 index 0000000..09be78c --- /dev/null +++ b/tmux/plugins/tmux-battery @@ -0,0 +1 @@ +Subproject commit 09be78c35ee84f858f724442b94ad045ade23eb0 diff --git a/tmux/plugins/tmux-copycat b/tmux/plugins/tmux-copycat new file mode 160000 index 0000000..6f9b9cd --- /dev/null +++ b/tmux/plugins/tmux-copycat @@ -0,0 +1 @@ +Subproject commit 6f9b9cd2d93872cef60e3ea7f7ae89598569ed25 diff --git a/tmux/plugins/tmux-online-status b/tmux/plugins/tmux-online-status new file mode 160000 index 0000000..ea86704 --- /dev/null +++ b/tmux/plugins/tmux-online-status @@ -0,0 +1 @@ +Subproject commit ea86704ced8a20f4a431116aa43f57edcf5a6312 diff --git a/tmux/plugins/tmux-open b/tmux/plugins/tmux-open new file mode 160000 index 0000000..f99d318 --- /dev/null +++ b/tmux/plugins/tmux-open @@ -0,0 +1 @@ +Subproject commit f99d3189c445188eae5fa9bfeabc95df16deca92 diff --git a/tmux/plugins/tmux-plugin-sysstat b/tmux/plugins/tmux-plugin-sysstat new file mode 160000 index 0000000..29e150f --- /dev/null +++ b/tmux/plugins/tmux-plugin-sysstat @@ -0,0 +1 @@ +Subproject commit 29e150f403151f2341f3abcb2b2487a5f011dd23 diff --git a/tmux/plugins/tmux-prefix-highlight b/tmux/plugins/tmux-prefix-highlight new file mode 160000 index 0000000..34f7125 --- /dev/null +++ b/tmux/plugins/tmux-prefix-highlight @@ -0,0 +1 @@ +Subproject commit 34f7125ae46e5123bedad03e08027332d1186186 diff --git a/tmux/plugins/tmux-sidebar b/tmux/plugins/tmux-sidebar new file mode 160000 index 0000000..2301452 --- /dev/null +++ b/tmux/plugins/tmux-sidebar @@ -0,0 +1 @@ +Subproject commit 23014524cab53f8d36373983500fe05a527a444d diff --git a/tmux/plugins/tpm b/tmux/plugins/tpm new file mode 160000 index 0000000..95f7833 --- /dev/null +++ b/tmux/plugins/tpm @@ -0,0 +1 @@ +Subproject commit 95f78336c3972f3e6648b7b3db754f2224320a5e diff --git a/tmux/renew_env.sh b/tmux/renew_env.sh new file mode 100755 index 0000000..31a4b7c --- /dev/null +++ b/tmux/renew_env.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -eu + +pane_fmt="#{pane_id} #{pane_in_mode} #{pane_input_off} #{pane_dead} #{pane_current_command}" +tmux list-panes -s -F "$pane_fmt" | awk ' + $2 == 0 && $3 == 0 && $4 == 0 && $5 ~ /(bash|zsh|ksh|fish)/ { print $1 } +' | while read -r pane_id; do + # renew environment variables according to update-environment tmux option + # also clear screen + tmux send-keys -t "$pane_id" 'Enter' 'eval "$(tmux show-env -s)"' 'Enter' 'C-l' +done; \ No newline at end of file diff --git a/tmux/tmux.conf b/tmux/tmux.conf new file mode 100644 index 0000000..9e240c4 --- /dev/null +++ b/tmux/tmux.conf @@ -0,0 +1,384 @@ +# ========================== +# === General settings === +# ========================== + +set -g default-terminal "screen-256color" +set -g history-limit 20000 +set -g buffer-limit 20 +set -sg escape-time 0 +set -g display-time 1500 +set -g remain-on-exit off +set -g repeat-time 300 +setw -g allow-rename off +setw -g automatic-rename off +setw -g aggressive-resize on + +# Change prefix key to C-a, easier to type, same to "screen" +unbind C-b +set -g prefix C-a + +# Set parent terminal title to reflect current window in tmux session +set -g set-titles on +set -g set-titles-string "#I:#W" + +# Start index of window/pane with 1, because we're humans, not computers +set -g base-index 1 +setw -g pane-base-index 1 + +# Enable mouse support +set -g mouse on + + +# ========================== +# === Key bindings === +# ========================== + +# Unbind default key bindings, we're going to override +unbind "\$" # rename-session +unbind , # rename-window +unbind % # split-window -h +unbind '"' # split-window +unbind } # swap-pane -D +unbind { # swap-pane -U +unbind [ # paste-buffer +unbind ] +unbind "'" # select-window +unbind n # next-window +unbind p # previous-window +unbind l # last-window +unbind M-n # next window with alert +unbind M-p # next window with alert +unbind o # focus thru panes +unbind & # kill-window +unbind "#" # list-buffer +unbind = # choose-buffer +unbind z # zoom-pane +unbind M-Up # resize 5 rows up +unbind M-Down # resize 5 rows down +unbind M-Right # resize 5 rows right +unbind M-Left # resize 5 rows left + + +# Edit configuration and reload +bind C-e new-window -n 'tmux.conf' "sh -c '\${EDITOR:-vim} ~/.tmux.conf && tmux source ~/.tmux.conf && tmux display \"Config reloaded\"'" + +# Reload tmux configuration +bind C-r source-file ~/.tmux.conf \; display "Config reloaded" + +# new window and retain cwd +bind c new-window -c "#{pane_current_path}" + +# Prompt to rename window right after it's created +set-hook -g after-new-window 'command-prompt -I "#{window_name}" "rename-window '%%'"' + +# Rename session and window +bind r command-prompt -I "#{window_name}" "rename-window '%%'" +bind R command-prompt -I "#{session_name}" "rename-session '%%'" + +# Split panes +bind | split-window -h -c "#{pane_current_path}" +bind _ split-window -v -c "#{pane_current_path}" + +# Select pane and windows +bind -r C-[ previous-window +bind -r C-] next-window +bind -r [ select-pane -t :.- +bind -r ] select-pane -t :.+ +bind -r Tab last-window # cycle thru MRU tabs +bind -r C-o swap-pane -D + +# Zoom pane +bind + resize-pane -Z + +# Link window +bind L command-prompt -p "Link window from (session:window): " "link-window -s %% -a" + +# Swap panes back and forth with 1st pane +# When in main-(horizontal|vertical) layouts, the biggest/widest panel is always @1 +bind \ if '[ #{pane_index} -eq 1 ]' \ + 'swap-pane -s "!"' \ + 'select-pane -t:.1 ; swap-pane -d -t 1 -s "!"' + +# Kill pane/window/session shortcuts +bind x kill-pane +bind X kill-window +bind C-x confirm-before -p "kill other windows? (y/n)" "kill-window -a" +bind Q confirm-before -p "kill-session #S? (y/n)" kill-session + +# Merge session with another one (e.g. move all windows) +# If you use adhoc 1-window sessions, and you want to preserve session upon exit +# but don't want to create a lot of small unnamed 1-window sessions around +# move all windows from current session to main named one (dev, work, etc) +bind C-u command-prompt -p "Session to merge with: " \ + "run-shell 'yes | head -n #{session_windows} | xargs -I {} -n 1 tmux movew -t %%'" + +# Detach from session +bind d detach +bind D if -F '#{session_many_attached}' \ + 'confirm-before -p "Detach other clients? (y/n)" "detach -a"' \ + 'display "Session has only 1 client attached"' + +# Hide status bar on demand +bind C-s if -F '#{s/off//:status}' 'set status off' 'set status on' + + + +# ================================================== +# === Window monitoring for activity and silence === +# ================================================== +bind m setw monitor-activity \; display-message 'Monitor window activity [#{?monitor-activity,ON,OFF}]' +bind M if -F '#{monitor-silence}' \ + 'setw monitor-silence 0 ; display-message "Monitor window silence [OFF]"' \ + 'command-prompt -p "Monitor silence: interval (s)" "setw monitor-silence %%"' + +# Activity bell and whistles +set -g visual-activity on + +# TODO: Does not work as well, check on newer versions +# set -g visual-silence on + +# BUG: bell-action other ignored · Issue #1027 · tmux/tmux · GitHub - https://github.com/tmux/tmux/issues/1027 +# set -g visual-bell on +# setw -g bell-action other + +# ================================================ +# === Copy mode, scroll and clipboard === +# ================================================ +set -g @copy_use_osc52_fallback on + +# Prefer vi style key table +setw -g mode-keys vi + +bind p paste-buffer +bind C-p choose-buffer + +# trigger copy mode by +bind -n M-Up copy-mode + +# Scroll up/down by 1 line, half screen, whole screen +bind -T copy-mode-vi M-Up send-keys -X scroll-up +bind -T copy-mode-vi M-Down send-keys -X scroll-down +bind -T copy-mode-vi M-PageUp send-keys -X halfpage-up +bind -T copy-mode-vi M-PageDown send-keys -X halfpage-down +bind -T copy-mode-vi PageDown send-keys -X page-down +bind -T copy-mode-vi PageUp send-keys -X page-up + +# When scrolling with mouse wheel, reduce number of scrolled rows per tick to "2" (default is 5) +bind -T copy-mode-vi WheelUpPane select-pane \; send-keys -X -N 2 scroll-up +bind -T copy-mode-vi WheelDownPane select-pane \; send-keys -X -N 2 scroll-down + +# wrap default shell in reattach-to-user-namespace if available +# there is some hack with `exec & reattach`, credits to "https://github.com/gpakosz/.tmux" +# don't really understand how it works, but at least window are not renamed to "reattach-to-user-namespace" +if -b "command -v reattach-to-user-namespace > /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"