1#!/usr/local/bin/python3.8
2#
3#    config.py
4#    Copyright (C) 2008 Canonical Ltd.
5#    Copyright (C) 2008-2014 Dustin Kirkland <kirkland@byobu.org>
6
7#
8#    Authors: Nick Barcet <nick.barcet@ubuntu.com>
9#             Dustin Kirkland <kirkland@byobu.org>
10#
11#    This program is free software: you can redistribute it and/or modify
12#    it under the terms of the GNU General Public License as published by
13#    the Free Software Foundation, version 3 of the License.
14#
15#    This program is distributed in the hope that it will be useful,
16#    but WITHOUT ANY WARRANTY; without even the implied warranty of
17#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18#    GNU General Public License for more details.
19#
20#    You should have received a copy of the GNU General Public License
21#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
23# If you change any strings, please generate localization information with:
24#       ./debian/rules get-po
25
26from __future__ import print_function
27import sys
28import os
29import os.path
30import time
31import string
32import subprocess
33import gettext
34import glob
35
36
37def error(msg):
38	print("ERROR: %s" % msg)
39	sys.exit(1)
40
41
42try:
43	import snack
44	from snack import *
45except Exception:
46	error("Could not import the python snack module")
47
48
49PKG = "byobu"
50HOME = os.getenv("HOME")
51USER = os.getenv("USER")
52BYOBU_CONFIG_DIR = os.getenv("BYOBU_CONFIG_DIR", HOME + "/.byobu")
53BYOBU_RUN_DIR = os.getenv("BYOBU_RUN_DIR", HOME + "/.cache/byobu")
54BYOBU_BACKEND = os.getenv("BYOBU_BACKEND", "tmux")
55BYOBU_SOCKETDIR = os.getenv("SOCKETDIR", "/var/run/screen")
56BYOBU_PREFIX = os.getenv("BYOBU_PREFIX", "@prefix@")
57SHARE = BYOBU_PREFIX + '/share/' + PKG
58DOC = BYOBU_PREFIX + '/share/doc/' + PKG
59if not os.path.exists(SHARE):
60	SHARE = BYOBU_CONFIG_DIR + "/" + SHARE
61if not os.path.exists(DOC):
62	DOC = BYOBU_PREFIX + '/share/doc/packages/' + PKG
63if not os.path.exists(DOC):
64	DOC = BYOBU_CONFIG_DIR + "/" + DOC
65DEF_ESC = "A"
66RELOAD = "If you are using the default set of keybindings, press\n<F5> or <ctrl-a-R> to activate these changes.\n\nOtherwise, exit this session and start a new one."
67RELOAD_FLAG = "%s/reload-required" % (BYOBU_RUN_DIR)
68ESC = ''
69snack.hotkeys[ESC] = ord(ESC)
70snack.hotkeys[ord(ESC)] = ESC
71gettext.bindtextdomain(PKG, SHARE + '/po')
72gettext.textdomain(PKG)
73_ = gettext.gettext
74
75
76def ioctl_GWINSZ(fd):
77	# Discover terminal width
78	try:
79		import fcntl
80		import termios
81		import struct
82		import os
83		cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
84	except Exception:
85		return None
86	return cr
87
88
89def reload_required():
90	try:
91		if not os.path.exists(BYOBU_CONFIG_DIR):
92			# 493 (decimal) is 0755 (octal)
93			# Use decimal for portability across all python versions
94			os.makedirs(BYOBU_CONFIG_DIR, 493)
95		f = open(RELOAD_FLAG, 'w')
96		f.close()
97		if BYOBU_BACKEND == "screen":
98			subprocess.call([BYOBU_BACKEND, "-X", "at", "0", "source", "%s/profile" % BYOBU_CONFIG_DIR])
99	except Exception:
100		True
101
102
103def terminal_size():
104	# decide on some terminal size
105	cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
106	# try open fds
107	if not cr:
108		# ...then ctty
109		try:
110			fd = os.open(os.ctermid(), os.O_RDONLY)
111			cr = ioctl_GWINSZ(fd)
112			os.close(fd)
113		except Exception:
114			pass
115	if not cr:
116		# env vars or finally defaults
117		try:
118			cr = (env['LINES'], env['COLUMNS'])
119		except Exception:
120			cr = (25, 80)
121	# reverse rows, cols
122	return int(cr[1] - 5), int(cr[0] - 5)
123
124
125def menu(snackScreen, size, isInstalled):
126	if isInstalled:
127		installtext = _("Byobu currently launches at login (toggle off)")
128	else:
129		installtext = _("Byobu currently does not launch at login (toggle on)")
130	li = Listbox(height=6, width=60, returnExit=1)
131	li.append(_("Help -- Quick Start Guide"), 1)
132	li.append(_("Toggle status notifications"), 2)
133	li.append(_("Change escape sequence"), 3)
134	li.append(installtext, 4)
135	bb = ButtonBar(snackScreen, (("Exit", "exit", ESC),), compact=1)
136	g = GridForm(snackScreen, _(" Byobu Configuration Menu"), 1, 2)
137	g.add(li, 0, 0, padding=(4, 2, 4, 2))
138	g.add(bb, 0, 1, padding=(1, 1, 0, 0))
139	if bb.buttonPressed(g.runOnce()) == "exit":
140		return 0
141	else:
142		return li.current()
143
144
145def messagebox(snackScreen, width, height, title, text, scroll=0, buttons=((_("Okay"), "okay"), (_("Cancel"), "cancel", ESC))):
146	t = Textbox(width, height, text, scroll=scroll)
147	bb = ButtonBar(snackScreen, buttons, compact=1)
148	g = GridForm(snackScreen, title, 1, 2)
149	g.add(t, 0, 0, padding=(0, 0, 0, 0))
150	g.add(bb, 0, 1, padding=(1, 1, 0, 0))
151	return bb.buttonPressed(g.runOnce())
152
153
154def help(snackScreen, size):
155	f = open(DOC + '/help.' + BYOBU_BACKEND + '.txt')
156	text = f.read()
157	f.close()
158	text = text.replace("<esckey>", getesckey(), 1)
159	t = Textbox(67, 16, text, scroll=1, wrap=1)
160	bb = ButtonBar(snackScreen, ((_("Menu"), "menu", ESC),), compact=1)
161	g = GridForm(snackScreen, _("Byobu Help"), 2, 4)
162	g.add(t, 1, 0)
163	g.add(bb, 1, 1, padding=(1, 1, 0, 0))
164	button = bb.buttonPressed(g.runOnce())
165	return 100
166
167
168def readstatus():
169	status = {}
170	glo = {}
171	loc = {}
172	for f in [SHARE + '/status/status', BYOBU_CONFIG_DIR + '/status']:
173		if os.path.exists(f):
174			try:
175				exec(open(f).read(), glo, loc)
176			except Exception:
177				error("Invalid configuration [%s]" % f)
178			if BYOBU_BACKEND == "tmux":
179				items = "%s %s" % (loc["tmux_left"], loc["tmux_right"])
180			else:
181				items = "%s %s %s %s" % (loc["screen_upper_left"], loc["screen_upper_right"], loc["screen_lower_left"], loc["screen_lower_right"])
182			for i in items.split():
183				if i.startswith("#"):
184					i = i.replace("#", "")
185					status[i] = "0"
186				else:
187					status[i] = "1"
188	li = []
189	keys = list(status.keys())
190	for i in sorted(keys):
191		window = [int(status[i]), i]
192		li.append(window)
193	return li
194
195
196def genstatusstring(s, status):
197	new = ""
198	glo = {}
199	loc = {}
200	exec(open(SHARE + '/status/status').read(), glo, loc)
201	for i in loc[s].split():
202		if i.startswith("#"):
203			i = i.replace("#", "")
204		if status[i] == 1:
205			new += " " + i
206		else:
207			new += " #" + i
208	return new
209
210
211def writestatus(items):
212	status = {}
213	path = BYOBU_CONFIG_DIR + '/status'
214	for i in items:
215		status[i[1]] = i[0]
216	for key in ["tmux_left", "tmux_right", "screen_upper_left", "screen_upper_right", "screen_lower_left", "screen_lower_right"]:
217		if key.startswith(BYOBU_BACKEND):
218			try:
219				f = open(path, "r")
220			except Exception:
221				f = open(SHARE + '/status/status', "r")
222			lines = f.readlines()
223			f.close()
224			try:
225				f = open(path, "w")
226			except Exception:
227				f = open(path, "a+")
228			for l in lines:
229				if l.startswith("%s=" % key):
230					val = genstatusstring(key, status)
231					f.write("%s=\"%s\"\n" % (key, val))
232				else:
233					f.write(l)
234			f.close
235
236
237def togglestatus(snackScreen, size):
238	itemlist = readstatus()
239	rl = Label("")
240	r = CheckboxTree(12, scroll=1)
241	count = 0
242	for item in itemlist:
243		if item[0] != -1:
244			r.append(item[1], count, selected=item[0])
245		count = count + 1
246	bb = ButtonBar(snackScreen, ((_("Apply"), "apply"), (_("Cancel"), "cancel", ESC)), compact=1)
247	g = GridForm(snackScreen, _("Toggle status notifications"), 2, 4)
248	g.add(rl, 0, 0, anchorLeft=1, anchorTop=1, padding=(4, 0, 0, 1))
249	g.add(r, 1, 0)
250	g.add(bb, 1, 1, padding=(4, 1, 0, 0))
251	if bb.buttonPressed(g.runOnce()) != "cancel":
252		count = 0
253		for item in itemlist:
254			if item[0] != -1:
255				item[0] = r.getEntryValue(count)[1]
256			count = count + 1
257		writestatus(itemlist)
258		reload_required()
259	return 100
260
261
262def install(snackScreen, size, isInstalled):
263	out = ""
264	if isInstalled:
265		if subprocess.call(["byobu-launcher-uninstall"]) == 0:
266			out = _("Byobu will not be launched next time you login.")
267		button = messagebox(snackScreen, 60, 2, _("Message"), out, buttons=((_("Menu"), )))
268		return 101
269	else:
270		if subprocess.call(["byobu-launcher-install"]) == 0:
271			out = _("Byobu will be launched automatically next time you login.")
272		button = messagebox(snackScreen, 60, 2, "Message", out, buttons=((_("Menu"), )))
273		return 100
274
275
276def appendtofile(p, s):
277	f = open(p, 'a')
278	try:
279		f.write(s)
280	except IOError:
281		f.close()
282		return
283	f.close()
284	return
285
286
287def getesckey():
288	line = ""
289	if BYOBU_BACKEND == "tmux":
290		path = BYOBU_CONFIG_DIR + '/keybindings.tmux'
291		if os.path.exists(path):
292			for l in open(path):
293				if l.startswith("set -g prefix "):
294					line = l
295		else:
296			return DEF_ESC
297	else:
298		path = BYOBU_CONFIG_DIR + '/keybindings'
299		if os.path.exists(path):
300			for l in open(path):
301				if l.startswith("escape "):
302					line = l
303		else:
304			return DEF_ESC
305	if line == "":
306		return DEF_ESC
307	esc = line[line.find('^') + 1]
308	if esc == "`":
309		esc = " "
310	return esc
311
312
313def setesckey(key):
314	if key.isalpha():
315		# throw away outputs in order that the view isn't broken
316		nullf = open(os.devnull, "w")
317		subprocess.call(["byobu-ctrl-a", "screen", key], stdout=nullf)
318		nullf.close()
319
320
321def chgesc(snackScreen, size):
322	esc = Entry(2, text=getesckey(), returnExit=1)
323	escl = Label(_("Escape key: ctrl-"))
324	bb = ButtonBar(snackScreen, ((_("Apply"), "apply"), (_("Cancel"), "cancel", ESC)), compact=1)
325	g = GridForm(snackScreen, _("Change escape sequence"), 2, 4)
326	g.add(escl, 0, 0, anchorLeft=1, padding=(1, 0, 0, 1))
327	g.add(esc, 1, 0, anchorLeft=1)
328	g.add(bb, 1, 1)
329	g.setTimer(100)
330	loop = 1
331	while loop:
332		which = g.run()
333		if which == "TIMER":
334			val = esc.value()
335			if len(val) > 1:
336				esc.set(val[1])
337			# Ensure that escape sequence is not \ or /
338			if val == '/' or val == '\\':
339				esc.set(DEF_ESC)
340			# Ensure that the escape sequence is not set to a number
341			try:
342				dummy = int(esc.value())
343				esc.set(DEF_ESC)
344			except Exception:
345				# do nothing
346				dummy = "foo"
347		else:
348			loop = 0
349	snackScreen.popWindow()
350	button = bb.buttonPressed(which)
351	if button != "cancel":
352		setesckey(esc.value())
353		reload_required()
354		if button == "exit":
355			return 0
356	return 100
357
358
359def autolaunch():
360	if os.path.exists(BYOBU_CONFIG_DIR + "/disable-autolaunch"):
361		return 0
362	try:
363		for line in open("%s/.profile" % HOME):
364			if "byobu-launch" in line:
365				return 1
366	except Exception:
367		return 0
368	if os.path.exists("/etc/profile.d/Z97-%s.sh" % PKG):
369		return 1
370	return 0
371
372
373def main():
374	"""This is the main loop of our utility"""
375	size = terminal_size()
376	snackScreen = SnackScreen()
377	snackScreen.drawRootText(1, 0, _('Byobu Configuration Menu'))
378	snackScreen.pushHelpLine(_('<Tab> between elements | <Enter> selects | <Esc> exits'))
379	isInstalled = autolaunch()
380	tag = 100
381	while tag > 0:
382		tag = menu(snackScreen, size, isInstalled)
383		if tag == 1:
384			tag = help(snackScreen, size)
385		elif tag == 2:
386			tag = togglestatus(snackScreen, size)
387		elif tag == 3:
388			tag = chgesc(snackScreen, size)
389		elif tag == 4:
390			tag = install(snackScreen, size, isInstalled)
391			isInstalled = autolaunch()
392	snackScreen.finish()
393	sys.exit(0)
394
395
396if __name__ == "__main__":
397	main()
398