diff --git a/home/bin/cssh b/home/bin/cssh index 78590ec..ce04dc7 100755 --- a/home/bin/cssh +++ b/home/bin/cssh @@ -1,163 +1,313 @@ -#!/bin/bash +#!/usr/bin/env python3 +import logging +from typing import Optional, List +import argparse +import re +import subprocess -HOSTNAME=`echo $@ | sed s/.*@//` -AS_ROOT='false' -if [ "$(echo $@ | grep '^root@' > /dev/null)" ] -then - AS_ROOT='true' -fi +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 -function getHostRealFqdn() -{ - local strHostname="$1" + +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 [@]" + 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" -} +# local strHostFqdn="$(host $strHostname | tail -1 | awk '{print $1}')" +# echo "$strHostFqdn" +# } -HOST_FQDN=$(getHostRealFqdn $HOSTNAME) +# 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 +# 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 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} +# 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" -} +# 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 - -DCONF_PROFILES_PATH='/org/gnome/terminal/legacy/profiles:' - -create_new_profile() -{ - # copied from https://askubuntu.com/questions/270469/how-can-i-create-a-new-profile-for-gnome-terminal-via- command-line - - 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 "]")" - local profile_id="$(uuidgen)" - - [ -z "$profile_ids_old" ] && local lb="[" # if there's no `list` key - [ ${#profile_ids[@]} -gt 0 ] && local delimiter=, # if the list is empty - dconf write $DCONF_PROFILES_PATH/list \ - "${profile_ids_old}${delimiter} '$profile_id']" - dconf write "$DCONF_PROFILES_PATH/:$profile_id"/visible-name "'$profile_name'" - echo $profile_id -} +# function on_exit () +# { +# set_bg "$THIS_OSX_TERM_WINDOW_ID" "$SOLARIZED_BACKGROUND" +# } -function get_terminal_profile_uuid() -{ - # 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 +# # 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 - local profile='' +# #if [ "$AS_ROOT" = 'true' ] +# #then +# # BG_COLOR="$DARK_ORANGE" +# #fi - local this_profile='' - local profile_uuid='' +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 - for this_profile in $(dconf list "$DCONF_PROFILES_PATH/") - do - # eg value for profile : ':b1dcc9dd-5262-4d8d-a863-c897e6d979b9/' - local this_profile_name='' - this_profile_name=$(dconf read $DCONF_PROFILES_PATH/${this_profile}visible-name | tr -d "'" ) - # echo "this_profile_name=$this_profile_name" - if [ "$this_profile_name" = "$profile_name" ] - then - profile_uuid=$(echo "$this_profile" | sed 's/^://' | sed 's|/||') - echo "$profile_uuid" - return 0 - fi - done +GSettingsKeyPath = str # eg org.gnome.Terminal.ProfilesList - # the profile named $profile_name doesn't exist... create it then - profile_uuid=$(create_new_profile $profile_name) - echo "$profile_uuid" -} +class GnomeSettings(): -function set_terminal_profile_bg_color() -{ - local profile_name="$1" # eg physix.ipr.univ-rennes1.fr - local bg_color="$2" # eg rgb(17, 25, 24) + 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[^]]*)\]$', 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)) - local profile_uuid='' - profile_uuid=$(get_terminal_profile_uuid $profile_name) - echo "profile_name=$profile_name profile_uuid=$profile_uuid" + strings = series_of_strings.split(', ') + for s in strings: + m = re.match(r"'(?P[^']*)'", s) + assert m is not None + values.append(m['unquoted_string']) + logging.debug('get_list: values = %s', str(values)) + return values - dconf write $DCONF_PROFILES_PATH/:$profile_uuid/use-theme-colors 'false' - dconf write $DCONF_PROFILES_PATH/:$profile_uuid/background-color "'rgb$bg_color'" -} + def set_list(self, key_id: GSettingsKeyPath, value: List[str]): -COLOR_SATURATION='0.3' -COLOR_VALUE='0.2' -OS_NAME=$(uname) -case "$OS_NAME" in - 'Darwin') - BG_COLOR=$(make_color.py $HOST_FQDN $COLOR_VALUE $COLOR_SATURATION osx) - THIS_OSX_TERM_WINDOW_ID=$(get_current_terminal_window_id) - set_bg "$THIS_OSX_TERM_WINDOW_ID" "$BG_COLOR" - trap on_exit EXIT - /usr/bin/ssh "$@" - ;; - 'Linux') - BG_COLOR=$(make_color.py $HOST_FQDN $COLOR_VALUE $COLOR_SATURATION linux) - if [ $? != 0 ] - then - echo "error : make_color.py failed" - exit 1 - fi - set_terminal_profile_bg_color "$HOST_FQDN" "$BG_COLOR" + # 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) - gnome-terminal --window-with-profile=$HOST_FQDN --title=$HOST_FQDN -- /usr/bin/ssh "$@" - ;; - *) - echo "error : unexpeced os name : $OS_NAME" - exit 1 -esac + 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[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[^']+)'$", 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[0-9]+), (?P[0-9]+), (?P[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() \ No newline at end of file