dotfiles/.config/Scripts/i3-quickterm

261 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import copy
import fcntl
import json
import os
import shlex
import subprocess
import sys
from contextlib import contextmanager, suppress
from pathlib import Path
import i3ipc
DEFAULT_CONF = {
"menu": "rofi -dmenu -p 'quickterm: ' -no-custom -auto-select",
"term": "st",
"history": "{$HOME}/.cache/i3/i3-quickterm.order",
"ratio": 0.25,
"pos": "top",
"shells": {
"haskell": "ghci",
"js": "node",
"python": "ipython3 --no-banner",
"shell": "{$SHELL}"
}
}
MARK_QT_PATTERN = 'quickterm_.*'
MARK_QT = 'quickterm_{}'
def TERM(executable, execopt='-e', execfmt='expanded', titleopt='-T'):
"""Helper to declare a terminal in the hardcoded list"""
if execfmt not in ('expanded', 'string'):
raise RuntimeError('Invalid execfmt')
fmt = executable
if titleopt is not None:
fmt += ' ' + titleopt + ' {title}'
fmt += ' {} {{{}}}'.format(execopt, execfmt)
return fmt
TERMS = {
'alacritty': TERM('alacritty', titleopt='-t'),
'gnome-terminal': TERM('gnome-terminal', execopt='--', titleopt=None),
'roxterm': TERM('roxterm'),
'st': TERM('st'),
'termite': TERM('termite', execfmt='string', titleopt='-t'),
'urxvt': TERM('urxvt'),
'xfce4-terminal': TERM('xfce4-terminal', execfmt='string'),
'xterm': TERM('xterm'),
}
def conf_path():
home_dir = os.environ['HOME']
xdg_dir = os.environ.get('XDG_CONFIG_DIR', '{}/.config'.format(home_dir))
return xdg_dir + '/i3/i3-quickterm.json'
def read_conf(fn):
try:
with open(fn, 'r') as f:
c = json.load(f)
return c
except Exception as e:
print('invalid config file: {}'.format(e), file=sys.stderr)
return {}
@contextmanager
def get_history_file(conf):
if conf['history'] is None:
yield None
return
p = Path(expand_command(conf['history'])[0])
os.makedirs(str(p.parent), exist_ok=True)
f = open(str(p), 'a+')
fcntl.lockf(f, fcntl.LOCK_EX)
try:
f.seek(0)
yield f
finally:
fcntl.lockf(f, fcntl.LOCK_UN)
f.close()
def expand_command(cmd, **rplc_map):
d = {'$' + k: v for k, v in os.environ.items()}
d.update(rplc_map)
return shlex.split(cmd.format(**d))
def move_back(conn, selector):
conn.command('{} floating enable, move scratchpad'
.format(selector))
def pop_it(conn, mark_name, pos='top', ratio=0.25):
ws, _ = get_current_workspace(conn)
wx, wy = ws['rect']['x'], ws['rect']['y']
wwidth, wheight = ws['rect']['width'], ws['rect']['height']
width = wwidth
height = int(wheight*ratio)
posx = wx
if pos == 'bottom':
margin = 6
posy = wy + wheight - height - margin
else: # pos == 'top'
posy = wy
conn.command('[con_mark={mark}],'
'resize set {width} px {height} px,'
'move absolute position {posx}px {posy}px,'
'move scratchpad,'
'scratchpad show'
''.format(mark=mark_name, posx=posx, posy=posy,
width=width, height=height))
def get_current_workspace(conn):
ws = [w for w in conn.get_workspaces() if w['focused']][0]
tree = conn.get_tree()
# wname = workspace['name']
ws_tree = [c for c in tree.descendents()
if c.type == 'workspace' and c.name == ws['name']][0]
return ws, ws_tree
def toggle_quickterm_select(conf, hist=None):
"""Hide a quickterm visible on current workspace or prompt
the user for a shell type"""
conn = i3ipc.Connection()
ws, ws_tree = get_current_workspace(conn)
# is there a quickterm opened in the current workspace?
qt = ws_tree.find_marked(MARK_QT_PATTERN)
if qt:
qt = qt[0]
move_back(conn, '[con_id={}]'.format(qt.id))
return
with get_history_file(conf) as hist:
# compute the list from conf + (maybe) history
hist_list = None
if hist is not None:
with suppress(Exception):
hist_list = json.load(hist)
# invalidate if different set from the configured shells
if set(hist_list) != set(conf['shells'].keys()):
hist_list = None
shells = hist_list or sorted(conf['shells'].keys())
proc = subprocess.Popen(expand_command(conf['menu']),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
for r in shells:
proc.stdin.write((r + '\n').encode())
stdout, _ = proc.communicate()
shell = stdout.decode().strip()
if shell not in conf['shells']:
return
if hist is not None:
# put the selected shell on top
shells = [shell] + [s for s in shells if s != shell]
hist.truncate(0)
json.dump(shells, hist)
toggle_quickterm(conf, shell)
def quoted(s):
return "'" + s + "'"
def term_title(shell):
return '{} - i3-quickterm'.format(shell)
def toggle_quickterm(conf, shell):
conn = i3ipc.Connection()
tree = conn.get_tree()
shell_mark = MARK_QT.format(shell)
qt = tree.find_marked(shell_mark)
# does it exist already?
if len(qt) == 0:
term = TERMS.get(conf['term'], conf['term'])
qt_cmd = "{} -i {}".format(sys.argv[0], shell)
term_cmd = expand_command(term, title=quoted(term_title(shell)),
expanded=qt_cmd,
string=quoted(qt_cmd))
os.execvp(term_cmd[0], term_cmd)
else:
qt = qt[0]
ws, ws_tree = get_current_workspace(conn)
move_back(conn, '[con_id={}]'.format(qt.id))
if qt.workspace().name != ws.name:
pop_it(conn, shell_mark, conf['pos'], conf['ratio'])
def launch_inplace(conf, shell):
conn = i3ipc.Connection()
shell_mark = MARK_QT.format(shell)
conn.command('mark {}'.format(shell_mark))
move_back(conn, '[con_mark={}]'.format(shell_mark))
pop_it(conn, shell_mark, conf['pos'], conf['ratio'])
prog_cmd = expand_command(conf['shells'][shell])
os.execvp(prog_cmd[0], prog_cmd)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--in-place', dest='in_place',
action='store_true')
parser.add_argument('shell', metavar='SHELL', nargs='?')
args = parser.parse_args()
conf = copy.deepcopy(DEFAULT_CONF)
conf.update(read_conf(conf_path()))
if args.shell is None:
toggle_quickterm_select(conf)
sys.exit(0)
if args.shell not in conf['shells']:
print('unknown shell: {}'.format(args.shell), file=sys.stderr)
sys.exit(1)
if args.in_place:
launch_inplace(conf, args.shell)
else:
toggle_quickterm(conf, args.shell)