cockpit 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. #!/usr/bin/env python
  2. import os
  3. import shlex
  4. import collections
  5. import datetime
  6. import logging
  7. import random
  8. import argparse
  9. import subprocess
  10. import curses
  11. class Configuration(object):
  12. def __init__(self, source, destination):
  13. self.source = os.path.abspath(source)
  14. self.destination = os.path.abspath(destination)
  15. class LineList(object):
  16. def __init__(self, window):
  17. self.window = window
  18. self.height, self.width = window.getmaxyx()
  19. self.current = 0
  20. self.lines = []
  21. def redraw(self):
  22. for y in range(len(self.lines)):
  23. line, attr = self.lines[y]
  24. self.window.addnstr(y, 0, line, self.width, attr)
  25. self.window.clrtoeol()
  26. self.window.refresh()
  27. def add_line(self, s, attr=0):
  28. self.lines.append((s, attr))
  29. if len(self.lines) > self.height:
  30. self.lines = self.lines[1:]
  31. self.redraw()
  32. def update_last(self, line=None, attr=None):
  33. if not self.lines:
  34. return
  35. old_line, old_attr = self.lines[-1]
  36. self.lines[-1] = (line or old_line, attr or old_attr)
  37. self.redraw()
  38. class Colors(object):
  39. NORMAL = 1
  40. SUCCESS = 2
  41. WARN = 3
  42. ERROR = 4
  43. STATUS_BAR = 5
  44. HIGHLIGHT = 6
  45. def __init__(self):
  46. curses.init_pair(Colors.NORMAL, curses.COLOR_WHITE, curses.COLOR_BLACK)
  47. curses.init_pair(Colors.SUCCESS, curses.COLOR_GREEN, curses.COLOR_BLACK)
  48. curses.init_pair(Colors.WARN, curses.COLOR_YELLOW, curses.COLOR_BLACK)
  49. curses.init_pair(Colors.ERROR, curses.COLOR_RED, curses.COLOR_BLACK)
  50. curses.init_pair(Colors.STATUS_BAR, curses.COLOR_BLACK, curses.COLOR_YELLOW)
  51. curses.init_pair(Colors.HIGHLIGHT, curses.COLOR_WHITE, curses.COLOR_BLACK)
  52. self.attrs = {
  53. Colors.NORMAL: curses.A_NORMAL,
  54. Colors.SUCCESS: curses.A_NORMAL,
  55. Colors.WARN: curses.A_BOLD,
  56. Colors.ERROR: curses.A_BOLD,
  57. Colors.STATUS_BAR: curses.A_NORMAL,
  58. Colors.HIGHLIGHT: curses.A_BOLD,
  59. }
  60. def get(self, code):
  61. return curses.color_pair(code) | self.attrs[code]
  62. class StatusBar(object):
  63. def __init__(self, window, colors):
  64. self.window = window
  65. self.c = colors
  66. def update(self, s):
  67. self.window.bkgd(' ', self.c.get(Colors.STATUS_BAR))
  68. self.window.addstr(s, self.c.get(Colors.STATUS_BAR))
  69. class LogList(LineList):
  70. def __init__(self, window, colors):
  71. self.line_list = LineList(window)
  72. self.c = colors
  73. def _log_time(self, s, attr):
  74. timestamp = datetime.datetime.now().strftime('%H:%M:%S')
  75. self.line_list.add_line('[{}] {}'.format(timestamp, s), attr)
  76. def info(self, s):
  77. self._log_time(s, self.c.get(Colors.NORMAL))
  78. def success(self, s):
  79. self._log_time(s, self.c.get(Colors.SUCCESS))
  80. def warn(self, s):
  81. self._log_time(s, self.c.get(Colors.WARN))
  82. def error(self, s):
  83. self._log_time(s, self.c.get(Colors.ERROR))
  84. class CommandList(LineList):
  85. def __init__(self, window, colors):
  86. self.line_list = LineList(window)
  87. self.normal = colors.get(Colors.NORMAL)
  88. self.highlight = colors.get(Colors.HIGHLIGHT)
  89. self.current = ''
  90. def add_character(self, c):
  91. self.current += c
  92. self.line_list.update_last(line='> {}'.format(self.current))
  93. def backspace(self):
  94. if self.current:
  95. self.current = self.current[:len(self.current) - 1]
  96. self.add_character('')
  97. def set_actions(self, actions):
  98. self.line_list.update_last(attr=self.normal)
  99. self.current = ''
  100. message = ' | '.join(('[{}] {}'.format(a.key, a.note) for a in actions.values()))
  101. self.line_list.add_line(message, self.highlight)
  102. class Action(object):
  103. def __init__(self, key, note, func, next_state):
  104. self.key = key
  105. self.note = note
  106. self.run = func
  107. self.next_state = next_state
  108. def __repr__(self):
  109. return '<Action:key={}>'.format(self.key)
  110. class StateMachine(object):
  111. START = 0
  112. QUIT = 1
  113. SYNC = 2
  114. CLEAN = 3
  115. FLATCORRECT = 4
  116. OPTIMIZE = 5
  117. def __init__(self):
  118. self.current = StateMachine.START
  119. self.transitions = {
  120. StateMachine.START: collections.OrderedDict(),
  121. StateMachine.CLEAN: collections.OrderedDict(),
  122. StateMachine.FLATCORRECT: collections.OrderedDict(),
  123. StateMachine.QUIT: collections.OrderedDict(),
  124. StateMachine.SYNC: collections.OrderedDict(),
  125. StateMachine.OPTIMIZE: collections.OrderedDict(),
  126. }
  127. def add_action(self, from_state, action):
  128. self.transitions[from_state][action.key] = action
  129. def transition(self, action):
  130. self.current = self.transitions[self.current][action.key].next_state
  131. @property
  132. def actions(self):
  133. return self.transitions[self.current]
  134. def is_valid_key(self, key):
  135. return key in (k for k in self.transitions[self.current].keys())
  136. class Application(object):
  137. def __init__(self, config):
  138. self.config = config
  139. self.running = True
  140. def run_command(self, cmd):
  141. try:
  142. p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  143. stdout, stderr = p.communicate()
  144. def output_lines(pipe, log_func):
  145. for line in pipe.split(os.linesep):
  146. if line:
  147. log_func(line)
  148. output_lines(stdout, self.log.info)
  149. output_lines(stderr, self.log.error)
  150. if p.returncode == 0:
  151. self.log.success("done")
  152. return True
  153. self.log.error("Command returned {}".format(p.returncode))
  154. return False
  155. except OSError as e:
  156. self.log.error("{}".format(e))
  157. return False
  158. def on_quit(self):
  159. self.running = False
  160. return True
  161. def on_sync(self):
  162. self.log.info("Syncing data ...")
  163. cmd = 'bash sync.sh {} {}'.format(self.config.source, self.config.destination)
  164. return self.run_command(cmd)
  165. def on_clean(self):
  166. self.log.info("Cleaning {}".format(self.config.destination))
  167. return True
  168. def on_flat_correct(self):
  169. num = 0
  170. data = dict(p=self.config.destination, num=num, step=123))
  171. cmd = ('tofu flatcorrect --verbose'
  172. ' --reduction-mode median'
  173. ' --projections "{p}/foo*.edf"'
  174. ' --darks {p}/darkend0000.edf'
  175. ' --flats {p}/ref*_0000.edf'
  176. ' --flats2 {p}/ref*_{num}.edf'
  177. ' --output {p}/fc/fc-%04i.tif'
  178. ' --number {num}'
  179. ' --step {step}'
  180. ' --absorptivity'
  181. ' --fix-nan-and-inf'.format(**data))
  182. return self.run_command(cmd)
  183. def on_optimize(self):
  184. self.log.info("Optimizing ...")
  185. return True
  186. def do_nothing(self):
  187. return True
  188. def _run(self, screen):
  189. curses.curs_set(False)
  190. height, width = screen.getmaxyx()
  191. colors = Colors()
  192. top_pane = screen.subwin(1, width, 0, 0)
  193. bottom_pane = screen.subwin(height - 1, width, 1, 0)
  194. left_pane = bottom_pane.subwin(height - 1, width / 2, 1, 0)
  195. right_pane = bottom_pane.subwin(height - 1, width / 2, 1, width / 2)
  196. status_bar = StatusBar(top_pane, colors)
  197. status_bar.update('Cockpit')
  198. cmd_window = CommandList(left_pane, colors)
  199. machine = StateMachine()
  200. quit = Action('q', 'Quit', self.on_quit, machine.QUIT)
  201. sync = Action('s', 'Sync data', self.on_sync, machine.SYNC)
  202. flatcorrect = Action('f', 'Flat correct', self.on_flat_correct, machine.FLATCORRECT)
  203. clean = Action('c', 'Clean', self.on_clean, machine.CLEAN)
  204. optimize = Action('o', 'Optimize', self.on_optimize, machine.OPTIMIZE)
  205. machine.add_action(machine.START, sync)
  206. machine.add_action(machine.START, clean)
  207. machine.add_action(machine.START, quit)
  208. machine.add_action(machine.SYNC, flatcorrect)
  209. machine.add_action(machine.SYNC, clean)
  210. machine.add_action(machine.SYNC, quit)
  211. machine.add_action(machine.FLATCORRECT, optimize)
  212. machine.add_action(machine.FLATCORRECT, quit)
  213. machine.add_action(machine.OPTIMIZE, sync)
  214. machine.add_action(machine.OPTIMIZE, clean)
  215. machine.add_action(machine.OPTIMIZE, quit)
  216. machine.add_action(machine.CLEAN, sync)
  217. machine.add_action(machine.CLEAN, quit)
  218. cmd_window.set_actions(machine.actions)
  219. self.log = LogList(right_pane, colors)
  220. self.log.info('Source dir set to {}'.format(self.config.source))
  221. self.log.info('Destination dir set to {}'.format(self.config.destination))
  222. while self.running:
  223. ci = screen.getch()
  224. cc = chr(ci) if ci < 256 else ''
  225. if machine.is_valid_key(cc):
  226. cmd_window.add_character(cc)
  227. action = machine.actions[cc]
  228. if action.run():
  229. machine.transition(action)
  230. cmd_window.set_actions(machine.actions)
  231. elif ci == curses.KEY_BACKSPACE:
  232. cmd_window.backspace()
  233. screen.refresh()
  234. def run(self):
  235. curses.wrapper(self._run)
  236. if __name__ == '__main__':
  237. parser = argparse.ArgumentParser()
  238. parser.add_argument('--source', help='Source directory', default='.', metavar='DIR')
  239. parser.add_argument('--destination', help='Destination directory', default='.', metavar='DIR')
  240. args = parser.parse_args()
  241. config = Configuration(args.source, args.destination)
  242. app = Application(config)
  243. app.run()