Browse Source

Initial commit

Matthias Vogelgesang 7 years ago
commit
7fc4b0c5a0
2 changed files with 339 additions and 0 deletions
  1. 336 0
      cockpit
  2. 3 0
      sync.sh

+ 336 - 0
cockpit

@@ -0,0 +1,336 @@
+#!/usr/bin/env python
+
+import os
+import shlex
+import collections
+import datetime
+import logging
+import random
+import argparse
+import subprocess
+import curses
+
+
+class Configuration(object):
+
+    def __init__(self, source, destination):
+        self.source = os.path.abspath(source)
+        self.destination = os.path.abspath(destination)
+
+
+class LineList(object):
+
+    def __init__(self, window):
+        self.window = window
+        self.height, self.width = window.getmaxyx()
+        self.current = 0
+        self.lines = []
+
+    def redraw(self):
+        for y in range(len(self.lines)):
+            line, attr = self.lines[y]
+            self.window.addnstr(y, 0, line, self.width, attr)
+            self.window.clrtoeol()
+
+        self.window.refresh()
+
+    def add_line(self, s, attr=0):
+        self.lines.append((s, attr))
+
+        if len(self.lines) > self.height:
+            self.lines = self.lines[1:]
+
+        self.redraw()
+
+    def update_last(self, line=None, attr=None):
+        if not self.lines:
+            return
+
+        old_line, old_attr = self.lines[-1]
+        self.lines[-1] = (line or old_line, attr or old_attr)
+        self.redraw()
+
+
+class Colors(object):
+
+    NORMAL = 1
+    SUCCESS = 2
+    WARN = 3
+    ERROR = 4
+    STATUS_BAR = 5
+    HIGHLIGHT = 6
+
+    def __init__(self):
+        curses.init_pair(Colors.NORMAL, curses.COLOR_WHITE, curses.COLOR_BLACK)
+        curses.init_pair(Colors.SUCCESS, curses.COLOR_GREEN, curses.COLOR_BLACK)
+        curses.init_pair(Colors.WARN, curses.COLOR_YELLOW, curses.COLOR_BLACK)
+        curses.init_pair(Colors.ERROR, curses.COLOR_RED, curses.COLOR_BLACK)
+        curses.init_pair(Colors.STATUS_BAR, curses.COLOR_BLACK, curses.COLOR_YELLOW)
+        curses.init_pair(Colors.HIGHLIGHT, curses.COLOR_WHITE, curses.COLOR_BLACK)
+
+        self.attrs = {
+            Colors.NORMAL: curses.A_NORMAL,
+            Colors.SUCCESS: curses.A_NORMAL,
+            Colors.WARN: curses.A_BOLD,
+            Colors.ERROR: curses.A_BOLD,
+            Colors.STATUS_BAR: curses.A_NORMAL,
+            Colors.HIGHLIGHT: curses.A_BOLD,
+        }
+
+    def get(self, code):
+        return curses.color_pair(code) | self.attrs[code]
+
+
+class StatusBar(object):
+
+    def __init__(self, window, colors):
+        self.window = window
+        self.c = colors
+
+    def update(self, s):
+        self.window.bkgd(' ', self.c.get(Colors.STATUS_BAR))
+        self.window.addstr(s, self.c.get(Colors.STATUS_BAR))
+
+
+class LogList(LineList):
+
+    def __init__(self, window, colors):
+        self.line_list = LineList(window)
+        self.c = colors
+
+    def _log_time(self, s, attr):
+        timestamp = datetime.datetime.now().strftime('%H:%M:%S')
+        self.line_list.add_line('[{}] {}'.format(timestamp, s), attr)
+
+    def info(self, s):
+        self._log_time(s, self.c.get(Colors.NORMAL))
+
+    def success(self, s):
+        self._log_time(s, self.c.get(Colors.SUCCESS))
+
+    def warn(self, s):
+        self._log_time(s, self.c.get(Colors.WARN))
+
+    def error(self, s):
+        self._log_time(s, self.c.get(Colors.ERROR))
+
+
+class CommandList(LineList):
+    
+    def __init__(self, window, colors):
+        self.line_list = LineList(window)
+        self.normal = colors.get(Colors.NORMAL)
+        self.highlight = colors.get(Colors.HIGHLIGHT)
+        self.current = ''
+
+    def add_character(self, c):
+        self.current += c
+        self.line_list.update_last(line='> {}'.format(self.current))
+
+    def backspace(self):
+        if self.current:
+            self.current = self.current[:len(self.current) - 1]
+
+        self.add_character('')
+
+    def set_actions(self, actions):
+        self.line_list.update_last(attr=self.normal)
+        self.current = ''
+
+        message = ' | '.join(('[{}] {}'.format(a.key, a.note) for a in actions.values()))
+        self.line_list.add_line(message, self.highlight)
+
+
+class Action(object):
+
+    def __init__(self, key, note, func, next_state):
+        self.key = key
+        self.note = note
+        self.run = func
+        self.next_state = next_state
+
+    def __repr__(self):
+        return '<Action:key={}>'.format(self.key)
+
+
+class StateMachine(object):
+
+    START = 0
+    QUIT = 1
+    SYNC = 2
+    CLEAN = 3
+    FLATCORRECT = 4
+    OPTIMIZE = 5
+
+    def __init__(self):
+        self.current = StateMachine.START
+
+        self.transitions = {
+            StateMachine.START: collections.OrderedDict(),
+            StateMachine.CLEAN: collections.OrderedDict(),
+            StateMachine.FLATCORRECT: collections.OrderedDict(),
+            StateMachine.QUIT: collections.OrderedDict(),
+            StateMachine.SYNC: collections.OrderedDict(),
+            StateMachine.OPTIMIZE: collections.OrderedDict(),
+        }
+
+    def add_action(self, from_state, action):
+        self.transitions[from_state][action.key] = action
+
+    def transition(self, action):
+        self.current = self.transitions[self.current][action.key].next_state
+
+    @property
+    def actions(self):
+        return self.transitions[self.current]
+
+    def is_valid_key(self, key):
+        return key in (k for k in self.transitions[self.current].keys())
+
+
+class Application(object):
+    def __init__(self, config):
+        self.config = config
+        self.running = True
+
+    def run_command(self, cmd):
+        try:
+            p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+            stdout, stderr = p.communicate()
+
+            def output_lines(pipe, log_func):
+                for line in pipe.split(os.linesep):
+                    if line:
+                        log_func(line)
+
+            output_lines(stdout, self.log.info)
+            output_lines(stderr, self.log.error)
+
+            if p.returncode == 0:
+                self.log.success("done")
+                return True
+
+            self.log.error("Command returned {}".format(p.returncode))
+            return False
+
+        except OSError as e:
+            self.log.error("{}".format(e))
+            return False
+
+    def on_quit(self):
+        self.running = False
+        return True
+
+    def on_sync(self):
+        self.log.info("Syncing data ...")
+        cmd = 'bash sync.sh {} {}'.format(self.config.source, self.config.destination)
+        return self.run_command(cmd)
+
+    def on_clean(self):
+        self.log.info("Cleaning {}".format(self.config.destination))
+        return True
+
+    def on_flat_correct(self):
+        num = 0
+        data = dict(p=self.config.destination, num=num, step=123))
+        cmd = ('tofu flatcorrect --verbose'
+               ' --reduction-mode median'
+               ' --projections "{p}/foo*.edf"'
+               ' --darks {p}/darkend0000.edf'
+               ' --flats {p}/ref*_0000.edf'
+               ' --flats2 {p}/ref*_{num}.edf'
+               ' --output {p}/fc/fc-%04i.tif'
+               ' --number {num}'
+               ' --step {step}'
+               ' --absorptivity'
+               ' --fix-nan-and-inf'.format(**data))
+        return self.run_command(cmd)
+
+    def on_optimize(self):
+        self.log.info("Optimizing ...")
+        return True
+
+    def do_nothing(self):
+        return True
+
+    def _run(self, screen):
+        curses.curs_set(False)
+
+        height, width = screen.getmaxyx()
+        colors = Colors()
+
+        top_pane = screen.subwin(1, width, 0, 0)
+        bottom_pane = screen.subwin(height - 1, width, 1, 0)
+
+        left_pane = bottom_pane.subwin(height - 1, width / 2, 1, 0)
+        right_pane = bottom_pane.subwin(height - 1, width / 2, 1, width / 2)
+
+        status_bar = StatusBar(top_pane, colors)
+        status_bar.update('Cockpit')
+
+        cmd_window = CommandList(left_pane, colors)
+
+        machine = StateMachine()
+
+        quit = Action('q', 'Quit', self.on_quit, machine.QUIT)
+        sync = Action('s', 'Sync data', self.on_sync, machine.SYNC)
+        flatcorrect = Action('f', 'Flat correct', self.on_flat_correct, machine.FLATCORRECT)
+        clean = Action('c', 'Clean', self.on_clean, machine.CLEAN)
+        optimize = Action('o', 'Optimize', self.on_optimize, machine.OPTIMIZE)
+
+        machine.add_action(machine.START, sync)
+        machine.add_action(machine.START, clean)
+        machine.add_action(machine.START, quit)
+
+        machine.add_action(machine.SYNC, flatcorrect)
+        machine.add_action(machine.SYNC, clean)
+        machine.add_action(machine.SYNC, quit)
+
+        machine.add_action(machine.FLATCORRECT, optimize)
+        machine.add_action(machine.FLATCORRECT, quit)
+
+        machine.add_action(machine.OPTIMIZE, sync)
+        machine.add_action(machine.OPTIMIZE, clean)
+        machine.add_action(machine.OPTIMIZE, quit)
+
+        machine.add_action(machine.CLEAN, sync)
+        machine.add_action(machine.CLEAN, quit)
+
+        cmd_window.set_actions(machine.actions)
+
+        self.log = LogList(right_pane, colors)
+        self.log.info('Source dir set to {}'.format(self.config.source))
+        self.log.info('Destination dir set to {}'.format(self.config.destination))
+
+        while self.running:
+            ci = screen.getch()
+            cc = chr(ci) if ci < 256 else ''
+
+            if machine.is_valid_key(cc):
+                cmd_window.add_character(cc)
+                action = machine.actions[cc]
+
+                if action.run():
+                    machine.transition(action)
+
+                cmd_window.set_actions(machine.actions)
+            elif ci == curses.KEY_BACKSPACE:
+                cmd_window.backspace()
+
+            screen.refresh()
+
+    def run(self):
+        curses.wrapper(self._run)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--source', help='Source directory', default='.', metavar='DIR')
+    parser.add_argument('--destination', help='Destination directory', default='.', metavar='DIR')
+
+    args = parser.parse_args()
+    config = Configuration(args.source, args.destination)
+
+    app = Application(config)
+    app.run()

+ 3 - 0
sync.sh

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+/usr/bin/rsync -v -P -t -r --bwlimit=200000 $1/* $2;