groups added

This commit is contained in:
ivan 2018-04-05 00:31:37 +03:00
parent c8069554eb
commit e430142fdc
3 changed files with 290 additions and 87 deletions

View File

@ -28,4 +28,10 @@ If you want to use bash autocompletion function with sshch, copy autocompletion
``` ```
sudo cp sshch_bash_completion.sh /etc/bash_completion.d/sshch sudo cp sshch_bash_completion.sh /etc/bash_completion.d/sshch
``` ```
(changes will come into effect with new bash session) (changes will come into effect with new bash session)
If you want to use zsh autocompletion:
1) Place File in a Directory where ZSH can find it
-> Search Path is Stored in $fpath
-> echo $fpath
2) Rename File to '_sshch'

View File

@ -10,7 +10,7 @@ def main():
url='https://github.com/zlaxy/sshch/', url='https://github.com/zlaxy/sshch/',
description='Ssh connection manager', description='Ssh connection manager',
license='DWTWL 2.5', license='DWTWL 2.5',
version='0.8', version='0.993',
py_modules=['sshch'], py_modules=['sshch'],
scripts=['sshch/sshch'], scripts=['sshch/sshch'],

View File

@ -20,9 +20,11 @@ import curses
from curses import textpad, panel from curses import textpad, panel
# https://github.com/zlaxy/sshch # https://github.com/zlaxy/sshch
version = "0.9" version = "0.993"
# path to conf file, default: ~/.config/sshch.conf # path to conf file, default: ~/.config/sshch.conf
conf_file = path.expanduser("~") + '/.config/sshch.conf' conf_file = path.expanduser("~") + '/.config/sshch.conf'
# expand groups by default
expand_default = False
def AddNewAlias(alias): def AddNewAlias(alias):
@ -39,6 +41,11 @@ def SetAliasString(alias, string):
conf.write(open(conf_file, "w")) conf.write(open(conf_file, "w"))
def SetGroupString(alias, string):
conf.set(alias, "group", string)
conf.write(open(conf_file, "w"))
def SetPassword(alias, string): def SetPassword(alias, string):
string = base64.b64encode(base64.b16encode( string = base64.b64encode(base64.b16encode(
base64.b32encode(string))) base64.b32encode(string)))
@ -56,22 +63,29 @@ def ConnectAlias(alias, command=False):
exec_string = "" exec_string = ""
if conf.has_option(alias, "password"): if conf.has_option(alias, "password"):
password = base64.b32decode(base64.b16decode( password = base64.b32decode(base64.b16decode(
base64.b64decode(conf.get(alias, "password")))) base64.b64decode(conf.get(alias, "password"))))
exec_string = 'sshpass -p "' + password + '" ' exec_string = 'sshpass -p "' + password + '" '
if conf.has_option(alias, "exec_string"): if conf.has_option(alias, "exec_string"):
exec_string = exec_string + conf.get(alias, "exec_string") exec_string = exec_string + conf.get(alias, "exec_string")
if command: if command:
exec_string = exec_string + " " + command exec_string = exec_string + " " + command
# Variables bellow is newer used
subprocess.Popen(exec_string, shell=True).communicate()[0] subprocess.Popen(exec_string, shell=True).communicate()[0]
def HoldConnection(alias): def HoldConnection(alias):
print("Connecting to " + alias + ". Press CTRL+C to cancel.") groups = []
time.sleep(1) connectaliases = []
while True: for a in conf.sections():
ConnectAlias(alias) if conf.has_option(a, "group"):
time.sleep(5) groups.append(a)
if alias in groups:
print("Can't hold connection with group.")
else:
print("Connecting to " + alias + ". Press CTRL+C to cancel.")
time.sleep(1)
while True:
ConnectAlias(alias)
time.sleep(5)
def CMDAdd(alias): def CMDAdd(alias):
@ -87,66 +101,161 @@ def CMDAdd(alias):
print(result) print(result)
def CMDEdit(alias): def CMDGroup(group):
if conf.has_section(alias): result = AddNewAlias(group)
prompt_edit = ("".join(["Enter connection string for existing alias ", if result:
"(example: ssh user@somehost.com):\n"])) prompt_add = ("".join(["Enter aliases for new group ",
"(example: alias1 alias2):\n"]))
string = "" string = ""
while string == "": while string == "":
string = input(prompt_edit) string = input(prompt_add)
SetAliasString(alias, string) SetGroupString(group, string)
else: else:
print("error: '" + alias + "' alias is not exists") print(result)
def CMDEdit(alias):
if conf.has_section(alias):
if conf.has_option(alias, "string"):
prompt_edit = ("".join(["Enter connection string for existing alias ",
"(example: ssh user@somehost.com):\n"]))
string = ""
while string == "":
string = input(prompt_edit)
SetAliasString(alias, string)
elif conf.has_option(alias, "group"):
prompt_edit = ("".join(["Enter aliases for existing group ",
"(example: alias1 alias2):\n"]))
string = ""
while string == "":
string = input(prompt_edit)
SetGroupString(alias, string)
else:
print("error: '" + alias + "' is not correct alias or group")
else:
print("error: '" + alias + "' alias or group does not exists")
def CMDPassword(alias): def CMDPassword(alias):
if conf.has_section(alias): groups = []
prompt_pass = ("[UNSAFE] Enter password for sshpass: ") connectaliases = []
string = "" for a in conf.sections():
string = getpass(prompt_pass) if conf.has_option(a, "group"):
if not string == "": groups.append(a)
SetPassword(alias, string) if alias in groups:
print("Can't set password for group.")
else: else:
print("error: '" + alias + "' alias is not exists") if conf.has_section(alias):
prompt_pass = ("[UNSAFE] Enter password for sshpass: ")
string = ""
string = getpass(prompt_pass)
if not string == "":
SetPassword(alias, string)
else:
print("error: '" + alias + "' alias does not exists")
def CMDRemove(alias): def CMDRemove(alias):
if conf.has_section(alias): if conf.has_section(alias):
prompt_remove = ("Type 'yes' if you sure to remove '" + alias + "' alias: ") prompt_remove = ("Type 'yes' if you sure to remove '" + alias + "' alias or group: ")
string = input(prompt_remove) string = input(prompt_remove)
if string == "yes": if string == "yes":
RemoveAliases([alias]) RemoveAliases([alias])
else: else:
print("'" + alias + "' alias was not deleted.") print("'" + alias + "' alias or group does not deleted.")
else: else:
print("error: '" + alias + "' alias is not exists.") print("error: '" + alias + "' alias or group does not exists.")
def CMDConnect(aliases, command=False): def CMDConnect(aliases, command=False):
groups = []
connectaliases = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
for alias in aliases: for alias in aliases:
if alias in groups:
group_aliases = conf.get(alias, "group").split()
for ga in group_aliases:
if not ga in connectaliases:
connectaliases.append(ga)
else:
if not alias in connectaliases:
connectaliases.append(alias)
for alias in connectaliases:
if conf.has_section(alias): if conf.has_section(alias):
print("Connecting to " + alias + "...") print("Connecting to " + alias + "...")
ConnectAlias(alias, command) ConnectAlias(alias, command)
print("... " + alias + " session finished.") print("... " + alias + " session finished.")
else: else:
print("error: '" + alias + "' alias is not exists") print("error: '" + alias + "' alias does not exists")
def CMDList(option, opt, value, parser): def CMDList(option, opt, value, parser):
print(' '.join(str(p) for p in conf.sections())) print(' '.join(str(p) for p in conf.sections()))
def GetTreeList(strings=True, expandlist=True):
aliases = []
groups = []
resultalias = []
resultstring = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
elif conf.has_option(a, "exec_string"):
aliases.append(a)
for g in groups:
resultalias.append(g)
resultstring.append(">> "+g)
group_aliases = conf.get(g, "group").split()
for ga in group_aliases:
if expandlist == False:
pass
elif expandlist == True or g in expandlist:
if conf.has_option(ga, "exec_string"):
resultalias.append(ga+" "+g)
result = "".join([" ", str(ga), " (", (conf.get(ga, "exec_string") if
conf.has_option(ga, "exec_string") else ""), ")",
(" [password]" if conf.has_option(ga, "password") else "")])
resultstring.append(result)
try:
aliases.remove(ga)
except ValueError:
pass
for a in aliases:
resultalias.append(a)
result = "".join([str(a), " (", (conf.get(a, "exec_string") if
conf.has_option(a, "exec_string") else ""), ")",
(" [password]" if conf.has_option(a, "password") else "")])
resultstring.append(result)
if strings:
return resultstring;
else:
return resultalias;
def CMDFullList(option, opt, value, parser): def CMDFullList(option, opt, value, parser):
for p in conf.sections(): for p in GetTreeList():
to_print = "".join([str(p), " - ", (conf.get(p, "exec_string") if print (p)
conf.has_option(p, "exec_string") else ""),
(" [password]" if conf.has_option(p, "password") else "")])
print(to_print)
def CursesConnect(screen, aliases, command=False): def CursesConnect(screen, aliases, command=False):
curses.endwin() curses.endwin()
groups = []
connectaliases = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
for alias in aliases: for alias in aliases:
if alias in groups:
group_aliases = conf.get(alias, "group").split()
for ga in group_aliases:
if not ga in connectaliases:
connectaliases.append(ga)
else:
if not alias in connectaliases:
connectaliases.append(alias)
for alias in connectaliases:
print("Connecting to " + alias + "...") print("Connecting to " + alias + "...")
ConnectAlias(alias, command) ConnectAlias(alias, command)
print("... " + alias + " session finished.") print("... " + alias + " session finished.")
@ -259,24 +368,29 @@ def CMDOptions():
opts.add_option('-a', '--add', action="store", type="string", opts.add_option('-a', '--add', action="store", type="string",
dest="add", metavar="alias", default=False, dest="add", metavar="alias", default=False,
help="add new alias for connection string") help="add new alias for connection string")
opts.add_option('-g', '--group', action="store", type="string",
dest="group", metavar="group", default=False,
help="add new group for aliases")
opts.add_option('-c', '--command', action="store", type="string", opts.add_option('-c', '--command', action="store", type="string",
dest="command", metavar="command", default=False, dest="command", metavar="command", default=False,
help="add command for executing alias") help="execute command for executing aliases or group")
opts.add_option('-k', '--keep', action="store", type="string", opts.add_option('-k', '--keep', action="store", type="string",
dest="keep", metavar="alias", default=False, dest="keep", metavar="alias", default=False,
help="hold connection with specified alias") help="hold connection with specified alias")
opts.add_option('-e', '--edit', action="store", type="string", opts.add_option('-e', '--edit', action="store", type="string",
dest='edit', metavar="alias", default=False, dest='edit', metavar="alias", default=False,
help="edit existing connection string") help="edit existing alias or group")
opts.add_option('-p', '--password', action="store", type="string", opts.add_option('-p', '--password', action="store", type="string",
dest='password', metavar="alias", default=False, dest='password', metavar="alias", default=False,
help="set and store password for sshpass [UNSAFE]") help="set and store password for sshpass [UNSAFE]")
opts.add_option('-r', '--remove', action="store", type="string", opts.add_option('-r', '--remove', action="store", type="string",
dest='remove', metavar="alias", default=False, dest='remove', metavar="alias", default=False,
help="remove existing alias of connection string") help="remove existing alias or group")
options, alias = opts.parse_args() options, alias = opts.parse_args()
if options.add: if options.add:
CMDAdd(options.add) CMDAdd(options.add)
if options.group:
CMDGroup(options.group)
if options.edit: if options.edit:
CMDEdit(options.edit) CMDEdit(options.edit)
if options.password: if options.password:
@ -295,18 +409,32 @@ def CursesMain():
help_screen = ("".join([" Press:\n", help_screen = ("".join([" Press:\n",
" 'z'/'x' or arrows - navigation\n", " 'z'/'x' or arrows - navigation\n",
" 'a'/'F2' - add new alias\n", " 'a'/'F2' - add new alias\n",
" 'e'/'F4' - edit existing alias\n", " 'g'/'F5' - add new group\n",
" 'e'/'F4' - edit existing alias/group\n",
" 'p'/'F6' - set alias's password for sshpass [UNSAFE]\n", " 'p'/'F6' - set alias's password for sshpass [UNSAFE]\n",
" 'space'/'insert' - select\n", " 'space'/'insert' - select\n",
" 'r'/'F8' - remove selected alias/aliases\n", " 'r'/'F8' - remove selected alias/aliases\n",
" 'c'/'F3' - execute specific command with selected alias/aliases\n", " 'c'/'F3' - execute specific command with selected alias/aliases\n",
" 'k'/'F7' - hold connection with selected alias\n", " 'k'/'F7' - hold connection with selected alias\n",
" 'enter'/'F9' - connect to selected alias/aliases\n", " 'enter'/'F9' - connect to selected alias/aliases,\n",
" expand/collapse group\n",
" 'q'/'F10' - quit\n", " 'q'/'F10' - quit\n",
" Run program with '--help' option to view command line help.\n", " Run program with '--help' option to view command line help.\n",
" Also, you can edit config file manually:\n", " Also, you can edit config file manually:\n",
" ", conf_file])) " ", conf_file]))
strings = conf.sections() if expand_default == True:
groups = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
expanded = groups
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
elif expand_default == False:
groups = []
expanded = groups
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings) row_num = len(strings)
selected_strings = [" " for i in range(0, row_num + 1)] selected_strings = [" " for i in range(0, row_num + 1)]
screen = curses.initscr() screen = curses.initscr()
@ -333,14 +461,7 @@ def CursesMain():
box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.", box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.",
width - 6, highlight_text) width - 6, highlight_text)
else: else:
if conf.has_option(strings[i - 1], "password"): exec_string = ["[", selected_strings[i], "] ", stringsfull[i - 1]]
password = " [password]"
else:
password = ""
exec_string = ["[", selected_strings[i], "] ", str(i), " ",
strings[i - 1], " (", (conf.get(strings[i - 1],
"exec_string") if conf.has_option(strings[i - 1],
"exec_string") else ""), ")", password]
if (i == position): if (i == position):
box.addnstr(i, 2, "".join(exec_string), width - 6, highlight_text) box.addnstr(i, 2, "".join(exec_string), width - 6, highlight_text)
else: else:
@ -382,34 +503,95 @@ def CursesMain():
CursesTextpadConfirm) CursesTextpadConfirm)
SetAliasString(add_alias.rstrip(), SetAliasString(add_alias.rstrip(),
add_string.replace("\n", "").rstrip()) add_string.replace("\n", "").rstrip())
strings = conf.sections() strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings) row_num = len(strings)
selected_strings.append(" ") selected_strings.append(" ")
pages = int(ceil(row_num / max_row)) pages = int(ceil(row_num / max_row))
box.refresh() box.refresh()
if key_pressed == ord('g') or key_pressed == ord(
'G') or key_pressed == curses.KEY_F5:
new_group_textpad = CursesTextpad(screen, 1, width - 8,
(height // 2) - 1, 4, "Enter name for new group:", "",
normal_text, highlight_text)
add_group = new_group_textpad.edit(CursesTextpadConfirm)
if not add_group.rstrip() == "":
add_result = AddNewAlias(add_group.rstrip())
if not add_result:
CursesPanel(screen, 3,
width - 6, (height // 2) - 1, 3, add_result,
normal_text, highlight_text)
else:
add_string = ""
while add_string.rstrip() == "":
string_textpad = CursesTextpad(screen, 3,
width - 8, (height // 2) - 1, 4,
"Enter aliases for new group (example: alias1 alias2):",
"", normal_text, highlight_text)
add_string = string_textpad.edit(
CursesTextpadConfirm)
SetGroupString(add_group.rstrip(),
add_string.replace("\n", "").rstrip())
expanded.append(add_group.rstrip())
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings)
selected_strings = [" " for i in range(0, row_num + 1)]
pages = int(ceil(row_num / max_row))
box.refresh()
if (key_pressed == ord('e') or key_pressed == ord( if (key_pressed == ord('e') or key_pressed == ord(
'E') or key_pressed == curses.KEY_F4) and row_num != 0: 'E') or key_pressed == curses.KEY_F4) and row_num != 0:
edit_string = "" edit_string = ""
while edit_string.rstrip() == "": groups = []
string_textpad = CursesTextpad(screen, 3, width - 8, for a in conf.sections():
(height // 2) - 1, 4, "Enter new execution string:", if conf.has_option(a, "group"):
(conf.get(strings[position - 1], groups.append(a)
"exec_string") if conf.has_option(strings[position - 1], if strings[position - 1] in groups:
"exec_string") else ""), while edit_string.rstrip() == "":
normal_text, highlight_text) string_textpad = CursesTextpad(screen, 3, width - 8,
edit_string = string_textpad.edit(CursesTextpadConfirm) (height // 2) - 1, 4, "Enter new aliases:",
SetAliasString(strings[position - 1], (conf.get(strings[position - 1],
edit_string.replace("\n", "").rstrip()) "group") if conf.has_option(strings[position - 1],
strings = conf.sections() "group") else ""),
normal_text, highlight_text)
edit_string = string_textpad.edit(CursesTextpadConfirm)
SetGroupString(strings[position - 1],
edit_string.replace("\n", "").rstrip())
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings)
selected_strings.append(" ")
pages = int(ceil(row_num / max_row))
box.refresh()
else:
while edit_string.rstrip() == "":
string_textpad = CursesTextpad(screen, 3, width - 8,
(height // 2) - 1, 4, "Enter new execution string:",
(conf.get(strings[position - 1].split()[0].strip(),
"exec_string") if conf.has_option(strings[position - 1].split()[0].strip(),
"exec_string") else ""),
normal_text, highlight_text)
edit_string = string_textpad.edit(CursesTextpadConfirm)
SetAliasString(strings[position - 1].split()[0].strip(),
edit_string.replace("\n", "").rstrip())
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
if (key_pressed == ord('p') or key_pressed == ord( if (key_pressed == ord('p') or key_pressed == ord(
'P') or key_pressed == curses.KEY_F6) and row_num != 0: 'P') or key_pressed == curses.KEY_F6) and row_num != 0:
set_password = "" groups = []
set_password = CursesPanel(screen, 4, width - 6, for a in conf.sections():
(height // 2) - 1, 3, if conf.has_option(a, "group"):
" Enter user password for sshpass and press 'enter':\n>", groups.append(a)
normal_text, highlight_text, "password") if not strings[position - 1].split()[0].strip() in groups:
if not set_password == "": set_password = ""
SetPassword(strings[position - 1], set_password) set_password = CursesPanel(screen, 4, width - 6,
(height // 2) - 1, 3,
" Enter user password for sshpass and press 'enter':\n>",
normal_text, highlight_text, "password")
if not set_password == "":
SetPassword(strings[position - 1].split()[0].strip(), set_password)
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
if (key_pressed == ord('r') or key_pressed == ord( if (key_pressed == ord('r') or key_pressed == ord(
'R') or key_pressed == curses.KEY_F8 or key_pressed == ( 'R') or key_pressed == curses.KEY_F8 or key_pressed == (
curses.KEY_DC)) and row_num != 0: curses.KEY_DC)) and row_num != 0:
@ -423,14 +605,15 @@ def CursesMain():
str(len(selected)), " selected aliases? (y/N)"])) str(len(selected)), " selected aliases? (y/N)"]))
else: else:
remove_confirm = ("".join(["Are you sure to remove '", remove_confirm = ("".join(["Are you sure to remove '",
strings[position - 1], "' alias? (y/N)"])) strings[position - 1].split()[0].strip(), "' alias? (y/N)"]))
selected.append(strings[position - 1]) selected.append(strings[position - 1].split()[0].strip())
remove_result = CursesPanel(screen, 4, width - 6, remove_result = CursesPanel(screen, 4, width - 6,
(height // 2) - 1, 3, remove_confirm, normal_text, (height // 2) - 1, 3, remove_confirm, normal_text,
highlight_text, "remove") highlight_text, "remove")
if remove_result == "confirm": if remove_result == "confirm":
RemoveAliases(selected) RemoveAliases(selected)
strings = conf.sections() strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings) row_num = len(strings)
selected_strings = [" " for i in range(0, row_num + 1)] selected_strings = [" " for i in range(0, row_num + 1)]
pages = int(ceil(row_num / max_row)) pages = int(ceil(row_num / max_row))
@ -444,7 +627,7 @@ def CursesMain():
if selected_strings[i] == "*": if selected_strings[i] == "*":
selected.append(strings[i - 1]) selected.append(strings[i - 1])
if not len(selected) > 0: if not len(selected) > 0:
selected.append(strings[position - 1]) selected.append(strings[position - 1].split()[0].strip())
command_textpad = CursesTextpad(screen, 3, width - 8, command_textpad = CursesTextpad(screen, 3, width - 8,
(height // 2) - 1, 4, (height // 2) - 1, 4,
"".join([ "".join([
@ -456,17 +639,38 @@ def CursesMain():
command_string.replace("\n", "").rstrip()) command_string.replace("\n", "").rstrip())
if (key_pressed == ord('k') or key_pressed == ord('K') or if (key_pressed == ord('k') or key_pressed == ord('K') or
key_pressed == (curses.KEY_F7)) and row_num != 0: key_pressed == (curses.KEY_F7)) and row_num != 0:
curses.endwin() groups = []
HoldConnection(strings[position - 1]) for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
if not strings[position - 1] in groups:
curses.endwin()
HoldConnection(strings[position - 1].split()[0].strip())
if (key_pressed == ord("\n") or key_pressed == ( if (key_pressed == ord("\n") or key_pressed == (
curses.KEY_F9)) and row_num != 0: curses.KEY_F9)) and row_num != 0:
selected = [] groups = []
for i in range(1, row_num + 1): for a in conf.sections():
if selected_strings[i] == "*": if conf.has_option(a, "group"):
selected.append(strings[i - 1]) groups.append(a)
if not len(selected) > 0: if strings[position - 1] in groups:
selected.append(strings[position - 1]) if strings[position - 1] in expanded:
CursesConnect(screen, selected) expanded.remove(strings[position - 1])
else:
expanded.append(strings[position - 1])
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings)
selected_strings.append(" ")
pages = int(ceil(row_num / max_row))
box.refresh()
else:
selected = []
for i in range(1, row_num + 1):
if selected_strings[i] == "*":
selected.append(strings[i - 1])
if not len(selected) > 0:
selected.append(strings[position - 1].split()[0].strip())
CursesConnect(screen, selected)
if (key_pressed == 32 or key_pressed == ( if (key_pressed == 32 or key_pressed == (
curses.KEY_IC)) and row_num != 0: curses.KEY_IC)) and row_num != 0:
if selected_strings[position] == ' ': if selected_strings[position] == ' ':
@ -537,14 +741,7 @@ def CursesMain():
box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.", box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.",
width - 6, highlight_text) width - 6, highlight_text)
else: else:
if conf.has_option(strings[i - 1], "password"): exec_string = ["[", selected_strings[i], "] ", stringsfull[i - 1]]
password = " [password]"
else:
password = ""
exec_string = ["[", selected_strings[i], "] ", str(i), " ",
strings[i - 1], " (", (conf.get(strings[i - 1],
"exec_string") if conf.has_option(strings[i - 1],
"exec_string") else ""), ")", password]
if (i + (max_row * (page - 1)) == (position + (max_row * (page - 1)))): if (i + (max_row * (page - 1)) == (position + (max_row * (page - 1)))):
box.addnstr(i - (max_row * (page - 1)), 2, "".join( box.addnstr(i - (max_row * (page - 1)), 2, "".join(
exec_string), width - 6, highlight_text) exec_string), width - 6, highlight_text)