313 lines
12 KiB
Python
Executable File
313 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import logging
|
|
from typing import Optional, List
|
|
import argparse
|
|
import re
|
|
import subprocess
|
|
|
|
|
|
class RgbColor():
|
|
red: int
|
|
green: int
|
|
blue: int
|
|
def __init__(self, red: int, green: int, blue: int):
|
|
self.red = red
|
|
self.green = green
|
|
self.blue = blue
|
|
|
|
|
|
UserId = str # eg 'root'
|
|
HostId = str # eg alambix50.ipr.univ-rennes.fr or alambix50
|
|
TermProfileName = str # terminal profile name eg 'alambix50.ipr.univ-rennes.fr'
|
|
TermProfileUuid = str # eg '2e0dec33-5ec2-4355-b671-7922010cee9f'
|
|
|
|
class SshTarget():
|
|
user_id: Optional[UserId]
|
|
host_id: HostId
|
|
# eg root@alambix50.ipr.univ-rennes.fr
|
|
def __init__(self, ssh_target_as_str: str):
|
|
parts = ssh_target_as_str.split('@')
|
|
assert len(parts) > 0 and len(parts) <= 2, "malformed ssh target : {ssh_target}. It is expected to be of the form [<user_id>@]<host_id>"
|
|
self.host_id = parts[-1] # eg 'alambix50.ipr.univ-rennes.fr'
|
|
self.user_id = None
|
|
if len(parts) > 1:
|
|
self.user_id = parts[0]
|
|
assert re.match(r'^[a-z]+$', self.user_id), f'malformed user id : {self.user_id}'
|
|
|
|
def __str__(self) -> str:
|
|
target_as_str = f'{self.host_id}'
|
|
if self.user_id:
|
|
target_as_str = f'{self.user_id}@{target_as_str}'
|
|
return target_as_str
|
|
|
|
# '''
|
|
# function getHostRealFqdn()
|
|
# {
|
|
# local strHostname="$1"
|
|
|
|
# local strHostFqdn="$(host $strHostname | tail -1 | awk '{print $1}')"
|
|
# echo "$strHostFqdn"
|
|
# }
|
|
|
|
# HOST_FQDN=$(getHostRealFqdn $HOSTNAME)
|
|
|
|
# SOLARIZED_BACKGROUND='{0, 8256, 10368}' #002b36 (solarized dark base03)
|
|
# SAILOR_BLUE='{8832, 14208, 18816}' # 2e4a62
|
|
# PRUNE='{15360, 10944, 14592}' #50394c
|
|
# TAUPE='{19968, 18816, 16512}' #686256
|
|
# DARK_ORANGE='{27456, 12864, 0}' #8f4300
|
|
# DARK_OLIVE='{8832, 11904, 5568}' #2e3e1d
|
|
|
|
# function get_current_terminal_window_id()
|
|
# {
|
|
# local terminal_windows_id=''
|
|
# terminal_windows_id=$(osascript -e "tell application \"Terminal\" to get id of window 1")
|
|
# echo "$terminal_windows_id"
|
|
# }
|
|
|
|
# function set_bg ()
|
|
# {
|
|
# local strOsxTermWindowId="$1"
|
|
# local strColor="$2" # eg {45000, 0, 0, 50000}
|
|
|
|
# osascript -e "tell application \"Terminal\" to set background color of (every window whose id is $strOsxTermWindowId) to $strColor"
|
|
# }
|
|
|
|
# function on_exit ()
|
|
# {
|
|
# set_bg "$THIS_OSX_TERM_WINDOW_ID" "$SOLARIZED_BACKGROUND"
|
|
# }
|
|
|
|
|
|
# # if [ $(echo $HOST_FQDN | grep '^simpatix') ]
|
|
# # then
|
|
# # BG_COLOR="$SAILOR_BLUE"
|
|
# # elif [ $(echo $HOST_FQDN | grep '^physix') ]
|
|
# # then
|
|
# # BG_COLOR="$TAUPE"
|
|
# # else
|
|
# # case $HOST_FQDN in
|
|
# # 'pr079234.spm.univ-rennes1.fr')
|
|
# # BG_COLOR="$SOLARIZED_BACKGROUND"
|
|
# # ;;
|
|
# # 'puppet3.ipr.univ-rennes1.fr')
|
|
# # BG_COLOR="$PRUNE"
|
|
# # ;;
|
|
# # *)
|
|
# # BG_COLOR="$DARK_OLIVE"
|
|
# # ;;
|
|
# # esac
|
|
# # fi
|
|
|
|
# #if [ "$AS_ROOT" = 'true' ]
|
|
# #then
|
|
# # BG_COLOR="$DARK_ORANGE"
|
|
# #fi
|
|
|
|
def is_valid_uuid(uuid: str) -> bool:
|
|
'''
|
|
uuid: eg 'b1dcc9dd-5262-4d8d-a863-c897e6d979b9'
|
|
'''
|
|
m = re.match(r'^[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]$', uuid)
|
|
return m is not None
|
|
|
|
GSettingsKeyPath = str # eg org.gnome.Terminal.ProfilesList
|
|
|
|
class GnomeSettings():
|
|
|
|
def get_list(self, key_id: GSettingsKeyPath) -> List[str]:
|
|
# 20260616-17:34:08 graffy@graffy-ws2:~$ gsettings get org.gnome.Terminal.ProfilesList list
|
|
# ['b1dcc9dd-5262-4d8d-a863-c897e6d979b9', '2e0dec33-5ec2-4355-b671-7922010cee9f']
|
|
proc = subprocess.run(['gsettings', 'get', key_id, 'list'], check=True, stdout=subprocess.PIPE)
|
|
lines = proc.stdout.decode('utf8').split('\n')
|
|
values = []
|
|
assert len(lines) == 2
|
|
m = re.match(r'^\[(?P<series_of_strings>[^]]*)\]$', lines[0])
|
|
assert m is not None
|
|
series_of_strings = m['series_of_strings']
|
|
logging.debug('get_list: series_of_strings = %s', str(series_of_strings))
|
|
|
|
strings = series_of_strings.split(', ')
|
|
for s in strings:
|
|
m = re.match(r"'(?P<unquoted_string>[^']*)'", s)
|
|
assert m is not None
|
|
values.append(m['unquoted_string'])
|
|
logging.debug('get_list: values = %s', str(values))
|
|
return values
|
|
|
|
def set_list(self, key_id: GSettingsKeyPath, value: List[str]):
|
|
|
|
# gsettings set org.gnome.Terminal.ProfilesList list "$(echo "$CURRENT_LIST" | tr -d "[]" | sed "s/$/,$PROFILE_ID/")"
|
|
value_as_str = ','.join([f"'{unq_str}'" for unq_str in value])
|
|
logging.debug('value_as_str = "%s"', value_as_str)
|
|
subprocess.run(['gsettings', 'set', key_id, 'list', f"[{value_as_str}]"], check=True)
|
|
|
|
def register_profile_uuid(self, profile_uuid: TermProfileUuid):
|
|
'''
|
|
profile_uuid: eg '2e0dec33-5ec2-4355-b671-7922010cee9f'
|
|
'''
|
|
assert is_valid_uuid(profile_uuid), f'invalid uuid: "{profile_uuid}"'
|
|
profiles_key_id = 'org.gnome.Terminal.ProfilesList'
|
|
profile_uuids = self.get_list(profiles_key_id)
|
|
assert profile_uuid not in profile_uuids
|
|
profile_uuids.append(profile_uuid)
|
|
self.set_list(profiles_key_id, profile_uuids)
|
|
|
|
|
|
class GnomeTerminal():
|
|
|
|
DCONF_PROFILES_PATH = '/org/gnome/terminal/legacy/profiles:'
|
|
|
|
# DCONF_PROFILES_PATH='/org/gnome/terminal/legacy/profiles:'
|
|
|
|
@staticmethod
|
|
def create_new_profile(profile_name: TermProfileName) -> TermProfileUuid:
|
|
'''
|
|
copied from https://askubuntu.com/questions/270469/how-can-i-create-a-new-profile-for-gnome-terminal-via- command-line
|
|
'''
|
|
|
|
# 20260616-16:24:12 graffy@graffy-ws2:~$ dconf list '/org/gnome/terminal/legacy/profiles:/'
|
|
# :2a05665f-baa7-484e-b179-4e94f3814238/
|
|
# :2e0dec33-5ec2-4355-b671-7922010cee9f/
|
|
# last command status : [0]
|
|
|
|
old_profile_uuids = GnomeTerminal.get_term_profiles()
|
|
# local profile_ids=($(dconf list $DCONF_PROFILES_PATH/ | grep ^: |\
|
|
# sed 's/\///g' | sed 's/://g'))
|
|
|
|
# local profile_name="$1"
|
|
# local profile_ids_old="$(dconf read "$DCONF_PROFILES_PATH"/list | tr -d "]")"
|
|
proc = subprocess.run(['uuidgen'], check=True, stdout=subprocess.PIPE)
|
|
lines = proc.stdout.decode('utf8').split('\n')
|
|
assert len(lines) == 2
|
|
|
|
profile_uuid = lines[0]
|
|
m = re.match(r'^[a-z0-9\-]+$', profile_uuid)
|
|
|
|
logging.info("creating profile profile_uuid=%s with profile_name=%s", profile_uuid, profile_name)
|
|
# echo "new_profiles_list=$new_profiles_list"
|
|
# dconf write $DCONF_PROFILES_PATH/list "$new_profiles_list"
|
|
proc = subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/visible-name", f"'{profile_name}'"], check=True)
|
|
|
|
assert GnomeTerminal.get_term_profile_name(profile_name) == profile_uuid
|
|
# dconf write /org/gnome/terminal/legacy/profiles:/:dc932a58-6a38-4511-876a-d453b5e33a26/visible-name "'toto'"
|
|
|
|
gsettings = GnomeSettings()
|
|
gsettings.register_profile_uuid(profile_uuid)
|
|
return profile_uuid
|
|
|
|
@staticmethod
|
|
def get_term_profiles() -> List[TermProfileUuid]:
|
|
proc = subprocess.run(['dconf', 'list', f"{GnomeTerminal.DCONF_PROFILES_PATH}/"], check=True, stdout=subprocess.PIPE)
|
|
profile_uuids = []
|
|
for line in proc.stdout.decode('utf8').split('\n'):
|
|
print(line)
|
|
# eg ':b1dcc9dd-5262-4d8d-a863-c897e6d979b9/'
|
|
m = re.match(r'^:?(?P<uuid>[0-9a-f\-]+)/$', line)
|
|
if m is None:
|
|
assert line in ['list', ''], f'unexpected format for line: {line}'
|
|
else:
|
|
profile_uuids.append(m['uuid'])
|
|
return profile_uuids
|
|
|
|
|
|
@staticmethod
|
|
def get_term_profile_name(profile_uuid: TermProfileUuid) -> Optional[TermProfileName]:
|
|
profile_name = None
|
|
proc = subprocess.run(['dconf', 'read', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/visible-name"], check=True, stdout=subprocess.PIPE)
|
|
lines = proc.stdout.decode('utf8').split('\n')
|
|
if (len(lines) >= 2):
|
|
|
|
assert len(lines) == 2, lines
|
|
assert lines[1] == ''
|
|
m = re.match(r"^\'(?P<profile_name>[^']+)'$", lines[0])
|
|
assert m, f'unexpected line : {line}'
|
|
profile_name = m['profile_name']
|
|
else:
|
|
pass # (the default profile has no name)
|
|
#assert False, f'unexpected case: the terminal profile profile_uuid={profile_uuid} has no name!!!'
|
|
return profile_name
|
|
|
|
@staticmethod
|
|
def get_terminal_profile_uuid(profile_name: TermProfileName) -> TermProfileUuid:
|
|
'''
|
|
returns the id of the profile which has the given name.
|
|
if the named profile doesn't exist, it creates it first
|
|
local profile_name="$1" # eg physix.ipr.univ-rennes1.fr
|
|
'''
|
|
|
|
profile_uuid = None
|
|
for profile_uuid in GnomeTerminal.get_term_profiles():
|
|
logging.debug(f'processing profile_uuid={profile_uuid}')
|
|
prof_name = GnomeTerminal.get_term_profile_name(profile_uuid)
|
|
logging.debug('prof_name = %s', prof_name)
|
|
if profile_name == prof_name:
|
|
return profile_uuid
|
|
|
|
logging.info('the profile named %s doesnt exist... create it then', profile_name)
|
|
profile_uuid = GnomeTerminal.create_new_profile(profile_name)
|
|
|
|
return profile_uuid
|
|
|
|
@staticmethod
|
|
def set_terminal_profile_bg_color(profile_name: TermProfileName, bg_color: RgbColor):
|
|
# function set_terminal_profile_bg_color()
|
|
# profile_name # eg physix.ipr.univ-rennes1.fr
|
|
# bg_color # eg RgbColor(17, 25, 24)
|
|
|
|
profile_uuid = GnomeTerminal.get_terminal_profile_uuid(profile_name)
|
|
logging.debug("profile_name=%s profile_uuid=%s", profile_name, profile_uuid)
|
|
|
|
subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/use-theme-colors", 'false'], check=True, stdout=subprocess.PIPE)
|
|
subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/background-color", f'({bg_color.red}, {bg_color.green}, {bg_color.blue})'], check=True, stdout=subprocess.PIPE)
|
|
|
|
|
|
def main():
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
arg_parser = argparse.ArgumentParser('cssh (colored ssh) opens a terminal window with a background color that depends on the fully qualified domain name of the target machine')
|
|
arg_parser.add_argument('ssh_target', help='eg root@alambix50.ipr.univ-rennes.fr')
|
|
args = arg_parser.parse_args()
|
|
logging.debug(args)
|
|
ssh_target = SshTarget(args.ssh_target) # eg 'root@alambix50.ipr.univ-rennes.fr'
|
|
|
|
|
|
# AS_ROOT='false'
|
|
# if [ "$(echo $@ | grep '^root@' > /dev/null)" ]
|
|
# then
|
|
# AS_ROOT='true'
|
|
# fi
|
|
|
|
color_saturation = 0.3
|
|
color_value = 0.2
|
|
proc = subprocess.run(['uname'], check=True, stdout=subprocess.PIPE)
|
|
os_name = proc.stdout.decode('utf8').strip()
|
|
if os_name == 'Darwin':
|
|
raise NotImplementedError()
|
|
# 'Darwin')
|
|
# BG_COLOR=$(make_color.py $HOST_FQDN $COLOR_VALUE $COLOR_SATURATION osx)
|
|
# THIS_OSX_TERM_WINDOW_ID=$(get_current_terminal_window_id)
|
|
# echo "THIS_OSX_TERM_WINDOW_ID=$THIS_OSX_TERM_WINDOW_ID"
|
|
# set_bg "$THIS_OSX_TERM_WINDOW_ID" "$BG_COLOR"
|
|
# trap on_exit EXIT
|
|
# /usr/bin/ssh "$@"
|
|
# ;;
|
|
elif os_name == 'Linux':
|
|
proc = subprocess.run(['make_color.py', ssh_target.host_id, str(color_value), str(color_saturation), 'linux'], check=True, stdout=subprocess.PIPE)
|
|
bg_color_as_str = proc.stdout.decode('utf8').strip()
|
|
match = re.match(r'\((?P<red>[0-9]+), (?P<green>[0-9]+), (?P<blue>[0-9]+)\)', bg_color_as_str)
|
|
assert match, f'unexpected color as string: {bg_color_as_str}'
|
|
bg_color = RgbColor(match['red'], match['green'], match['blue'])
|
|
logging.debug('bg_color = %s', str(bg_color)) # eg (35, 43, 51)
|
|
|
|
GnomeTerminal.set_terminal_profile_bg_color(ssh_target.host_id, bg_color)
|
|
|
|
profile_uuid = GnomeTerminal.get_terminal_profile_uuid(ssh_target.host_id)
|
|
|
|
launch_term_command = ['gnome-terminal', f'--window-with-profile={profile_uuid}', f'--title={ssh_target.host_id}', '--', '/usr/bin/ssh', f'{str(ssh_target)}']
|
|
logging.debug('launch_term_command = %s', launch_term_command)
|
|
subprocess.run(launch_term_command, check=True)
|
|
else:
|
|
raise NotImplementedError(f'unhandled os : "{os_name}"')
|
|
|
|
|
|
main() |