1
\$\begingroup\$

I have been working on a Python Curses terminal user interface for RSync for a little while now. I recently visited code review and recieved some help with cleaning up the code to make it more pythonic. I worked for a bit to break up classes, simplify functions, etc. Now, I'm returning to see if my code is more complicated than it needs to be. Since I'm learning while programing, I'm trying to achieve loose coupling and high cohesion where it makes sense, but am also well.. learning while programming. During this adventure it seems some areas have become more simple and more reusable however, some areas of the code I feel like I have to inject multiple variables or instances to make something work.

The main function of the code sets up some instances to create left and right file manager panels, then allows for tabbing between the panels.

#!/usr/bin/env python3.6
import os
import curses
import curses.textpad
import curses.panel 
import constants as CONST
from pathlib import Path
from options_menu import OptionsMenu
from ssh import SSH
from window_manager import WinManager
from file_explorer import FileExplorer
from reset_window import ResetWindow
from menu_bar import MenuBar
from status_bar import StatusBar
from key_press import KeyPress
from display import Display
def prepare_curses():
 """Setup terminal for curses operations."""
 stdscr.erase()
 stdscr.keypad(True)
 curses.noecho()
 curses.cbreak()
 curses.curs_set(0)
 curses.start_color()
 curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
 curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
 curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLACK)
 curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_CYAN)
 stdscr.bkgdset(curses.color_pair(3))
def end_curses():
 """Gracefully reset terminal to normal settings."""
 stdscr.keypad(0)
 curses.echo()
 curses.nocbreak()
 curses.endwin()
 os.system('reset')
class Main:
 """Main function handles high-level switching between windows and file menus."""
 def __init__(self, menu_bar, win_manager, file_explorers, status_bar):
 _menu_bar = menu_bar
 _win_manager = win_manager
 _file_explorers = file_explorers
 _status_bar = status_bar
 _current_panel = 0
 ready_to_exit = False
 while ready_to_exit is not True:
 _win_manager.upd_panel(_current_panel, _file_explorers[_current_panel].full_path)
 Display(_file_explorers[_current_panel])
 key_press = KeyPress(
 _file_explorers[_current_panel],
 _file_explorers[_current_panel].data,
 _file_explorers[_current_panel].position,
 )
 _file_explorers[_current_panel].scroll()
 self.call_menu(key_press, _menu_bar, left_file_explorer)
 _current_panel = self.switch_panels(key_press.tab_event, _current_panel, _status_bar)
 ready_to_exit = self.exit_main_loop(key_press.key)
 curses.doupdate()
 def exit_main_loop(self, event):
 if event == CONST.CONST_LET_Q_LWRCSE_KEY:
 return True
 def call_menu(self, key_press, _menu_bar, _left_file_explorer):
 _call_menu_event = key_press.menu_event
 _key_press = key_press.key
 ready_to_return = False
 if _call_menu_event is True:
 while ready_to_return is not True:
 _menu_bar(_key_press)
 Display(_menu_bar)
 _key_press = KeyPress(
 _menu_bar,
 _menu_bar.menu_item,
 _menu_bar.position
 )
 
 self.call_ssh(_key_press.selected_menu_item)
 _ssh_is_enabled = self.ssh_enabled()
 ready_to_return = self.return_to_main_loop(_key_press.key, _ssh_is_enabled)
 _menu_bar.close_menu()
 ResetWindow(_left_file_explorer)
 else:
 return _call_menu_event
 def update_all_views(self, _menu_bar, _file_explorers, _status_bar):
 _left_file_explorer = _file_explorers[0]
 _right_file_explorer = _file_explorers[1]
 _menu_bar.draw_menu_bar()
 ResetWindow(_left_file_explorer)
 ResetWindow(_right_file_explorer)
 _status_bar.refresh(1)
 curses.doupdate()
 def call_ssh(self, selected_menu_item):
 if selected_menu_item == 'ssh':
 ssh_object.start(win_manager, stdscr, None)
 def ssh_enabled(self):
 return ssh_object.is_enabled
 def return_to_main_loop(self,event,ssh_is_enabled):
 if event in (
 CONST.CONST_LET_F_LWRCSE_KEY,
 CONST.CONST_LET_O_LWRCSE_KEY,
 CONST.CONST_LET_Q_LWRCSE_KEY
 ) or ssh_is_enabled is True:
 return True
 else:
 return False
 def switch_panels(self, tab_event, _current_panel, _status_bar):
 if tab_event is True:
 _current_panel = win_manager.set_active_panel(_current_panel)
 _status_bar.refresh(_current_panel)
 return _current_panel
 else:
 return _current_panel
if __name__ == '__main__':
 #Setup curses and begin initializing various packages to draw the menubar,
 #left and right windows, statusbar, a ssh object for storage, a global options object
 #for storage and setup the file explorers.
 stdscr = curses.initscr()
 screen_height, screen_width = stdscr.getmaxyx()
 prepare_curses()
 menu_bar = MenuBar(stdscr)
 win_manager = WinManager(stdscr)
 left_win = win_manager.draw_left_win()
 right_win = win_manager.draw_right_win()
 ssh_object = SSH()
 left_file_explorer = FileExplorer(win_manager, left_win)
 right_file_explorer = FileExplorer(win_manager, right_win, ssh_object=ssh_object)
 file_explorers = [left_file_explorer, right_file_explorer]
 left_file_explorer.get_file_explorers(file_explorers)
 right_file_explorer.get_file_explorers(file_explorers)
 status_bar = StatusBar(stdscr)
 global_options = OptionsMenu(stdscr)
 main = Main(menu_bar, win_manager, file_explorers, status_bar)
 ssh_object.ssh_close()
 end_curses()

From here, the code relies heavily on the FileExplorer and KeyPress classes which I've also included below. The FileExplorer class is responsible for iterating over the files and folders in a given directory, parsing them and saving them in a dictionary. FileExplorer is also meant to do the same thing but for files over SSH, to ultimately allow the use of RSync from/to a remote system.

"""FileExplorer Class and ResetPath Class"""
import curses
import os
import stat
from ssh import SSH
from human_readable_size import human_readable_size
from window_manager import WinManager
from pop_ups import PopUpFileOpener
from pop_ups import PopUpCopyFile
from pathlib import Path
class ResetPath:
 """Used to reset the path to '/' when FileExplorer messses up"""
 def __init__(self):
 self.errors = 0
 def error(self, obj):
 self.obj = obj
 self.errors += 1
 if self.errors == 2:
 self.obj.path = '/'
 self.errors = 0
class FileExplorer:
 """Work horse of the application
 Does the bulk of the work by taking a path and listing directorys/files within that
 path. Also prints the list of directorys/files and is responsible for navigating
 through those files. The ssh_explorer method is meant to closely mirror the explorer
 method but is modified for paramiko where appropriate.
 """
 #path = '/'
 paths = [None, None]
 prev_paths = ['/', '/']
 path_errors = ResetPath()
 def __init__(self, win_manager, window, path='/', ssh_object=None):
 self.ssh_obj = ssh_object
 self.win_manager = win_manager
 self.window = window
 self.screen_height, self.screen_width = self.win_manager.stdscr.getmaxyx()
 height, width = self.window.getmaxyx()
 start_y, start_x = self.window.getbegyx()
 self.ssh_path_depth = 0
 self.depth = 0
 self.height = height
 self.width = width - 2
 self.start_y = start_y + 1
 self.start_x = start_x + 1
 self.position = 0
 self.scroller = 0
 self.event = None
 self.ssh_path = None
 self.ssh_path_hist = ['/']
 self.path_info = list()
 
 self.draw_pad()
 self.explorer(path)
 self.menu()
 def explorer(self, path):
 self.path = path
 if isinstance(self.ssh_obj, SSH):
 if self.ssh_obj.is_enabled:
 self.use_ssh_explorer(path)
 return
 #cleaned_path = self.clean_path(path) #setting up for future use
 _full_path = self.get_full_path(path) #create full path
 path_obj = Path(_full_path)
 file_dir_list = self.walk_tree(path_obj) #get files and folders from directory
 data_list = self.file_dir_iter(file_dir_list) #parse files, identify if file is dir, get size
 data_list = self.sort_file_list(data_list) #sort the list of files and folder
 data_list = self.create_list_index(data_list) #add index
 data = self.list_to_dict(data_list) #convert list to index
 self.data = self.root_or_dot_dot(_full_path, data) #determine if parent dir exists
 
 def ssh_explorer(self, ssh_path):
 self.next_ssh_path = self.get_full_path(ssh_path)
 ssh_files_folders_dir = self.ssh_obj.sftp.listdir(self.next_ssh_path)
 ssh_files_folders_attr = self.ssh_obj.sftp.listdir_attr(self.next_ssh_path)
 self.ssh_get_abs_path(self.next_ssh_path)
 data_list = self.ssh_parse_files(ssh_files_folders_dir, ssh_files_folders_attr)
 data_list = self.sort_file_list(data_list)
 data_list = self.create_list_index(data_list)
 self.data = self.list_to_dict(data_list)
 self.data = self.root_or_dot_dot(self.next_ssh_path, self.data)
 
 def ssh_parse_files(self, files_folders_dir, files_folders_attr):
 data_list = list()
 #item = 0
 for file_name, attr in zip(files_folders_dir, files_folders_attr):
 #is_dir = Path.is_dir()
 is_dir = stat.S_ISDIR(attr.st_mode)
 size = attr.st_size
 size = human_readable_size(size, suffix="B")
 if is_dir:
 is_dir = '/'
 else:
 is_dir = ''
 data_list.append([is_dir+file_name, size])
 #item += 1
 return data_list
 def file_dir_iter(self, files_dirs):
 """Requires list object"""
 data_list = list()
 for item in files_dirs:
 #_full_path = os.path.join(self.full_path, item)
 _full_path = Path(self.full_path, item)
 #is_dir = os.path.isdir(_full_path)
 is_dir = Path.is_dir(_full_path)
 size = os.path.getsize(_full_path)
 #size = Path.stat().st_size
 size = human_readable_size(size, suffix="B")
 if is_dir:
 #item = os.path.join(' ',item)
 item = '/' + item
 item = item.strip()
 # item = os.path.normpath(item).strip()
 else:
 item 
 data_list.append([item, size])
 return data_list
 def create_list_index(self, list):
 """Requires list object"""
 i = 1
 for item in list:
 item.insert(0,i)
 i = i + 1
 return list
 def use_ssh_explorer(self, path):
 if self.win_manager.active_panel == 1:
 self.right_file_explorer.ssh_explorer(path)
 def get_full_path(self, path):
 #self.depth_test = len(path.parts)
 if path == '/':
 self.path_info.append('/')
 self.depth = 0
 elif path == ('..'):
 if self.depth != 0:
 
 self.path_info.pop()
 self.depth -= 1
 else:
 self.path_info.append(path)
 self.depth += 1
 self.full_path = ''.join(map(str, self.path_info))
 return self.full_path.replace('//','/')
 def walk_tree(self, path):
 file_dir_list = list()
 for file_obj in path.iterdir():
 file_dir_list.append(file_obj.name)
 return file_dir_list
 def sort_file_list(self, list):
 """Requires list object"""
 data_list = sorted(list, key=lambda x:x[:1])
 return data_list
 def get_file_explorers(self, file_explorers):
 '''imports file explorers'''
 self.file_explorers = file_explorers
 self.left_file_explorer = file_explorers[0]
 self.right_file_explorer = file_explorers[1]
 def clean_path(self, path):
 return path.replace('//','/')
 def list_to_dict(self, data_list):
 item = 0
 data_dict = dict()
 data_dict = {item[0]: item[1:] for item in data_list}
 return data_dict
 def root_or_dot_dot(self, path, data):
 if path == '/':
 data[0] = ['/','']
 else:
 data[0] = ['..','']
 return data
 def join_path(self, path_1, path_2):
 return os.path.join(path_1,path_2)
 def draw_pad(self):
 '''draws pad larger than the file window so that it is scrollable'''
 self.pad = curses.newpad(self.height + 800, self.width) #size of pad
 self.pad.scrollok(True)
 self.pad.idlok(True)
 self.pad.keypad(True)
 self.pad.bkgd(curses.color_pair(1))
 def menu(self):
 '''creates selectable list of items, files, folders'''
 self.pad.erase()
 self.height, self.width = self.window.getmaxyx()
 self.screen_height, self.screen_width = self.win_manager.stdscr.getmaxyx()
 self.max_height = self.height -2
 self.bottom = self.max_height #+ len(self.tup) #self.max_height
 self.scroll_line = self.max_height - 3
 self.pad.setscrreg(0,self.max_height) #self.bottom -2)
 self.width = self.width - 2
 self.pad_refresh = lambda: self.pad.noutrefresh(
 self.scroller,
 0,
 self.start_y,
 self.start_x,
 self.bottom,
 self.screen_width - 2
 )
 #par = '[ ]' # can be added to the msg below to create a selector, likely to be removed
 for index, items in self.data.items():
 padding = self.width - len(items[0]) - 5
 if index == self.position:
 mode = curses.A_REVERSE
 else:
 mode = curses.A_NORMAL
 msg = f'{index:>3}{" "}{items[0]}{items[1]:>{padding}}'
 self.pad.addstr(index, 0, str(msg), mode)
 if mode == curses.A_REVERSE:
 self.cursor = self.pad.getyx()[0]
 self.pad_refresh()
 def set_paths(self, path):
 panel = WinManager.active_panel
 if panel == 0:
 oth_panel = 1
 else:
 oth_panel = 0
 if path is None:
 self.paths[panel] = self.prev_paths[panel].replace('//','/')
 else:
 self.prev_paths[panel] = self.paths[panel]
 self.paths[oth_panel] = self.prev_paths[oth_panel]
 def enter(self):
 """Changes the directory or opens file when enter key is called"""
 _selected_path = self.get_file_name()
 is_dir = _selected_path.startswith('/')
 if self.position != 0:
 _path_to_open = self.join_path(self.full_path,_selected_path)
 if is_dir:
 if WinManager.active_panel == 1:
 if self.ssh_obj.is_enabled:
 self.new_path = self.path 
 self.pad_refresh()
 self.position = self.scroller = 0
 self.paths[self.win_manager.active_panel] = _path_to_open
 self.set_paths(_path_to_open)
 self.explorer(_path_to_open)
 else:
 PopUpFileOpener(self.file_explorers, self.win_manager.stdscr, _path_to_open)
 else:
 path_parent = Path(self.full_path).parent
 if self.ssh_obj:
 if self.ssh_obj.is_enabled and self.win_manager.active_panel == 1:
 self.par_dir = '..'
 self.set_paths(self.full_path)
 self.explorer(_selected_path)
 self.win_manager.upd_panel(self.win_manager.active_panel, self.full_path)
 def ssh_explorer_attr(self):
 if glbl_opts.low_bandwidth:
 size = 0
 else:
 self.ssh_files_folders_attr = self.ssh_obj.sftp.listdir_attr(
 path=self.next_ssh_path)
 size_list = []
 for entry in self.ssh_files_folders_attr:
 size = entry.st_size
 return size_list
 def ssh_get_abs_path(self, path):
 self.ssh_abs_path = self.ssh_obj.sftp.normalize(path)
 def go_to_top(self):
 self.position = 0
 self.scroller = 0
 def go_to_bottom(self):
 data_length = len(self.data)
 self.position = data_length - 1
 self.scroller = data_length - self.scroll_line - 1
 def del_selected_items(self, sel_file):
 PopUpDelete(sel_file)
 def copy_selected_items(self):
 file_name = self.get_file_name()
 if self.position != 0:
 if self.win_manager.active_panel == 0:
 #self.from_file = self.path + '/' + file_name
 self.from_file = self.left_file_explorer.full_path + '/' + file_name
 self.to_path = self.right_file_explorer.full_path
 elif self.win_manager.active_panel == 1:
 self.from_file = self.right_file_explorer.full_path + '/' + file_name
 self.to_path = self.left_file_explorer.full_path
 PopUpCopyFile(
 self.file_explorers,
 self.win_manager.stdscr,
 self.from_file,
 self.to_path,
 file_name
 )
 def start_rsync(self):
 file_name = self.data[self.position][0]
 left_panel_path = left_file_explorer.abs_path
 right_panel_path = right_file_explorer.ssh_abs_path
 if self.win_manager.active_panel == 0:
 self.from_file = left_panel_path + '/' + file_name
 self.to_path = right_panel_path
 elif self.win_manager.active_panel == 1:
 self.from_file = right_panel_path + '/' + file_name
 self.to_path = left_panel_path
 if self.position != 0:
 rsync_obj = RSync(0).start(self.from_file, self.to_path, file_name)
 
 def scroll(self):
 if self.cursor > self.scroll_line:
 self.data_length = len(self.data) - 1
 if self.position != self.data_length:
 self.scroller = self.position - self.scroll_line
 if self.scroller == -1:
 self.scroller = 0
 if self.position == 0:
 self.scroller = 0
 def get_file_name(self):
 return self.data[self.position][0]
 def is_dir(self, path):
 if path.startswith('/'):
 self.full_path = os.path.join(path, '')
 return True
 return False

Here is the KeyPress class. The KeyPress class is well responsible for awaiting a keypress and then either going up or down, left or right in a menu, or kicking off some function such as copying a file, creating a directory or starting rsync, if ssh'd into a remote system.

"""KeyPress Class"""
import curses
import constants as CONST
from pop_ups import PopUpNewDir
from navigate import navigate
class KeyPress:
 """Call function based on keypress
 This Class utilizes the curses module to evaluate a keypress (arrow keys, enter, escape, etc)
 and complete an action."""
 def __init__(self, obj, items=None, position=None):
 self.selected_menu_item = None
 self.obj = obj
 self.items = items
 self.tab_event = False
 self.menu_event = False
 self.position = position
 self.get_input(obj)
 self.key_or_arrow_event()
 def __call__(self, *args, **kwds):
 self.selected_menu_item = None
 return self.key
 def get_input(self, obj):
 obj.window.keypad(True)
 try:
 self.key = obj.window.getch()
 except KeyboardInterrupt:
 return
 def key_or_arrow_event(self):
 if self.key in (curses.KEY_LEFT, curses.KEY_RIGHT, curses.KEY_UP, curses.KEY_DOWN):
 self.arrow_event()
 else:
 self.key_event()
 
 def arrow_event(self):
 if self.key == curses.KEY_UP:
 self.obj.position = navigate(-1, self.items, self.position)
 elif self.key == curses.KEY_DOWN:
 self.obj.position = navigate(1, self.items, self.position)
 elif self.key == curses.KEY_RIGHT:
 self.obj.position = navigate(1, self.items, self.position)
 elif self.key == curses.KEY_LEFT:
 self.obj.position = navigate(-1, self.items, self.position)
 def key_event(self):
 event = self.key
 if event == CONST.CONST_ENTER_KEY:
 enter_key_event = self.enter_key()
 self.selected_menu_item = self.items[self.position]
 if enter_key_event is not None: #use this or selected_menu_item... test and delete one or other
 event = enter_key_event
 return event
 #elif event == 'ssh':
 # return self.open_program(event)
 elif event == CONST.CONST_ESCAPE_KEY:
 return event
 elif event == CONST.CONST_TAB_KEY:
 self.tab_event = True
 elif event in (CONST.CONST_LET_F_LWRCSE_KEY, CONST.CONST_LET_O_LWRCSE_KEY):
 self.menu_event = True
 elif event == CONST.CONST_LET_Q_LWRCSE_KEY:
 return event
 elif event == CONST.CONST_LET_X_LWRCSE_KEY:
 self.close_ssh_key(event)
 elif event == CONST.CONST_NUM_5_KEY:
 self.copy_key()
 elif event == CONST.CONST_LET_N_LWRCSE_KEY:
 self.new_dir_key(obj, event)
 elif event == CONST.CONST_LET_B_LWRCSE_KEY:
 self.to_bottom_key(obj)
 elif event == CONST.CONST_LET_T_LWRCSE_KEY:
 self.to_top_key(obj)
 elif event == CONST.CONST_NUM_9_KEY:
 self.delete_key(obj, items, position)
 else:
 pass
 def to_top_key(obj):
 self.obj.go_to_top()
 def to_bottom_key(obj):
 self.obj.go_to_bottom()
 def delete_key(obj, items, position):
 item = items[position][0]
 self.obj.del_selected_items(obj._exp.path + '/' + item)
 def new_dir_key():
 PopUpNewDir()
 def enter_key(self):
 item = self.obj.enter()
 if item is not None:
 return item
 def close_ssh_key():
 if self.ssh_obj.is_enabled:
 self.ssh_obj.ssh.close()
 self.ssh_obj.enabled()
 win_manager.upd_panel()
 #right_file_explorer.explorer(right_file_explorer.path)
 #right_file_explorer.menu()
 #ResetWindow(right_file_explorer)
 else:
 pass
 def copy_key(self):
 self.obj.copy_selected_items()

What I'm ultimately trying to achieve is making the code more transportable, more loosely coupled, but ultimately to help me learn so I can build good 'programming' habits and write better! I have the program up on github here with all of the classes https://github.com/1amdash/syncrpy-tui

asked Nov 17, 2022 at 2:19
\$\endgroup\$
0

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.