@@ -0,0 +1,11 @@ | |||
name = "JLD2View" | |||
uuid = "a494b24a-6d3b-4871-b66b-22839c4c06d4" | |||
authors = ["francois "] | |||
version = "0.1.0" | |||
[deps] | |||
Electron = "a1bb12fb-d4d1-54b4-b10a-ee7951ef7ad3" | |||
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" | |||
Mustache = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70" | |||
NativeFileDialog = "e1fe445b-aa65-4df4-81c1-2041507f0fd4" | |||
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" |
@@ -0,0 +1,3 @@ | |||
[deps] | |||
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" | |||
Scratch = "6c6a2e73-6563-6170-7368-637461726353" |
@@ -0,0 +1,38 @@ | |||
using Pkg; Pkg.activate(@__DIR__); Pkg.instantiate() | |||
using PackageCompiler | |||
using Scratch | |||
Pkg.activate(joinpath(@__DIR__, "..")) | |||
using JLD2View | |||
scratchdir = get_scratch!(JLD2View, "sysimages") | |||
# List of packages to include in the sysimage | |||
packages = :JLD2View #Symbol.(keys(Pkg.project().dependencies)) | |||
# Sysimage base name | |||
sysimage_name = "sysimage" | |||
# Sysimage extension | |||
sysimage_ext = if Sys.iswindows() | |||
".dll" | |||
elseif Sys.isapple() | |||
".dylib" | |||
else | |||
".so" | |||
end | |||
sysimage_path = joinpath(scratchdir, sysimage_name * sysimage_ext) | |||
@info("Creating system image", | |||
name=sysimage_path, | |||
packages) | |||
create_sysimage( | |||
packages, | |||
sysimage_path = sysimage_path, | |||
precompile_execution_file = joinpath(@__DIR__, "precomp.jl"), | |||
# Optional: generate a "portable" sysimage on x86-64 architectures | |||
cpu_target = "generic;sandybridge,-xsaveopt,clone_all;haswell,-rdrnd,base(1)", | |||
) |
@@ -0,0 +1,39 @@ | |||
using JLD2 | |||
using JLD2View | |||
using JLD2View: send_signal | |||
mktempdir() do path | |||
d = Dict("int" => 1, | |||
"float" => 2.0, | |||
"vector" => rand(1000), | |||
"matrix" => rand(1000, 1000)) | |||
d["dict"] = deepcopy(d) | |||
d["array"] = [deepcopy(d), deepcopy(d)] | |||
fname = joinpath(path, "tmp.jld2") | |||
JLD2.save(fname, d) | |||
(viewer, task) = jld2_view(fname, sync=false) | |||
sleep(1) | |||
function simulate(action, arg) | |||
println("Simulating action: $action $arg") | |||
send_signal(viewer, action, arg) | |||
sleep(2) | |||
end | |||
simulate(:select_key, "int") | |||
simulate(:select_key, "float") | |||
simulate(:select_key, "vector"); simulate(:back, 0) | |||
simulate(:select_key, "matrix"); simulate(:back, 0) | |||
simulate(:select_key, "dict"); begin | |||
simulate(:select_key, "vector"); simulate(:back, 1) | |||
simulate(:select_key, "matrix"); simulate(:back, 1) | |||
end; simulate(:back, 0) | |||
simulate(:select_key, "array"); begin | |||
simulate(:select_key, 1); simulate(:back, 1) | |||
end; simulate(:back, 0) | |||
println("Terminating event loop") | |||
schedule(task, :stop, error=true) | |||
wait(task) | |||
end |
@@ -0,0 +1,28 @@ | |||
import Pkg; Pkg.activate(@__DIR__, io=IOBuffer()) | |||
using JLD2View | |||
using NativeFileDialog | |||
path = get(ARGS, 1) do | |||
pick_file(filterlist="jld2") | |||
end | |||
const HELP = """ | |||
Usage: | |||
jld2view [JLD2_PATH] | |||
Display the contents of a JLD2 file. If no file is provided in the command line, | |||
a file-picker dialog box will appear to select one interactively. | |||
""" | |||
if path == "--help" | |||
println(HELP) | |||
exit(0) | |||
elseif path == "" # pick_file cancelled | |||
exit(0) | |||
elseif isfile(path) | |||
jld2_view(path) | |||
else | |||
@error "File not found" path | |||
println("\n", HELP) | |||
exit(1) | |||
end |
@@ -0,0 +1,15 @@ | |||
<html> | |||
<head> | |||
<title>JLD2view</title> | |||
<style>{{:STYLE}}</style> | |||
</head> | |||
<body> | |||
Loading file `{{:FNAME}}`... | |||
<script> | |||
function showLoading() { | |||
true; | |||
} | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,78 @@ | |||
// Author: Jared Goodwin | |||
// showLoading() - Display loading wheel. | |||
// Requires ECMAScript 6 (any modern browser). | |||
function showLoading() { | |||
if (document.getElementById("divLoadingFrame") != null) { | |||
return; | |||
} | |||
var style = document.createElement("style"); | |||
style.id = "styleLoadingWindow"; | |||
style.innerHTML = ` | |||
.loading-frame { | |||
position: fixed; | |||
background-color: rgba(0, 0, 0, 0.5); | |||
left: 0; | |||
top: 0; | |||
right: 0; | |||
bottom: 0; | |||
z-index: 4; | |||
} | |||
.loading-track { | |||
height: 50px; | |||
display: inline-block; | |||
position: absolute; | |||
top: calc(50% - 50px); | |||
left: 50%; | |||
} | |||
.loading-dot { | |||
height: 5px; | |||
width: 5px; | |||
background-color: white; | |||
border-radius: 100%; | |||
opacity: 0; | |||
} | |||
.loading-dot-animated { | |||
animation-name: loading-dot-animated; | |||
animation-direction: alternate; | |||
animation-duration: .75s; | |||
animation-iteration-count: infinite; | |||
animation-timing-function: ease-in-out; | |||
} | |||
@keyframes loading-dot-animated { | |||
from { | |||
opacity: 0; | |||
} | |||
to { | |||
opacity: 1; | |||
} | |||
} | |||
` | |||
document.body.appendChild(style); | |||
var frame = document.createElement("div"); | |||
frame.id = "divLoadingFrame"; | |||
frame.classList.add("loading-frame"); | |||
for (var i = 0; i < 10; i++) { | |||
var track = document.createElement("div"); | |||
track.classList.add("loading-track"); | |||
var dot = document.createElement("div"); | |||
dot.classList.add("loading-dot"); | |||
track.style.transform = "rotate(" + String(i * 36) + "deg)"; | |||
track.appendChild(dot); | |||
frame.appendChild(track); | |||
} | |||
document.body.appendChild(frame); | |||
var wait = 0; | |||
var dots = document.getElementsByClassName("loading-dot"); | |||
for (var i = 0; i < dots.length; i++) { | |||
window.setTimeout(function(dot) { | |||
dot.classList.add("loading-dot-animated"); | |||
}, wait, dots[i]); | |||
wait += 150; | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
#jld2 { | |||
padding-left: 1em; | |||
padding-top: 0.5em; | |||
} | |||
#jld2 td { | |||
padding-top: 0.25em; | |||
padding-bottom: 0.25em; | |||
padding-left: 0.5em; | |||
padding-right: 0.5em; | |||
} | |||
#jld2 tr { | |||
cursor: pointer; | |||
} | |||
#jld2 tr:hover td { | |||
background: lightblue; | |||
} | |||
#warning { | |||
padding: 0.75em 1em; | |||
border-radius: 4px; | |||
border-style: solid; | |||
border-width: 1px; | |||
margin-top: 0.5em; | |||
background-color: rgba(252, 248, 227, 1); | |||
border-color: rgba(177, 161, 129, 1); | |||
color: rgba(138, 109, 59, 1); | |||
} | |||
a { | |||
text-decoration: none; | |||
color: black; | |||
} | |||
.key { | |||
font-weight: bold; | |||
font-family: monospace; | |||
} | |||
.value { | |||
font-family: monospace; | |||
} |
@@ -0,0 +1,26 @@ | |||
<html> | |||
<head> | |||
<title>JLD2view</title> | |||
<style>{{{:STYLE}}}</style> | |||
<script>{{{:SPIN_WHL}}}</script> | |||
</head> | |||
<body> | |||
<a class="key" href="#" onclick='sendMessageToJulia("back|0")'>[{{:FNAME}}]</a> | |||
{{#:PATH}} | |||
⊳ <a class="key" href="#" onclick='sendMessageToJulia("back|{{:idx}}")'>{{:name}}</a> | |||
{{/:PATH}} | |||
{{#:WARNING}} | |||
<div id="warning"><b>WARNING:</b> {{:text}}</div> | |||
{{/:WARNING}} | |||
<table id="jld2" cellspacing="0"> | |||
{{#:DATA}} | |||
<tr onclick='sendMessageToJulia("select|{{:idx}}")'> | |||
<td><span class="key">⊳ {{:key}}</span></td> | |||
<td><span class="value">{{:val}}</span></td> | |||
</tr> | |||
{{/:DATA}} | |||
</table> | |||
</body> | |||
</html> |
@@ -0,0 +1,3 @@ | |||
[deps] | |||
JLD2View = "a494b24a-6d3b-4871-b66b-22839c4c06d4" | |||
Scratch = "6c6a2e73-6563-6170-7368-637461726353" |
@@ -0,0 +1,30 @@ | |||
#!/bin/bash | |||
#= | |||
exec julia --color=no --startup-file=no --compile=min --optimize=0 "${BASH_SOURCE[0]}" "$@" | |||
=# | |||
using Pkg | |||
Pkg.activate(@__DIR__, io=IOBuffer()) | |||
using Scratch | |||
julia = Base.julia_cmd()[1] | |||
script = joinpath(@__DIR__, "..", "main.jl") | |||
args = String["--compile=min", "--optimize=0"] | |||
scratchdir = get_scratch!(Pkg.project().dependencies["JLD2View"], "sysimages") | |||
sysimage = joinpath(scratchdir, "sysimage.so") | |||
if (ispath(sysimage)) | |||
push!(args, "--sysimage=$sysimage") | |||
end | |||
cmd = `$julia $args $script $ARGS` | |||
try | |||
run(cmd) | |||
catch e | |||
process = first(e.procs) | |||
exit(process.exitcode) | |||
end | |||
# Local Variables: | |||
# mode: julia | |||
# End: |
@@ -0,0 +1,183 @@ | |||
module JLD2View | |||
using Electron | |||
using Mustache | |||
using JLD2 | |||
using Printf | |||
export jld2_view | |||
function pretty_repr(x) | |||
io = IOBuffer() | |||
show(IOContext(io, :compact=>true, :limit=>true), "text/plain", x) | |||
String(take!(io)) | |||
end | |||
function pretty_repr(x::Vector) | |||
n = length(x) | |||
if n < 10 && all(e isa Number for e in x) | |||
"[" * join((pretty_repr(e) for e in x), ", ") * "]" | |||
else | |||
"<$n-element Vector>" | |||
end | |||
end | |||
function pretty_repr(x::Matrix) | |||
m, n = size(x) | |||
"<$m×$n Matrix>" | |||
end | |||
function pretty_repr(x::Dict) | |||
n = length(keys(x)) | |||
"<$n-entry Dict>" | |||
end | |||
const STYLE = String(read( joinpath(@__DIR__, "..", "resources", "style.css"))) | |||
const SPIN_WHL = String(read( joinpath(@__DIR__, "..", "resources", "spinning_wheel.js"))) | |||
const PAGE_LOAD = Mustache.load(joinpath(@__DIR__, "..", "resources", "load.html")) | |||
const PAGE_VIEW = Mustache.load(joinpath(@__DIR__, "..", "resources", "view.html")) | |||
mutable struct Viewer | |||
app | |||
win | |||
fname | |||
data | |||
path | |||
keys | |||
function Viewer(path) | |||
viewer = new() | |||
viewer.fname = last(splitpath(path)) | |||
create_win!(viewer) | |||
@info "Loading JLD2 file" path | |||
@time viewer.data = JLD2.load(path) | |||
update!(viewer, ()) | |||
end | |||
end | |||
function get_data(viewer, path=viewer.path) | |||
f(dict, ::Tuple{}) = dict | |||
f(dict, path) = f(dict[first(path)], Base.tail(path)) | |||
return f(viewer.data, path) | |||
end | |||
function create_win!(viewer) | |||
viewer.app = Application() | |||
viewer.win = Window(viewer.app, render(PAGE_LOAD, STYLE=STYLE, FNAME=viewer.fname)) | |||
end | |||
function update!(viewer, path) | |||
Electron.run(viewer.win, "setTimeout(showLoading, 100)") | |||
viewer.path = path | |||
@info "Refreshing view" path = join(("⌂", path...), " ⊳ ") | |||
@time begin | |||
oldpath = path | |||
warning = nothing | |||
keys = Base.keys(get_data(viewer)) | |||
N = 1000 | |||
if length(keys) > N | |||
warning = (; text="showing only the first $N entries (out of $(length(keys)))") | |||
keys = Iterators.take(keys, N) | |||
end | |||
viewer.keys = keys = collect(keys) | |||
values = map(keys) do key | |||
pretty_repr(get_data(viewer, (path..., key))) | |||
end | |||
data = [(idx=i, key=keys[i], val=values[i]) for i in eachindex(keys)] | |||
html = render(PAGE_VIEW; STYLE, SPIN_WHL, | |||
DATA = data, | |||
FNAME = viewer.fname, | |||
PATH = [(idx=i, name=x) for (i,x) in enumerate(path)], | |||
WARNING = warning) | |||
open("index.html", "w") do f | |||
write(f, html) | |||
end | |||
Electron.load(viewer.win, html) | |||
end | |||
return viewer | |||
end | |||
function run!(viewer) | |||
if !viewer.app.exists | |||
@warn "Window does not exist any more. Creating a new one." | |||
create_win!(viewer) | |||
update!(viewer, ()) | |||
end | |||
ch = msgchannel(viewer.win) | |||
try | |||
while true | |||
msg = try | |||
take!(ch) | |||
catch e | |||
if isa(e, InvalidStateException) && e.state === :closed | |||
@info "Window closed. Terminating" | |||
break | |||
else | |||
rethrow() | |||
end | |||
end | |||
action, arg = split(msg, "|") | |||
arg = parse(Int, arg) | |||
path = if action == "back" | |||
viewer.path[1:arg] | |||
elseif action == "select" | |||
(viewer.path..., viewer.keys[arg]) | |||
end | |||
if any(typeof(get_data(viewer, path)) .<: (Dict, AbstractArray)) | |||
update!(viewer, path) | |||
end | |||
end | |||
finally | |||
close(viewer.app) | |||
sleep(0.1) | |||
end | |||
end | |||
send_signal(viewer, action::Symbol, args...) = send_signal(viewer, Val(action), args...) | |||
function send_signal(viewer, ::Val{:back}, idx::Integer) | |||
Electron.run(viewer.win, "sendMessageToJulia('back|$idx')") | |||
end | |||
function send_signal(viewer, ::Val{:select}, idx::Integer) | |||
Electron.run(viewer.win, "sendMessageToJulia('select|$idx')") | |||
end | |||
function send_signal(viewer, ::Val{:select_key}, key) | |||
idx = findfirst(==(key), viewer.keys) | |||
@assert !isnothing(idx) | |||
send_signal(viewer, :select, idx) | |||
end | |||
function jld2_view(fname; sync=true) | |||
viewer = Viewer(fname) | |||
task = @async try | |||
run!(viewer) | |||
catch e | |||
e == :stop || rethrow() | |||
end | |||
sync && wait(task) | |||
return (viewer, task) | |||
end | |||
function install() | |||
julia = first(Base.julia_cmd()) | |||
make = joinpath(@__DIR__, "..", "build", "make.jl") | |||
run(`$julia $make`) | |||
end | |||
end # module |