parent
4ad1073c6e
commit
49ddfa505d
|
|
@ -0,0 +1,365 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022-2023 Salvador E. Tropea
|
||||
# Copyright (c) 2022-2023 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPL-3.0
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
"""
|
||||
Dependencies:
|
||||
- name: KiCad PCB/SCH Diff
|
||||
version: 2.5.0
|
||||
role: mandatory
|
||||
github: INTI-CMNB/KiDiff
|
||||
command: kicad-diff.py
|
||||
pypi: kidiff
|
||||
downloader: pytool
|
||||
id: KiDiff
|
||||
- from: Git
|
||||
role: Compare with files in the repo
|
||||
- from: KiAuto
|
||||
role: Compare schematics
|
||||
version: 2.2.0
|
||||
"""
|
||||
import datetime
|
||||
import pwd
|
||||
import os
|
||||
from shutil import copy2
|
||||
from subprocess import CalledProcessError
|
||||
from tempfile import mkdtemp, NamedTemporaryFile
|
||||
from .error import KiPlotConfigurationError
|
||||
from .gs import GS
|
||||
from .kicad.color_theme import load_color_theme
|
||||
from .kiplot import load_any_sch, run_command
|
||||
from .layer import Layer
|
||||
from .out_base import VariantOptions
|
||||
from .macros import macros, document, output_class # noqa: F401
|
||||
from . import log
|
||||
|
||||
logger = log.get_logger()
|
||||
STASH_MSG = 'KiBot_Changes_Entry'
|
||||
TOOLTIP_HTML = '<div>Commit: {hash}</br>Date: {dt}</br>Author: {author}</br>Description:</br>{desc}</div>'
|
||||
# Icons for modified status
|
||||
EMPTY_IMG = ('<span class="iconify" style="padding-left: 0px; padding-right: 0px; width: 14px; height: 14px; color: #ff0000;"'
|
||||
' data-inline="false"; data-icon="bx:bx-x"></span>')
|
||||
SCH_IMG = ('<span class="iconify" style="padding-left: 0px; padding-right: 0px; width: 14px; height: 14px; color: #A6E22E;"'
|
||||
' data-inline="false"; data-icon="carbon:schematics"></span>')
|
||||
PCB_IMG = ('<span class="iconify" style="padding-left: 0px; padding-right: 0px; width: 14px; height: 14px; color: #F92672;"'
|
||||
' data-inline="false"; data-icon="codicon:circuit-board"></span>')
|
||||
TXT_IMG = ('<span class="iconify" style="padding-left: 0px; padding-right: 0px; width: 14px; height: 14px; color: #888888;"'
|
||||
' data-inline="false"; data-icon="bi:file-earmark-text"></span>')
|
||||
HASH_LOCAL = '_local_'
|
||||
|
||||
|
||||
def get_cur_user():
|
||||
try:
|
||||
name = pwd.getpwuid(os.geteuid())[4]
|
||||
return name.split(',')[0]
|
||||
except Exception:
|
||||
return 'Local user'
|
||||
|
||||
|
||||
class KiRiOptions(VariantOptions):
|
||||
def __init__(self):
|
||||
with document:
|
||||
self.output = GS.def_global_output
|
||||
""" *Filename for the output (%i=diff_pcb/diff_sch, %x=pdf) """
|
||||
self.color_theme = '_builtin_classic'
|
||||
""" *Selects the color theme. Only applies to KiCad 6.
|
||||
To use the KiCad 6 default colors select `_builtin_default`.
|
||||
Usually user colors are stored as `user`, but you can give it another name """
|
||||
super().__init__()
|
||||
self._expand_id = 'diff'
|
||||
self._expand_ext = 'pdf'
|
||||
|
||||
def get_targets(self, out_dir):
|
||||
# TODO: Implement
|
||||
return [self._parent.expand_filename(out_dir, self.output)]
|
||||
|
||||
def add_to_cache(self, name, hash):
|
||||
cmd = [self.command, '--no_reader', '--only_cache', '--old_file_hash', hash[:7], '--cache_dir', self.cache_dir,
|
||||
'--kiri_mode', '--all_pages']
|
||||
if self.incl_file:
|
||||
cmd.extend(['--layers', self.incl_file])
|
||||
if GS.debug_enabled:
|
||||
cmd.insert(1, '-'+'v'*GS.debug_level)
|
||||
cmd.extend([name, name])
|
||||
self.name_used_for_cache = name
|
||||
run_command(cmd)
|
||||
|
||||
def run_git(self, cmd, cwd=None, just_raise=False):
|
||||
if cwd is None:
|
||||
cwd = self.repo_dir
|
||||
return run_command([self.git_command]+cmd, change_to=cwd, just_raise=just_raise)
|
||||
|
||||
def git_dirty(self, file):
|
||||
return self.run_git(['status', '--porcelain', '-uno', file])
|
||||
|
||||
def remove_git_worktree(self, name):
|
||||
logger.debug('Removing temporal checkout at '+name)
|
||||
self.run_git(['worktree', 'remove', '--force', name])
|
||||
|
||||
def create_layers_incl(self, layers):
|
||||
self.incl_file = None
|
||||
if not isinstance(layers, type):
|
||||
layers = Layer.solve(layers)
|
||||
# TODO no list (ALL)
|
||||
self._solved_layers = layers
|
||||
logger.debug('Including layers:')
|
||||
with NamedTemporaryFile(mode='w', suffix='.lst', delete=False) as f:
|
||||
self.incl_file = f.name
|
||||
for la in layers:
|
||||
logger.debug('- {} ({})'.format(la.layer, la.id))
|
||||
f.write(str(la.id)+'\n')
|
||||
|
||||
def do_cache(self, name, tmp_wd, hash):
|
||||
name_copy = self.run_git(['ls-files', '--full-name', name])
|
||||
name_copy = os.path.join(tmp_wd, name_copy)
|
||||
logger.debug('- Using temporal copy: '+name_copy)
|
||||
self.add_to_cache(name_copy, hash)
|
||||
return name_copy
|
||||
|
||||
def save_pcb_layers(self, hash=None):
|
||||
subdir = os.path.join(hash[:7], '_KIRI_') if hash is not None else ''
|
||||
with open(os.path.join(self.cache_dir, subdir, 'pcb_layers'), 'wt') as f:
|
||||
for la in self._solved_layers:
|
||||
f.write(str(la.id)+'|'+la.layer+'\n')
|
||||
|
||||
def solve_layer_colors(self):
|
||||
# Color theme
|
||||
self._color_theme = load_color_theme(self.color_theme)
|
||||
if self._color_theme is None:
|
||||
raise KiPlotConfigurationError("Unable to load `{}` color theme".format(self.color_theme))
|
||||
# Assign a color if none was defined
|
||||
layer_id2color = self._color_theme.layer_id2color
|
||||
for la in self._solved_layers:
|
||||
if la._id in layer_id2color:
|
||||
la.color = layer_id2color[la._id]
|
||||
else:
|
||||
la.color = "#000000"
|
||||
|
||||
def create_layers(self, f):
|
||||
template = self.load_html_template('layers', 11)
|
||||
for i, la in enumerate(self._solved_layers):
|
||||
# TODO: Configure checked?
|
||||
checked = 'checked="checked"' if i == 0 else ''
|
||||
f.write(template.format(i=i+1, layer_id_padding='%02d' % (i+1), layer_name=la.suffix,
|
||||
layer_id=la.id, layer_color=la.color, checked=checked))
|
||||
|
||||
def save_sch_sheet(self, hash, name_sch):
|
||||
# Load the schematic. Really worth?
|
||||
sch = load_any_sch(name_sch, GS.sch_basename)
|
||||
with open(os.path.join(self.cache_dir, hash[:7], '_KIRI_', 'sch_sheets'), 'wt') as f:
|
||||
base_dir = os.path.dirname(name_sch)
|
||||
for s in sch.all_sheets:
|
||||
fname = s.fname
|
||||
no_ext = os.path.splitext(os.path.basename(fname))[0]
|
||||
rel_name = os.path.relpath(fname, base_dir)
|
||||
if s.sheet_path_h == '/':
|
||||
instance_name = sheet_path = GS.sch_basename
|
||||
else:
|
||||
instance_name = os.path.basename(s.sheet_path_h)
|
||||
sheet_path = s.sheet_path_h.replace('/', '-')
|
||||
sheet_path = GS.sch_basename+'-'+sheet_path[1:]
|
||||
f.write(f'{no_ext}|{rel_name}||{instance_name}|{sheet_path}\n')
|
||||
|
||||
def create_pages(self, f):
|
||||
template = self.load_html_template('pages', 11)
|
||||
for i, s in enumerate(sorted(GS.sch.all_sheets, key=lambda s: s.sheet_path_h)):
|
||||
fname = s.fname
|
||||
checked = 'checked="checked"' if i == 0 else ''
|
||||
base_name = os.path.basename(fname)
|
||||
rel_name = os.path.relpath(fname, GS.sch_dir)
|
||||
if s.sheet_path_h == '/':
|
||||
instance_name = sheet_path = GS.sch_basename
|
||||
else:
|
||||
instance_name = os.path.basename(s.sheet_path_h)
|
||||
sheet_path = s.sheet_path_h.replace('/', '-')
|
||||
sheet_path = GS.sch_basename+'-'+sheet_path[1:]
|
||||
f.write(template.format(i=i+1, page_name=instance_name, page_filename_path=rel_name,
|
||||
page_filename=base_name, checked=checked))
|
||||
|
||||
def load_html_template(self, type, tabs):
|
||||
""" Load a template used to generate an HTML section.
|
||||
Outside of the code for easier modification/customization. """
|
||||
with open(os.path.join(GS.get_resource_path('kiri'), f'{type}_template.html'), 'rt') as f:
|
||||
template = f.read()
|
||||
template = template.replace('${', '{')
|
||||
template = template.replace('$(printf "%02d" {i})', '{i02}')
|
||||
template = template.replace('{class}', '{cls}')
|
||||
template = template.replace('\t\t', '\t'*tabs)
|
||||
return template
|
||||
|
||||
def create_index(self, commits):
|
||||
# Get the KiRi template
|
||||
with open(os.path.join(GS.get_resource_path('kiri'), 'index.html'), 'rt') as f:
|
||||
template = f.read()
|
||||
today = datetime.datetime.today().strftime('%Y-%m-%d')
|
||||
# Replacement keys
|
||||
rep = {}
|
||||
rep['PROJECT_TITLE'] = GS.pro_basename or GS.sch_basename or GS.pcb_basename or 'unknown'
|
||||
rep['SCH_TITLE'] = GS.sch_title or 'No title'
|
||||
rep['SCH_REVISION'] = GS.sch_rev or ''
|
||||
rep['SCH_DATE'] = GS.sch_date or today
|
||||
rep['PCB_TITLE'] = GS.pcb_title or 'No title'
|
||||
rep['PCB_REVISION'] = GS.pcb_rev or ''
|
||||
rep['PCB_DATE'] = GS.pcb_date or today
|
||||
# Fill the template
|
||||
with open(os.path.join(self.cache_dir, 'web', 'index.html'), 'wt') as f:
|
||||
for ln in iter(template.splitlines()):
|
||||
for k, v in rep.items():
|
||||
ln = ln.replace(f'[{k}]', v)
|
||||
f.write(ln+'\n')
|
||||
if ln.endswith('<!-- FILL_COMMITS_HERE -->'):
|
||||
self.create_commits(f, commits)
|
||||
elif ln.endswith('<!-- FILL_PAGES_HERE -->'):
|
||||
self.create_pages(f)
|
||||
elif ln.endswith('<!-- FILL_LAYERS_HERE -->'):
|
||||
self.create_layers(f)
|
||||
|
||||
def create_commits(self, f, commits):
|
||||
template = self.load_html_template('commits', 8)
|
||||
for i, c in enumerate(commits):
|
||||
hash = c[0][:7]
|
||||
dt = c[1].split()[0]
|
||||
author = c[2]+' '
|
||||
desc = c[3]
|
||||
tooltip = TOOLTIP_HTML.format(hash=hash, dt=dt, author=author, desc=desc)
|
||||
cls = 'text-warning' if hash == HASH_LOCAL else 'text-info'
|
||||
icon_pcb = PCB_IMG if c[0] in self.commits_with_changed_pcb else EMPTY_IMG
|
||||
icon_sch = SCH_IMG if c[0] in self.commits_with_changed_sch else EMPTY_IMG
|
||||
# TODO What's this? if we only track changes in PCB/Sch this should be empty
|
||||
icon_txt = TXT_IMG
|
||||
f.write(template.format(i=i+1, hash=hash, tooltip=tooltip, text=c[3], cls=cls, i02='%02d' % (i+1),
|
||||
date=dt, user=author, pcb_icon=icon_pcb, sch_icon=icon_sch, txt_icon=icon_txt, hash_label=hash))
|
||||
|
||||
def get_modified_status(self, pcb_file, sch_files):
|
||||
res = self.run_git(['log', '--pretty=format:%H', '--', pcb_file])
|
||||
self.commits_with_changed_pcb = set(res.split())
|
||||
res = self.run_git(['log', '--pretty=format:%H', '--'] + sch_files)
|
||||
self.commits_with_changed_sch = set(res.split())
|
||||
if GS.debug_level > 1:
|
||||
logger.debug(f'Commits with changes in the PCB: {self.commits_with_changed_pcb}')
|
||||
logger.debug(f'Commits with changes in the Schematics: {self.commits_with_changed_sch}')
|
||||
|
||||
def create_kiri_files(self):
|
||||
src_dir = GS.get_resource_path('kiri')
|
||||
copy2(os.path.join(src_dir, 'redirect.html'), os.path.join(self.cache_dir, 'index.html'))
|
||||
copy2(os.path.join(src_dir, 'kiri-server'), os.path.join(self.cache_dir, 'kiri-server'))
|
||||
web_dir = os.path.join(self.cache_dir, 'web')
|
||||
os.makedirs(web_dir, exist_ok=True)
|
||||
copy2(os.path.join(src_dir, 'blank.svg'), os.path.join(web_dir, 'blank.svg'))
|
||||
copy2(os.path.join(src_dir, 'favicon.ico'), os.path.join(web_dir, 'favicon.ico'))
|
||||
copy2(os.path.join(src_dir, 'kiri.css'), os.path.join(web_dir, 'kiri.css'))
|
||||
copy2(os.path.join(src_dir, 'kiri.js'), os.path.join(web_dir, 'kiri.js'))
|
||||
|
||||
def run(self, name):
|
||||
self.cache_dir = self._parent.output_dir
|
||||
self.command = self.ensure_tool('KiDiff')
|
||||
self.git_command = self.ensure_tool('Git')
|
||||
# Get a list of files for the project
|
||||
GS.check_sch()
|
||||
sch_files = GS.sch.get_files()
|
||||
self.repo_dir = GS.sch_dir
|
||||
GS.check_pcb()
|
||||
# Get a list of hashes where we have changes
|
||||
# TODO implement a limit -n X
|
||||
res = self.run_git(['log', "--date=format:%Y-%m-%d %H:%M:%S", '--pretty=format:%H | %ad | %an | %s', '--',
|
||||
GS.pcb_file] + sch_files)
|
||||
hashes = [r.split(' | ') for r in res.split('\n')]
|
||||
self.create_layers_incl(self.layers)
|
||||
self.solve_layer_colors()
|
||||
# Get more information about what is changed
|
||||
self.get_modified_status(GS.pcb_file, sch_files)
|
||||
# TODO ensure we have at least 2
|
||||
try:
|
||||
git_tmp_wd = None
|
||||
try:
|
||||
for h in hashes:
|
||||
hash = h[0]
|
||||
git_tmp_wd = mkdtemp()
|
||||
logger.debug('Checking out '+hash+' to '+git_tmp_wd)
|
||||
self.run_git(['worktree', 'add', git_tmp_wd, hash])
|
||||
self.run_git(['submodule', 'update', '--init', '--recursive'], cwd=git_tmp_wd)
|
||||
# Generate SVGs for the schematic
|
||||
name_sch = self.do_cache(GS.sch_file, git_tmp_wd, hash)
|
||||
# Generate SVGs for the PCB
|
||||
self.do_cache(GS.pcb_file, git_tmp_wd, hash)
|
||||
# List of layers
|
||||
self.save_pcb_layers(hash)
|
||||
# Schematic hierarchy
|
||||
self.save_sch_sheet(hash, name_sch)
|
||||
self.remove_git_worktree(git_tmp_wd)
|
||||
git_tmp_wd = None
|
||||
finally:
|
||||
if git_tmp_wd:
|
||||
self.remove_git_worktree(git_tmp_wd)
|
||||
# Do we have modifications?
|
||||
sch_dirty = self.git_dirty(GS.sch_file)
|
||||
pcb_dirty = self.git_dirty(GS.pcb_file)
|
||||
if sch_dirty or pcb_dirty:
|
||||
# Include the current files
|
||||
name_sch = self.do_cache(GS.sch_file, GS.sch_dir, HASH_LOCAL)
|
||||
self.save_sch_sheet(HASH_LOCAL, name_sch)
|
||||
self.do_cache(GS.pcb_file, GS.pcb_dir, HASH_LOCAL)
|
||||
self.save_pcb_layers(HASH_LOCAL)
|
||||
hashes.insert(0, (HASH_LOCAL, datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S'), get_cur_user(),
|
||||
'Local changes not committed'))
|
||||
if pcb_dirty:
|
||||
self.commits_with_changed_pcb.add(HASH_LOCAL)
|
||||
if sch_dirty:
|
||||
self.commits_with_changed_sch.add(HASH_LOCAL)
|
||||
finally:
|
||||
if self.incl_file:
|
||||
os.remove(self.incl_file)
|
||||
self.create_kiri_files()
|
||||
self.create_index(hashes)
|
||||
|
||||
|
||||
@output_class
|
||||
class KiRi(BaseOutput): # noqa: F821
|
||||
""" KiRi
|
||||
Generates a PDF with the differences between two PCBs or schematics.
|
||||
Recursive git submodules aren't supported (submodules inside submodules) """
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._category = ['PCB/docs', 'Schematic/docs']
|
||||
self._both_related = True
|
||||
with document:
|
||||
self.options = KiRiOptions
|
||||
""" *[dict] Options for the `diff` output """
|
||||
self.layers = Layer
|
||||
""" *[list(dict)|list(string)|string] [all,selected,copper,technical,user]
|
||||
List of PCB layers to use. When empty all available layers are used.
|
||||
Note that if you want to support adding/removing layers you should specify a list here """
|
||||
|
||||
@staticmethod
|
||||
def layer2dict(la):
|
||||
return {'layer': la.layer, 'suffix': la.suffix, 'description': la.description}
|
||||
|
||||
@staticmethod
|
||||
def has_repo(git_command, file):
|
||||
try:
|
||||
run_command([git_command, 'ls-files', '--error-unmatch', file], change_to=os.path.dirname(file), just_raise=True)
|
||||
except CalledProcessError:
|
||||
logger.debug("File `{}` not inside a repo".format(file))
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_conf_examples(name, layers):
|
||||
outs = []
|
||||
git_command = GS.check_tool(name, 'Git')
|
||||
# TODO: Implement
|
||||
if (GS.pcb_file and GS.sch_file and KiRi.has_repo(git_command, GS.pcb_file) and
|
||||
KiRi.has_repo(git_command, GS.sch_file)):
|
||||
gb = {}
|
||||
gb['name'] = 'basic_{}'.format(name)
|
||||
gb['comment'] = 'Interactive diff between commits'
|
||||
gb['type'] = name
|
||||
gb['dir'] = 'diff'
|
||||
gb['layers'] = [KiRi.layer2dict(la) for la in layers]
|
||||
outs.append(gb)
|
||||
return outs
|
||||
|
||||
def run(self, name):
|
||||
self.options.layers = self.layers
|
||||
super().run(name)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 210 297"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="blank.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="1.4285714"
|
||||
inkscape:cy="548.57143"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2048"
|
||||
inkscape:window-height="1088"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Commit ${i} -->
|
||||
<input class="chkGroup" type="checkbox" id="${hash}" name="commit" value="${hash}" onchange="update_commits()">
|
||||
<label class="text-sm-left list-group-item" style="display: block; width: 445px; margin-left: 0px;" for="${hash}">
|
||||
<table data-toggle="tooltip" title="${tooltip}">
|
||||
<tr>
|
||||
<td rowspan=2 style="vertical-align: top; width: 1.8em;">
|
||||
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="15" height="15">
|
||||
<path d="M7.5 10.5a3 3 0 010-6m0 6a3 3 0 000-6m0 6V15m0-10.5V0" stroke="currentColor"></path>
|
||||
</svg>
|
||||
</td>
|
||||
<td style="white-space:nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<span class="text-muted"> $(printf "%02d" ${i}) | </span> <span class="text-success font-weight-normal">${hash_label}</span> <span class="text-muted"> | </span> ${sch_icon} ${pcb_icon} ${txt_icon} <span class="text-muted font-weight-normal"> | ${date} | ${user}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<em class="${class}" style=" line-height: 0.7;">${text}</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</label>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -0,0 +1,319 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.rawgit.com/ccampbell/mousetrap/825ce50c/mousetrap.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="kiri.css" integrity="">
|
||||
<title>[PROJECT_TITLE]</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="server_offline" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div class="container fill no-gutters" style="background-color: #333; overflow-y: hidden; overflow-x: hidden; padding: 0px 10px 0px 10px; margin: 0px;">
|
||||
<div class="row fill align-items-start no-gutters" style="overflow-y: hidden; overflow-x: hidden; padding: 0px 0px 0px 0px; margin: 0px;">
|
||||
<div class="col align-self-start justify-content-start col-3 no-gutters" style="height: 100%; overflow-y: hidden; overflow-x: hidden; padding: 0px 0px 0px 0px; margin: 0px;">
|
||||
<div class="list-group 3 no-gutters" style="padding: 0px 0px 0px 0px; margin: 0px 0px 0px 8px; height: 95%;">
|
||||
<span style="margin-top: 1em"></span>
|
||||
<h3 class="text-light" style="margin-left: 5px;">Kicad Revision Inspector</h3>
|
||||
<div id="sch_title" style="display: inline; margin-left: 5px;">
|
||||
<h4 class="text-warning text-bold" style="margin-bottom: 0px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">[SCH_TITLE]</h4>
|
||||
<p class="text-light text-sm" >Rev. [SCH_REVISION] ([SCH_DATE])</p>
|
||||
</div>
|
||||
<div id="pcb_title" style="display: none; margin-left: 5px;">
|
||||
<h4 class="text-info text-bold" style="margin-bottom: 0px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">[PCB_TITLE]</h4>
|
||||
<p class="text-light text-sm">Rev. [PCB_REVISION] ([PCB_DATE])</p>
|
||||
</div>
|
||||
<p class="text-light" >
|
||||
<span style="margin-left:5px; margin-right:0.5em" class="iconify" data-icon="fa-solid:code-branch" data-inline="false"></span>
|
||||
Commits
|
||||
</p>
|
||||
<div class="form-check scrollbox" style="padding: 0px 4px 8px 0px; margin: 0px; margin-right: 4px; overflow-y: scroll">
|
||||
<form id="commits_form" class="overflow-auto scrollbox-content">
|
||||
<!-- FILL_COMMITS_HERE -->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col align-self-start justify-content-start col-7 no-gutters" style="background-color: #333; padding: 0px 0px 0px 0px; margin: 0px; height: 90vh;">
|
||||
|
||||
<div class="row no-gutters" style="width: 100%; padding: 0px 0px 0px 0px; margin: 0px;">
|
||||
|
||||
<div class="container no-gutters" style="width: 100%; flex: 1; padding: 0px 0px 0px 0px; margin: 0px;">
|
||||
|
||||
<div class="row no-gutters" style="width: 100%; margin: 0px 0px 0px 5px;">
|
||||
|
||||
<form class="inline" style="padding: 0px 0px 0px 0px; margin: 20px 0px 0px 10px;">
|
||||
<div id="view_mode" class="form-group row btn-group btn-group-toggle" data-toggle="buttons" role="group" aria-label="View Mode">
|
||||
<label id="show_sch_lbl" data-toggle="tooltip" title="Schematic View (s)" class="btn btn-secondary active" onclick="show_sch()">
|
||||
<input id="show_sch" type="radio" name="view_mode" value="show_sch" checked>
|
||||
<span class="iconify" style="width: 20px; height: 20px;" data-icon="carbon:schematics" data-inline="false"></span>
|
||||
</label>
|
||||
<label id="show_pcb_lbl" data-toggle="tooltip" title="Layout View (s)" class="btn btn-secondary" onclick="show_pcb()">
|
||||
<input id="show_pcb" type="radio" name="view_mode" value="show_pcb" >
|
||||
<span class="iconify" style="width: 20px; height: 20px;" data-icon="codicon:circuit-board" data-inline="false"></span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<span style="width: 2em"></span>
|
||||
|
||||
<form class="inline" style="padding: 0px 0px 0px 0px; margin: 20px 0px 0px 20px;">
|
||||
<div id="svg_controls" class="form-group btn-group btn-group-toggle" role="group" aria-label="Zoom Level">
|
||||
<button id="zoom-in" data-toggle="tooltip" title="Zoom In (+)" name="svg_controls" type="button" aria-pressed="false" class="btn btn-secondary" onclick="this.blur();">
|
||||
<span class="iconify" style="width: 20px; height: 20px;" data-icon="akar-icons:zoom-in" data-inline="false"></span>
|
||||
</button>
|
||||
<button id="zoom-out" data-toggle="tooltip" title="Zoom Out (-)" name="svg_controls" type="button" aria-pressed="false" class="btn btn-secondary" onclick="this.blur();" >
|
||||
<span class="iconify" style="width: 20px; height: 20px;" data-icon="akar-icons:zoom-out" data-inline="false"></span>
|
||||
</button>
|
||||
<button id="zoom-fit" data-toggle="tooltip" title="Fit View (0)" name="svg_controls" type="button" aria-pressed="false" class="btn btn-secondary" onclick="this.blur();" >
|
||||
<span class="iconify" style="width: 20px; height: 20px;" data-icon="carbon:center-to-fit" data-inline="false"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<span style="width: 1em"></span>
|
||||
|
||||
<form class="inline" style="padding: 0px 0px 0px 0px; margin: 20px 0px 0px 20px;">
|
||||
<div id="info_controls" class="form-group btn-group btn-group-toggle" role="group" aria-label="Info">
|
||||
<button id="info-btn" data-toggle="modal" title="Info (i)" name="info_controls" type="button" aria-pressed="false" class="btn btn-secondary" data-target="#shortcuts-modal" onclick="this.blur();">
|
||||
<span class="iconify" style="width: 20px; height: 20px;" data-icon="akar-icons:info" data-inline="false"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col no-gutters" style="width: 100%; padding: 0px 0px 0px 0px; margin: 0px; height: 100%; ">
|
||||
<div class="col no-gutters" style="width: 100%; height: 90%; padding: 0px 0px 0px 0px; margin: 0px;">
|
||||
<div id="legend" class="rounded no-gutters" style="background-color: #222; width: 100%; padding: 8px 5px 8px 5px; margin: 0px;">
|
||||
<iframe name="hidden_post_target" class="hidden_iframe"></iframe>
|
||||
<span id=commit1_legend style="margin-left:0.5em; margin-right:0.2em; color: #00FFFF; width: 10px; height: 10px;" class="iconify" data-icon="teenyicons-square-solid"></span>
|
||||
<small id="commit1_legend_text" class="text-sm text-light">
|
||||
Newer
|
||||
<span class="text-monospace">
|
||||
<form id="KICAD_COMMIT_1" class="form-inline" style="display: inline;" action="index.html" method="post" target="hidden_post_target">
|
||||
<input id="commit1_hash" type="hidden" name="hash" value="[KICAD_COMMIT_1]">
|
||||
<input id="commit1_kicad_pro_path" type="hidden" name="kicad_pro_path" value="[COMMIT_1_KICAD_PRO]">
|
||||
(<a data-toggle="tooltip" title="Launch Kicad at this Rev" id="commit1_legend_hash" href="javascript:{}" onclick="document.getElementById('KICAD_COMMIT_1').submit();">[COMMIT_1_HASH]</a>)
|
||||
</form>
|
||||
</span>
|
||||
</small>
|
||||
<span style="display: inline; width: 2em;"></span>
|
||||
<span id="commit2_legend" style="display: inline; margin-left:1em; margin-right:0.2em; color: #880808; width: 10px; height: 10px;" class="iconify" data-icon="teenyicons-square-solid"></span>
|
||||
<small id="commit2_legend_text" class="text-sm text-light">
|
||||
Older
|
||||
<span class="text-monospace">
|
||||
<form id="KICAD_COMMIT_2" class="form-inline" style="display: inline;" action="index.html" method="post" target="hidden_post_target">
|
||||
<input id="commit2_hash" type="hidden" name="hash" value="[KICAD_COMMIT_2]">
|
||||
<input id="commit2_kicad_pro_path" type="hidden" name="kicad_pro_path" value="[COMMIT_2_KICAD_PRO]">
|
||||
(<a data-toggle="tooltip" title="Launch Kicad at this Rev" id="commit2_legend_hash" href="javascript:{}" onclick="document.getElementById('KICAD_COMMIT_2').submit();">[COMMIT_2_HASH]</a>)
|
||||
</form>
|
||||
</span>
|
||||
</small>
|
||||
<span id="commit3_legend" style="margin-left:1em; margin-right:0.2em; color: #807F7F; width: 10px; height: 10px;" class="iconify" data-icon="teenyicons-square-solid"></span>
|
||||
<small id="commit3_legend_text" class="text-sm text-light">
|
||||
Unchanged
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div id="diff-container" class="position-relative" style="height: 94%; padding: 0px">
|
||||
<!-- SVGS_GOES_HERE -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col align-self-start justify-content-start col-1 no-gutters" style="width: 100%; padding: 0px 0px 0px 0px; margin: 0px; height: 100vh;" >
|
||||
|
||||
<div class="list-group no-gutters" id="pages_list" style="display: inline;">
|
||||
<div class="form-check no-gutters" style="width: 180px; padding-top: 1em">
|
||||
<p class="text-light">
|
||||
<span style="margin-right:0.5em; width: 20px; height: 20px;" class="iconify" data-icon="gridicons:next-page" data-inline="false"></span>
|
||||
Pages
|
||||
</p>
|
||||
<div class="row" style="height: 85%; width: 300px;">
|
||||
<div class="col" style="height: 100%; width: 100%">
|
||||
<div id="pages_list_div" class="form-check scrollbox" style="height: 100%; width: 100%; padding: 0px 0px 8px 0px; margin: 0px; overflow-y: scroll; max-height: 90vh;">
|
||||
<form id="pages_list_form" class="overflow-auto scrollbox-content">
|
||||
<!-- FILL_PAGES_HERE -->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group no-gutters" id="layers_list" style="display: none; height: 100%;">
|
||||
<div class="form-check no-gutters" style="width: 180px; padding-top: 1em; height: 100%; ">
|
||||
<p class="text-light">
|
||||
<span style="margin-right:0.5em; width: 20px; height: 20px;" class="iconify" data-icon="teenyicons:layers-solid" data-inline="false"></span>
|
||||
Layers
|
||||
</p>
|
||||
<div class="row" style="height: 85%; width: 300px;">
|
||||
<div class="col" style="height: 100%; width: 100%">
|
||||
<div id="layers_list_div" class="form-check scrollbox" style="height: 100%; width: 100%; padding: 0px 0px 8px 0px; margin: 0px; overflow-y: scroll; max-height: 90vh;">
|
||||
<form id="layers_list_form" class="overflow-auto scrollbox-content">
|
||||
<!-- FILL_LAYERS_HERE -->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade text-light" id="shortcuts-modal" tabindex="-1" role="dialog" aria-labelledby="shortcuts-modal-title" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="shortcuts-modal-title">Shortcuts</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<h5>Commits List</h5>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd><span class="iconify" data-icon="typcn-arrow-up" data-inline="false"></span></kbd></small></td>
|
||||
<td>Move commits par upwards</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd><span class="iconify" data-icon="typcn-arrow-down" data-inline="false"></span></kbd></small></td>
|
||||
<td>Move commits par downwards</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>[</kbd> or <kbd>Ctrl + <span class="iconify" data-icon="typcn-arrow-up" data-inline="false"></span></kbd></small></td>
|
||||
<td>Move 2nd commit upwards</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>]</kbd> or <kbd>Ctrl + <span class="iconify" data-icon="typcn-arrow-down" data-inline="false"></span></kbd></small></td>
|
||||
<td>Move 2nd commit downwards</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>r</kbd></small></td>
|
||||
<td>Reset commit selection to the top 2 commits</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr class="hr_shortcuts">
|
||||
<h5>View, sheets and layers</h5>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>s</kbd></small></td>
|
||||
<td>Switch Schematic/Layout view</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>q</kbd></small></td>
|
||||
<td>Toggle visibility of newer commit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>w</kbd></small></td>
|
||||
<td>Toggle visibility of older commit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd><span class="iconify" data-icon="typcn-arrow-right" data-inline="false"></span></kbd></small></td>
|
||||
<td>Select next page/layer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd><span class="iconify" data-icon="typcn-arrow-left" data-inline="false"></span></kbd></small></td>
|
||||
<td>Select previous page/layer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>Ctrl + <span class="iconify" data-icon="typcn-arrow-right" data-inline="false"></span></kbd></small></td>
|
||||
<td>Select next page/layer (cycling)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>Ctrl + <span class="iconify" data-icon="typcn-arrow-left" data-inline="false"></span></kbd></small></td>
|
||||
<td>Select previous page/layer (cycling)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr class="hr_shortcuts">
|
||||
<h5>Diff Pan and Zoom</h5>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>+</kbd></small></td>
|
||||
<td>Zoom in</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>-</kbd></small></td>
|
||||
<td>Zoom out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>0</kbd></small></td>
|
||||
<td>Zoom fit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>Alt + <span class="iconify" data-icon="typcn-arrow-up" data-inline="false"></span></kbd></small></td>
|
||||
<td>Pan svg up</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>Alt + <span class="iconify" data-icon="typcn-arrow-down" data-inline="false"></span></kbd></small></td>
|
||||
<td>Pan svg down</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>Alt + <span class="iconify" data-icon="typcn-arrow-left" data-inline="false"></span></kbd></small></td>
|
||||
<td>Pan svg left</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>Alt + <span class="iconify" data-icon="typcn-arrow-right" data-inline="false"></span></kbd></small></td>
|
||||
<td>Pan svg right</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr class="hr_shortcuts">
|
||||
<h5>Miscellaneous</h5>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>f</kbd></small></td>
|
||||
<td>Toggle full screen view</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut_col"><small class="text-sm text-muted"><kbd>i</kbd></small></td>
|
||||
<td>Shows this info view</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<small class="text-sm">
|
||||
<a href="https://github.com/leoheck/kiri" target="_blank">Kicad Revision Inspector (KiRI)</a> by Leandro Heck
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script>
|
||||
<script src="https://code.iconify.design/2/2.2.1/iconify.min.js" integrity=""></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js" integrity=""></script>
|
||||
<script src="./kiri.js" integrity="" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
import sys
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
|
||||
import webbrowser
|
||||
import socketserver
|
||||
|
||||
from subprocess import PIPE, Popen
|
||||
from typing import List, Tuple
|
||||
|
||||
from urllib.parse import unquote
|
||||
|
||||
www_dir_path = None
|
||||
httpd = None
|
||||
default_port = 8080
|
||||
|
||||
|
||||
class WebServerHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(
|
||||
*args, directory=os.path.realpath(www_dir_path, **kwargs)
|
||||
)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
try:
|
||||
|
||||
self.send_response(200)
|
||||
# self.send_header('Location', self.path)
|
||||
self.end_headers()
|
||||
|
||||
content_len = int(self.headers.get('content-length', 0))
|
||||
post_body = unquote(self.rfile.read(content_len)).split("&")
|
||||
|
||||
# with open("post.txt","a") as f:
|
||||
# f.write(str(post_body))
|
||||
# f.write("\n")
|
||||
|
||||
commit_hash = post_body[0].split("=")[1]
|
||||
kicad_pro_path = post_body[1].split("=")[1]
|
||||
|
||||
cmd = "kicad_rev {}".format(kicad_pro_path)
|
||||
print("Cmd:", cmd)
|
||||
process = subprocess.Popen(cmd.split(" "), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
# self._set_response()
|
||||
# self.wfile.write("POST request for {}".format(self.path).encode('utf-8'))
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
httpd.server_close()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def parse_cli_args():
|
||||
parser = argparse.ArgumentParser(description="Kicad PCB visual diffs.")
|
||||
parser.add_argument(
|
||||
"-d", "--display", type=str, help="Set DISPLAY value, default :1.0", default=":1.0",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--nested", action='store_true', help="Fix paths when it was a nested project"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--port", type=int, help="Force the weverver on an specific port"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--port_range", type=int, default=10, help="Port range to try"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-S", "--server-only", action='store_true', help="Port range to try"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--webserver-disable",
|
||||
action="store_true",
|
||||
help="Does not execute webserver (just generate images)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="count", default=0, help="Increase verbosity (-vvv)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i", "--ip", type=str, help="Override default IP address", default="127.0.0.1",
|
||||
)
|
||||
parser.add_argument(
|
||||
"www_dir", metavar="WWW_DIR", help="A folder with web/index.html inside"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose >= 3:
|
||||
print("")
|
||||
print("Command Line Arguments")
|
||||
print(args)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def launch_webserver(ip, request_handler, port, kicad_project, server_only):
|
||||
|
||||
global httpd
|
||||
httpd = socketserver.TCPServer(("", port), request_handler)
|
||||
|
||||
with httpd:
|
||||
if args.nested:
|
||||
url = "{ip}:{port}/{nested_project}/web/index.html".format(
|
||||
ip=ip,
|
||||
port=str(port),
|
||||
nested_project=kicad_project
|
||||
)
|
||||
else:
|
||||
url = "http://{ip}:{port}/web/index.html".format(
|
||||
ip=ip,
|
||||
port=str(port)
|
||||
)
|
||||
|
||||
print("")
|
||||
print("Starting webserver at {}".format(url))
|
||||
print("(Hit Ctrl+C to exit)")
|
||||
if not server_only:
|
||||
webbrowser.open(url)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
def run_cmd(path: str, cmd: List[str]) -> Tuple[str, str]:
|
||||
|
||||
p = Popen(
|
||||
cmd,
|
||||
stdin=PIPE,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
close_fds=True,
|
||||
encoding="utf-8",
|
||||
cwd=path,
|
||||
)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
p.wait()
|
||||
|
||||
return stdout.strip("\n "), stderr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
args = parse_cli_args()
|
||||
|
||||
if args.www_dir:
|
||||
|
||||
www_dir_path = os.path.abspath(args.www_dir)
|
||||
kicad_project = ".."
|
||||
|
||||
# Assume it is running outside of the webserver the folder
|
||||
index_html = os.path.realpath(os.path.join(www_dir_path, "web", "index.html"))
|
||||
if not os.path.exists(index_html):
|
||||
print("Could not find index.html")
|
||||
exit(1)
|
||||
|
||||
if args.verbose:
|
||||
print("")
|
||||
print("www_dir_path:", www_dir_path)
|
||||
print("kicad_project:", kicad_project)
|
||||
print("index_html:", index_html)
|
||||
print("")
|
||||
|
||||
else:
|
||||
print("www directory is missing")
|
||||
exit(1)
|
||||
|
||||
if not args.webserver_disable:
|
||||
|
||||
socketserver.TCPServer.allow_reuse_address = True
|
||||
request_handler = WebServerHandler
|
||||
|
||||
if args.port:
|
||||
try:
|
||||
launch_webserver(args.ip, request_handler, args.port, kicad_project, args.server_only)
|
||||
except Exception:
|
||||
print("Specified port {port} is in use".format(port=port))
|
||||
pass
|
||||
else:
|
||||
for i in range(args.port_range):
|
||||
try:
|
||||
port = default_port + i
|
||||
launch_webserver(args.ip, request_handler, port, kicad_project, args.server_only)
|
||||
except Exception:
|
||||
# print("Specified port {port} is in use".format(port=port))
|
||||
pass
|
||||
print("Specified ports are in use.")
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
|
||||
html, body {
|
||||
height : 100%;
|
||||
margin : 0px;
|
||||
font-size : 1em; /* Normalize view on firefox */
|
||||
line-height : 1.5em; /* Normalize view on firefox */
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.fill {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-gutters {
|
||||
margin-right: 0px;
|
||||
margin-left: 0px;
|
||||
> .col,
|
||||
> [class*="col-"] {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
Commits List
|
||||
==============================
|
||||
*/
|
||||
|
||||
.list-group-item {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.list-group input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-group input[type="checkbox"] + .list-group-item {
|
||||
background-color: #222;
|
||||
color: #dbdbdb;
|
||||
font-family: monospace;
|
||||
padding: 1px;
|
||||
margin: 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.list-group input[type="checkbox"] + .list-group-item:before {
|
||||
color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.list-group input[type="checkbox"]:checked + .list-group-item {
|
||||
background-color: #111;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.list-group input[type="checkbox"]:checked + .list-group-item:before {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
Scrollbar
|
||||
==============================
|
||||
*/
|
||||
|
||||
.scrollbox {
|
||||
overflow: auto;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.scrollbox-content,
|
||||
.scrollbox:hover,
|
||||
.scrollbox:focus {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(90, 90, 90);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
Toolbar buttons
|
||||
==============================
|
||||
*/
|
||||
|
||||
.btn-group input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-group input[type="radio"] + .btn-group-item {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.btn-group input[type="radio"] + .btn-group-item:before {
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
Layers List
|
||||
==============================
|
||||
*/
|
||||
|
||||
.list-group input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-group input[type="radio"] + .list-group-item {
|
||||
background-color: #222222;
|
||||
color: #dbdbdb;
|
||||
font-family: monospace;
|
||||
padding: 2px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.list-group input[type="radio"] + .list-group-item:before {
|
||||
color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.list-group input[type="radio"]:checked + .list-group-item {
|
||||
background-color: #111111;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.list-group input[type="radio"]:checked + .list-group-item:before {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
==============================
|
||||
*/
|
||||
|
||||
#diff-container {
|
||||
/* border: 1px solid #111;*/
|
||||
margin-top: 15px;
|
||||
background-color: #222;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#svg-id {
|
||||
/* border: 1px solid #111;*/
|
||||
margin-top: 15px;
|
||||
background-color: #222;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
Layers colors
|
||||
==============================
|
||||
*/
|
||||
|
||||
.F_Cu {
|
||||
filter : invert(28%) sepia(50%) saturate(2065%) hue-rotate(334deg) brightness(73%) contrast(97%);
|
||||
}
|
||||
|
||||
.In1_Cu {
|
||||
filter : invert(69%) sepia(39%) saturate(1246%) hue-rotate(17deg) brightness(97%) contrast(104%);
|
||||
}
|
||||
|
||||
.In2_Cu {
|
||||
filter : invert(14%) sepia(79%) saturate(5231%) hue-rotate(293deg) brightness(91%) contrast(119%);
|
||||
}
|
||||
|
||||
.In3_Cu {
|
||||
filter : invert(14%) sepia(79%) saturate(5231%) hue-rotate(293deg) brightness(91%) contrast(119%);
|
||||
}
|
||||
|
||||
.In4_Cu {
|
||||
filter : invert(14%) sepia(79%) saturate(5231%) hue-rotate(293deg) brightness(91%) contrast(119%);
|
||||
}
|
||||
|
||||
.B_Cu {
|
||||
filter : invert(44%) sepia(14%) saturate(2359%) hue-rotate(70deg) brightness(103%) contrast(82%);
|
||||
}
|
||||
|
||||
.F_Mask {
|
||||
filter : invert(27%) sepia(51%) saturate(1920%) hue-rotate(269deg) brightness(89%) contrast(96%);
|
||||
}
|
||||
|
||||
.B_Mask {
|
||||
filter : invert(22%) sepia(56%) saturate(2652%) hue-rotate(277deg) brightness(94%) contrast(87%);
|
||||
}
|
||||
|
||||
.F_Paste {
|
||||
filter : invert(57%) sepia(60%) saturate(6%) hue-rotate(314deg) brightness(92%) contrast(99%);
|
||||
}
|
||||
|
||||
.B_Paste {
|
||||
filter : invert(91%) sepia(47%) saturate(4033%) hue-rotate(139deg) brightness(82%) contrast(91%);
|
||||
}
|
||||
|
||||
.F_SilkS {
|
||||
filter : invert(46%) sepia(44%) saturate(587%) hue-rotate(132deg) brightness(101%) contrast(85%);
|
||||
}
|
||||
|
||||
.B_SilkS {
|
||||
filter : invert(14%) sepia(27%) saturate(2741%) hue-rotate(264deg) brightness(95%) contrast(102%);
|
||||
}
|
||||
|
||||
.Edge_Cuts {
|
||||
filter : invert(79%) sepia(79%) saturate(401%) hue-rotate(6deg) brightness(88%) contrast(88%);
|
||||
}
|
||||
|
||||
.Margin {
|
||||
filter : invert(74%) sepia(71%) saturate(5700%) hue-rotate(268deg) brightness(89%) contrast(84%);
|
||||
}
|
||||
|
||||
.Dwgs_User {
|
||||
filter : invert(40%) sepia(68%) saturate(7431%) hue-rotate(203deg) brightness(89%) contrast(98%);
|
||||
}
|
||||
|
||||
.Cmts_User {
|
||||
filter : invert(73%) sepia(10%) saturate(1901%) hue-rotate(171deg) brightness(95%) contrast(102%);
|
||||
}
|
||||
|
||||
.Eco1_User {
|
||||
filter : invert(25%) sepia(98%) saturate(2882%) hue-rotate(109deg) brightness(90%) contrast(104%);
|
||||
}
|
||||
|
||||
.Eco2_User {
|
||||
filter : invert(85%) sepia(21%) saturate(5099%) hue-rotate(12deg) brightness(91%) contrast(102%);
|
||||
}
|
||||
|
||||
.F_Fab {
|
||||
filter : invert(71%) sepia(21%) saturate(4662%) hue-rotate(21deg) brightness(103%) contrast(100%);
|
||||
}
|
||||
|
||||
.B_Fab {
|
||||
filter : invert(60%) sepia(0%) saturate(0%) hue-rotate(253deg) brightness(87%) contrast(90%);
|
||||
}
|
||||
|
||||
.F_Adhes {
|
||||
filter : invert(38%) sepia(49%) saturate(1009%) hue-rotate(254deg) brightness(88%) contrast(86%);
|
||||
}
|
||||
|
||||
.B_Adhes {
|
||||
filter : invert(24%) sepia(48%) saturate(2586%) hue-rotate(218deg) brightness(88%) contrast(92%);
|
||||
}
|
||||
|
||||
.F_CrtYd {
|
||||
filter : invert(73%) sepia(1%) saturate(0%) hue-rotate(116deg) brightness(92%) contrast(91%);
|
||||
}
|
||||
|
||||
.B_CrtYd {
|
||||
filter : invert(79%) sepia(92%) saturate(322%) hue-rotate(3deg) brightness(89%) contrast(92%);
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
** ============================*/
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
label#layers, label#pages {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
vertical-align:top;
|
||||
}
|
||||
|
||||
#server_offline {
|
||||
position: fixed;
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
** ============================*/
|
||||
|
||||
a:link {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
#commit1_legend_hash {
|
||||
color: #28a745;
|
||||
}
|
||||
#commit2_legend_hash {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
** ============================*/
|
||||
|
||||
.hidden_iframe {
|
||||
position:absolute; top:-1px; left:-1px; width:1px; height:1px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.shortcut_col {
|
||||
width: 150px;
|
||||
text-align: right;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: white;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,6 @@
|
|||
<!-- Layer ${i} -->
|
||||
<input id="layer-${layer_id_padding}" value="layer-${layer_name}" type="radio" name="layers" onchange="change_layer()" ${checked}>
|
||||
<label for="layer-${layer_id_padding}" id="label-layer-${layer_id_padding}" data-toggle="tooltip" title="${layer_id}, ${layer_name}" class="rounded text-sm-left list-group-item radio-box" onclick="change_layer_onclick()">
|
||||
<span style="margin-left:0.5em; margin-right:0.1em; color: ${layer_color}" class="iconify" data-icon="teenyicons-square-solid" data-inline="false"></span>
|
||||
${layer_name}
|
||||
</label>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<!-- Page ${i} -->
|
||||
<input id="${page_name}" data-toggle="tooltip" title="${page_filename_path}" type="radio" value="${page_filename}" name="pages" ${checked} onchange="change_page()">
|
||||
<label for="${page_name}" data-toggle="tooltip" title="${page_filename_path}" id="label-${page_name}" class="rounded text-sm-left list-group-item radio-box" onclick="change_page_onclick()" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<span data-toggle="tooltip" title="${page_filename_path}" style="margin-left:0.5em; margin-right:0.1em;" class="iconify" data-icon="gridicons:pages" data-inline="false"></span>
|
||||
${page_name}
|
||||
</label>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv='refresh' content='0; URL=/web/index.html'>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue