#!/usr/bin/env python3
"""Live Kodi harness: real screenshots of the addon from the running Kodi.

Talks to the listener in service.py (wcs/devtools/harness_service.py) through
request/response files in the addon_data harness directory. Every capture is a
real Kodi frame (TakeScreenshot), normalized to 1920x1080 skin pixels.

Commands:
  status                         Kodi process, listener heartbeat, GUI resolution
  start | restart | stop         launch / relaunch (reloads service.py) / quit Kodi
  shot    [--control ID --xml X] screenshot of whatever Kodi shows now
  dialog  XML [--profile P] [--focus ID] [--props K=V] [--control ID] [--ref IMG]
                                 open any addon XML with harness fixture data in
                                 real Kodi, capture it, and compare it with the
                                 offline render and (optionally) a reference
  open    TARGET                 navigate to a part of the addon, then capture;
                                 TARGET = plugin://... URL, "action=my_tv_dialog"
                                 shorthand, or a raw Kodi builtin
  keys    Down Down Select       send navigation actions, then capture
  info    LABEL...               read Kodi info labels (e.g. System.CurrentWindow)
  window  NAME [--path P] [--focus ID]   open a Kodi window (Home, Videos, 10025 ...), capture
  skin    estuary|milionar       switch the active skin, confirm "keep skin"
  reload                         ReloadSkin(), wait for Home, fail on skin errors in kodi.log
  jsonrpc '{...}'                run a JSON-RPC call inside Kodi, print the result
  mark                           write a unique marker into kodi.log
  log     --since-mark           print kodi.log lines after the last marker

--native (shot/open/keys/window): keep the real frame (e.g. 2560x1440), no scaling;
--region/--control are still given in skin pixels (1920x1080) and converted.

Common options: --out PATH, --settle MS, --region x0,y0,x1,y1, --pad N,
--no-exact (skip the 1920x1080 fullscreen safeguard).
Outputs default to scripts/dialog_harness/out/live/.
"""
import argparse
import json
import os
import subprocess
import sys
import time
import uuid

from PIL import Image

HERE = os.path.dirname(os.path.abspath(__file__))
ADDON_ROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
ADDON_ID = 'plugin.video.milionar'
KODI_HOME = os.path.expanduser('~/Library/Application Support/Kodi')
HARNESS_DIR = os.path.join(KODI_HOME, 'userdata', 'addon_data', ADDON_ID, 'harness')
SKIN_DIR = os.path.join(ADDON_ROOT, 'resources', 'skins', 'Default', '1080i')
OUT_DIR = os.path.join(HERE, 'out', 'live')
KODI_LOG = os.path.expanduser('~/Library/Logs/kodi.log')
MARK_FILE = os.path.join(HERE, 'out', '.log_mark')
SKIN_ERRORS = ('Skin has invalid include', 'Unable to load', 'Error loading', 'XML error',
               'Missing include', 'Skin file', 'Include \'')
TARGET_W, TARGET_H = 1920, 1080

sys.path.insert(0, HERE)


# --------------------------------------------------------------------------- transport

def kodi_running():
    return subprocess.run(['pgrep', '-x', 'Kodi'], capture_output=True).returncode == 0


def heartbeat_age():
    path = os.path.join(HARNESS_DIR, 'heartbeat')
    if not os.path.exists(path):
        return None
    return time.time() - os.path.getmtime(path)


def enable():
    os.makedirs(HARNESS_DIR, exist_ok=True)
    open(os.path.join(HARNESS_DIR, 'ENABLED'), 'w').close()


def request(ops, timeout=60.0):
    """Send ops to the in-Kodi listener and return the response dict."""
    if not kodi_running():
        raise SystemExit('Kodi is not running. Run: kodi_live.py start')
    enable()
    request_id = uuid.uuid4().hex[:12]
    req_path = os.path.join(HARNESS_DIR, 'request.json')
    deadline = time.time() + 10
    while os.path.exists(req_path) or os.path.exists(req_path + '.processing'):
        if time.time() > deadline:
            raise SystemExit('previous harness request still pending: ' + req_path)
        time.sleep(0.1)
    tmp = req_path + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as handle:
        json.dump({'id': request_id, 'ops': ops}, handle, ensure_ascii=False)
    os.replace(tmp, req_path)
    resp_path = os.path.join(HARNESS_DIR, 'response-{}.json'.format(request_id))
    deadline = time.time() + timeout
    while time.time() < deadline:
        if os.path.exists(resp_path):
            with open(resp_path, encoding='utf-8') as handle:
                data = json.load(handle)
            os.remove(resp_path)
            if not data.get('ok'):
                failed = [r for r in data.get('results', []) if not r.get('ok')]
                detail = failed[-1].get('trace', '') if failed else ''
                raise SystemExit('harness error: {}\n{}'.format(data.get('error'), detail))
            return data
        time.sleep(0.1)
    age = heartbeat_age()
    raise SystemExit('no response in {:.0f}s (listener heartbeat age: {}). '
                     'After changing service.py run: kodi_live.py restart'.format(
                         timeout, 'none' if age is None else '{:.1f}s'.format(age)))


def wait_listener(timeout=90.0):
    enable()
    deadline = time.time() + timeout
    while time.time() < deadline:
        age = heartbeat_age()
        if age is not None and age < 3:
            return True
        time.sleep(0.5)
    return False


# --------------------------------------------------------------------------- capture helpers

def ping():
    return request([{'op': 'ping'}])['results'][0]['result']


def ensure_exact(exact):
    """Make sure Kodi renders at 1920x1080; returns an undo op list (may be empty)."""
    info = ping()
    size = (info['screen_w'], info['screen_h'])
    if size == (TARGET_W, TARGET_H) or not exact:
        return [], size
    # Already a larger 16:9 GUI (e.g. fullscreen 2560x1440): scale down, never toggle --
    # toggling resizes the GUI while the dialog lays out its text
    if size[0] >= TARGET_W and size[0] * TARGET_H == size[1] * TARGET_W:
        return [], size
    print('Kodi renders at {}x{}; switching to fullscreen for an exact 1920x1080 frame'.format(*size))
    request([{'op': 'builtin', 'cmd': 'Action(togglefullscreen)'}, {'op': 'sleep', 'ms': 1500}])
    new = ping()
    new_size = (new['screen_w'], new['screen_h'])
    undo = [{'op': 'builtin', 'cmd': 'Action(togglefullscreen)'}, {'op': 'sleep', 'ms': 800}]
    if new_size != (TARGET_W, TARGET_H):
        request(undo)
        print('fullscreen gives {}x{}, not 1920x1080 -- falling back to scaling'.format(*new_size))
        return [], size
    return undo, new_size


def normalize(path):
    """Scale a capture to 1920x1080 skin pixels if needed; returns a note or None."""
    img = Image.open(path)
    if img.size == (TARGET_W, TARGET_H):
        return None
    note = 'captured {}x{}, scaled to 1920x1080 (not pixel-exact)'.format(*img.size)
    img.convert('RGB').resize((TARGET_W, TARGET_H), Image.LANCZOS).save(path)
    return note


def fixture(profile, props_pairs):
    from render_dialog import BASE_PROPS, PROFILES, SEARCH_ITEMS
    props = dict(BASE_PROPS)
    props.update(PROFILES.get(profile, {}))
    items = {}
    if profile == 'search':
        items = {50: SEARCH_ITEMS}
    for pair in props_pairs or []:
        key, _, value = pair.partition('=')
        props[key] = value
    return props, items


def load_kwargs(spec):
    """--kwargs is a JSON file path or a JSON object string."""
    if not spec:
        return {}
    if os.path.exists(spec):
        with open(spec, encoding='utf-8') as handle:
            return json.load(handle)
    return json.loads(spec)


def resolve_xml(xml):
    if os.path.isabs(xml) and os.path.exists(xml):
        return os.path.relpath(xml, SKIN_DIR), xml
    candidate = os.path.join(SKIN_DIR, xml)
    if os.path.exists(candidate):
        return xml, candidate
    candidate = os.path.abspath(xml)
    if os.path.exists(candidate):
        return os.path.relpath(candidate, SKIN_DIR), candidate
    raise SystemExit('XML not found: {} (relative to {})'.format(xml, SKIN_DIR))


def backdrop_spec(value):
    """--backdrop black|none|#RRGGBB|PATH -> (listener op dict or None, PIL base image or None)."""
    if not value or value == 'none':
        return None, None
    if value == 'black':
        value = '#000000'
    if value.startswith('#') and len(value) == 7:
        rgb = tuple(int(value[i:i + 2], 16) for i in (1, 3, 5))
        base = Image.new('RGBA', (TARGET_W, TARGET_H), rgb + (255,))
        path = os.path.join(HARNESS_DIR, 'backdrop_{}.png'.format(value[1:].lower()))
        if not os.path.exists(path):
            os.makedirs(HARNESS_DIR, exist_ok=True)
            base.convert('RGB').save(path)
        return {'image': path}, base
    path = os.path.abspath(value)
    if not os.path.exists(path):
        raise SystemExit('backdrop image not found: ' + value)
    base = Image.open(path).convert('RGBA').resize((TARGET_W, TARGET_H), Image.LANCZOS)
    return {'image': path}, base


def offline_render(xml_abs, props, items, focus, out_path, base=None):
    from kodi_renderer import Renderer, parse_po, STRINGS_PO
    data = dict(props)
    if items:
        data['__items__'] = items
    renderer = Renderer(data, focus_ids=[focus] if focus is not None else [], strings=parse_po(STRINGS_PO))
    image = renderer.render(xml_abs)
    flat = base.copy() if base is not None else Image.new('RGBA', image.size, (0, 0, 0, 255))
    flat.alpha_composite(image)
    flat.convert('RGB').save(out_path)
    return renderer.rects


def control_box(rects, control_id, pad):
    if control_id not in rects:
        raise SystemExit('control {} not found in XML layout (known: {})'.format(
            control_id, sorted(rects)[:40]))
    x, y, w, h = rects[control_id]
    return (max(0, x - pad), max(0, y - pad), min(TARGET_W, x + w + pad), min(TARGET_H, y + h + pad))


def crop_save(path, box, suffix):
    root, ext = os.path.splitext(path)
    out = '{}_{}{}'.format(root, suffix, ext)
    Image.open(path).crop(box).save(out)
    return out


def parse_box(text):
    from compare_images import parse_box as _parse
    return _parse(text)


def report(title, info):
    print('=' * 72)
    print(title)
    print('-' * 72)
    for key, value in info.items():
        print('{:<18}{}'.format(key, value))


# --------------------------------------------------------------------------- commands

def cmd_status(args):
    running = kodi_running()
    age = heartbeat_age()
    info = {'kodi running': running,
            'listener': 'alive' if age is not None and age < 5 else 'no heartbeat',
            'harness dir': HARNESS_DIR}
    if running:
        try:
            p = ping()
            info.update({'kodi': p['kodi'], 'gui size': '{}x{}'.format(p['screen_w'], p['screen_h']),
                         'resolution': p['resolution'], 'window': p['current_window'],
                         'window id': p['current_window_id'], 'dialog id': p['current_dialog_id']})
        except SystemExit as exc:
            info['ping'] = str(exc).splitlines()[0]
    report('KODI LIVE STATUS', info)


def cmd_start(args):
    enable()
    if not kodi_running():
        subprocess.run(['open', '-a', 'Kodi'], check=True)
    ok = wait_listener()
    print('listener ready' if ok else 'Kodi started but listener did not answer (check kodi.log)')


def cmd_stop(args):
    if kodi_running():
        subprocess.run(['osascript', '-e', 'quit app "Kodi"'], capture_output=True)
        deadline = time.time() + 20
        while kodi_running() and time.time() < deadline:
            time.sleep(0.5)
        if kodi_running():
            subprocess.run(['pkill', '-x', 'Kodi'])
            time.sleep(2)
    print('Kodi stopped')


def cmd_restart(args):
    cmd_stop(args)
    subprocess.run(['find', ADDON_ROOT, '-type', 'd', '-name', '__pycache__', '-prune',
                    '-exec', 'rm', '-rf', '{}', '+'])
    hb = os.path.join(HARNESS_DIR, 'heartbeat')
    if os.path.exists(hb):
        os.remove(hb)
    cmd_start(args)


def capture(ops_before, out, exact, settle, native=False):
    """Run ops, then TakeScreenshot. native=True keeps the real frame size (no scaling)."""
    undo, size = ([], None) if native else ensure_exact(exact)
    ops = list(ops_before)
    if settle:
        ops.append({'op': 'sleep', 'ms': settle})
    ops.append({'op': 'screenshot', 'path': out})
    try:
        data = request(ops)
    finally:
        if undo:
            request(undo)
    note = None if native else normalize(out)
    return data['results'][-1]['result'], note


def finish_capture(args, out, result, note, xml_abs=None, rects=None):
    info = {'capture': out, 'window': result.get('current_window'),
            'window id': result.get('current_window_id'), 'dialog id': result.get('current_dialog_id'),
            'gui size': '{}x{}'.format(result.get('screen_w'), result.get('screen_h'))}
    if note:
        info['NOTE'] = note
    native = getattr(args, 'native', False)
    factor = (result.get('screen_w') or TARGET_W) / float(TARGET_W) if native else 1.0
    box = parse_box(args.region) if getattr(args, 'region', None) else None
    if getattr(args, 'control', None) is not None:
        if rects is None:
            if not xml_abs:
                raise SystemExit('--control needs --xml to locate the control')
            props, items = fixture(getattr(args, 'profile', 'episode'), getattr(args, 'props', []))
            rects = offline_render(xml_abs, props, items, None, os.path.join(OUT_DIR, '_layout.png'))
        box = control_box(rects, args.control, args.pad)
    if box:
        if factor != 1.0:
            info['crop box (skin px)'] = box
            box = tuple(int(round(v * factor)) for v in box)
        info['crop'] = crop_save(out, box, 'crop')
        info['crop box'] = box
    if native:
        info['size'] = '{}x{} native, skin px x{:.4g}'.format(*Image.open(out).size, factor)
    report('LIVE CAPTURE', info)
    return box


def cmd_shot(args):
    os.makedirs(OUT_DIR, exist_ok=True)
    out = os.path.abspath(args.out or os.path.join(OUT_DIR, 'shot.png'))
    xml_abs = resolve_xml(args.xml)[1] if args.xml else None
    result, note = capture([], out, not args.no_exact, args.settle, args.native)
    finish_capture(args, out, result, note, xml_abs)


def cmd_open(args):
    os.makedirs(OUT_DIR, exist_ok=True)
    target = args.target
    if target.startswith('action='):
        target = 'plugin://{}/?{}'.format(ADDON_ID, target)
    if target.startswith('plugin://'):
        cmd = 'RunPlugin({})'.format(target) if args.run else 'ActivateWindow(Videos,{},return)'.format(target)
    else:
        cmd = target
    ops = [{'op': 'builtin', 'cmd': cmd}]
    if args.wait:
        ops.append({'op': 'wait', 'cond': args.wait, 'timeout_ms': 15000})
    out = os.path.abspath(args.out or os.path.join(OUT_DIR, 'open.png'))
    xml_abs = resolve_xml(args.xml)[1] if args.xml else None
    result, note = capture(ops, out, not args.no_exact, args.settle, args.native)
    finish_capture(args, out, result, note, xml_abs)


def cmd_keys(args):
    os.makedirs(OUT_DIR, exist_ok=True)
    ops = []
    for key in args.keys:
        # plain names are actions (Down, Select, Back); anything with "(" is a builtin (SetFocus(110))
        cmd = key if '(' in key else 'Action({})'.format(key)
        ops.append({'op': 'builtin', 'cmd': cmd})
        ops.append({'op': 'sleep', 'ms': args.step})
    out = os.path.abspath(args.out or os.path.join(OUT_DIR, 'keys.png'))
    xml_abs = resolve_xml(args.xml)[1] if args.xml else None
    result, note = capture(ops, out, not args.no_exact, args.settle, args.native)
    finish_capture(args, out, result, note, xml_abs)


def cmd_info(args):
    data = request([{'op': 'info', 'labels': args.labels, 'conds': args.cond or []}])
    print(json.dumps(data['results'][0]['result'], ensure_ascii=False, indent=2))


def new_log_offset():
    return os.path.getsize(KODI_LOG) if os.path.exists(KODI_LOG) else 0


def read_log_from(offset):
    with open(KODI_LOG, 'rb') as handle:
        handle.seek(offset)
        return handle.read().decode('utf-8', 'replace').splitlines()


def cmd_mark(args):
    token = 'harness-{}'.format(uuid.uuid4().hex[:10])
    request([{'op': 'mark', 'token': token}])
    os.makedirs(os.path.dirname(MARK_FILE), exist_ok=True)
    with open(MARK_FILE, 'w') as handle:
        handle.write(token)
    print(token)
    return token


def lines_since_mark():
    if not os.path.exists(MARK_FILE):
        raise SystemExit('no mark yet: run kodi_live.py mark')
    token = open(MARK_FILE).read().strip()
    lines = read_log_from(0)
    for index in range(len(lines) - 1, -1, -1):
        if 'MARK ' + token in lines[index]:
            return lines[index + 1:]
    raise SystemExit('mark {} not found in {}'.format(token, KODI_LOG))


def cmd_log(args):
    if not args.since_mark:
        raise SystemExit('log needs --since-mark')
    for line in lines_since_mark():
        print(line)


def skin_error_lines(lines):
    return [line for line in lines
            if ('ERROR' in line or 'WARNING' in line) and any(key in line for key in SKIN_ERRORS)]


def cmd_skin(args):
    name = args.name if args.name.startswith('skin.') else 'skin.' + args.name
    data = request([{'op': 'skin', 'name': name}], timeout=90)
    print(json.dumps(data['results'][0]['result'], ensure_ascii=False))
    print('active skin:', ping()['skin'])


def cmd_reload(args):
    cmd_mark(args)
    data = request([{'op': 'reload'}], timeout=60)
    result = data['results'][0]['result']
    print('reload: home={} skin={}'.format(result['home'], result['skin']))
    errors = skin_error_lines(lines_since_mark())
    for line in errors:
        print(line)
    if errors or not result['home']:
        raise SystemExit('reload FAILED: {} skin error line(s)'.format(len(errors)))
    print('kodi.log clean')


def cmd_jsonrpc(args):
    data = request([{'op': 'jsonrpc', 'request': json.loads(args.json)}])
    print(json.dumps(data['results'][0]['result'], ensure_ascii=False, indent=2))


def cmd_window(args):
    os.makedirs(OUT_DIR, exist_ok=True)
    target = args.name
    if args.path:
        cmd = 'ActivateWindow({},{},return)'.format(target, args.path)
    else:
        cmd = 'ActivateWindow({})'.format(target)
    ops = [{'op': 'builtin', 'cmd': cmd}, {'op': 'sleep', 'ms': 800}]
    if args.focus is not None:
        ops.append({'op': 'builtin', 'cmd': 'SetFocus({})'.format(args.focus)})
    out = os.path.abspath(args.out or os.path.join(OUT_DIR, 'window.png'))
    result, note = capture(ops, out, not args.no_exact, args.settle or 1500, args.native)
    finish_capture(args, out, result, note)


def cmd_dialog(args):
    from compare_images import run as compare_run
    rel, xml_abs = resolve_xml(args.xml)
    name = os.path.splitext(os.path.basename(rel))[0]
    out_dir = os.path.abspath(args.out_dir or os.path.join(OUT_DIR, name))
    os.makedirs(out_dir, exist_ok=True)
    live = os.path.join(out_dir, 'live.png')
    offline = os.path.join(out_dir, 'offline.png')
    props, items = fixture(args.profile, args.props)
    backdrop, base = backdrop_spec(args.backdrop)

    try:
        rects = offline_render(xml_abs, props, items, args.focus, offline, base)
    except Exception as exc:  # the live frame is still worth capturing
        print('WARNING: offline render failed ({}: {}); live capture only'.format(type(exc).__name__, exc))
        rects, offline = {}, None
    with open(os.path.join(out_dir, 'rects.json'), 'w') as handle:
        json.dump({str(k): v for k, v in sorted(rects.items())}, handle, indent=1)

    undo, _ = ensure_exact(not args.no_exact)
    op = {'op': 'dialog', 'xml': rel, 'props': props,
          'items': {str(k): v for k, v in items.items()},
          'focus': args.focus, 'settle_ms': args.settle or 1200,
          'screenshot': live, 'close': not args.keep_open, 'backdrop': backdrop,
          'measure': [int(x) for x in args.measure.split(',')] if args.measure else []}
    try:
        data = request([op], timeout=90)
    finally:
        if undo:
            request(undo)
    result = data['results'][0]['result']
    note = normalize(live)

    box = parse_box(args.region) if args.region else None
    if args.control is not None:
        box = control_box(rects, args.control, args.pad)
    info = {'xml': rel, 'live': live, 'offline render': offline,
            'focused id': result.get('focused_id'),
            'gui size': '{}x{}'.format(result.get('screen_w'), result.get('screen_h'))}
    if note:
        info['NOTE'] = note
    if box:
        info['region'] = box
    if result.get('class_trace') is not None:
        info['class trace'] = result['class_trace'] or 'onInit NOT called'
    for control_id, rect in sorted((result.get('measured') or {}).items()):
        info['kodi rect {}'.format(control_id)] = rect
    report('LIVE DIALOG  {}'.format(rel), info)

    if offline:
        print('\n[live vs offline render] -- renderer accuracy / Kodi-only effects')
        compare_run(offline, live, region=box, out_dir=os.path.join(out_dir, 'vs_offline'))
    if args.ref:
        print('\n[live vs reference] -- the design target')
        compare_run(args.ref, live, region=box, ref_region=parse_box(args.ref_region) if args.ref_region else None,
                    out_dir=os.path.join(out_dir, 'vs_ref'))


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest='cmd', required=True)

    def common(p, xml_opt=True):
        p.add_argument('--out')
        p.add_argument('--settle', type=int, default=0, help='extra wait before capture (ms)')
        p.add_argument('--region', help='crop x0,y0,x1,y1 in 1920x1080 pixels')
        p.add_argument('--control', type=int, help='crop to this control id (needs XML layout)')
        p.add_argument('--pad', type=int, default=8)
        p.add_argument('--profile', default='episode')
        p.add_argument('--props', action='append', default=[])
        p.add_argument('--no-exact', action='store_true')
        p.add_argument('--native', action='store_true',
                       help='keep the real frame size (2560x1440); --region stays in skin pixels')
        if xml_opt:
            p.add_argument('--xml', help='dialog XML used to locate --control')

    sub.add_parser('status')
    sub.add_parser('start')
    sub.add_parser('stop')
    sub.add_parser('restart')

    p = sub.add_parser('shot')
    common(p)

    p = sub.add_parser('open')
    p.add_argument('target')
    p.add_argument('--run', action='store_true', help='RunPlugin instead of opening in Videos window')
    p.add_argument('--wait', help='Kodi condition to wait for, e.g. "Window.IsActive(1101)"')
    common(p)

    p = sub.add_parser('keys')
    p.add_argument('keys', nargs='+')
    p.add_argument('--step', type=int, default=250)
    common(p)

    p = sub.add_parser('window')
    p.add_argument('name')
    p.add_argument('--path')
    p.add_argument('--focus', type=int)
    common(p)

    p = sub.add_parser('skin')
    p.add_argument('name')

    sub.add_parser('reload')
    sub.add_parser('mark')

    p = sub.add_parser('jsonrpc')
    p.add_argument('json')

    p = sub.add_parser('log')
    p.add_argument('--since-mark', action='store_true')

    sub.add_parser('close', help='close dialogs left open by dialog --keep-open')

    p = sub.add_parser('info')
    p.add_argument('labels', nargs='*', default=['System.CurrentWindow', 'System.CurrentControl'])
    p.add_argument('--cond', action='append')

    p = sub.add_parser('dialog')
    p.add_argument('xml')
    p.add_argument('--focus', type=int)
    p.add_argument('--ref', help='design reference image to compare against')
    p.add_argument('--ref-region', help='region in the reference image (if its layout differs)')
    p.add_argument('--out-dir')
    p.add_argument('--keep-open', action='store_true')
    p.add_argument('--measure', help='comma-separated control ids: report Kodi\'s real x/y/w/h')
    p.add_argument('--class', dest='python_class',
                   help='run the real dialog class, e.g. wcs.ui.dialogs.DialogWcsSearch:DialogWcsSearch')
    p.add_argument('--kwargs', help='JSON file or JSON string with the class kwargs (fixture case)')
    p.add_argument('--backdrop', default='black',
                   help='black (default) | none (real Kodi screen) | #RRGGBB | image path')
    common(p, xml_opt=False)

    args = parser.parse_args()
    {'status': cmd_status, 'start': cmd_start, 'stop': cmd_stop, 'restart': cmd_restart,
     'shot': cmd_shot, 'open': cmd_open, 'keys': cmd_keys, 'info': cmd_info,
     'window': cmd_window, 'skin': cmd_skin, 'reload': cmd_reload, 'mark': cmd_mark,
     'jsonrpc': cmd_jsonrpc, 'log': cmd_log,
     'dialog': cmd_dialog, 'close': lambda a: request([{'op': 'close_kept'}])}[args.cmd](args)


if __name__ == '__main__':
    main()
