Change-Id: I4b459f3bc2c81fa6b48cbf110f61be0667086144
Reviewed-on: http://photon-jenkins.eng.vmware.com:8082/1625
Tested-by: gerrit-photon <photon-checkins@vmware.com>
Reviewed-by: suezzelur <anishs@vmware.com>
5 | 5 |
old mode 100644 |
6 | 6 |
new mode 100755 |
... | ... |
@@ -20,7 +20,7 @@ class ConfirmWindow(Window): |
20 | 20 |
('No', self.exit_function, False) |
21 | 21 |
] |
22 | 22 |
self.menu = Menu(menu_starty, maxx, items, can_navigate_outside = False, horizontal=True) |
23 |
- super(ConfirmWindow, self).__init__(height, width, maxy, maxx, 'Confirm', False, self.menu) |
|
23 |
+ super(ConfirmWindow, self).__init__(height, width, maxy, maxx, 'Confirm', False, self.menu, items=[]) |
|
24 | 24 |
self.addstr(0,0, message) |
25 | 25 |
|
26 | 26 |
def exit_function(self, yes): |
27 | 27 |
old mode 100644 |
28 | 28 |
new mode 100755 |
... | ... |
@@ -19,6 +19,11 @@ class Device(object): |
19 | 19 |
return Device.wrap_devices_from_list(devices_list) |
20 | 20 |
|
21 | 21 |
@staticmethod |
22 |
+ def refresh_devices_bytes(): |
|
23 |
+ devices_list = subprocess.check_output(['lsblk', '-S', '--bytes', '-I', '8', '-n', '--output', 'NAME,SIZE,MODEL'], stderr=open(os.devnull, 'w')) |
|
24 |
+ return Device.wrap_devices_from_list(devices_list) |
|
25 |
+ |
|
26 |
+ @staticmethod |
|
22 | 27 |
def wrap_devices_from_list(list): |
23 | 28 |
devices = [] |
24 | 29 |
deviceslines = list.splitlines() |
27 | 32 |
old mode 100644 |
28 | 33 |
new mode 100755 |
... | ... |
@@ -64,7 +64,7 @@ class Installer(object): |
64 | 64 |
self.progress_width = self.width - self.progress_padding |
65 | 65 |
self.starty = (self.maxy - self.height) / 2 |
66 | 66 |
self.startx = (self.maxx - self.width) / 2 |
67 |
- self.window = Window(self.height, self.width, self.maxy, self.maxx, 'Installing Photon', False) |
|
67 |
+ self.window = Window(self.height, self.width, self.maxy, self.maxx, 'Installing Photon', False, items =[]) |
|
68 | 68 |
self.progress_bar = ProgressBar(self.starty + 3, self.startx + self.progress_padding / 2, self.progress_width) |
69 | 69 |
|
70 | 70 |
signal.signal(signal.SIGINT, self.exit_gracefully) |
... | ... |
@@ -231,6 +231,7 @@ class Installer(object): |
231 | 231 |
fsck = 2 |
232 | 232 |
|
233 | 233 |
if 'mountpoint' in partition and partition['mountpoint'] == '/': |
234 |
+ options = options + ',barrier,noatime,noacl,data=ordered' |
|
234 | 235 |
fsck = 1 |
235 | 236 |
|
236 | 237 |
if partition['filesystem'] == 'swap': |
... | ... |
@@ -21,6 +21,7 @@ import random |
21 | 21 |
import urllib |
22 | 22 |
import urllib2 |
23 | 23 |
import modules.commons |
24 |
+from partitionISO import PartitionISO |
|
24 | 25 |
from diskpartitioner import DiskPartitioner |
25 | 26 |
from packageselector import PackageSelector |
26 | 27 |
from custompackageselector import CustomPackageSelector |
... | ... |
@@ -227,10 +228,12 @@ class IsoInstaller(object): |
227 | 227 |
install_config = {'iso_system': False} |
228 | 228 |
license_agreement = License(self.maxy, self.maxx) |
229 | 229 |
select_disk = SelectDisk(self.maxy, self.maxx, install_config) |
230 |
+ select_partition = PartitionISO(self.maxy, self.maxx, install_config) |
|
230 | 231 |
package_selector = PackageSelector(self.maxy, self.maxx, install_config, options_file) |
231 | 232 |
|
232 | 233 |
self.alpha_chars = range(65, 91) |
233 | 234 |
self.alpha_chars.extend(range(97,123)) |
235 |
+ partition_accepted_chars = list(range(48, 58)) |
|
234 | 236 |
hostname_accepted_chars = list(self.alpha_chars) |
235 | 237 |
# Adding the numeric chars |
236 | 238 |
hostname_accepted_chars.extend(range(48, 58)) |
... | ... |
@@ -246,7 +249,8 @@ class IsoInstaller(object): |
246 | 246 |
self.validate_hostname, # validation function of the input |
247 | 247 |
None, # post processing of the input field |
248 | 248 |
'Choose the hostname for your system', 'Hostname:', 2, install_config, |
249 |
- random_hostname) |
|
249 |
+ random_hostname, |
|
250 |
+ True) |
|
250 | 251 |
root_password_reader = WindowStringReader( |
251 | 252 |
self.maxy, self.maxx, 10, 70, |
252 | 253 |
'password', |
... | ... |
@@ -290,6 +294,8 @@ class IsoInstaller(object): |
290 | 290 |
items = items + [ |
291 | 291 |
(license_agreement.display, False), |
292 | 292 |
(select_disk.display, True), |
293 |
+ (select_partition.display, False), |
|
294 |
+ (select_disk.guided_partitions, False), |
|
293 | 295 |
(package_selector.display, True), |
294 | 296 |
(hostname_reader.get_user_string, True), |
295 | 297 |
(root_password_reader.get_user_string, True), |
298 | 300 |
old mode 100644 |
299 | 301 |
new mode 100755 |
... | ... |
@@ -22,7 +22,7 @@ class License(object): |
22 | 22 |
self.text_height = self.win_height - 6 |
23 | 23 |
self.text_width = self.win_width - 6 |
24 | 24 |
|
25 |
- self.window = Window(self.win_height, self.win_width, self.maxy, self.maxx, 'Welcome to the Photon installer', False) |
|
25 |
+ self.window = Window(self.win_height, self.win_width, self.maxy, self.maxx, 'Welcome to the Photon installer', False, items=[]) |
|
26 | 26 |
|
27 | 27 |
def display(self, params): |
28 | 28 |
accept_decline_items = [ |
29 | 29 |
old mode 100644 |
30 | 30 |
new mode 100755 |
... | ... |
@@ -10,7 +10,7 @@ from action import Action |
10 | 10 |
from sets import Set |
11 | 11 |
|
12 | 12 |
class Menu(Action): |
13 |
- def __init__(self, starty, maxx, items, height = 0, selector_menu = False, can_navigate_outside = True, horizontal = False, default_selected = 0): |
|
13 |
+ def __init__(self, starty, maxx, items, height = 0, selector_menu = False, can_navigate_outside = True, horizontal = False, default_selected = 0, save_sel = False, tab_enable = True): |
|
14 | 14 |
self.can_navigate_outside = can_navigate_outside |
15 | 15 |
self.horizontal = horizontal |
16 | 16 |
self.horizontal_padding = 10 |
... | ... |
@@ -20,6 +20,8 @@ class Menu(Action): |
20 | 20 |
self.items_strings = [] |
21 | 21 |
self.width = self.lengthen_items() |
22 | 22 |
self.num_items = len(self.items) |
23 |
+ self.save_sel = save_sel |
|
24 |
+ self.tab_enable=tab_enable |
|
23 | 25 |
if height == 0 or height > self.num_items: |
24 | 26 |
self.height = self.num_items |
25 | 27 |
else: |
... | ... |
@@ -61,6 +63,9 @@ class Menu(Action): |
61 | 61 |
self.panel.hide() |
62 | 62 |
curses.panel.update_panels() |
63 | 63 |
|
64 |
+ def can_save_sel(self, can_save_sel): |
|
65 |
+ self.save_sel = can_save_sel |
|
66 |
+ |
|
64 | 67 |
def lengthen_items(self): |
65 | 68 |
width = 0 |
66 | 69 |
for item in self.items: |
... | ... |
@@ -179,13 +184,31 @@ class Menu(Action): |
179 | 179 |
else: |
180 | 180 |
self.selected_items.add(self.position) |
181 | 181 |
elif key in [ord('\t')] and self.can_navigate_outside: |
182 |
+ if not self.tab_enable: |
|
183 |
+ continue |
|
182 | 184 |
self.refresh(False) |
183 |
- return ActionResult(False, None) |
|
185 |
+ if self.save_sel: |
|
186 |
+ return ActionResult(False, {'diskIndex': self.position}) |
|
187 |
+ else: |
|
188 |
+ return ActionResult(False, None) |
|
184 | 189 |
|
185 | 190 |
elif key == curses.KEY_UP or key == curses.KEY_LEFT: |
191 |
+ if not self.tab_enable and key==curses.KEY_LEFT: |
|
192 |
+ if self.save_sel: |
|
193 |
+ return ActionResult(False, {'diskIndex': self.position, 'direction':-1}) |
|
194 |
+ elif self.selector_menu: |
|
195 |
+ result = self.items[self.position][1](self.selected_items) |
|
196 |
+ else: |
|
197 |
+ result = self.items[self.position][1](self.items[self.position][2]) |
|
198 |
+ return ActionResult(False, {'direction': -1}) |
|
186 | 199 |
self.navigate(-1) |
187 | 200 |
|
188 | 201 |
elif key == curses.KEY_DOWN or key == curses.KEY_RIGHT: |
202 |
+ if not self.tab_enable and key==curses.KEY_RIGHT: |
|
203 |
+ if self.save_sel: |
|
204 |
+ return ActionResult(False, {'diskIndex': self.position, 'direction':1}) |
|
205 |
+ else: |
|
206 |
+ return ActionResult(False, {'direction': 1}) |
|
189 | 207 |
self.navigate(1) |
190 | 208 |
|
191 | 209 |
elif key == curses.KEY_NPAGE: |
194 | 212 |
old mode 100644 |
195 | 213 |
new mode 100755 |
... | ... |
@@ -155,7 +155,7 @@ class OstreeInstaller(Installer): |
155 | 155 |
|
156 | 156 |
|
157 | 157 |
deployment_fstab = os.path.join(deployment, "etc/fstab") |
158 |
- self.run("echo \"/dev/sda3 / ext4 defaults 1 1 \" >> {} ".format(deployment_fstab), "Adding / mount point in fstab") |
|
158 |
+ self.run("echo \"/dev/sda3 / ext4 defaults,barrier,noatime,noacl,data=ordered 1 1 \" >> {} ".format(deployment_fstab), "Adding / mount point in fstab") |
|
159 | 159 |
self.run("echo \"/dev/sda2 /boot ext4 defaults 1 2 \" >> {} ".format(deployment_fstab), "Adding /boot mount point in fstab") |
160 | 160 |
self.run("mount --bind {} {}".format(deployment, self.photon_root)) |
161 | 161 |
self.progress_bar.update_loading_message("Starting post install modules") |
168 | 168 |
old mode 100644 |
169 | 169 |
new mode 100755 |
... | ... |
@@ -28,7 +28,7 @@ class PackageSelector(object): |
28 | 28 |
|
29 | 29 |
self.load_package_list(options_file) |
30 | 30 |
|
31 |
- self.window = Window(self.win_height, self.win_width, self.maxy, self.maxx, 'Select Installation', True, self.package_menu) |
|
31 |
+ self.window = Window(self.win_height, self.win_width, self.maxy, self.maxx, 'Select Installation', True, self.package_menu, can_go_next=True, position=1) |
|
32 | 32 |
|
33 | 33 |
@staticmethod |
34 | 34 |
def get_packages_to_install(options, config_type, output_data_path): |
... | ... |
@@ -74,7 +74,7 @@ class PackageSelector(object): |
74 | 74 |
visible_options_cnt = visible_options_cnt + 1 |
75 | 75 |
|
76 | 76 |
|
77 |
- self.package_menu = Menu(self.menu_starty, self.maxx, self.package_menu_items, default_selected = default_selected) |
|
77 |
+ self.package_menu = Menu(self.menu_starty, self.maxx, self.package_menu_items, default_selected = default_selected, tab_enable=False) |
|
78 | 78 |
|
79 | 79 |
def exit_function(self, selected_item_params): |
80 | 80 |
self.install_config['type'] = selected_item_params[0]; |
81 | 81 |
new file mode 100755 |
... | ... |
@@ -0,0 +1,196 @@ |
0 |
+from window import Window |
|
1 |
+from windowstringreader import WindowStringReader |
|
2 |
+from textpane import TextPane |
|
3 |
+from readmultext import ReadMulText |
|
4 |
+from confirmwindow import ConfirmWindow |
|
5 |
+from actionresult import ActionResult |
|
6 |
+from device import Device |
|
7 |
+ |
|
8 |
+class PartitionISO(object): |
|
9 |
+ def __init__(self, maxy, maxx, install_config): |
|
10 |
+ self.maxx = maxx |
|
11 |
+ self.maxy = maxy |
|
12 |
+ self.win_width = maxx - 4 |
|
13 |
+ self.win_height = maxy - 4 |
|
14 |
+ self.install_config = install_config |
|
15 |
+ self.path_checker = [] |
|
16 |
+ |
|
17 |
+ self.win_starty = (self.maxy - self.win_height) / 2 |
|
18 |
+ self.win_startx = (self.maxx - self.win_width) / 2 |
|
19 |
+ |
|
20 |
+ self.text_starty = self.win_starty + 4 |
|
21 |
+ self.text_height = self.win_height - 6 |
|
22 |
+ self.text_width = self.win_width - 6 |
|
23 |
+ self.install_config['partitionsnumber'] = 0 |
|
24 |
+ self.devices = Device.refresh_devices_bytes() |
|
25 |
+ self.has_slash = False |
|
26 |
+ self.has_remain = False |
|
27 |
+ self.has_empty = False |
|
28 |
+ |
|
29 |
+ self.disk_size = [] |
|
30 |
+ for index, device in enumerate(self.devices): |
|
31 |
+ self.disk_size.append((device.path, int(device.size) / 1048576)) |
|
32 |
+ |
|
33 |
+ self.window = Window(self.win_height, self.win_width, self.maxy, self.maxx, 'Welcome to the Photon installer', False, items=[], can_go_next=False) |
|
34 |
+ Device.refresh_devices() |
|
35 |
+ |
|
36 |
+ def display(self, params): |
|
37 |
+ if 'skipPrevs' in self.install_config and self.install_config['skipPrevs'] == True: |
|
38 |
+ self.delete() |
|
39 |
+ return ActionResult(False, {'goBack':True}) |
|
40 |
+ if 'autopartition' in self.install_config and self.install_config['autopartition'] == True: |
|
41 |
+ return ActionResult(True, None) |
|
42 |
+ if 'delete_partition' in self.install_config and self.install_config['delete_partition'] == True: |
|
43 |
+ self.delete() |
|
44 |
+ self.install_config['delete_partition']=False |
|
45 |
+ |
|
46 |
+ self.device_index = self.install_config['diskindex'] |
|
47 |
+ |
|
48 |
+ self.disk_buttom_items = [] |
|
49 |
+ self.disk_buttom_items.append(('<Next>', self.next)) |
|
50 |
+ self.disk_buttom_items.append(('<Create New>', self.create_function)) |
|
51 |
+ self.disk_buttom_items.append(('<Delete All>', self.delete_function)) |
|
52 |
+ self.disk_buttom_items.append(('<Go Back>', self.go_back)) |
|
53 |
+ |
|
54 |
+ self.text_items = [] |
|
55 |
+ self.text_items.append(('Disk', 20)) |
|
56 |
+ self.text_items.append(('Size', 5)) |
|
57 |
+ self.text_items.append(('Type', 5)) |
|
58 |
+ self.text_items.append(('Mountpoint', 20)) |
|
59 |
+ self.table_space = 5 |
|
60 |
+ |
|
61 |
+ title = 'Current partitions:\n' |
|
62 |
+ self.window.addstr(0, (self.win_width - len(title)) / 2, title) |
|
63 |
+ |
|
64 |
+ info = "Unpartitioned space: "+str(self.disk_size[self.device_index][1])+ " MB, Total size: "+ str(int(self.devices[self.device_index].size)/ 1048576)+" MB" |
|
65 |
+ |
|
66 |
+ self.text_pane = TextPane(self.text_starty, self.maxx, self.text_width, |
|
67 |
+ "EULA.txt", self.text_height, self.disk_buttom_items, |
|
68 |
+ partition = True, popupWindow = True, install_config = self.install_config, |
|
69 |
+ text_items = self.text_items, table_space=self.table_space, default_start=1, info=info, size_left=str(self.disk_size[self.device_index][1])) |
|
70 |
+ |
|
71 |
+ self.window.set_action_panel(self.text_pane) |
|
72 |
+ |
|
73 |
+ return self.window.do_action() |
|
74 |
+ |
|
75 |
+ def validate_partition(self, pstr): |
|
76 |
+ if not pstr: |
|
77 |
+ return ActionResult(False, None) |
|
78 |
+ sizedata = pstr[0] |
|
79 |
+ mtdata = pstr[2] |
|
80 |
+ typedata = pstr[1] |
|
81 |
+ devicedata = self.devices[self.device_index].path |
|
82 |
+ |
|
83 |
+ #no empty fields unless swap |
|
84 |
+ if typedata == 'swap' and (len(mtdata)!=0 or len(typedata) == 0 or len(devicedata) == 0): |
|
85 |
+ return False, "invalid swap data " |
|
86 |
+ |
|
87 |
+ if typedata != 'swap' and (len(sizedata) == 0 or len(mtdata) == 0 or len(typedata) == 0 or len(devicedata) == 0): |
|
88 |
+ if not self.has_empty and len(mtdata) != 0 and len(typedata) != 0 and len(devicedata) != 0: |
|
89 |
+ self.has_empty = True |
|
90 |
+ else: |
|
91 |
+ return False, "Input cannot be empty" |
|
92 |
+ |
|
93 |
+ if typedata !='swap' and typedata!='ext3' and typedata!='ext4': |
|
94 |
+ return False, "Invalid type" |
|
95 |
+ |
|
96 |
+ if len(mtdata)!=0 and mtdata[0] !='/': |
|
97 |
+ return False, "Invalid path" |
|
98 |
+ |
|
99 |
+ if mtdata in self.path_checker: |
|
100 |
+ return False, "Path already existed" |
|
101 |
+ #validate disk: must be one of the existing disks |
|
102 |
+ i = self.device_index |
|
103 |
+ |
|
104 |
+ #valid size: must not exceed memory limit |
|
105 |
+ curr_size = self.disk_size[i][1] |
|
106 |
+ if len(sizedata)!=0: |
|
107 |
+ try: |
|
108 |
+ int(sizedata) |
|
109 |
+ except ValueError: |
|
110 |
+ return False, "invalid device size" |
|
111 |
+ |
|
112 |
+ if int(curr_size) - int(sizedata) < 0: |
|
113 |
+ return False, "invalid device size" |
|
114 |
+ #if valid, update the size and return true |
|
115 |
+ new_size = (self.disk_size[i][0], int(curr_size)- int(sizedata)) |
|
116 |
+ self.disk_size[i] =new_size |
|
117 |
+ |
|
118 |
+ if mtdata=="/": |
|
119 |
+ self.has_slash=True |
|
120 |
+ |
|
121 |
+ self.path_checker.append(mtdata) |
|
122 |
+ return True, None |
|
123 |
+ |
|
124 |
+ def create_function(self): |
|
125 |
+ self.window.hide_window() |
|
126 |
+ |
|
127 |
+ self.install_config['partition_disk'] = self.devices[self.device_index].path |
|
128 |
+ self.partition_items = [] |
|
129 |
+ self.partition_items.append(('Size in MB: '+str(self.disk_size[self.device_index][1])+' available')) |
|
130 |
+ self.partition_items.append(('Type: (ext3, ext4, swap)')) |
|
131 |
+ self.partition_items.append(('Mountpoint:')) |
|
132 |
+ self.create_window = ReadMulText( |
|
133 |
+ self.maxy, self.maxx, 0, |
|
134 |
+ self.install_config, |
|
135 |
+ str(self.install_config['partitionsnumber']) + 'partition_info', |
|
136 |
+ self.partition_items, |
|
137 |
+ None, |
|
138 |
+ None, |
|
139 |
+ None, |
|
140 |
+ self.validate_partition, #validation function of the input |
|
141 |
+ None, |
|
142 |
+ True, |
|
143 |
+ ) |
|
144 |
+ result = self.create_window.do_action() |
|
145 |
+ if result.success: |
|
146 |
+ self.install_config['partitionsnumber'] = self.install_config['partitionsnumber'] + 1 |
|
147 |
+ |
|
148 |
+ #parse the input in install config |
|
149 |
+ return self.display(False) |
|
150 |
+ |
|
151 |
+ def delete_function(self): |
|
152 |
+ self.delete() |
|
153 |
+ return self.display(False) |
|
154 |
+ |
|
155 |
+ def go_back(self): |
|
156 |
+ self.delete() |
|
157 |
+ self.window.hide_window() |
|
158 |
+ self.text_pane.hide() |
|
159 |
+ return ActionResult(False, {'goBack':True}) |
|
160 |
+ |
|
161 |
+ def next(self): |
|
162 |
+ if self.install_config['partitionsnumber'] == 0: |
|
163 |
+ window_height=9 |
|
164 |
+ window_width=40 |
|
165 |
+ window_starty=(self.maxy-window_height)/2+5 |
|
166 |
+ confirm_window=ConfirmWindow(window_height,window_width,self.maxy, self.maxx, window_starty, 'Partition information cannot be empty', info=True) |
|
167 |
+ confirm_window.do_action() |
|
168 |
+ return self.display(False) |
|
169 |
+ #must have / |
|
170 |
+ if not self.has_slash: |
|
171 |
+ window_height=9 |
|
172 |
+ window_width=40 |
|
173 |
+ window_starty=(self.maxy-window_height)/2+5 |
|
174 |
+ confirm_window=ConfirmWindow(window_height,window_width,self.maxy, self.maxx, window_starty, 'Missing /', info=True) |
|
175 |
+ confirm_window.do_action() |
|
176 |
+ return self.display(False) |
|
177 |
+ |
|
178 |
+ self.window.hide_window() |
|
179 |
+ self.text_pane.hide() |
|
180 |
+ return ActionResult(True, {'goNext':True}) |
|
181 |
+ |
|
182 |
+ def delete(self): |
|
183 |
+ for i in range(int(self.install_config['partitionsnumber'])): |
|
184 |
+ self.install_config[str(i)+'partition_info'+str(0)] = '' |
|
185 |
+ self.install_config[str(i)+'partition_info'+str(1)] = '' |
|
186 |
+ self.install_config[str(i)+'partition_info'+str(2)] = '' |
|
187 |
+ self.install_config[str(i)+'partition_info'+str(3)] = '' |
|
188 |
+ del self.disk_size[:] |
|
189 |
+ for index, device in enumerate(self.devices): |
|
190 |
+ self.disk_size.append((device.path, int(device.size) / 1048576)) |
|
191 |
+ del self.path_checker[:] |
|
192 |
+ self.has_slash = False |
|
193 |
+ self.has_remain = False |
|
194 |
+ self.has_empty = False |
|
195 |
+ self.install_config['partitionsnumber'] = 0 |
0 | 196 |
old mode 100644 |
1 | 197 |
new mode 100755 |
... | ... |
@@ -10,7 +10,7 @@ import math |
10 | 10 |
from curses import panel |
11 | 11 |
|
12 | 12 |
class ProgressBar(object): |
13 |
- def __init__(self, starty, startx, width): |
|
13 |
+ def __init__(self, starty, startx, width, new_win=False): |
|
14 | 14 |
self.timer = None |
15 | 15 |
self.loadding_timer = None |
16 | 16 |
self.timer_lock = threading.Lock() |
... | ... |
@@ -26,6 +26,20 @@ class ProgressBar(object): |
26 | 26 |
self.window.bkgd(' ', curses.color_pair(2)) #defaultbackground color |
27 | 27 |
self.progress = 0 |
28 | 28 |
|
29 |
+ self.new_win = new_win |
|
30 |
+ self.x=startx |
|
31 |
+ self.y=starty |
|
32 |
+ |
|
33 |
+ if new_win: |
|
34 |
+ self.contentwin = curses.newwin(7, width+2) |
|
35 |
+ self.contentwin.bkgd(' ', curses.color_pair(2)) #Default Window color |
|
36 |
+ self.contentwin.erase() |
|
37 |
+ self.contentwin.box() |
|
38 |
+ self.contentpanel = curses.panel.new_panel(self.contentwin) |
|
39 |
+ self.contentpanel.move(starty-1, startx-1) |
|
40 |
+ self.contentpanel.hide() |
|
41 |
+ |
|
42 |
+ |
|
29 | 43 |
self.panel = panel.new_panel(self.window) |
30 | 44 |
self.panel.move(starty, startx) |
31 | 45 |
self.panel.hide() |
... | ... |
@@ -93,6 +107,11 @@ class ProgressBar(object): |
93 | 93 |
self.render_progress() |
94 | 94 |
|
95 | 95 |
def show(self): |
96 |
+ if self.new_win: |
|
97 |
+ self.contentpanel.top() |
|
98 |
+ self.contentpanel.move(self.y-1, self.x-1) |
|
99 |
+ self.contentpanel.show() |
|
100 |
+ |
|
96 | 101 |
self.refresh() |
97 | 102 |
self.panel.top() |
98 | 103 |
self.panel.show() |
... | ... |
@@ -135,6 +154,8 @@ class ProgressBar(object): |
135 | 135 |
self.loadding_timer.cancel() |
136 | 136 |
self.loadding_timer = None |
137 | 137 |
|
138 |
+ if self.new_win: |
|
139 |
+ self.contentpanel.hide() |
|
138 | 140 |
self.panel.hide() |
139 | 141 |
panel.update_panels() |
140 | 142 |
|
141 | 143 |
new file mode 100755 |
... | ... |
@@ -0,0 +1,211 @@ |
0 |
+#! /usr/bin/python2 |
|
1 |
+# |
|
2 |
+# Copyright (C) 2015 vmware inc. |
|
3 |
+# |
|
4 |
+# Author: Yang Yao <yaoyang@vmware.com> |
|
5 |
+ |
|
6 |
+import curses |
|
7 |
+import sys |
|
8 |
+from actionresult import ActionResult |
|
9 |
+from action import Action |
|
10 |
+from window import Window |
|
11 |
+from confirmwindow import ConfirmWindow |
|
12 |
+ |
|
13 |
+class ReadMulText(Action): |
|
14 |
+ def __init__(self, maxy, maxx, y, install_config, field, display_string, confirmation_error_msg, |
|
15 |
+ echo_char, accepted_chars, validation_fn, conversion_fn, can_cancel, default_string = None): |
|
16 |
+ self.maxy = maxy |
|
17 |
+ self.maxx = maxx |
|
18 |
+ self.y = y |
|
19 |
+ self.install_config = install_config |
|
20 |
+ self.field = field |
|
21 |
+ self.horizontal_padding = 10 |
|
22 |
+ self.confirmation_error_msg = confirmation_error_msg |
|
23 |
+ self.echo_char = echo_char |
|
24 |
+ self.accepted_chars = accepted_chars |
|
25 |
+ self.validation_fn = validation_fn |
|
26 |
+ self.conversion_fn = conversion_fn |
|
27 |
+ self.default_string = default_string |
|
28 |
+ self.display_string = display_string |
|
29 |
+ self.textwin_width = maxx - self.horizontal_padding -2 |
|
30 |
+ self.textwin_width = self.textwin_width*2/3 |
|
31 |
+ self.visible_text_width = self.textwin_width - 1 |
|
32 |
+ self.position = 0 |
|
33 |
+ self.height = len(self.display_string) * 4 + 2 |
|
34 |
+ self.menu_pos = 0 |
|
35 |
+ |
|
36 |
+ self.textwin = curses.newwin(self.height, self.textwin_width + 2)#self.textwin_width) |
|
37 |
+ self.textwin.bkgd(' ', curses.color_pair(2)) |
|
38 |
+ self.textwin.keypad(1) |
|
39 |
+ |
|
40 |
+ self.shadowwin = curses.newwin(self.height, self.textwin_width + 2) |
|
41 |
+ self.shadowwin.bkgd(' ', curses.color_pair(0)) #Default shadow color |
|
42 |
+ |
|
43 |
+ self.panel = curses.panel.new_panel(self.textwin) |
|
44 |
+ self.panel.move((maxy-self.height)/2, (maxx - self.textwin_width) / 2 -1) |
|
45 |
+ self.panel.hide() |
|
46 |
+ self.shadowpanel = curses.panel.new_panel(self.shadowwin) |
|
47 |
+ self.shadowpanel.move((maxy-self.height)/2+1, (maxx - self.textwin_width) / 2) |
|
48 |
+ self.shadowpanel.hide() |
|
49 |
+ curses.panel.update_panels() |
|
50 |
+ |
|
51 |
+ self.init_text() |
|
52 |
+ self.textwin.box() |
|
53 |
+ self.maxlength = 255 |
|
54 |
+ |
|
55 |
+ #initialize the accepted characters |
|
56 |
+ if accepted_chars: |
|
57 |
+ self.accepted_chars = accepted_chars |
|
58 |
+ else: |
|
59 |
+ self.accepted_chars = range(32, 127) |
|
60 |
+ |
|
61 |
+ def hide(self): |
|
62 |
+ return |
|
63 |
+ |
|
64 |
+ def init_text(self): |
|
65 |
+ self.shadowpanel.show() |
|
66 |
+ curses.panel.update_panels() |
|
67 |
+ |
|
68 |
+ self.x = 0; |
|
69 |
+ #initialize the ---- |
|
70 |
+ dashes = '_' * self.textwin_width |
|
71 |
+ cury = self.y+1 |
|
72 |
+ self.str = [] |
|
73 |
+ |
|
74 |
+ for string in self.display_string: |
|
75 |
+ self.textwin.addstr(cury, 1, string) |
|
76 |
+ self.textwin.addstr(cury+1, 1, dashes) |
|
77 |
+ cury = cury + 4 |
|
78 |
+ self.str.append('') |
|
79 |
+ |
|
80 |
+ #remove the error messages |
|
81 |
+ spaces = ' ' * self.textwin_width |
|
82 |
+ #self.textwin.addstr(self.y + 2, 0, spaces) |
|
83 |
+ self.update_menu() |
|
84 |
+ |
|
85 |
+ def do_action(self): |
|
86 |
+ self.init_text() |
|
87 |
+ curses.curs_set(1) |
|
88 |
+ |
|
89 |
+ if self.default_string != None: |
|
90 |
+ self.textwin.addstr(self.y, 0, self.default_string) |
|
91 |
+ self.str = self.default_string |
|
92 |
+ |
|
93 |
+ while True: |
|
94 |
+ if len(self.str[self.position]) > self.visible_text_width: |
|
95 |
+ curs_loc = self.visible_text_width + 1 |
|
96 |
+ else: |
|
97 |
+ curs_loc = len(self.str[self.position]) +1 |
|
98 |
+ ch = self.textwin.getch(self.y+2+self.position*4, curs_loc) |
|
99 |
+ |
|
100 |
+ update_text = False |
|
101 |
+ if ch in [curses.KEY_ENTER, ord('\n')]: |
|
102 |
+ if self.menu_pos==1: |
|
103 |
+ curses.curs_set(0) |
|
104 |
+ self.shadowpanel.hide() |
|
105 |
+ return ActionResult(False, None) |
|
106 |
+ if self.confirmation_error_msg: |
|
107 |
+ if self.str != self.install_config[self.field]: |
|
108 |
+ curses.curs_set(0) |
|
109 |
+ conf_message_height = 8 |
|
110 |
+ conf_message_width = 48 |
|
111 |
+ conf_message_button_y = (self.maxy - conf_message_height) / 2 + 5 |
|
112 |
+ confrim_window = ConfirmWindow(conf_message_height, conf_message_width, self.maxy, self.maxx, conf_message_button_y, self.confirmation_error_msg, True) |
|
113 |
+ confrim_window.do_action() |
|
114 |
+ return ActionResult(False, {'goBack': True}) |
|
115 |
+ self.set_field() |
|
116 |
+ else: |
|
117 |
+ if not self.validate_input(): |
|
118 |
+ continue |
|
119 |
+ self.set_field() |
|
120 |
+ curses.curs_set(0) |
|
121 |
+ self.shadowpanel.hide() |
|
122 |
+ return ActionResult(True, None) |
|
123 |
+ elif ch == curses.KEY_UP: |
|
124 |
+ self.refresh(-1) |
|
125 |
+ |
|
126 |
+ elif ch == curses.KEY_DOWN: |
|
127 |
+ self.refresh(1) |
|
128 |
+ |
|
129 |
+ elif ch in [ord('\t')]: |
|
130 |
+ self.refresh(1, reset=True) |
|
131 |
+ |
|
132 |
+ elif ch ==curses.KEY_LEFT: |
|
133 |
+ self.menu_refresh(1) |
|
134 |
+ |
|
135 |
+ elif ch == curses.KEY_RIGHT: |
|
136 |
+ self.menu_refresh(-1) |
|
137 |
+ |
|
138 |
+ elif ch == curses.KEY_BACKSPACE: |
|
139 |
+ # Handle the backspace case |
|
140 |
+ self.str[self.position] = self.str[self.position][:len(self.str[self.position]) - 1] |
|
141 |
+ update_text = True |
|
142 |
+ |
|
143 |
+ elif len(self.str[self.position]) < self.maxlength and ch in self.accepted_chars: |
|
144 |
+ self.str[self.position] += chr(ch) |
|
145 |
+ update_text = True |
|
146 |
+ |
|
147 |
+ if update_text: |
|
148 |
+ self.update_text() |
|
149 |
+ |
|
150 |
+ def menu_refresh(self, n): |
|
151 |
+ self.menu_pos+=n |
|
152 |
+ if self.menu_pos<0: |
|
153 |
+ self.menu_pos=0 |
|
154 |
+ elif self.menu_pos>=1: |
|
155 |
+ self.menu_pos=1 |
|
156 |
+ self.update_menu() |
|
157 |
+ |
|
158 |
+ def update_menu(self): |
|
159 |
+ if self.menu_pos==1: |
|
160 |
+ self.textwin.addstr(self.height-2, 5, '<Cancel>', curses.color_pair(3)) |
|
161 |
+ else: |
|
162 |
+ self.textwin.addstr(self.height-2, 5, '<Cancel>') |
|
163 |
+ if self.menu_pos==0: |
|
164 |
+ self.textwin.addstr(self.height-2, self.textwin_width-len('<OK>')-5, '<OK>',curses.color_pair(3)) |
|
165 |
+ else: |
|
166 |
+ self.textwin.addstr(self.height-2, self.textwin_width-len('<OK>')-5, '<OK>') |
|
167 |
+ |
|
168 |
+ |
|
169 |
+ |
|
170 |
+ def update_text(self): |
|
171 |
+ if len(self.str[self.position]) > self.visible_text_width: |
|
172 |
+ text = self.str[self.position][-self.visible_text_width:] |
|
173 |
+ else: |
|
174 |
+ text = self.str[self.position] |
|
175 |
+ if self.echo_char: |
|
176 |
+ text = self.echo_char * len(text) |
|
177 |
+ |
|
178 |
+ text = text + '_' * (self.visible_text_width - len(self.str[self.position])) |
|
179 |
+ self.textwin.addstr(self.y+2+self.position*4, 1, text) |
|
180 |
+ |
|
181 |
+ def refresh(self, n, reset=False): |
|
182 |
+ self.position += n |
|
183 |
+ if self.position < 0: |
|
184 |
+ self.position = 0 |
|
185 |
+ elif self.position >= len(self.display_string): |
|
186 |
+ if reset: |
|
187 |
+ self.position=0 |
|
188 |
+ else: |
|
189 |
+ self.position = len(self.display_string)-1 |
|
190 |
+ |
|
191 |
+ def set_field(self): |
|
192 |
+ i = 0 |
|
193 |
+ for string in self.display_string: |
|
194 |
+ if self.conversion_fn: |
|
195 |
+ self.install_config[self.field+str(i)] = self.conversion_fn(self.str[i]) |
|
196 |
+ else: |
|
197 |
+ self.install_config[self.field+str(i)] = self.str[i] |
|
198 |
+ i = i + 1 |
|
199 |
+ |
|
200 |
+ def validate_input(self): |
|
201 |
+ if self.validation_fn: |
|
202 |
+ success, err = self.validation_fn(self.str) |
|
203 |
+ if not success: |
|
204 |
+ spaces = ' ' * (int(self.textwin_width) - len(self.display_string[0])) |
|
205 |
+ self.textwin.addstr(self.y + 1, len(self.display_string[0]), spaces) |
|
206 |
+ self.textwin.addstr(self.y + 1, len(self.display_string[0]), err, curses.color_pair(4)) |
|
207 |
+ return success |
|
208 |
+ else: |
|
209 |
+ return True |
|
210 |
+ |
0 | 211 |
old mode 100644 |
1 | 212 |
new mode 100755 |
... | ... |
@@ -11,7 +11,7 @@ from action import Action |
11 | 11 |
from confirmwindow import ConfirmWindow |
12 | 12 |
|
13 | 13 |
class ReadText(Action): |
14 |
- def __init__(self, maxy, maxx, textwin, y, install_config, field, confirmation_error_msg, echo_char, accepted_chars, validation_fn, conversion_fn, default_string = None): |
|
14 |
+ def __init__(self, maxy, maxx, textwin, y, install_config, field, confirmation_error_msg, echo_char, accepted_chars, validation_fn, conversion_fn, default_string = None, tab_enabled=True): |
|
15 | 15 |
self.textwin = textwin |
16 | 16 |
self.maxy = maxy |
17 | 17 |
self.maxx = maxx |
... | ... |
@@ -26,10 +26,14 @@ class ReadText(Action): |
26 | 26 |
self.default_string = default_string |
27 | 27 |
self.textwin_width = self.textwin.getmaxyx()[1] - 1 |
28 | 28 |
self.visible_text_width = self.textwin_width - 1 |
29 |
+ self.tab_enabled=tab_enabled |
|
29 | 30 |
|
30 | 31 |
self.init_text() |
31 | 32 |
self.maxlength = 255 |
32 | 33 |
|
34 |
+ if not tab_enabled: |
|
35 |
+ self.textwin.keypad(1) |
|
36 |
+ |
|
33 | 37 |
#initialize the accepted characters |
34 | 38 |
if accepted_chars: |
35 | 39 |
self.accepted_chars = accepted_chars |
... | ... |
@@ -50,13 +54,23 @@ class ReadText(Action): |
50 | 50 |
spaces = ' ' * self.textwin_width |
51 | 51 |
self.textwin.addstr(self.y + 2, 0, spaces) |
52 | 52 |
|
53 |
- def do_action(self): |
|
54 |
- self.init_text() |
|
55 |
- curses.curs_set(1) |
|
56 |
- |
|
57 |
- if self.default_string != None: |
|
58 |
- self.textwin.addstr(self.y, 0, self.default_string) |
|
59 |
- self.str = self.default_string |
|
53 |
+ def do_action(self, returned=False, go_back=False): |
|
54 |
+ if returned: |
|
55 |
+ if len(self.str) > self.visible_text_width: |
|
56 |
+ text = self.str[-self.visible_text_width:] |
|
57 |
+ else: |
|
58 |
+ text = self.str |
|
59 |
+ if self.echo_char: |
|
60 |
+ text = self.echo_char * len(text) |
|
61 |
+ # Add the dashes |
|
62 |
+ text = text + '_' * (self.visible_text_width - len(self.str)) |
|
63 |
+ self.textwin.addstr(self.y, 0, text) |
|
64 |
+ if not returned: |
|
65 |
+ curses.curs_set(1) |
|
66 |
+ self.init_text() |
|
67 |
+ if self.default_string != None: |
|
68 |
+ self.textwin.addstr(self.y, 0, self.default_string) |
|
69 |
+ self.str = self.default_string |
|
60 | 70 |
|
61 | 71 |
while True: |
62 | 72 |
if len(self.str) > self.visible_text_width: |
... | ... |
@@ -67,9 +81,11 @@ class ReadText(Action): |
67 | 67 |
|
68 | 68 |
update_text = False |
69 | 69 |
if ch in [curses.KEY_ENTER, ord('\n')]: |
70 |
+ curses.curs_set(0) |
|
71 |
+ if go_back: |
|
72 |
+ return ActionResult(False, {'goBack': True}) |
|
70 | 73 |
if self.confirmation_error_msg: |
71 | 74 |
if self.str != self.install_config[self.field]: |
72 |
- curses.curs_set(0) |
|
73 | 75 |
conf_message_height = 8 |
74 | 76 |
conf_message_width = 48 |
75 | 77 |
conf_message_button_y = (self.maxy - conf_message_height) / 2 + 5 |
... | ... |
@@ -83,10 +99,14 @@ class ReadText(Action): |
83 | 83 |
self.set_field() |
84 | 84 |
curses.curs_set(0) |
85 | 85 |
return ActionResult(True, None) |
86 |
+ elif ch ==curses.KEY_LEFT and not self.tab_enabled: |
|
87 |
+ return ActionResult(False, {'direction': -1}) |
|
88 |
+ elif ch ==curses.KEY_RIGHT and not self.tab_enabled: |
|
89 |
+ return ActionResult(False, {'direction': 1}) |
|
86 | 90 |
elif ch in [ord('\t')]: |
87 | 91 |
curses.curs_set(0) |
88 | 92 |
return ActionResult(False, None) |
89 |
- elif ch == 127: |
|
93 |
+ elif ch == curses.KEY_BACKSPACE: #originally ==127 |
|
90 | 94 |
# Handle the backspace case |
91 | 95 |
self.str = self.str[:len(self.str) - 1] |
92 | 96 |
|
93 | 97 |
old mode 100644 |
94 | 98 |
new mode 100755 |
... | ... |
@@ -29,30 +29,61 @@ class SelectDisk(object): |
29 | 29 |
self.menu_height = 5 |
30 | 30 |
self.progress_padding = 5 |
31 | 31 |
self.progress_width = self.win_width - self.progress_padding |
32 |
- self.progress_bar = ProgressBar(self.win_starty + 6, self.win_startx + self.progress_padding / 2, self.progress_width) |
|
32 |
+ self.progress_bar = ProgressBar(self.win_starty + 6, self.win_startx + self.progress_padding / 2, self.progress_width, new_win=True) |
|
33 | 33 |
|
34 |
- self.window = Window(self.win_height, self.win_width, self.maxy, self.maxx, 'Setup your disk', True) |
|
34 |
+ self.disk_buttom_items = [] |
|
35 |
+ self.disk_buttom_items.append(('<Custom>', self.custom_function, False)) |
|
36 |
+ self.disk_buttom_items.append(('<Auto>', self.auto_function, False)) |
|
37 |
+ |
|
38 |
+ self.window = Window(self.win_height, self.win_width, self.maxy, self.maxx, 'Select a disk', True, items = self.disk_buttom_items, menu_helper = self.save_index, position = 2, tab_enabled=False) |
|
39 |
+ self.partition_window = Window(self.win_height, self.win_width, self.maxy, self.maxx, 'Partition', True) |
|
35 | 40 |
self.devices = Device.refresh_devices() |
36 | 41 |
|
37 |
- def guided_partitions(self, device_index): |
|
42 |
+ def guided_partitions(self, params): |
|
43 |
+ if not 'diskindex' in self.install_config: |
|
44 |
+ return ActionResult(False, None); |
|
45 |
+ |
|
46 |
+ device_index = self.install_config['diskindex'] |
|
47 |
+ |
|
38 | 48 |
menu_height = 9 |
39 | 49 |
menu_width = 40 |
40 | 50 |
menu_starty = (self.maxy - menu_height) / 2 + 5 |
51 |
+ self.install_config['delete_partition'] = True |
|
41 | 52 |
confrim_window = ConfirmWindow(menu_height, menu_width, self.maxy, self.maxx, menu_starty, 'This will erase the disk.\nAre you sure?') |
42 | 53 |
confirmed = confrim_window.do_action().result['yes'] |
43 | 54 |
|
44 |
- if not confirmed: |
|
45 |
- return ActionResult(confirmed, None) |
|
55 |
+ if confirmed == False: |
|
56 |
+ self.install_config['skipPrevs'] = True |
|
57 |
+ return ActionResult(False, {'goBack':True}) |
|
46 | 58 |
|
59 |
+ self.install_config['skipPrevs'] = False |
|
47 | 60 |
self.progress_bar.initialize('Partitioning...') |
48 | 61 |
self.progress_bar.show() |
49 | 62 |
self.progress_bar.show_loading('Partitioning') |
50 | 63 |
|
51 | 64 |
# Do the partitioning |
52 |
- self.window.clearerror() |
|
53 |
- partitions_data = modules.commons.partition_disk(self.devices[device_index].path, modules.commons.default_partitions) |
|
65 |
+ if 'partitionsnumber' in self.install_config: |
|
66 |
+ if (int(self.install_config['partitionsnumber']) == 0): |
|
67 |
+ partitions_data = modules.commons.partition_disk(self.devices[device_index].path, modules.commons.default_partitions) |
|
68 |
+ else: |
|
69 |
+ partitions = [] |
|
70 |
+ for i in range (int (self.install_config['partitionsnumber'])): |
|
71 |
+ if len(self.install_config[str(i)+'partition_info'+str(0)])==0: |
|
72 |
+ sizedata=0 |
|
73 |
+ else: |
|
74 |
+ sizedata = int(self.install_config[str(i)+'partition_info'+str(0)]) |
|
75 |
+ mtdata = self.install_config[str(i)+'partition_info'+str(2)] |
|
76 |
+ typedata = self.install_config[str(i)+'partition_info'+str(1)] |
|
77 |
+ |
|
78 |
+ partitions = partitions + [ |
|
79 |
+ {"mountpoint": mtdata, "size": sizedata, "filesystem": typedata}, |
|
80 |
+ ] |
|
81 |
+ partitions_data = modules.commons.partition_disk(self.devices[device_index].path, partitions) |
|
82 |
+ else: |
|
83 |
+ partitions_data = modules.commons.partition_disk(self.devices[device_index].path, modules.commons.default_partitions) |
|
84 |
+ |
|
54 | 85 |
if partitions_data == None: |
55 |
- self.window.adderror('Partitioning failed, you may try again') |
|
86 |
+ self.partition_window.adderror('Partitioning failed, you may try again') |
|
56 | 87 |
else: |
57 | 88 |
self.install_config['disk'] = partitions_data |
58 | 89 |
|
... | ... |
@@ -60,7 +91,9 @@ class SelectDisk(object): |
60 | 60 |
return ActionResult(partitions_data != None, None) |
61 | 61 |
|
62 | 62 |
def display(self, params): |
63 |
- self.window.addstr(0, 0, 'First, we will setup your disks.\n\nWe have detected {0} disks, choose disk to be auto-partitioned:'.format(len(self.devices))) |
|
63 |
+ if 'skipPrevs' in self.install_config: |
|
64 |
+ self.install_config['skipPrevs'] = False |
|
65 |
+ self.window.addstr(0, 0, 'Please select a disk and a method how to partition it:\nAuto - single partition for /, no swap partition.\nCustom - for customized partitioning') |
|
64 | 66 |
|
65 | 67 |
self.disk_menu_items = [] |
66 | 68 |
|
... | ... |
@@ -70,15 +103,27 @@ class SelectDisk(object): |
70 | 70 |
self.disk_menu_items.append( |
71 | 71 |
( |
72 | 72 |
'{2} - {1} @ {0}'.format(device.path, device.size, device.model), |
73 |
- self.guided_partitions, |
|
73 |
+ self.save_index, |
|
74 | 74 |
index |
75 | 75 |
) |
76 | 76 |
) |
77 | 77 |
|
78 |
- self.disk_menu = Menu(self.menu_starty, self.maxx, self.disk_menu_items, self.menu_height) |
|
78 |
+ self.disk_menu = Menu(self.menu_starty, self.maxx, self.disk_menu_items, self.menu_height, tab_enable=False) |
|
79 |
+ self.disk_menu.can_save_sel(True) |
|
79 | 80 |
|
80 | 81 |
self.window.set_action_panel(self.disk_menu) |
81 | 82 |
return self.window.do_action() |
82 | 83 |
|
84 |
+ def save_index(self, device_index): |
|
85 |
+ self.install_config['diskindex'] = device_index |
|
86 |
+ return ActionResult(True, None) |
|
87 |
+ |
|
88 |
+ def auto_function(self, params): #default is no partition |
|
89 |
+ self.install_config['autopartition'] = True |
|
90 |
+ self.install_config['partitionsnumber'] = 0 |
|
91 |
+ return ActionResult(True, None) |
|
83 | 92 |
|
93 |
+ def custom_function(self, params): #custom minimize partition number is 1 |
|
94 |
+ self.install_config['autopartition'] = False |
|
95 |
+ return ActionResult(True, None) |
|
84 | 96 |
|
85 | 97 |
old mode 100644 |
86 | 98 |
new mode 100755 |
... | ... |
@@ -9,18 +9,29 @@ from actionresult import ActionResult |
9 | 9 |
from action import Action |
10 | 10 |
|
11 | 11 |
class TextPane(Action): |
12 |
- def __init__(self, starty, maxx, width, text_file_path, height, menu_items): |
|
12 |
+ def __init__(self, starty, maxx, width, text_file_path, height, menu_items, partition = False, popupWindow = False, install_config = {}, text_items = [], table_space = 0, default_start = 0, info=[], size_left=[]): |
|
13 | 13 |
self.head_position = 0 #This is the start of showing |
14 |
- self.menu_position = 0 |
|
14 |
+ self.menu_position = default_start |
|
15 | 15 |
self.lines = [] |
16 |
- self.menu_items = menu_items; |
|
16 |
+ self.menu_items = menu_items |
|
17 |
+ self.text_items = text_items |
|
18 |
+ self.table_space = table_space |
|
19 |
+ self.info=info |
|
20 |
+ self.size_left=size_left |
|
17 | 21 |
|
18 | 22 |
self.width = width |
19 | 23 |
|
20 |
- self.read_file(text_file_path, self.width - 3); |
|
21 |
- |
|
24 |
+ if partition == False: |
|
25 |
+ self.read_file(text_file_path, self.width - 3); |
|
26 |
+ else: |
|
27 |
+ self.install_config=install_config |
|
28 |
+ self.partition(); |
|
29 |
+ |
|
22 | 30 |
self.num_items = len(self.lines) |
23 |
- self.text_height = height - 2 |
|
31 |
+ if self.info: |
|
32 |
+ self.text_height = height - 4 |
|
33 |
+ else: |
|
34 |
+ self.text_height = height - 2 |
|
24 | 35 |
|
25 | 36 |
# Check if we need to add a scroll bar |
26 | 37 |
if self.num_items > self.text_height: |
... | ... |
@@ -29,7 +40,10 @@ class TextPane(Action): |
29 | 29 |
self.show_scroll = False |
30 | 30 |
|
31 | 31 |
# Some calculation to detitmine the size of the scroll filled portion |
32 |
- self.filled = int(round(self.text_height * self.text_height / float(self.num_items))) |
|
32 |
+ if (self.num_items == 0): |
|
33 |
+ self.filled = 0 |
|
34 |
+ else: |
|
35 |
+ self.filled = int(round(self.text_height * self.text_height / float(self.num_items))) |
|
33 | 36 |
if self.filled == 0: |
34 | 37 |
self.filled += 1 |
35 | 38 |
for i in [1, 2]: |
... | ... |
@@ -38,6 +52,7 @@ class TextPane(Action): |
38 | 38 |
|
39 | 39 |
self.window = curses.newwin(height, self.width) |
40 | 40 |
self.window.bkgd(' ', curses.color_pair(2)) |
41 |
+ self.popupWindow = popupWindow |
|
41 | 42 |
|
42 | 43 |
self.window.keypad(1) |
43 | 44 |
self.panel = curses.panel.new_panel(self.window) |
... | ... |
@@ -46,6 +61,37 @@ class TextPane(Action): |
46 | 46 |
self.panel.hide() |
47 | 47 |
curses.panel.update_panels() |
48 | 48 |
|
49 |
+ def partition(self): |
|
50 |
+ if self.install_config == {}: |
|
51 |
+ return |
|
52 |
+ |
|
53 |
+ tstring = '' |
|
54 |
+ #calculate the start index for each item and draw the title |
|
55 |
+ for index, item in enumerate(self.text_items): |
|
56 |
+ tstring = tstring + item[0] + ' '*(item[1] - len(item[0])) + ' '*self.table_space |
|
57 |
+ |
|
58 |
+ self.lines.append(tstring) |
|
59 |
+ #draw the table |
|
60 |
+ for i in range (int (self.install_config['partitionsnumber'])): |
|
61 |
+ pdisk = self.install_config['partition_disk'] |
|
62 |
+ if len(pdisk) > self.text_items[0][1]: |
|
63 |
+ pdisk = pdisk[-self.text_items[0][1]:] |
|
64 |
+ psize = self.install_config[str(i)+'partition_info'+str(0)] |
|
65 |
+ if len(psize) > self.text_items[1][1]: |
|
66 |
+ psize = psize[-self.text_items[1][1]:] |
|
67 |
+ if len(psize)==0: |
|
68 |
+ psize='<'+self.size_left+'>' |
|
69 |
+ ptype = self.install_config[str(i)+'partition_info'+str(1)] |
|
70 |
+ if len(ptype) > self.text_items[2][1]: |
|
71 |
+ ptype = ptype[-self.text_items[2][1]:] |
|
72 |
+ pmountpoint = self.install_config[str(i)+'partition_info'+str(2)] |
|
73 |
+ if len(pmountpoint) > self.text_items[3][1]: |
|
74 |
+ pmountpoint = pmountpoint[-self.text_items[3][1]:] |
|
75 |
+ pstring = pdisk + ' '*(self.text_items[0][1] - len(pdisk)) + ' ' * self.table_space + psize + ' '*(self.text_items[1][1] - len(psize)) + ' ' * self.table_space + ptype + ' '*(self.text_items[2][1] - len(ptype) + self.table_space) + pmountpoint |
|
76 |
+ self.lines.append(pstring) |
|
77 |
+ |
|
78 |
+ |
|
79 |
+ |
|
49 | 80 |
def read_file(self, text_file_path, line_width): |
50 | 81 |
with open(text_file_path, "r") as f: |
51 | 82 |
for line in f: |
... | ... |
@@ -73,11 +119,12 @@ class TextPane(Action): |
73 | 73 |
self.lines.append(line + ' ' * (line_width - len(line))) |
74 | 74 |
|
75 | 75 |
def navigate(self, n): |
76 |
- self.head_position += n |
|
77 |
- if self.head_position < 0: |
|
78 |
- self.head_position = 0 |
|
79 |
- elif self.head_position > (len(self.lines) - self.text_height + 1): |
|
80 |
- self.head_position = len(self.lines) - self.text_height + 1 |
|
76 |
+ if self.show_scroll: |
|
77 |
+ self.head_position += n |
|
78 |
+ if self.head_position < 0: |
|
79 |
+ self.head_position = 0 |
|
80 |
+ elif self.head_position > (len(self.lines) - self.text_height + 1): |
|
81 |
+ self.head_position = len(self.lines) - self.text_height + 1 |
|
81 | 82 |
|
82 | 83 |
def navigate_menu(self, n): |
83 | 84 |
self.menu_position += n |
... | ... |
@@ -134,10 +181,16 @@ class TextPane(Action): |
134 | 134 |
mode = curses.color_pair(3) |
135 | 135 |
else: |
136 | 136 |
mode = curses.color_pair(2) |
137 |
- |
|
138 |
- self.window.addstr(self.text_height + 1, xpos - len(item[0]) - 4, item[0], mode) |
|
137 |
+ if self.info: |
|
138 |
+ self.window.addstr(self.text_height + 3, xpos - len(item[0]) - 4, item[0], mode) |
|
139 |
+ else: |
|
140 |
+ self.window.addstr(self.text_height + 1, xpos - len(item[0]) - 4, item[0], mode) |
|
139 | 141 |
xpos = xpos - len(item[0]) - 4 |
140 | 142 |
|
143 |
+ if self.info: |
|
144 |
+ self.window.addstr(self.text_height+1, 5, self.info) |
|
145 |
+ |
|
146 |
+ |
|
141 | 147 |
self.render_scroll_bar() |
142 | 148 |
|
143 | 149 |
self.window.refresh() |
... | ... |
@@ -160,7 +213,6 @@ class TextPane(Action): |
160 | 160 |
if key in [curses.KEY_ENTER, ord('\n')]: |
161 | 161 |
self.hide() |
162 | 162 |
return self.menu_items[self.menu_position][1]() |
163 |
- |
|
164 | 163 |
if key == curses.KEY_UP: |
165 | 164 |
self.navigate(-1) |
166 | 165 |
elif key == curses.KEY_DOWN: |
167 | 166 |
old mode 100644 |
168 | 167 |
new mode 100755 |
... | ... |
@@ -10,8 +10,9 @@ from action import Action |
10 | 10 |
|
11 | 11 |
class Window(Action): |
12 | 12 |
|
13 |
- def __init__(self, height, width, maxy, maxx, title, can_go_back, action_panel = None): |
|
13 |
+ def __init__(self, height, width, maxy, maxx, title, can_go_back, action_panel = None, items = [], menu_helper = None, position = 0, tab_enabled = True, can_go_next = False, read_text=False): |
|
14 | 14 |
self.can_go_back = can_go_back |
15 |
+ self.can_go_next = can_go_next |
|
15 | 16 |
self.height = height |
16 | 17 |
self.width = width; |
17 | 18 |
self.y = (maxy - height) / 2 |
... | ... |
@@ -22,9 +23,39 @@ class Window(Action): |
22 | 22 |
self.contentwin.bkgd(' ', curses.color_pair(2)) #Default Window color |
23 | 23 |
self.contentwin.erase() |
24 | 24 |
self.contentwin.box() |
25 |
+ self.tab_enabled=tab_enabled |
|
26 |
+ self.read_text=read_text |
|
27 |
+ |
|
28 |
+ self.position = position |
|
29 |
+ self.items = items |
|
30 |
+ self.menu_helper = menu_helper |
|
25 | 31 |
self.contentwin.addstr(0, (width - 1 - len(title)) / 2 , title)# |
32 |
+ newy = 5; |
|
33 |
+ |
|
26 | 34 |
if self.can_go_back: |
27 | 35 |
self.contentwin.addstr(height - 3, 5, '<Go Back>') |
36 |
+ if self.can_go_next and self.can_go_back: |
|
37 |
+ self.update_next_item() |
|
38 |
+ |
|
39 |
+ self.dist = 0 |
|
40 |
+ |
|
41 |
+ if items: |
|
42 |
+ #To select items, we need to identify up left right keys |
|
43 |
+ |
|
44 |
+ self.dist=self.width-11 |
|
45 |
+ self.dist-=len('<Go Back>') |
|
46 |
+ count=0 |
|
47 |
+ for item in self.items: |
|
48 |
+ self.dist-=len(item[0]) |
|
49 |
+ count+=1 |
|
50 |
+ self.dist=self.dist/count |
|
51 |
+ self.contentwin.keypad(1) |
|
52 |
+ newy += len('<Go Back>') |
|
53 |
+ newy += self.dist |
|
54 |
+ for item in self.items: |
|
55 |
+ self.contentwin.addstr(height - 3, newy, item[0]) |
|
56 |
+ newy += len(item[0]) |
|
57 |
+ newy += self.dist |
|
28 | 58 |
|
29 | 59 |
self.textwin = curses.newwin(height - 5, width - 5) |
30 | 60 |
self.textwin.bkgd(' ', curses.color_pair(2)) #Default Window color |
... | ... |
@@ -37,51 +68,213 @@ class Window(Action): |
37 | 37 |
self.shadowpanel = curses.panel.new_panel(self.shadowwin) |
38 | 38 |
|
39 | 39 |
self.action_panel = action_panel |
40 |
- |
|
40 |
+ self.refresh(0, True) |
|
41 | 41 |
self.hide_window() |
42 | 42 |
|
43 |
+ def update_next_item(self): |
|
44 |
+ self.position=1 |
|
45 |
+ self.items.append(('<Next>', self.next_function, False)) |
|
46 |
+ self.tab_enabled=False |
|
47 |
+ |
|
48 |
+ |
|
49 |
+ def next_function(self, params): |
|
50 |
+ return ActionResult(True, None) |
|
51 |
+ |
|
43 | 52 |
def set_action_panel(self, action_panel): |
44 | 53 |
self.action_panel = action_panel |
45 | 54 |
|
55 |
+ def update_menu(self,action_result): |
|
56 |
+ if action_result.result and 'goNext' in action_result.result and action_result.result['goNext']: |
|
57 |
+ return ActionResult(True, None) |
|
58 |
+ if self.position == 0: |
|
59 |
+ self.contentwin.addstr(self.height - 3, 5, '<Go Back>') |
|
60 |
+ self.contentwin.refresh() |
|
61 |
+ self.hide_window() |
|
62 |
+ self.action_panel.hide() |
|
63 |
+ return ActionResult(False, None) |
|
64 |
+ else: |
|
65 |
+ if (action_result.result != None and 'diskIndex' in action_result.result): |
|
66 |
+ params = action_result.result['diskIndex'] |
|
67 |
+ if self.menu_helper: |
|
68 |
+ self.menu_helper(params) |
|
69 |
+ |
|
70 |
+ result = self.items[self.position-1][1](None) |
|
71 |
+ if result.success: |
|
72 |
+ self.hide_window() |
|
73 |
+ self.action_panel.hide() |
|
74 |
+ return result |
|
75 |
+ else: |
|
76 |
+ if 'goBack' in result.result and result.result['goBack']: |
|
77 |
+ self.contentwin.refresh() |
|
78 |
+ self.hide_window() |
|
79 |
+ self.action_panel.hide() |
|
80 |
+ return ActionResult(False, None) |
|
81 |
+ |
|
46 | 82 |
def do_action(self): |
47 | 83 |
self.show_window() |
84 |
+ if self.tab_enabled: |
|
85 |
+ self.refresh(0, False) |
|
86 |
+ else: |
|
87 |
+ self.refresh(0, True) |
|
48 | 88 |
action_result = self.action_panel.do_action() |
49 | 89 |
|
50 | 90 |
if action_result.success: |
91 |
+ if action_result.result and 'goNext' in action_result.result and action_result.result['goNext']: |
|
92 |
+ return ActionResult(True, None) |
|
93 |
+ if self.position!=0: #saving the disk index |
|
94 |
+ self.items[self.position-1][1](None) |
|
95 |
+ if self.items: |
|
96 |
+ return self.update_menu(action_result) |
|
51 | 97 |
self.hide_window() |
52 | 98 |
return action_result |
53 | 99 |
else: |
100 |
+ if not self.tab_enabled and action_result.result !=None and 'direction' in action_result.result: |
|
101 |
+ self.refresh(action_result.result['direction'], True) |
|
54 | 102 |
if (action_result.result != None and 'goBack' in action_result.result and action_result.result['goBack']): |
55 | 103 |
self.hide_window() |
56 | 104 |
self.action_panel.hide() |
57 | 105 |
return action_result |
58 | 106 |
else: |
59 | 107 |
#highlight the GoBack and keep going |
60 |
- self.contentwin.addstr(self.height - 3, 5, '<Go Back>', curses.color_pair(3)) |
|
61 |
- self.contentwin.refresh() |
|
108 |
+ self.refresh(0, True) |
|
62 | 109 |
|
63 | 110 |
while action_result.success == False: |
64 |
- key = self.contentwin.getch() |
|
65 |
- if key in [curses.KEY_ENTER, ord('\n')]: |
|
66 |
- #remove highlight from Go Back |
|
67 |
- self.contentwin.addstr(self.height - 3, 5, '<Go Back>') |
|
68 |
- self.contentwin.refresh() |
|
69 |
- self.hide_window() |
|
70 |
- self.action_panel.hide() |
|
71 |
- return ActionResult(False, None) |
|
72 |
- elif key in [ord('\t')]: |
|
73 |
- #remove highlight from Go Back |
|
74 |
- self.contentwin.addstr(self.height - 3, 5, '<Go Back>') |
|
75 |
- self.contentwin.refresh() |
|
76 |
- # go do the action inside the panel |
|
77 |
- action_result = self.action_panel.do_action() |
|
111 |
+ if self.read_text: |
|
112 |
+ is_go_back= self.position==0 |
|
113 |
+ action_result = self.action_panel.do_action(returned=True, go_back=is_go_back) |
|
78 | 114 |
if action_result.success: |
115 |
+ if self.items: |
|
116 |
+ return self.update_menu(action_result) |
|
79 | 117 |
self.hide_window() |
80 | 118 |
return action_result |
81 | 119 |
else: |
82 |
- #highlight the GoBack and keep going |
|
83 |
- self.contentwin.addstr(self.height - 3, 5, '<Go Back>', curses.color_pair(3)) |
|
84 |
- self.contentwin.refresh() |
|
120 |
+ if (action_result.result != None and 'goBack' in action_result.result and action_result.result['goBack']): |
|
121 |
+ self.hide_window() |
|
122 |
+ self.action_panel.hide() |
|
123 |
+ return action_result |
|
124 |
+ if action_result.result and 'direction' in action_result.result: |
|
125 |
+ self.refresh(action_result.result['direction'], True) |
|
126 |
+ else: |
|
127 |
+ key = self.contentwin.getch() |
|
128 |
+ if key in [curses.KEY_ENTER, ord('\n')]: |
|
129 |
+ #remove highlight from Go Back |
|
130 |
+ if self.position == 0: |
|
131 |
+ self.contentwin.addstr(self.height - 3, 5, '<Go Back>') |
|
132 |
+ self.contentwin.refresh() |
|
133 |
+ self.hide_window() |
|
134 |
+ self.action_panel.hide() |
|
135 |
+ return ActionResult(False, None) |
|
136 |
+ else: |
|
137 |
+ if (action_result.result != None and 'diskIndex' in action_result.result): |
|
138 |
+ params = action_result.result['diskIndex'] |
|
139 |
+ if self.menu_helper: |
|
140 |
+ self.menu_helper(params) |
|
141 |
+ result = self.items[self.position-1][1](None) |
|
142 |
+ if result.success: |
|
143 |
+ self.hide_window() |
|
144 |
+ self.action_panel.hide() |
|
145 |
+ return result |
|
146 |
+ else: |
|
147 |
+ if 'goBack' in result.result and result.result['goBack']: |
|
148 |
+ self.contentwin.refresh() |
|
149 |
+ self.hide_window() |
|
150 |
+ self.action_panel.hide() |
|
151 |
+ return ActionResult(False, None) |
|
152 |
+ elif key in [ord('\t')]: |
|
153 |
+ if not self.tab_enabled: |
|
154 |
+ continue; |
|
155 |
+ #remove highlight from Go Back |
|
156 |
+ self.refresh(0, False) |
|
157 |
+ # go do the action inside the panel |
|
158 |
+ action_result = self.action_panel.do_action() |
|
159 |
+ if action_result.success: |
|
160 |
+ self.hide_window() |
|
161 |
+ return action_result |
|
162 |
+ else: |
|
163 |
+ #highlight the GoBack and keep going |
|
164 |
+ self.refresh(0, True) |
|
165 |
+ elif key == curses.KEY_UP or key == curses.KEY_LEFT: |
|
166 |
+ if key == curses.KEY_UP and self.tab_enabled==False: |
|
167 |
+ self.action_panel.navigate(-1) |
|
168 |
+ action_result = self.action_panel.do_action() |
|
169 |
+ if action_result.success: |
|
170 |
+ if self.items: |
|
171 |
+ return self.update_menu(action_result) |
|
172 |
+ self.hide_window() |
|
173 |
+ return action_result |
|
174 |
+ else: |
|
175 |
+ if 'direction' in action_result.result: |
|
176 |
+ #highlight the GoBack and keep going |
|
177 |
+ self.refresh(action_result.result['direction'], True) |
|
178 |
+ else: |
|
179 |
+ self.refresh(-1, True) |
|
180 |
+ |
|
181 |
+ elif key == curses.KEY_DOWN or key == curses.KEY_RIGHT: |
|
182 |
+ if key == curses.KEY_DOWN and self.tab_enabled==False: |
|
183 |
+ self.action_panel.navigate(1) |
|
184 |
+ action_result = self.action_panel.do_action() |
|
185 |
+ if action_result.success: |
|
186 |
+ if self.items: |
|
187 |
+ return self.update_menu(action_result) |
|
188 |
+ self.hide_window() |
|
189 |
+ return action_result |
|
190 |
+ else: |
|
191 |
+ if 'direction' in action_result.result: |
|
192 |
+ #highlight the GoBack and keep going |
|
193 |
+ self.refresh(action_result.result['direction'], True) |
|
194 |
+ else: |
|
195 |
+ self.refresh(1, True) |
|
196 |
+ |
|
197 |
+ |
|
198 |
+ def refresh(self, n, select): |
|
199 |
+ if not self.can_go_back: |
|
200 |
+ return |
|
201 |
+ self.position += n |
|
202 |
+ if self.position < 0: |
|
203 |
+ self.position = 0 |
|
204 |
+ elif self.items and self.position > len(self.items): #add 1 for the <go back> |
|
205 |
+ self.position = len(self.items) |
|
206 |
+ |
|
207 |
+ if not self.items and not self.can_go_next: |
|
208 |
+ self.position = 0 |
|
209 |
+ #add the highlight |
|
210 |
+ newy = 5; |
|
211 |
+ if self.position == 0: #go back |
|
212 |
+ if select: |
|
213 |
+ self.contentwin.addstr(self.height - 3, 5, '<Go Back>', curses.color_pair(3)) |
|
214 |
+ elif self.items: #show user the last selected items |
|
215 |
+ self.contentwin.addstr(self.height - 3, 5, '<Go Back>', curses.color_pair(1)) |
|
216 |
+ else: #if Go back is the only one shown, do not highlight at all |
|
217 |
+ self.contentwin.addstr(self.height - 3, 5, '<Go Back>') |
|
218 |
+ |
|
219 |
+ newy += len('<Go Back>') |
|
220 |
+ newy += self.dist |
|
221 |
+ |
|
222 |
+ if self.items: |
|
223 |
+ for item in self.items: |
|
224 |
+ self.contentwin.addstr(self.height - 3, newy, item[0]) |
|
225 |
+ newy += len(item[0]) |
|
226 |
+ newy += self.dist |
|
227 |
+ |
|
228 |
+ else: |
|
229 |
+ self.contentwin.addstr(self.height - 3, 5, '<Go Back>') |
|
230 |
+ newy += len('<Go Back>') |
|
231 |
+ newy += self.dist |
|
232 |
+ index = 1 |
|
233 |
+ for item in self.items: |
|
234 |
+ if index == self.position: |
|
235 |
+ if select: |
|
236 |
+ self.contentwin.addstr(self.height - 3, newy, item[0], curses.color_pair(3)) |
|
237 |
+ else: |
|
238 |
+ self.contentwin.addstr(self.height - 3, newy, item[0], curses.color_pair(1)) |
|
239 |
+ |
|
240 |
+ else: |
|
241 |
+ self.contentwin.addstr(self.height - 3, newy, item[0]) |
|
242 |
+ newy += len(item[0]) |
|
243 |
+ newy += self.dist |
|
244 |
+ index += 1 |
|
245 |
+ |
|
246 |
+ self.contentwin.refresh() |
|
85 | 247 |
|
86 | 248 |
def show_window(self): |
87 | 249 |
y = self.y |
... | ... |
@@ -101,6 +294,9 @@ class Window(Action): |
101 | 101 |
curses.panel.update_panels() |
102 | 102 |
curses.doupdate() |
103 | 103 |
|
104 |
+ if self.can_go_next: |
|
105 |
+ self.position = 1 |
|
106 |
+ |
|
104 | 107 |
def hide_window(self): |
105 | 108 |
self.shadowpanel.hide() |
106 | 109 |
self.contentpanel.hide() |
107 | 110 |
old mode 100644 |
108 | 111 |
new mode 100755 |
... | ... |
@@ -9,7 +9,7 @@ from window import Window |
9 | 9 |
from readtext import ReadText |
10 | 10 |
|
11 | 11 |
class WindowStringReader(object): |
12 |
- def __init__(self, maxy, maxx, height, width, field, confirmation_err_msg, echo_char, accepted_chars, validation_fn, conversion_fn, title, display_string, inputy, install_config, default_string = None): |
|
12 |
+ def __init__(self, maxy, maxx, height, width, field, confirmation_err_msg, echo_char, accepted_chars, validation_fn, conversion_fn, title, display_string, inputy, install_config, default_string = None, tab_enabled=False): |
|
13 | 13 |
self.title = title |
14 | 14 |
self.display_string = display_string |
15 | 15 |
self.install_config = install_config |
... | ... |
@@ -22,9 +22,11 @@ class WindowStringReader(object): |
22 | 22 |
|
23 | 23 |
self.startx = (self.maxx - self.width) / 2 |
24 | 24 |
self.starty = (self.maxy - self.height) / 2 |
25 |
+ self.tab_enabled=False |
|
26 |
+ self.can_go_next=True |
|
25 | 27 |
|
26 |
- self.window = Window(self.height, self.width, self.maxy, self.maxx, self.title, True) |
|
27 |
- self.read_text = ReadText(maxy, maxx, self.window.content_window(), self.inputy, install_config, field, confirmation_err_msg, echo_char, accepted_chars, validation_fn, conversion_fn, default_string) |
|
28 |
+ self.window = Window(self.height, self.width, self.maxy, self.maxx, self.title, True, items=[], tab_enabled=self.tab_enabled, position=1, can_go_next=self.can_go_next, read_text=self.can_go_next) |
|
29 |
+ self.read_text = ReadText(maxy, maxx, self.window.content_window(), self.inputy, install_config, field, confirmation_err_msg, echo_char, accepted_chars, validation_fn, conversion_fn, default_string, tab_enabled=self.tab_enabled) |
|
28 | 30 |
self.window.set_action_panel(self.read_text) |
29 | 31 |
self.window.addstr(0, 0, self.display_string) |
30 | 32 |
|