#!/usr/bin/env python3
"""JM Browser — a tiny Chrome-like browser for JM Tech Power.

Opens a chromeless app window with a real tab strip, an omnibox (URL + search),
back/forward/reload/home, a new-tab page, a loading bar, and keyboard shortcuts —
all framing the live website. It bundles NOTHING from the site (it just loads
jmtechpower.com), so there's nothing to keep in sync. Pure standard-library
Python 3; uses your installed Chromium/Chrome/Edge/Brave for the window.
"""
import base64
import hashlib
import json
import os
import secrets
import shutil
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

VERSION = "3.0.0"
OAUTH_CLIENT_ID = "jmc_jmbrowser"     # public client (PKCE, no secret)
OAUTH_SCOPE = "profile email"
OAUTH = {"verifier": None, "state": None, "user": None, "start": "", "port": 0,
         "last_ping": 0.0, "started": False}
DEFAULT_HOST = "https://www.jmtechpower.com"
PROFILE_DIR = os.path.join(os.path.expanduser("~"), ".config", "jm", "browser-profile")

SHELL_HTML = r"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>JM Browser</title>
<style>
:root{--bg:#dee1e6;--chrome:#f1f3f4;--tab:#fff;--tabd:#cfd3d8;--text:#202124;--muted:#5f6368;--accent:#01be01;--omni:#fff}
.dark{--bg:#202124;--chrome:#35363a;--tab:#202124;--tabd:#2a2b2e;--text:#e8eaed;--muted:#9aa0a6;--omni:#3c4043}
*{box-sizing:border-box}
html,body{margin:0;height:100%;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:var(--bg)}
body{display:flex;flex-direction:column;color:var(--text)}
/* tab strip */
#tabstrip{display:flex;align-items:flex-end;gap:2px;background:var(--bg);padding:6px 6px 0;height:40px;flex:none;-webkit-app-region:drag}
.tab{display:flex;align-items:center;gap:7px;max-width:230px;min-width:120px;height:34px;padding:0 8px 0 12px;background:var(--tabd);
     color:var(--text);border-radius:10px 10px 0 0;cursor:default;font-size:12.5px;white-space:nowrap;overflow:hidden;-webkit-app-region:no-drag}
.tab.active{background:var(--tab)}
.tab .fav{width:14px;height:14px;flex:none;border-radius:3px;background:var(--accent);display:inline-block;background-size:cover}
.tab .ttl{flex:1;overflow:hidden;text-overflow:ellipsis}
.tab .x{flex:none;width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:13px}
.tab .x:hover{background:rgba(128,128,128,.3);color:var(--text)}
#newtab{width:30px;height:30px;border:none;background:none;color:var(--muted);font-size:20px;border-radius:50%;cursor:pointer;-webkit-app-region:no-drag}
#newtab:hover{background:rgba(128,128,128,.2)}
/* toolbar */
#toolbar{display:flex;align-items:center;gap:4px;background:var(--chrome);padding:6px 10px;height:46px;flex:none}
.nav{width:32px;height:32px;border:none;background:none;color:var(--text);font-size:16px;border-radius:50%;cursor:pointer;flex:none}
.nav:hover{background:rgba(128,128,128,.18)}
.nav:disabled{opacity:.35;cursor:default}
#omniwrap{flex:1;display:flex;align-items:center;height:34px;background:var(--omni);border-radius:18px;padding:0 14px;gap:8px;border:1px solid transparent}
#omniwrap:focus-within{border-color:var(--accent);background:var(--tab)}
#lock{color:var(--muted);font-size:12px;flex:none}
#omni{flex:1;border:none;background:none;outline:none;font-size:13.5px;color:var(--text)}
#menu{position:relative}
#menuPop{position:absolute;right:0;top:38px;background:var(--tab);color:var(--text);border:1px solid var(--tabd);border-radius:10px;
        box-shadow:0 8px 28px rgba(0,0,0,.25);padding:6px;min-width:200px;display:none;z-index:30}
#menuPop.show{display:block}
#menuPop button{display:block;width:100%;text-align:left;border:none;background:none;color:var(--text);padding:9px 12px;border-radius:7px;font-size:13.5px;cursor:pointer}
#menuPop button:hover{background:rgba(128,128,128,.15)}
#acct{position:relative}
.pill{height:30px;border:none;border-radius:16px;background:var(--accent);color:#fff;font-weight:700;font-size:12.5px;padding:0 14px;cursor:pointer;white-space:nowrap;max-width:200px;overflow:hidden;text-overflow:ellipsis}
#acctPop{position:absolute;right:0;top:38px;background:var(--tab);color:var(--text);border:1px solid var(--tabd);border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.25);padding:10px;min-width:190px;display:none;z-index:31}
#acctPop.show{display:block}
#acctName{font-size:13px;margin-bottom:8px;word-break:break-all}
#acctPop button{width:100%;border:none;background:rgba(128,128,128,.14);color:var(--text);padding:8px;border-radius:7px;cursor:pointer}
/* loading bar */
#load{height:3px;background:transparent;flex:none;overflow:hidden}
#load.on::after{content:"";display:block;height:100%;width:40%;background:var(--accent);animation:slide 1s linear infinite}
@keyframes slide{0%{margin-left:-40%}100%{margin-left:100%}}
/* views */
#views{flex:1;position:relative;background:#fff}
#views iframe{position:absolute;inset:0;width:100%;height:100%;border:none;background:#fff}
.hidden{display:none!important}
/* new tab page */
#ntp{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:26px;background:var(--bg);color:var(--text)}
#ntp .brand{font-size:30px;font-weight:800;letter-spacing:.3px}
#ntp .brand span{color:var(--accent)}
#ntp .tiles{display:grid;grid-template-columns:repeat(3,120px);gap:16px}
#ntp .tile{height:84px;background:var(--tab);border:1px solid var(--tabd);border-radius:14px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;cursor:pointer;font-size:13px;text-decoration:none;color:var(--text)}
#ntp .tile:hover{box-shadow:0 4px 14px rgba(0,0,0,.12);transform:translateY(-1px)}
#ntp .tile .ic{font-size:24px}
#star{cursor:pointer;color:var(--muted);flex:none;font-size:15px}
#star.on{color:var(--accent)}
#bookmarks{display:flex;align-items:center;gap:3px;background:var(--chrome);padding:3px 10px;height:30px;flex:none;overflow-x:auto;border-top:1px solid rgba(128,128,128,.15)}
#bookmarks:empty{display:none}
.bm{display:flex;align-items:center;gap:6px;padding:3px 9px;border-radius:7px;font-size:12px;color:var(--text);white-space:nowrap;cursor:pointer}
.bm:hover{background:rgba(128,128,128,.18)}
.bm .bx{color:var(--muted);font-size:11px}
.tab .fav{background-color:var(--accent);background-size:cover;background-position:center}
</style></head>
<body>
<div id="tabstrip"></div>
<div id="toolbar">
  <button class="nav" id="back" title="Back (Alt+Left)">&#9664;</button>
  <button class="nav" id="fwd" title="Forward (Alt+Right)">&#9654;</button>
  <button class="nav" id="rel" title="Reload (Ctrl+R)">&#10227;</button>
  <button class="nav" id="home" title="Home">&#8962;</button>
  <div id="omniwrap"><span id="lock">&#128274;</span>
    <input id="omni" spellcheck="false" autocomplete="off" placeholder="Search JM Tech Power or type a URL">
    <span id="star" title="Bookmark this page (Ctrl+D)">&#9734;</span></div>
  <div id="acct"><button class="pill" id="acctbtn" onclick="signIn()">Sign in</button>
    <div id="acctPop"><div id="acctName"></div><button onclick="signOut()">Sign out</button></div></div>
  <div id="menu"><button class="nav" id="menubtn" title="Menu">&#8942;</button>
    <div id="menuPop">
      <button onclick="newTab()">New tab</button>
      <button onclick="act('reload')">Reload</button>
      <button onclick="ext()">Open in system browser</button>
      <button onclick="toggleDark()">Toggle dark mode</button>
    </div>
  </div>
</div>
<div id="bookmarks"></div>
<div id="load"></div>
<div id="views"><div id="ntp" class="hidden"></div></div>
<script>
var HOST="__HOST__";
var START="__START__";
var LINKS=[["Home","⌂","/"],["Apps","▣","/apps"],["Articles","✎","/articles"],
           ["Chat","✉","/chat"],["JMDrive","☁","/drive"],["Settings","⚙","/settings/tokens"]];
var tabs=[], active=null, seq=0;
var views=document.getElementById('views'), strip=document.getElementById('tabstrip'),
    omni=document.getElementById('omni'), ntp=document.getElementById('ntp'), load=document.getElementById('load');

function cur(){ return tabs.find(function(t){return t.id===active;}); }
function host(u){ try{ return new URL(u).host; }catch(e){ return u; } }
function titleFor(u){ if(!u) return "New Tab"; var h=host(u); return h.replace(/^www\./,''); }

function buildNTP(){
  ntp.innerHTML='<div class="brand">JM<span> Browser</span></div>'+
    '<input id="ntpq" autocomplete="off" placeholder="Search JM Tech Power or type a URL" '+
      'onkeydown="if(event.key===\'Enter\')navigate(this.value)" '+
      'style="width:min(560px,82%);padding:13px 18px;border:1px solid var(--tabd);border-radius:26px;font-size:15px;background:var(--tab);color:var(--text);outline:none">'+
    '<div class="tiles">'+
    LINKS.map(function(l){return '<a class="tile" onclick="go(HOST+\''+l[2]+'\')"><span class="ic">'+l[1]+'</span>'+l[0]+'</a>';}).join('')+
    '</div>';
}

function renderTabs(){
  strip.innerHTML='';
  tabs.forEach(function(t){
    var d=document.createElement('div'); d.className='tab'+(t.id===active?' active':'');
    var fav = t.url ? '<span class="fav" style="background-image:url(https://www.google.com/s2/favicons?domain='+host(t.url)+'&sz=32)"></span>' : '<span class="fav"></span>';
    d.innerHTML=fav+'<span class="ttl"></span><span class="x">×</span>';
    d.querySelector('.ttl').textContent=t.title;
    d.onclick=function(e){ if(e.target.className==='x'){closeTab(t.id);} else switchTo(t.id); };
    d.onmousedown=function(e){ if(e.button===1){ e.preventDefault(); closeTab(t.id); } };
    strip.appendChild(d);
  });
  var plus=document.createElement('button'); plus.id='newtab'; plus.textContent='+'; plus.title='New tab (Ctrl+T)';
  plus.onclick=function(){newTab();}; strip.appendChild(plus);
}

function showView(){
  var t=cur();
  tabs.forEach(function(x){ if(x.frame) x.frame.classList.toggle('hidden', x.id!==active); });
  if(!t || !t.url){ ntp.classList.remove('hidden'); } else { ntp.classList.add('hidden'); }
  omni.value=(t && t.url) ? t.url : '';
  document.getElementById('back').disabled = !t || t.idx<=0;
  document.getElementById('fwd').disabled = !t || t.idx>=t.stack.length-1;
  if(t && t.frame) t.frame.style.zoom = t.zoom||1;
  updateStar(); saveSession();
}
function esc(s){return (''+s).replace(/[&<>"]/g,function(c){return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]);});}
function bms(){ try{ return JSON.parse(localStorage.getItem('jmb_bookmarks')||'[]'); }catch(e){ return []; } }
function saveBms(a){ try{ localStorage.setItem('jmb_bookmarks',JSON.stringify(a)); }catch(e){} renderBms(); updateStar(); }
function renderBms(){
  var bar=document.getElementById('bookmarks'), a=bms();
  bar.innerHTML=a.map(function(b,i){return '<span class="bm" data-i="'+i+'"><span>&#9733;</span>'+esc(b.title)+'<span class="bx" data-x="'+i+'">×</span></span>';}).join('');
  bar.querySelectorAll('.bm').forEach(function(elm){ elm.onclick=function(e){
    var xi=e.target.getAttribute('data-x'); if(xi!==null){ var a2=bms(); a2.splice(+xi,1); saveBms(a2); }
    else { go(a[+elm.getAttribute('data-i')].url); } }; });
}
function toggleBookmark(){ var t=cur(); if(!t||!t.url) return; var a=bms();
  var i=a.findIndex(function(b){return b.url===t.url;});
  if(i>=0) a.splice(i,1); else a.push({title:t.title||titleFor(t.url),url:t.url}); saveBms(a); }
function updateStar(){ var t=cur(), on=!!(t&&t.url&&bms().some(function(b){return b.url===t.url;}));
  var s=document.getElementById('star'); s.classList.toggle('on',on); s.innerHTML=on?'&#9733;':'&#9734;'; }
function saveSession(){ try{ localStorage.setItem('jmb_session',JSON.stringify(tabs.map(function(t){return t.url;}))); }catch(e){} }
function setZoom(z){ var t=cur(); if(!t) return; t.zoom=Math.max(0.5,Math.min(2.5,z)); if(t.frame) t.frame.style.zoom=t.zoom; }
function zoom(d){ var t=cur(); if(t) setZoom((t.zoom||1)+d); }

function newTab(url){
  var f=document.createElement('iframe'); f.className='hidden';
  f.onload=function(){ load.classList.remove('on'); };
  views.appendChild(f);
  var t={id:++seq, frame:f, stack:[], idx:-1, url:'', title:'New Tab', zoom:1};
  tabs.push(t); active=t.id;
  renderTabs(); showView();
  if(url) go(url); else { setTimeout(function(){omni.focus();},0); }
}
function closeTab(id){
  var i=tabs.findIndex(function(t){return t.id===id;}); if(i<0) return;
  var t=tabs[i]; if(t.frame) t.frame.remove(); tabs.splice(i,1);
  if(active===id) active = tabs.length ? tabs[Math.min(i,tabs.length-1)].id : null;
  if(!tabs.length){ newTab(); return; }
  renderTabs(); showView();
}
function switchTo(id){ active=id; renderTabs(); showView(); }

function setSrc(t,u){ load.classList.add('on'); t.frame.src=u; t.url=u; t.title=titleFor(u); }
function go(u){
  var t=cur(); if(!t) { newTab(u); return; }
  if(t.idx<t.stack.length-1) t.stack=t.stack.slice(0,t.idx+1);
  t.stack.push(u); t.idx=t.stack.length-1;
  setSrc(t,u); renderTabs(); showView();
}
function act(which){
  var t=cur(); if(!t) return;
  if(which==='back' && t.idx>0){ t.idx--; setSrc(t,t.stack[t.idx]); }
  else if(which==='fwd' && t.idx<t.stack.length-1){ t.idx++; setSrc(t,t.stack[t.idx]); }
  else if(which==='reload'){ if(t.url){ var u=t.url; t.frame.src='about:blank'; load.classList.add('on'); setTimeout(function(){t.frame.src=u;},30);} }
  else if(which==='home'){ go(START); }
  showView();
}
function navigate(v){
  v=(v||'').trim(); if(!v) return;
  var isUrl=/^https?:\/\//.test(v) || (/^[\w.-]+\.[a-z]{2,}(\/.*)?$/i.test(v) && !/\s/.test(v));
  var u = /^https?:\/\//.test(v) ? v
        : v.charAt(0)==='/' ? HOST.replace(/\/+$/,'')+v
        : isUrl ? 'https://'+v
        : HOST.replace(/\/+$/,'')+'/search?q='+encodeURIComponent(v);
  go(u);
}
function ext(){ var t=cur(); try{ window.open((t&&t.url)||HOST,'_blank'); }catch(e){} }
function toggleDark(){ document.body.classList.toggle('dark'); try{localStorage.setItem('jmb_dark',document.body.classList.contains('dark')?'1':'0');}catch(e){} closeMenu(); }
function closeMenu(){ document.getElementById('menuPop').classList.remove('show'); }

document.getElementById('back').onclick=function(){act('back');};
document.getElementById('fwd').onclick=function(){act('fwd');};
document.getElementById('rel').onclick=function(){act('reload');};
document.getElementById('home').onclick=function(){act('home');};
document.getElementById('menubtn').onclick=function(e){e.stopPropagation();document.getElementById('menuPop').classList.toggle('show');};
document.addEventListener('click',function(e){ if(!e.target.closest('#menu')) closeMenu(); });
omni.addEventListener('keydown',function(e){ if(e.key==='Enter') navigate(omni.value); });
document.addEventListener('keydown',function(e){
  if((e.ctrlKey||e.metaKey)&&e.key==='t'){e.preventDefault();newTab();}
  else if((e.ctrlKey||e.metaKey)&&e.key==='w'){e.preventDefault();if(active)closeTab(active);}
  else if((e.ctrlKey||e.metaKey)&&e.key==='l'){e.preventDefault();omni.focus();omni.select();}
  else if(((e.ctrlKey||e.metaKey)&&e.key==='r')||e.key==='F5'){e.preventDefault();act('reload');}
  else if(e.altKey&&e.key==='ArrowLeft'){e.preventDefault();act('back');}
  else if(e.altKey&&e.key==='ArrowRight'){e.preventDefault();act('fwd');}
  else if((e.ctrlKey||e.metaKey)&&e.key==='d'){e.preventDefault();toggleBookmark();}
  else if((e.ctrlKey||e.metaKey)&&(e.key==='='||e.key==='+')){e.preventDefault();zoom(0.1);}
  else if((e.ctrlKey||e.metaKey)&&e.key==='-'){e.preventDefault();zoom(-0.1);}
  else if((e.ctrlKey||e.metaKey)&&e.key==='0'){e.preventDefault();setZoom(1);}
});
document.getElementById('star').onclick=toggleBookmark;
try{ if(localStorage.getItem('jmb_dark')==='1') document.body.classList.add('dark'); }catch(e){}
function renderUser(u){
  var b=document.getElementById('acctbtn');
  if(u){ b.textContent='\\u{1F464} '+(u.name||u.username||'Account');
    b.onclick=function(e){e.stopPropagation();document.getElementById('acctPop').classList.toggle('show');};
    document.getElementById('acctName').textContent=(u.name||'')+(u.email?(' \\u00b7 '+u.email):''); }
  else { b.textContent='Sign in'; b.onclick=signIn; document.getElementById('acctPop').classList.remove('show'); }
}
function signIn(){ fetch('/signin').then(function(r){return r.json();}).then(function(d){ if(d.url) window.open(d.url,'_blank'); }); }
function signOut(){ fetch('/signout').then(function(){ renderUser(null); }); }
function pollMe(){ fetch('/me').then(function(r){return r.json();}).then(function(d){ renderUser(d.user||null); }).catch(function(){}); }
document.addEventListener('click',function(e){ if(!e.target.closest('#acct')) document.getElementById('acctPop').classList.remove('show'); });
setInterval(pollMe,2500); pollMe();
setInterval(function(){ fetch('/ping').catch(function(){}); },3000);
buildNTP();
renderBms();
(function(){ var s=[]; try{ s=JSON.parse(localStorage.getItem('jmb_session')||'[]'); }catch(e){}
  s=s.filter(function(u){return u;});
  if(s.length){ s.forEach(function(u){ newTab(u); }); } else { newTab(START); } })();
</script></body></html>
"""


def find_browser():
    if sys.platform == "darwin":
        for p in ("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
                  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
                  "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
                  "/Applications/Chromium.app/Contents/MacOS/Chromium"):
            if os.path.exists(p):
                return [p]
    if os.name == "nt":
        pf = os.environ.get("ProgramFiles", r"C:\Program Files")
        pfx = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
        for p in (os.path.join(pf, r"Google\Chrome\Application\chrome.exe"),
                  os.path.join(pfx, r"Google\Chrome\Application\chrome.exe"),
                  os.path.join(pfx, r"Microsoft\Edge\Application\msedge.exe"),
                  os.path.join(pf, r"Microsoft\Edge\Application\msedge.exe")):
            if os.path.exists(p):
                return [p]
    for name in ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser",
                 "microsoft-edge", "microsoft-edge-stable", "brave-browser"):
        p = shutil.which(name)
        if p:
            return [p]
    return None


def host():
    return (os.environ.get("JM_HOST") or DEFAULT_HOST).rstrip("/")


def _b64url(b):
    return base64.urlsafe_b64encode(b).decode().rstrip("=")


def _oauth_finish(code, redirect_uri):
    """Exchange the auth code (PKCE, no secret) and fetch userinfo."""
    data = urllib.parse.urlencode({
        "grant_type": "authorization_code", "code": code, "redirect_uri": redirect_uri,
        "client_id": OAUTH_CLIENT_ID, "code_verifier": OAUTH["verifier"] or ""}).encode()
    req = urllib.request.Request(host() + "/oauth/token", data=data, method="POST",
                                 headers={"Content-Type": "application/x-www-form-urlencoded",
                                          "Accept": "application/json"})
    tok = json.loads(urllib.request.urlopen(req, timeout=20).read())
    at = tok.get("access_token")
    if not at:
        return None
    ureq = urllib.request.Request(host() + "/oauth/userinfo", headers={"Authorization": "Bearer " + at})
    return json.loads(urllib.request.urlopen(ureq, timeout=20).read())


class Handler(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def _send(self, body, ctype="text/html; charset=utf-8", code=200):
        if isinstance(body, str):
            body = body.encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _json(self, obj, code=200):
        self._send(json.dumps(obj), "application/json", code)

    def do_GET(self):
        u = urllib.parse.urlparse(self.path)
        q = urllib.parse.parse_qs(u.query)
        OAUTH["last_ping"] = time.time()
        if u.path == "/":
            return self._send(SHELL_HTML.replace("__HOST__", host()).replace("__START__", OAUTH["start"]))
        if u.path == "/ping":
            return self._json({"ok": True})
        if u.path == "/me":
            return self._json({"user": OAUTH["user"]})
        if u.path == "/signout":
            OAUTH["user"] = None
            return self._json({"ok": True})
        if u.path == "/signin":
            OAUTH["verifier"] = secrets.token_urlsafe(48)
            OAUTH["state"] = secrets.token_urlsafe(16)
            challenge = _b64url(hashlib.sha256(OAUTH["verifier"].encode()).digest())
            redirect_uri = "http://127.0.0.1:%d/jmauth" % OAUTH["port"]
            params = urllib.parse.urlencode({
                "client_id": OAUTH_CLIENT_ID, "redirect_uri": redirect_uri, "response_type": "code",
                "scope": OAUTH_SCOPE, "state": OAUTH["state"],
                "code_challenge": challenge, "code_challenge_method": "S256"})
            return self._json({"url": host() + "/oauth/authorize?" + params})
        if u.path == "/jmauth":
            code = q.get("code", [""])[0]
            if q.get("error"):
                return self._send("<body style='font-family:sans-serif;text-align:center;padding:60px'>"
                                  "<h2>Sign-in cancelled</h2><p>You can close this tab.</p></body>", code=400)
            if not code or q.get("state", [""])[0] != OAUTH["state"]:
                return self._send("<h2>Sign-in failed (bad state)</h2>", code=400)
            try:
                info = _oauth_finish(code, "http://127.0.0.1:%d/jmauth" % OAUTH["port"])
            except Exception as exc:
                return self._send("<h2>Sign-in error</h2><pre>%s</pre>" % exc, code=502)
            if not info:
                return self._send("<h2>Sign-in failed</h2>", code=400)
            OAUTH["user"] = info
            who = info.get("name") or info.get("username") or "you"
            return self._send("<body style='font-family:sans-serif;text-align:center;padding:60px'>"
                              "<h2>&#10003; Signed in as %s</h2><p>You can close this tab and return to JM Browser.</p>"
                              "<script>setTimeout(function(){window.close();},1500);</script></body>" % who)
        return self._send("not found", code=404)


def _watchdog(httpd):
    while True:
        time.sleep(2)
        if OAUTH["started"] and time.time() - OAUTH["last_ping"] > 12:
            httpd.shutdown()
            return


def launch(start_url):
    OAUTH["start"] = start_url
    OAUTH["last_ping"] = time.time()
    httpd = None
    for port in (8717, 8718, 8719):
        try:
            httpd = ThreadingHTTPServer(("127.0.0.1", port), Handler)
            OAUTH["port"] = port
            break
        except OSError:
            continue
    if not httpd:
        print("Could not bind a local port (8717-8719); is JM Browser already running?")
        return
    url = "http://127.0.0.1:%d/" % OAUTH["port"]
    threading.Thread(target=_watchdog, args=(httpd,), daemon=True).start()
    browser = find_browser()
    if browser:
        os.makedirs(PROFILE_DIR, exist_ok=True)
        args = browser + ["--app=" + url, "--user-data-dir=" + PROFILE_DIR,
                          "--window-size=1180,820", "--no-first-run", "--no-default-browser-check"]
        if sys.platform.startswith("linux"):
            args.append("--class=JMBrowser")
        try:
            subprocess.Popen(args)
        except Exception:
            browser = None
    if not browser:
        import webbrowser
        print("Opening JM Browser at %s" % url)
        webbrowser.open(url)
    OAUTH["started"] = True
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass


def main(argv=None):
    argv = sys.argv[1:] if argv is None else argv
    if "--version" in argv:
        print("JM Browser %s" % VERSION)
        return
    if "--help" in argv or "-h" in argv:
        print(__doc__)
        print("Usage: jm-browser [url]      open JM Search (or a url) in a Chrome-like window")
        print("       jm-browser --version")
        return
    arg = next((a for a in argv if not a.startswith("-")), None)
    if arg:
        url = arg if arg.startswith(("http://", "https://")) else "https://" + arg
    else:
        url = host() + "/search"     # default: open JM Search
    launch(url)


if __name__ == "__main__":
    main()
