Experimental KiRi integration

Very raw, but working
This commit is contained in:
Salvador E. Tropea 2023-11-30 11:06:32 -03:00
parent 4ad1073c6e
commit 49ddfa505d
11 changed files with 2961 additions and 0 deletions

365
kibot/out_kiri.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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">&times;</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>

204
kibot/resources/kiri/kiri-server Executable file
View File

@ -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.")

View File

@ -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;
}

1632
kibot/resources/kiri/kiri.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='refresh' content='0; URL=/web/index.html'>
</head>
<body>
</body>
</html>