Browse Source

Added optimization display / refactored data display / Added most method doc strings

master
Doug Le Tough 4 years ago
parent
commit
affb8160cf
8 changed files with 346 additions and 51 deletions
  1. +82
    -14
      engine/__init__.py
  2. +11
    -0
      init_colors.sh
  3. +3
    -1
      pycursesvirtmgr.py
  4. BIN
      screenshots/devices.png
  5. BIN
      screenshots/domains.png
  6. +229
    -34
      ui/__init__.py
  7. +6
    -2
      ui/input_manager.py
  8. +15
    -0
      ui/window.py

+ 82
- 14
engine/__init__.py View File

@ -2,16 +2,29 @@
# -*- coding: utf-8 -*-
import libvirt
import xml.etree.ElementTree as ET
class Engine(object):
def __init__(self, protocol=None, host=None):
""" The real interface to libvirt """
# The domain states list is shared by all engines
domain_states = {
libvirt.VIR_DOMAIN_NOSTATE: 'No state',
libvirt.VIR_DOMAIN_RUNNING: 'Running',
libvirt.VIR_DOMAIN_BLOCKED: 'Blocked',
libvirt.VIR_DOMAIN_PAUSED: 'Paused',
libvirt.VIR_DOMAIN_SHUTDOWN: 'Shut down',
libvirt.VIR_DOMAIN_SHUTOFF: 'Shut off',
libvirt.VIR_DOMAIN_CRASHED: 'Crashed',
libvirt.VIR_DOMAIN_PMSUSPENDED: 'PM suspended',
}
def __init__(self, protocol='qemu:///', host='system'):
""" Init the engine
Default protocol for connecting to hypervisor is qemu:///
Default host is system '"""
self.protocol = protocol
self.host = host
if not protocol:
self.protocol = 'qemu:///'
if not host:
self.host = 'system'
self.connection = None
self.domains = self.get_domains()
self.storage_pools = self.get_storage_pools()
@ -22,38 +35,55 @@ class Engine(object):
self.devices = self.get_devices()
def connect(self):
""" Connect to hypervisor via libvirt """
self.connection = libvirt.open('%s%s' % (self.protocol, self.host))
def disconnect(self):
""" disconnect from hypervisor """
self.connection.close()
def get_domains(self):
""" Get domains infos and status from hypervisor """
domains = []
self.connect()
for dom in self.connection.listAllDomains():
control_info = None
disk_errors = None
cpu_stats = None
virt_cpus_number = None
memory_stats = None
if dom.state()[0] == 1:
control_info = dom.controlInfo()
disk_errors = dom.diskErrors()
cpu_stats = dom.getCPUStats(True)
virt_cpus_number = len(dom.getCPUStats(True))
memory_stats = dom.memoryStats()
domain = {
'id': dom.ID(),
'name': dom.name(),
'os_type': dom.OSType(),
'xml': dom.XMLDesc(),
'state': dom.state(),
'max_memory': dom.maxMemory(),
'max_memory': dom.maxMemory(),
'autostart': dom.autostart(),
'control_info': dom.controlInfo(),
'disk_errors': dom.diskErrors(),
'cpu_stats': dom.getCPUStats(True),
'virt_cpus_number': len(dom.getCPUStats(True)),
'control_info': control_info,
'disk_errors': disk_errors,
'cpu_stats': cpu_stats,
'virt_cpus_number': virt_cpus_number,
'has_snapshot': dom.hasCurrentSnapshot(),
'has_image': dom.hasManagedSaveImage(),
'is_active': dom.isActive(),
'is_persitent': dom.isPersistent(),
'is_persistent': dom.isPersistent(),
'is_updated': dom.isUpdated(),
'memory_stats': dom.memoryStats(),
'memory_stats': memory_stats,
}
domains.append(domain)
self.disconnect()
return domains
def get_networks(self):
""" Get networks infos and status from hypervisor """
networks = []
self.connect()
for net in self.connection.listAllNetworks():
@ -71,6 +101,7 @@ class Engine(object):
return networks
def get_network_filters(self):
""" Get networks_filters infos from hypervisor """
nw_filters = []
self.connect()
for filt in self.connection.listAllNWFilters():
@ -84,6 +115,7 @@ class Engine(object):
return nw_filters
def get_interfaces(self):
""" Get interfaces infos and status from hypervisor """
interfaces = []
self.connect()
for interf in self.connection.listAllInterfaces():
@ -98,6 +130,7 @@ class Engine(object):
return interfaces
def get_secrets(self):
""" Get secrets infos from hypervisor """
secrets = []
self.connect()
for sec in self.connection.listAllSecrets():
@ -114,6 +147,7 @@ class Engine(object):
return secrets
def get_storage_pools(self):
""" Get storage_pools infos and status from hypervisor """
pools = []
self.connect()
for sto in self.connection.listAllStoragePools():
@ -133,6 +167,7 @@ class Engine(object):
return pools
def get_storage_volumes(self, storage_pool):
""" Get storage_volumes infos from storage_pool """
volumes = []
for vol in storage_pool['volumes_list']:
volume = {
@ -144,11 +179,44 @@ class Engine(object):
return volumes
def get_devices(self):
""" Get devices infos from hypervisor """
self.connect()
devices = self.connection.listAllDevices()
devices = []
devs = self.connection.listAllDevices()
for dev in devs:
root = ET.fromstring(dev.XMLDesc())
device = {
'name': None,
'type': None,
'product': None,
'driver': None,
'vendor': None,
'model': None,
}
for node in root.iter():
# XML is shit
if node.tag == 'name':
if device['name'] == None:
device['name'] = node.text
if node.tag == 'driver':
for driver in node.iter():
if driver.tag == 'name':
device['driver'] = driver.text
if node.tag == 'capability' and node.attrib['type'] in ['pci', 'usb_device', 'storage', 'system']:
device['type'] = node.attrib['type']
for cap in node.iter():
if cap.tag == 'product':
device['product'] = cap.text
elif cap.tag == 'vendor':
device['vendor'] = cap.text
elif cap.tag == 'model':
device['model'] = cap.text
if device['name'] and device['type']:
devices.append(device)
return devices
def debug(self):
""" Log all information gathered for debugging purpose """
debug = 80 * '*'+'\n'
debug += 80 * '*'+'\n'
debug += 'Engine status:\n'


+ 11
- 0
init_colors.sh View File

@ -0,0 +1,11 @@
c=0
for fg in curses.COLOR_BLACK curses.COLOR_WHITE curses.COLOR_RED curses.COLOR_GREEN curses.COLOR_YELLOW curses.COLOR_BLUE curses.COLOR_MAGENTA curses.COLOR_CYAN
do
for bg in curses.COLOR_BLACK curses.COLOR_WHITE curses.COLOR_RED curses.COLOR_GREEN curses.COLOR_YELLOW curses.COLOR_BLUE curses.COLOR_MAGENTA curses.COLOR_CYAN
do
if [ $bg != $fg ]; then
c=$(( c+1 ))
echo "curses.init_pair($c, $fg, $bg)"
fi
done
done

+ 3
- 1
pycursesvirtmgr.py View File

@ -7,9 +7,11 @@ import ui
if __name__ == "__main__":
#~ engine = engine.Engine()
#~ ui = ui.UI(15, 150, engine, debug=True)
try:
engine = engine.Engine()
ui = ui.UI(40, 165, engine, debug=True)
ui = ui.UI(15, 150, engine, debug=True)
except Exception as e:
# Restore terminal state
try:


BIN
screenshots/devices.png View File

Before After
Width: 1366  |  Height: 768  |  Size: 85 KiB

BIN
screenshots/domains.png View File

Before After
Width: 1366  |  Height: 768  |  Size: 55 KiB

+ 229
- 34
ui/__init__.py View File

@ -6,6 +6,7 @@ import locale
import curses
import ui.window
import ui.input_manager
from operator import itemgetter
locale.setlocale(locale.LC_ALL, '')
@ -37,24 +38,71 @@ class UI(object):
self.log('* Colors', 'info')
curses.start_color()
# Color pairs
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_BLACK)
curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_RED)
## Color pairs (fg, bg)
# 1 to 7 : Black FG
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_RED)
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_GREEN)
curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_GREEN)
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_CYAN)
curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_BLUE)
curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_BLACK)
curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_WHITE)
curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_RED)
curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_YELLOW)
curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_GREEN)
curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_BLUE)
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_CYAN)
# 8 to 14 : White FG
curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK)
curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_RED)
curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_GREEN)
curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_YELLOW)
curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_BLUE)
curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_CYAN)
curses.init_pair(15, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
curses.init_pair(16, curses.COLOR_WHITE, curses.COLOR_BLUE)
# 15 to 21 : Red FG
curses.init_pair(15, curses.COLOR_RED, curses.COLOR_BLACK)
curses.init_pair(16, curses.COLOR_RED, curses.COLOR_WHITE)
curses.init_pair(17, curses.COLOR_RED, curses.COLOR_GREEN)
curses.init_pair(18, curses.COLOR_RED, curses.COLOR_YELLOW)
curses.init_pair(19, curses.COLOR_RED, curses.COLOR_BLUE)
curses.init_pair(20, curses.COLOR_RED, curses.COLOR_MAGENTA)
curses.init_pair(21, curses.COLOR_RED, curses.COLOR_CYAN)
# 22 to 28 : Green FG
curses.init_pair(22, curses.COLOR_GREEN, curses.COLOR_BLACK)
curses.init_pair(23, curses.COLOR_GREEN, curses.COLOR_WHITE)
curses.init_pair(24, curses.COLOR_GREEN, curses.COLOR_RED)
curses.init_pair(25, curses.COLOR_GREEN, curses.COLOR_YELLOW)
curses.init_pair(26, curses.COLOR_GREEN, curses.COLOR_BLUE)
curses.init_pair(27, curses.COLOR_GREEN, curses.COLOR_MAGENTA)
curses.init_pair(28, curses.COLOR_GREEN, curses.COLOR_CYAN)
# 29 to 35 : Yellow FG
curses.init_pair(29, curses.COLOR_YELLOW, curses.COLOR_BLACK)
curses.init_pair(30, curses.COLOR_YELLOW, curses.COLOR_WHITE)
curses.init_pair(31, curses.COLOR_YELLOW, curses.COLOR_RED)
curses.init_pair(32, curses.COLOR_YELLOW, curses.COLOR_GREEN)
curses.init_pair(33, curses.COLOR_YELLOW, curses.COLOR_BLUE)
curses.init_pair(34, curses.COLOR_YELLOW, curses.COLOR_MAGENTA)
curses.init_pair(35, curses.COLOR_YELLOW, curses.COLOR_CYAN)
# 36 to 42 : Blue FG
curses.init_pair(36, curses.COLOR_BLUE, curses.COLOR_BLACK)
curses.init_pair(37, curses.COLOR_BLUE, curses.COLOR_WHITE)
curses.init_pair(38, curses.COLOR_BLUE, curses.COLOR_RED)
curses.init_pair(39, curses.COLOR_BLUE, curses.COLOR_GREEN)
curses.init_pair(40, curses.COLOR_BLUE, curses.COLOR_YELLOW)
curses.init_pair(41, curses.COLOR_BLUE, curses.COLOR_MAGENTA)
curses.init_pair(42, curses.COLOR_BLUE, curses.COLOR_CYAN)
# 43 to 49 : Magenta FG
curses.init_pair(43, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
curses.init_pair(44, curses.COLOR_MAGENTA, curses.COLOR_WHITE)
curses.init_pair(45, curses.COLOR_MAGENTA, curses.COLOR_RED)
curses.init_pair(46, curses.COLOR_MAGENTA, curses.COLOR_GREEN)
curses.init_pair(47, curses.COLOR_MAGENTA, curses.COLOR_YELLOW)
curses.init_pair(48, curses.COLOR_MAGENTA, curses.COLOR_BLUE)
curses.init_pair(49, curses.COLOR_MAGENTA, curses.COLOR_CYAN)
# 50 to 56 : Cyan FG
curses.init_pair(50, curses.COLOR_CYAN, curses.COLOR_BLACK)
curses.init_pair(51, curses.COLOR_CYAN, curses.COLOR_WHITE)
curses.init_pair(52, curses.COLOR_CYAN, curses.COLOR_RED)
curses.init_pair(53, curses.COLOR_CYAN, curses.COLOR_GREEN)
curses.init_pair(54, curses.COLOR_CYAN, curses.COLOR_YELLOW)
curses.init_pair(55, curses.COLOR_CYAN, curses.COLOR_BLUE)
curses.init_pair(56, curses.COLOR_CYAN, curses.COLOR_MAGENTA)
self.log('* Getting screen size', 'info')
self.screen_y, self.screen_x = self.stdscr.getmaxyx()
@ -89,6 +137,7 @@ class UI(object):
# Windows definitions
#####################################
def load_window_definitions(self):
""" Load all windows definitions """
windows = [
{
'name': 'Infrastructure',
@ -96,7 +145,9 @@ class UI(object):
},
{
'name': 'Devices',
'action_keys': {},
'action_keys': {
's': ('Select device', self.display_select_devices),
},
},
{
'name': 'Domains',
@ -141,6 +192,7 @@ class UI(object):
return windows
def load_menu_keys(self):
""" Load main menu keys """
menu_keys = {
curses.KEY_F1: ('F1', 'Domains', self.display_domains),
curses.KEY_F2: ('F2', 'Devices', self.display_devices),
@ -201,6 +253,7 @@ class UI(object):
# Display functions
#####################################
def display_window(self, window_name):
""" Display window which name is passed as argument """
self.log('Trying display window: %s' % window_name, 'info')
for definition in self.windows_definitions:
self.log('Testing: %s.' % definition['name'], 'info')
@ -210,69 +263,202 @@ class UI(object):
self.set_current_window(self.windows[window_name])
return
def optimize_window_display_width(self, width, data):
""" Optimize data display adjusting field lengths and/or
gap between fields """
self.log('Optimizing data:', 'info')
field_lengths = [0] * len(data[0])
field_positions = []
# Longest possible record in data set
max_record_length = 0
# Longest fields length
# Index of the longest field in data set
max_field_length = 0
longest_field_index = None
# window width
w_width = width
for record in data:
field_index = 0
for field in record:
data_field = str(field[0])
# Store the max len of each field
if len(data_field) > field_lengths[field_index]:
field_lengths[field_index] = len(data_field)
# Store the longest field length and its index
if len(data_field) > max_field_length:
max_field_length = len(data_field)
longest_field_index = field_index
field_index +=1
# Longest possible record in data set
max_record_length = sum(field_lengths)
# The longest possible record should fit in window
if max_record_length < w_width:
self.log('* max_record_length < w_width', 'info')
self.log('* %d < %d' % (max_record_length, w_width), 'info')
# Compute the best interval between field
gap = (w_width - max_record_length) / len(field_lengths)
self.log('* offset: %d' % gap, 'info')
# The longest possible record will definitely fit in window
field_position = 0
for field_length in field_lengths:
field_positions.append(field_position)
field_position += (field_length + gap)
return (field_positions, data)
# There is not enough room for each field to be displayed
# TODO: Adjust new_length to ponctual need
new_length = 13
self.log('* max_record_length > w_width', 'warning')
self.log('* w_width: %s' % w_width, 'warning')
self.log('* max_record_length: %s' % max_record_length, 'warning')
self.log('* max_field_length: %s' % max_field_length, 'warning')
self.log('* new_length: %s' % new_length , 'warning')
for line in data:
field_index = 0
for field in line:
if field[0] and len(field[0]) > new_length:
self.log('* len(field) > new_length', 'warning')
self.log('* Field: %s' % field[0], 'warning')
field = (field[0][0:new_length], field[1])
self.log('* Truncated field: %s' % field[0], 'warning')
line[field_index] = field
field_index += 1
# Continue optimization until data fits in window
return self.optimize_window_display_width(width, data)
def display_data_line(self, line, line_position):
""" Display one line of data at line_position """
field_num = 0
for field in line:
data_string, data_color = field
data_string = str(data_string)
self.data_pad.addstr(line_position, self.fields_positions[field_num], data_string, data_color)
field_num += 1
def display_data(self, data):
self.data = data
""" Display data:
data is a list of tuples lists
[[('text', attr), ('text', attr), ...], [('text', attr), ('text', attr), ...]]"""
w_height, w_width = self.current_window.main_window.getmaxyx()
self.data_pad = self.current_window.main_window.subpad(w_height - 5, w_width - 6, 3, 3)
self.data_pad.scrollok(True)
self.lower_data_line = 0
self.upper_data_line = 0
self.last_data_line = 0
self.fields_positions, self.data = self.optimize_window_display_width(w_width, data)
while self.last_data_line < w_height - 5 and self.lower_data_line < len(data):
str_data = ' %d\t%s ' % (self.lower_data_line + 1, data[self.lower_data_line])
self.data_pad.addstr(self.last_data_line, 3, str_data, curses.color_pair(9) + curses.A_BOLD)
self.display_data_line(data[self.lower_data_line], self.last_data_line)
self.last_data_line += 2
self.lower_data_line += 1
self.data_pad.overwrite(self.current_window.main_window)
self.data_pad.noutrefresh()
def display_infrastructure(self):
""" Get infrastructure data from engine and put it in shape for display """
self.display_window('Infrastructure')
self.run()
def display_domains(self):
""" Get domains data from engine and put it in shape for display """
self.display_window('Domains')
data = []
for domain in self.engine.domains:
str_data = '%s\t%s\t%s\t%s' % (domain['id'], domain['name'], domain['state'], domain['is_active'])
data.append(str_data)
# Colors definition according to state
domain_state_colors = [('No state', curses.color_pair(9) + curses.A_BOLD),
('Running', curses.color_pair(10) + curses.A_BOLD),
('Blocked', curses.color_pair(9) + curses.A_BOLD + curses.A_BLINK),
('Paused', curses.color_pair(12) + curses.A_BOLD),
('Shut down', curses.color_pair(14) + curses.A_BOLD),
('Shut off', curses.color_pair(9) + curses.A_BOLD),
('Crashed', curses.color_pair(9) + curses.A_BOLD + curses.A_BLINK),
('PM suspended', curses.color_pair(14) + curses.A_BOLD + curses.A_BLINK)]
domain_active_colors = [('Not active', curses.color_pair(9) + curses.A_BOLD),
('Active', curses.color_pair(10) + curses.A_BOLD)]
domain_persistence_colors = [('Transcient', curses.color_pair(29) + curses.A_BOLD + curses.A_BLINK),
('Persistent', curses.color_pair(22) + curses.A_BOLD)]
domain_has_image_colors = [('No image', curses.color_pair(22) + curses.A_BOLD),
('Image', curses.color_pair(36) + curses.A_BOLD)]
for domain in sorted(self.engine.domains, key=itemgetter('name')):
try:
domain_state, domain_state_color = domain_state_colors[domain['state'][0]]
except:
domain_state = 'Unknown'
domain_state_color = curses.color_pair(11) + curses.A_BOLD
domain_active, domain_active_color = domain_active_colors[domain['is_active']]
domain_persistence, domain_persistence_color = domain_persistence_colors[domain['is_persistent']]
domain_has_image, domain_has_image_color = domain_has_image_colors[domain['has_image']]
data_line = [
(str(domain['id']), curses.A_BOLD),
(str(domain['name']),curses.A_BOLD),
(' %s ' % domain_state, domain_state_color),
(' %s ' % domain_active, domain_active_color),
('%s' % domain_persistence, domain_persistence_color),
('%s' % domain_has_image, domain_has_image_color),
]
data.append(data_line)
self.display_data(data)
self.refresh_ui()
self.run()
def display_devices(self):
""" Get devices data from engine and put it in shape for display """
self.display_window('Devices')
data = []
for device in self.engine.devices:
str_data = '%s ' % str(device)
data.append(str_data)
device_type_colors = { 'pci': curses.color_pair(29) + curses.A_BOLD,
'usb_device': curses.color_pair(36) + curses.A_BOLD,
'storage': curses.color_pair(43) + curses.A_BOLD,
'system': curses.color_pair(50) + curses.A_BOLD,
}
for device in sorted(self.engine.devices, key=itemgetter('type'), reverse=False):
device_type_color = device_type_colors[device['type']]
data_line = [
(str(device['type']), device_type_color),
(str(device['name']), curses.A_BOLD),
(str(device['product']), curses.A_BOLD),
(str(device['vendor']), curses.A_BOLD),
(str(device['model']), curses.A_BOLD),
(str(device['driver']), curses.A_BOLD),
]
data.append(data_line)
self.display_data(data)
self.refresh_ui()
self.run()
def display_storages(self):
""" Get storages data from engine and put it in shape for display """
self.display_window('Storages')
self.run()
def display_networks(self):
""" Get networks data from engine and put it in shape for display """
self.display_window('Networks')
self.run()
def display_interfaces(self):
""" Get onterfaces data from engine and put it in shape for display """
self.display_window('Interfaces')
self.run()
def display_secrets(self):
""" Get secrets data from engine and put it in shape for display """
self.display_window('Secrets')
self.run()
def display_overview(self):
""" Get overview data from engine and put it in shape for display """
self.display_window('Overview')
self.run()
#####################################
# Select select functions
#####################################
#
# NOT NECESSARY ? TO BE REMOVED
#
def display_select_window(self, window_name):
self.log('Displaying select window: %s' % window_name, 'info')
@ -296,30 +482,40 @@ class UI(object):
self.display_select_window('Secrets')
self.run()
def display_select_devices(self):
self.display_select_window('Device')
self.run()
#####################################
# Add functions
#####################################
def display_add_window(self, window_name):
""" Display add window for specific category """
self.log('Displaying add window: %s' % window_name, 'info')
def display_add_domain(self):
""" Display add window for domains """
self.display_add_window('Domains')
self.run()
def display_add_storage(self):
""" Display add window for storages """
self.display_add_window('Storages')
self.run()
def display_add_network(self):
""" Display add window for networks """
self.display_add_window('Networks')
self.run()
def display_add_interface(self):
""" Display add window for interfaces """
self.display_add_window('Interfaces')
self.run()
def display_add_secret(self):
""" Display add window for secrets """
self.display_add_window('Secrets')
self.run()
@ -328,6 +524,8 @@ class UI(object):
# Main U.I functions
#####################################
def set_current_window(self, window):
""" Set the given window as current window and display
window specific action keys """
self.log('Setting current window: %s.' % window.title, 'info')
self.current_window = window
start_line = 3
@ -340,17 +538,16 @@ class UI(object):
self.refresh_ui()
def page_down(self):
""" Scroll one line down the displayed data """
self.log('Scrolling down: %s.' % self.current_window.title, 'info')
if self.lower_data_line < len(self.data):
str_data = ' %d\t%s' % (self.lower_data_line + 1, self.data[self.lower_data_line])
height, width = self.data_pad.getmaxyx()
scroll_value = 2
if self.last_data_line == height:
scroll_value = 1
self.last_data_line -= 1
self.data_pad.scroll(scroll_value)
self.data_pad.addstr(height - 1 , 3, str_data, curses.color_pair(9) + curses.A_BOLD)
self.display_data_line(self.data[self.lower_data_line], height - 1)
self.lower_data_line += 1
self.upper_data_line += 1
self.data_pad.noutrefresh()
@ -358,16 +555,13 @@ class UI(object):
self.run()
def page_up(self):
""" Scroll one line up the displayed data """
self.log('Scrolling up: %s.' % self.current_window.title, 'info')
if self.upper_data_line > 0:
str_data = ' %d\t%s' % (self.upper_data_line, self.data[self.upper_data_line -1])
height, width = self.data_pad.getmaxyx()
scroll_value = -2
#~ if self.last_data_line == height:
#~ scroll_value = 1
#~ self.last_data_line -= 1
self.data_pad.scroll(scroll_value)
self.data_pad.addstr(0 , 3, str_data, curses.color_pair(9) + curses.A_BOLD)
self.display_data_line(self.data[self.upper_data_line], 0)
self.lower_data_line -= 1
self.upper_data_line -= 1
self.data_pad.noutrefresh()
@ -375,6 +569,7 @@ class UI(object):
self.run()
def next_field(self):
""" Navigate through fields """
self.log('Next field: %s.' % self.current_window.title, 'info')
self.run()


+ 6
- 2
ui/input_manager.py View File

@ -5,17 +5,21 @@ import time
class InputManager(object):
""" Basicly manage keyboard inputs from user """
def __init__(self, ui):
self.ui = ui
self.stop = False
def run(self):
""" Wait for input """
while not self.stop:
self.check_input()
# Calm down !
time.sleep(.1)
time.sleep(.01)
self.check_input()
def check_input(self):
""" Check if input is valid
and redirect to related action """
key = self.ui.stdscr.getch()
# we don't need to ckeck for non typed keys !
if key != -1:


+ 15
- 0
ui/window.py View File

@ -7,13 +7,16 @@ import curses.textpad
class Window(object):
""" The base window object """
def __init__(self, height, width, y, x, title=None):
""" Init a boxed curses window with a panel """
self.main_window = curses.newwin(height, width, y, x)
self.main_panel = curses.panel.new_panel(self.main_window)
self.title = title
self.set_title(title)
def set_title(self, title):
""" Set title to the window """
self.main_window.box()
if title:
self.main_window.addstr(0, 1, title, curses.A_BOLD)
@ -21,13 +24,23 @@ class Window(object):
class MenuWindow(Window):
""" Basic window with menu_keys
This window display the entire application main menu
at bottom of U.I """
def __init__(self, *args, **kwargs):
""" Init a boxed curses window with a panel
MenuWindow have a specific attribute menu_keys"""
super(MenuWindow, self).__init__(*args, **kwargs)
self.menu_keys = None
class ActionWindow(object):
""" ActionWindow is a double curses window:
The left window is the main window displaying data
The right window display a specific menu driven from
action_keys attribute """
def __init__(self, height, width, y, x, title=None):
""" Init the two windows """
self.main_window = curses.newwin(height, width - 45, y, x)
self.action_window = curses.newwin(height, 45, y, width - 45)
self.main_panel = curses.panel.new_panel(self.main_window)
@ -37,6 +50,7 @@ class ActionWindow(object):
self.action_keys = None
def set_title(self, title):
""" Set title to the two windows """
self.main_window.box()
self.action_window.box()
if title:
@ -46,4 +60,5 @@ class ActionWindow(object):
self.action_window.noutrefresh()
def set_action_keys(self, keys):
""" Set specific action keys """
self.action_keys = keys

Loading…
Cancel
Save