Coverage for src/lib/executor.py: 100.00%
153 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 10:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 10:53 +0000
1#!/usr/bin/env python3
3# Standard libraries
4from errno import ENOENT
5from getpass import getuser
6from os import chdir, environ
7from time import sleep
8from typing import List, Optional
10# Components
11from ..engines.engine import Engine
12from ..package.bundle import Bundle
13from ..prints.colors import Colors
14from ..system.platform import Platform
16# Executor
17class Executor:
19 # Key constants (arrows)
20 KEY_UP: str = '\033[A'
21 KEY_DOWN: str = '\033[B'
22 KEY_LEFT: str = '\033[D'
23 KEY_RIGHT: str = '\033[C'
25 # Key constants (actions)
26 KEY_BACKSPACE: str = '\b'
27 KEY_DELETE: str = '\033[3~'
28 KEY_END: str = '\033[F'
29 KEY_END_2: str = '\033[4~'
30 KEY_ENTER: str = '\r'
31 KEY_ESC: str = '\033'
32 KEY_HOME: str = '\033[H'
33 KEY_HOME_2: str = '\033[1~'
34 KEY_INSERT: str = '\033[2~'
35 KEY_PAGEUP: str = '\033[5~'
36 KEY_PAGEDOWN: str = '\033[6~'
37 KEY_MACRO: str = '\033[M'
38 KEY_PAUSE: str = '\033[P'
39 KEY_SPACE: str = ' '
40 KEY_TAB: str = '\t'
42 # Key constants (Ctrl+?)
43 KEY_CTRL_A: str = '\x01'
44 KEY_CTRL_B: str = '\x02'
45 KEY_CTRL_C: str = '\x03'
46 KEY_CTRL_D: str = '\x04'
47 KEY_CTRL_E: str = '\x05'
48 KEY_CTRL_F: str = '\x06'
49 KEY_CTRL_G: str = '\x07'
50 KEY_CTRL_H: str = '\x08'
51 KEY_CTRL_I: str = '\x09'
52 KEY_CTRL_J: str = '\x0A'
53 KEY_CTRL_K: str = '\x0B'
54 KEY_CTRL_L: str = '\x0C'
55 KEY_CTRL_M: str = '\x0D'
56 KEY_CTRL_N: str = '\x0E'
57 KEY_CTRL_O: str = '\x0F'
58 KEY_CTRL_P: str = '\x10'
59 KEY_CTRL_Q: str = '\x11'
60 KEY_CTRL_R: str = '\x12'
61 KEY_CTRL_S: str = '\x13'
62 KEY_CTRL_T: str = '\x14'
63 KEY_CTRL_U: str = '\x15'
64 KEY_CTRL_V: str = '\x16'
65 KEY_CTRL_W: str = '\x17'
66 KEY_CTRL_X: str = '\x18'
67 KEY_CTRL_Y: str = '\x19'
68 KEY_CTRL_Z: str = '\x1A'
70 # Key constants (F?)
71 KEY_F1: str = '\033[[A'
72 KEY_F2: str = '\033[[B'
73 KEY_F3: str = '\033[[C'
74 KEY_F4: str = '\033[[D'
75 KEY_F5: str = '\033[[E'
76 KEY_F6: str = '\033[17~'
77 KEY_F7: str = '\033[18~'
78 KEY_F8: str = '\033[19~'
79 KEY_F9: str = '\033[20~'
80 KEY_F10: str = '\033[21~'
81 KEY_F11: str = '\033[23~'
82 KEY_F12: str = '\033[24~'
84 # Delays
85 DELAY_INIT: float = 1.0
86 DELAY_PRESS: float = 0.5
87 DELAY_PROMPT: float = 1.0
89 # Labels
90 LABEL_HOST: str = 'preview'
91 LABEL_TOOL: str = 'executor'
93 # Members
94 __delay_init: float
95 __delay_press: float
96 __delay_prompt: float
97 __engine: Optional[Engine]
98 __host: str = environ[Bundle.ENV_HOST] if Bundle.ENV_HOST in environ else LABEL_HOST
99 __masks: List[str] = []
100 __tool: str = environ[Bundle.ENV_TOOL] if Bundle.ENV_TOOL in environ else LABEL_TOOL
102 # Constructor, pylint: disable=too-many-arguments,too-many-positional-arguments
103 def __init__(
104 self,
105 command: str = '',
106 delay_init: float = DELAY_INIT,
107 delay_press: float = DELAY_PRESS,
108 delay_prompt: float = DELAY_PROMPT,
109 hold_prompt: bool = False,
110 workdir: str = '',
111 ) -> None:
113 # Prepare delays
114 self.__delay_init = float(delay_init)
115 self.__delay_press = float(delay_press)
116 self.__delay_prompt = float(delay_prompt)
118 # Prepare workdir
119 if workdir:
120 self.__prompt(f'cd {workdir}', hold_prompt=hold_prompt)
121 chdir(workdir)
123 # Prepare members
124 self.__engine = None
126 # Prepare command
127 self.__prompt(command, hold_prompt=hold_prompt)
128 if command:
130 # Spawn command
131 self.__engine = Engine(command)
133 # Delay executor initialization
134 if self.__delay_init > 0.0:
135 Executor.sleep(self.__delay_init)
136 self.read()
138 # Configure
139 @staticmethod
140 def configure(
141 host: str = LABEL_HOST,
142 tool: str = LABEL_TOOL,
143 masks: Optional[List[str]] = None,
144 strips: Optional[List[str]] = None,
145 ) -> None:
147 # Prepare host
148 Executor.__host = host
150 # Deprecate strips
151 if strips: # pragma: no cover
152 raise SystemError('Parameter "strips" is deprecated, use "masks" instead')
154 # Prepare masks
155 if masks:
156 Executor.__masks = masks
157 else:
158 Executor.__masks = []
160 # Prepare tool
161 Executor.__tool = tool
163 # Prepare colors
164 Colors.prepare()
166 # Control key, pylint: disable=no-self-use
167 def __control_key(self, key: str) -> bytes:
169 # Acquire key value
170 key = key.lower()
171 try:
172 value = ord(key)
173 except TypeError:
174 value = 0
176 # Handle alphabetical key
177 if 97 <= value <= 122:
178 value = value - ord('a') + 1
179 return bytes([value])
181 # List specific keys
182 mappings = {
183 '@': 0,
184 '`': 0,
185 '[': 27,
186 '{': 27,
187 '\\': 28,
188 '|': 28,
189 ']': 29,
190 '}': 29,
191 '^': 30,
192 '~': 30,
193 '_': 31,
194 '?': 127
195 }
197 # Handle specific keys
198 if key in mappings:
199 return bytes([mappings[key]])
201 # Unknown fallback
202 return bytes()
204 # Prompt
205 def __prompt(self, command: str, hold_prompt: bool = False) -> None:
207 # Display prompt
208 print(
209 f'{Colors.GREEN_THIN}{getuser()}{Colors.RESET}'
210 f'@{Colors.RED_THIN}{self.__host}{Colors.RESET}'
211 f':{Colors.YELLOW_THIN}~/{self.__tool}{Colors.RESET}$ ', end='')
212 Platform.flush()
214 # Delay prompt
215 Executor.sleep(self.__delay_prompt)
217 # Display command
218 if command:
219 print(f'{command} ', end='')
220 Platform.flush()
221 Executor.sleep(self.__delay_prompt)
222 print(' ')
223 Platform.flush()
225 # Return prompt
226 elif not hold_prompt:
227 print(' ')
228 Platform.flush()
230 # Press
231 def press(self, key: str, control: bool = False) -> 'Executor':
233 # Execution check
234 if not self.__engine:
235 return self
237 # Delay press
238 Executor.sleep(self.__delay_press)
240 # Press Ctrl+key
241 if control:
242 self.__engine.send(self.__control_key(key))
244 # Press key
245 else:
246 self.__engine.send(key)
248 # Result
249 return self
251 # Read
252 def read(self) -> 'Executor':
254 # Execution check
255 if not self.__engine:
256 return self
258 # Read stream
259 self.__engine.read(Executor.__masks)
261 # Result
262 return self
264 # Wait
265 def wait(self, delay: float) -> 'Executor':
267 # Delay execution
268 Executor.sleep(delay)
270 # Result
271 return self
273 # Finish
274 def finish(self, force: bool = False) -> int:
276 # Execution check
277 if not self.__engine:
278 return ENOENT
280 # Read and wait execution
281 if not force:
282 try:
283 while self.__engine.isalive():
284 self.read()
285 except KeyboardInterrupt:
286 pass
288 # Terminate process
289 self.__engine.terminate(force=force)
291 # Result
292 return self.__engine.status()
294 # Sleep
295 @staticmethod
296 def sleep(delay: float) -> None:
298 # Delay execution
299 sleep(delay)