1#!/usr/local/bin/python3.8
2#
3#    select-session.py
4#    Copyright (C) 2010 Canonical Ltd.
5#    Copyright (C) 2012-2014 Dustin Kirkland <kirkland@byobu.org>
6#
7#    Authors: Dustin Kirkland <kirkland@byobu.org>
8#             Ryan C. Thompson <rct@thompsonclan.org>
9#
10#    This program is free software: you can redistribute it and/or modify
11#    it under the terms of the GNU General Public License as published by
12#    the Free Software Foundation, version 3 of the License.
13#
14#    This program is distributed in the hope that it will be useful,
15#    but WITHOUT ANY WARRANTY; without even the implied warranty of
16#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17#    GNU General Public License for more details.
18#
19#    You should have received a copy of the GNU General Public License
20#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
22
23import os
24import re
25import sys
26import subprocess
27try:
28	# For Python3, try and import input from builtins
29	from builtins import input
30except Exception:
31	# But fall back to using the default input
32	True
33
34
35PKG = "byobu"
36SHELL = os.getenv("SHELL", "/usr/local/bin/bash")
37HOME = os.getenv("HOME")
38BYOBU_CONFIG_DIR = os.getenv("BYOBU_CONFIG_DIR", HOME + "/.byobu")
39BYOBU_BACKEND = os.getenv("BYOBU_BACKEND", "tmux")
40choice = -1
41sessions = []
42text = []
43reuse_sessions = os.path.exists("%s/.reuse-session" % (BYOBU_CONFIG_DIR))
44
45BYOBU_UPDATE_ENVVARS = ["DISPLAY", "DBUS_SESSION_BUS_ADDRESS", "SESSION_MANAGER", "GPG_AGENT_INFO", "XDG_SESSION_COOKIE", "XDG_SESSION_PATH", "GNOME_KEYRING_CONTROL", "GNOME_KEYRING_PID", "GPG_AGENT_INFO", "SSH_ASKPASS", "SSH_AUTH_SOCK", "SSH_AGENT_PID", "WINDOWID", "UPSTART_JOB", "UPSTART_EVENTS", "UPSTART_SESSION", "UPSTART_INSTANCE"]
46
47
48def get_sessions():
49	sessions = []
50	i = 0
51	output = False
52	if BYOBU_BACKEND == "screen":
53		try:
54			output = subprocess.Popen(["screen", "-ls"], stdout=subprocess.PIPE).communicate()[0]
55		except subprocess.CalledProcessError as cpe:
56			# screen -ls seems to always return 1
57			if cpe.returncode != 1:
58				raise
59			else:
60				output = cpe.output
61		if sys.stdout.encoding is None:
62			output = output.decode("UTF-8")
63		else:
64			output = output.decode(sys.stdout.encoding)
65		if output:
66			for s in output.splitlines():
67				s = re.sub(r'\s+', ' ', s)
68				# Ignore hidden sessions (named sessions that start with a "." or a "_")
69				if s and s != " " and (s.find(" ") == 0 and len(s) > 1 and s.count("..") == 0 and s.count("._") == 0):
70					text.append("screen: %s" % s.strip())
71					items = s.split(" ")
72					sessions.append("screen____%s" % items[1])
73					i += 1
74	if BYOBU_BACKEND == "tmux":
75		output = subprocess.Popen(["tmux", "list-sessions"], stdout=subprocess.PIPE).communicate()[0]
76		if sys.stdout.encoding is None:
77			output = output.decode("UTF-8")
78		else:
79			output = output.decode(sys.stdout.encoding)
80		if output:
81			for s in output.splitlines():
82				# Ignore hidden sessions (named sessions that start with a "_")
83				if s and not s.startswith("_") and s.find("-") == -1:
84					text.append("tmux: %s" % s.strip())
85					sessions.append("tmux____%s" % s.split(":")[0])
86					i += 1
87	return sessions
88
89
90def cull_zombies(session_name):
91	# When using tmux session groups, closing a client will leave
92	# unattached "zombie" sessions that will never be reattached.
93	# Search for and kill any unattached hidden sessions in the same group
94	if BYOBU_BACKEND == "tmux":
95		output = subprocess.Popen(["tmux", "list-sessions"], stdout=subprocess.PIPE).communicate()[0]
96		if sys.stdout.encoding is None:
97			output = output.decode("UTF-8")
98		else:
99			output = output.decode(sys.stdout.encoding)
100		if not output:
101			return
102
103		# Find the master session to extract the group name. We use
104		# the group number to be extra sure the right session is getting
105		# killed. We don't want to accidentally kill the wrong one
106		pattern = "^%s:.+\\((group [^\\)]+)\\).*$" % session_name
107		master = re.search(pattern, output, re.MULTILINE)
108		if not master:
109			return
110
111		# Kill all the matching hidden & unattached sessions
112		pattern = "^_%s-\\d+:.+\\(%s\\)$" % (session_name, master.group(1))
113		for s in re.findall(pattern, output, re.MULTILINE):
114			subprocess.Popen(["tmux", "kill-session", "-t", s.split(":")[0]])
115
116
117def update_environment(session):
118	backend, session_name = session.split("____", 2)
119	for var in BYOBU_UPDATE_ENVVARS:
120		value = os.getenv(var)
121		if value:
122			if backend == "tmux":
123				cmd = ["tmux", "setenv", "-t", session_name, var, value]
124			else:
125				cmd = ["screen", "-S", session_name, "-X", "setenv", var, value]
126			subprocess.call(cmd, stdout=open(os.devnull, "w"))
127
128
129def attach_session(session):
130	update_environment(session)
131	backend, session_name = session.split("____", 2)
132	cull_zombies(session_name)
133	# must use the binary, not the wrapper!
134	if backend == "tmux":
135		if reuse_sessions:
136			os.execvp("tmux", ["tmux", "-u", "new-session", "-t", session_name, ";", "set-option", "destroy-unattached"])
137		else:
138			os.execvp("tmux", ["tmux", "-u", "attach", "-t", session_name])
139	else:
140		os.execvp("screen", ["screen", "-AOxRR", session_name])
141
142
143sessions = get_sessions()
144
145show_shell = os.path.exists("%s/.always-select" % (BYOBU_CONFIG_DIR))
146if len(sessions) > 1 or show_shell:
147	sessions.append("NEW")
148	text.append("Create a new Byobu session (%s)" % BYOBU_BACKEND)
149	sessions.append("SHELL")
150	text.append("Run a shell without Byobu (%s)" % SHELL)
151
152if len(sessions) > 1:
153	sys.stdout.write("\nByobu sessions...\n\n")
154	tries = 0
155	while tries < 3:
156		i = 1
157		for s in text:
158			sys.stdout.write("  %d. %s\n" % (i, s))
159			i += 1
160		try:
161			try:
162				user_input = input("\nChoose 1-%d [1]: " % (i - 1))
163			except Exception:
164				user_input = ""
165			if not user_input or user_input == "":
166				choice = 1
167				break
168			try:
169				choice = int(user_input)
170			except Exception:
171				choice = int(eval(user_input))
172			if choice >= 1 and choice < i:
173				break
174			else:
175				tries += 1
176				choice = -1
177				sys.stderr.write("\nERROR: Invalid input\n")
178		except KeyboardInterrupt:
179			sys.stdout.write("\n")
180			sys.exit(0)
181		except Exception:
182			if choice == "" or choice == -1:
183				choice = 1
184				break
185			tries += 1
186			choice = -1
187			sys.stderr.write("\nERROR: Invalid input\n")
188elif len(sessions) == 1:
189	# Auto-select the only session
190	choice = 1
191
192if choice >= 1:
193	if sessions[choice - 1] == "NEW":
194		# Create a new session
195		if BYOBU_BACKEND == "tmux":
196			os.execvp("byobu", ["byobu", "new-session", SHELL])
197		else:
198			os.execvp("byobu", ["byobu", SHELL])
199	elif sessions[choice - 1] == "SHELL":
200		os.execvp(SHELL, [SHELL])
201	else:
202		# Attach to the chosen session; must use the binary, not the wrapper!
203		attach_session(sessions[choice - 1])
204
205# No valid selection, default to the youngest session, create if necessary
206if BYOBU_BACKEND == "tmux":
207	os.execvp("tmux", ["tmux"])
208else:
209	os.execvp("screen", ["screen", "-AOxRR"])
210