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