commit 09f905f4ab5430d50033152ec58671b5491d4156 Author: TEC Date: Fri Sep 8 18:09:47 2023 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba39cc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Manifest.toml diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..3b19f00 --- /dev/null +++ b/Project.toml @@ -0,0 +1,42 @@ +name = "Setup" +uuid = "1e701ad1-9860-423b-8651-7f81d307e3cf" +authors = ["TEC "] +version = "0.1.0" + +[deps] +BaseDirs = "18cc8868-cbac-4acf-b575-c8ff214dc66f" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" + +[weakdeps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +Gadfly = "c91e804a-d5a3-530f-b6f0-dfbca275c004" +HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" +JpegTurbo = "b835a17e-a41a-41e7-81f0-2f016b05efe0" +KittyTerminalImages = "b7fa5abe-5c7d-46c6-a1ae-1026d0d509b9" +Netpbm = "f09324ee-3d7c-5217-9330-fc30815ba969" +OhMyREPL = "5fb14364-9ced-5910-84b2-373655c76a03" +PNGFiles = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883" +QOI = "4b34888f-f399-49d4-9bb3-47ed5cae4e65" +TiffImages = "731e570b-9d59-4bfa-96dc-6df516fadf69" + +[extensions] +cairomakie = "CairoMakie" +gadfly = "Gadfly" +hdf5 = "HDF5" +image_jpeg = "JpegTurbo" +image_netpbm = "Netpbm" +image_png = "PNGFiles" +image_qoi = "QOI" +image_tiff = "TiffImages" +ohmyrepl = "OhMyREPL" +prettycolors = "Colors" + +[compat] +julia = "1.7" diff --git a/README.org b/README.org new file mode 100644 index 0000000..37322ac --- /dev/null +++ b/README.org @@ -0,0 +1,85 @@ +#+title: Setup.jl +#+author: TEC + +This is my personal Julia configuration. It has grown far beyond my wildest imagination. + +* Installation + +#+begin_src shell +julia --startup-file=no -e 'using Pkg; Pkg.add(url="https://git.tecosaur.net/tec/Setup.jl.git"); using Setup; Setup.install()' +#+end_src + +* Features +** Autoloading + +Instead of loading packages whose functionality might be needed at some point, +load them on-demand. Lazy loading for the win! + +The following packages are currently supported: ++ The standard library (=Base64=, =CRC32c=, =Dates=, =Distributions=, =Downloads=, + =LinearAlgebra=, =Markdown=, =Mmap=, =Pkg=, =Printf=, =REPL=, =Random=, =SHA=, =Serialisation=, + =Statistics=, =StatsBase=, =TOML=, =Tar=, =Test=, =Threads=, =UUIDs=) ++ =BaseDirs= ++ =BenchmarkTools= ++ =CSV= ++ =Cthulhu= ++ =DataFrames= ++ =DataToolkit= ++ =Debugger= ++ =Distributions= ++ =JET= ++ =JSON3= ++ =Org= ++ =WhyNotEqual= + +** Function/Structure inspection + +Get a sense of a function or structure with the =about= function/macro. + +** Environment tweaks +*** Shorter activation + +Add =a= as a shorthand for =activate= + +*** Easier temp environments + +Add =-t= as a shorthand for =--temp=, with the activate change this means opening a +temporary environment is now a bit quicker: + +#+begin_example +pkg> a -t +#+end_example + +instead of + +#+begin_example +pkg> activate --temp +#+end_example + + +*** Environment stack + +Load and unload named environments with the new =pkg> (un)stack= set of commands. + +** Command puns + +For more easily running ~Cmd~​s, we now have the following syntactic puns. + +#+begin_src julia +~ `cmd` # read to string +! `cmd` # print output +!~ `cmd` # read and print output +`a` | `b` # pipe commands +#+end_src + +** Package configuration + +Thanks to package extensions, it's fairly straightforward to bundle per-package +customisation code. Currently this covers the following: + ++ Setting the Makie theme based on the terminal colours when loading =CairoMakie= ++ Pushing the =Gadfly= display ++ Changing the =HDF5= icons ++ Loading =ImageShow= when =JpegTurbo=, =Netpbm=, =PNGFiles=, =QOI=, or =TiffImages= are loaded ++ Load an appropriate =OhMyREPL= theme on startup ++ Add pretty colour show methods for =Color= types diff --git a/ext/cairomakie.jl b/ext/cairomakie.jl new file mode 100644 index 0000000..91b10bc --- /dev/null +++ b/ext/cairomakie.jl @@ -0,0 +1,102 @@ +module cairomakie + +using Setup +using CairoMakie +import Pkg + +import Setup.theme_term + +import CairoMakie.Colors.FixedPointNumbers.N0f8 +import CairoMakie.Colors.RGB + +import CairoMakie.Makie: Theme, RGBAf, Attributes, Axis, Axis3, Legend, Colorbar + +function theme_term() + termcolors = Setup.termcolors() + + if isempty(termcolors) + return CairoMakie.Makie.current_default_theme() + end + + function tocolor(name::Symbol) + rgb = get(termcolors, name, (r=0x00, g=0x00, b=0x00)) + RGB(reinterpret(N0f8, rgb.r), + reinterpret(N0f8, rgb.g), + reinterpret(N0f8, rgb.b)) + end + + bg, fg, red, green, yellow, blue, magenta, cyan, + lbg, lfg, lred, lgreen, lyellow, lblue, lmagenta, lcyan = + tocolor.((:black, :white, :red, :green, :yellow, :blue, + :magenta, :cyan, :light_black, :light_white, + :light_red, :light_green, :light_yellow, + :light_blue, :light_magenta, :light_cyan)) + + onecolors(α = 1.0) = + RGBAf.([blue, yellow, green, magenta, red, cyan, lblue, + lyellow, lgreen, lmagenta, lred, lcyan], + α) + + Theme( + backgroundcolor = bg, + textcolor = fg, + linecolor = fg, + font = :regular, + fonts = Attributes( + bold = "JetBrains Mono Bold", + bold_italic = "JetBrains Mono Bold Italic", + italic = "JetBrains Mono Italic", + regular = "JetBrains Mono Medium"), + palette = merge(Attributes( + color = onecolors(1.0), + patchcolor = onecolors(0.8) + ), CairoMakie.Makie.default_palettes), + Axis = ( + backgroundcolor = :transparent, + bottomspinecolor = fg, + topspinecolor = fg, + leftspinecolor = fg, + rightspinecolor = fg, + xgridcolor = RGBAf(1, 1, 1, 0.16), + ygridcolor = RGBAf(1, 1, 1, 0.16), + xtickcolor = fg, + ytickcolor = fg), + Legend = ( + framecolor = fg, + bgcolor = bg), + Axis3 = ( + xgridcolor = RGBAf(1, 1, 1, 0.16), + ygridcolor = RGBAf(1, 1, 1, 0.16), + zgridcolor = RGBAf(1, 1, 1, 0.16), + xspinecolor_1 = fg, + yspinecolor_1 = fg, + zspinecolor_1 = fg, + xspinecolor_2 = fg, + yspinecolor_2 = fg, + zspinecolor_2 = fg, + xspinecolor_3 = fg, + yspinecolor_3 = fg, + zspinecolor_3 = fg, + xticklabelpad = 3, + yticklabelpad = 3, + zticklabelpad = 6, + xtickcolor = fg, + ytickcolor = fg, + ztickcolor = fg), + Colorbar = ( + tickcolor = fg, + spinecolor = fg, + topspinecolor = fg, + bottomspinecolor = fg, + leftspinecolor = fg, + rightspinecolor = fg)) +end + +function __init__() + if isinteractive() + Setup.load_termimage_pkg() + CairoMakie.set_theme!(theme_term()) + end +end + +end diff --git a/ext/gadfly.jl b/ext/gadfly.jl new file mode 100644 index 0000000..0fa1588 --- /dev/null +++ b/ext/gadfly.jl @@ -0,0 +1,9 @@ +module gadfly + +using Gadfly + +function __init__() + pushdisplay(Gadfly.GadflyDisplay()) +end + +end diff --git a/ext/hdf5.jl b/ext/hdf5.jl new file mode 100644 index 0000000..1b2a892 --- /dev/null +++ b/ext/hdf5.jl @@ -0,0 +1,22 @@ +module hdf5 + +using HDF5 + +import HDF5._tree_icon + +function __init__() + _tree_icon(::Type{HDF5.Attribute}) = + HDF5.SHOW_TREE_ICONS[] ? "\e[33m\e[39m" : "[A]" + _tree_icon(::Type{HDF5.Group}) = + HDF5.SHOW_TREE_ICONS[] ? "\e[34m\e[39m" : "[G]" + _tree_icon(::Type{HDF5.Dataset}) = + HDF5.SHOW_TREE_ICONS[] ? "\e[35m\e[39m" : "[D]" + _tree_icon(::Type{HDF5.Datatype}) = + HDF5.SHOW_TREE_ICONS[] ? "\e[36m\e[39m" : "[T]" + _tree_icon(::Type{HDF5.File}) = + HDF5.SHOW_TREE_ICONS[] ? "\e[34m\e[39m" : "[F]" + _tree_icon(::Type) = + HDF5.SHOW_TREE_ICONS[] ? "\e[31m❓\e[39m" : "[?]" +end + +end diff --git a/ext/image_jpeg.jl b/ext/image_jpeg.jl new file mode 100644 index 0000000..497cd9d --- /dev/null +++ b/ext/image_jpeg.jl @@ -0,0 +1,3 @@ +module image_jpeg +include("images.jl") +end diff --git a/ext/image_netpbm.jl b/ext/image_netpbm.jl new file mode 100644 index 0000000..06db456 --- /dev/null +++ b/ext/image_netpbm.jl @@ -0,0 +1,3 @@ +module image_netpbm +include("images.jl") +end diff --git a/ext/image_png.jl b/ext/image_png.jl new file mode 100644 index 0000000..91bc728 --- /dev/null +++ b/ext/image_png.jl @@ -0,0 +1,3 @@ +module image_png +include("images.jl") +end diff --git a/ext/image_qoi.jl b/ext/image_qoi.jl new file mode 100644 index 0000000..fba85ea --- /dev/null +++ b/ext/image_qoi.jl @@ -0,0 +1,3 @@ +module image_qoi +include("images.jl") +end diff --git a/ext/image_tiff.jl b/ext/image_tiff.jl new file mode 100644 index 0000000..e847b62 --- /dev/null +++ b/ext/image_tiff.jl @@ -0,0 +1,3 @@ +module image_tiff +include("images.jl") +end diff --git a/ext/images.jl b/ext/images.jl new file mode 100644 index 0000000..38dc78d --- /dev/null +++ b/ext/images.jl @@ -0,0 +1,29 @@ +# This is loaded by all other `image_*.jl` files + +using Setup + +const IMAGE_SHOW_PKG, IMAGE_IO_PKG, IMAGE_MAGICK_PKG = splat(Base.PkgId).([ + (Base.UUID("4e3cecfd-b093-5904-9786-8bbb286a6a31"), "ImageShow"), + (Base.UUID("82e4d734-157c-48bb-816b-45c225c6df19"), "ImageIO"), + (Base.UUID("6218d12a-5da1-5696-b52f-db25d2ecc6d1"), "ImageMagick")]) + +function __init__() + if isinteractive() + Setup.load_termimage_pkg() + if !haskey(Base.loaded_modules, IMAGE_SHOW_PKG) && + !isnothing(Base.find_package(IMAGE_SHOW_PKG.name)) && + (!isnothing(Base.find_package(IMAGE_IO_PKG.name)) || + !isnothing(Base.find_package(IMAGE_MAGICK_PKG.name))) + # Don't to this async because it may affect a `display` call + # that's just about to occur. + if !isnothing(Base.find_package(IMAGE_IO_PKG.name)) + Base.require(IMAGE_IO_PKG) + elseif !isnothing(Base.find_package(IMAGE_MAGICK_PKG.name)) + Base.require(IMAGE_MAGICK_PKG) + else + return + end + Base.require(IMAGE_SHOW_PKG) + end + end +end diff --git a/ext/ohmyrepl.jl b/ext/ohmyrepl.jl new file mode 100644 index 0000000..a78b5ae --- /dev/null +++ b/ext/ohmyrepl.jl @@ -0,0 +1,28 @@ +module ohmyrepl + +using Setup +using OhMyREPL + +function __init__() + theme = Setup.termtheme() + if theme == :dark + OhMyREPL.colorscheme!("OneDark") + elseif theme == :light + OhMyREPL.colorscheme!("TomorrowDay") + else theme == :unknown + OhMyREPL.colorscheme!("Monokai16") + end + + if Setup.TERM[] == "xterm-kitty" + OhMyREPL.input_prompt!(:green) do + print("\e]133;A\e\\") + "julia> " + end + OhMyREPL.output_prompt!(:red) do + print("\e]133;C\e\\") + "" + end + end +end + +end diff --git a/ext/prettycolors.jl b/ext/prettycolors.jl new file mode 100644 index 0000000..4eeff6d --- /dev/null +++ b/ext/prettycolors.jl @@ -0,0 +1,58 @@ +module prettycolors + +using Colors +using LinearAlgebra # for the Adjoint type + +import Base.show + +colorcode(rgb::Vector{<:Integer}, background=false) = + string("\e[", if background "48" else "38" end, + ";2;", rgb[1], ";", rgb[2], ";", rgb[3], "m") + +colorcode(c::Union{RGB, RGB24}, background=false) = + colorcode(reinterpret.([red(c), green(c), blue(c)]), background) + +colorcode(c::RGB{Float64}, background=false) = + colorcode(round.(Int, 255 .* [red(c), green(c), blue(c)]), background) + +colorcode(c::Colorant, background=false) = + colorcode(convert(RGB24, c), background) + +function show(io::IO, ::MIME"text/plain", c::Colorant) + if get(io, :color, false) && get(ENV, "COLORTERM", nothing) == "truecolor" + print(colorcode(c, true), " \e[0m ") + end + show(io, c) +end + +function show(io::IO, ::MIME"text/plain", cs::Adjoint{<:Colorant, <:Vector{<:Colorant}}) + summary(io, cs) + print(":\n") + if get(io, :color, false) && get(ENV, "COLORTERM", nothing) == "truecolor" + print(" ", join(colorcode.(cs,true), if length(cs) <= 40 " " else " " end), + if length(cs) <= 40 " \e[0m" else " \e[0m" end) + else + Base.print_array(IOContext(io, :SHOWN_SET => cs), cs) + end +end + +function show(io::IO, ::MIME"text/plain", cs::Matrix{<:Colorant}) + summary(io, cs) + print(":\n") + if get(io, :color, false) && *(size(cs)...) <= 1000 && get(ENV, "COLORTERM", nothing) == "truecolor" + for csrow in eachrow(cs) + println(" ", join(colorcode.(csrow,true), + if size(cs,2) <= 40 " " else " " end), + if size(cs,2) <= 40 " \e[0m" else " \e[0m" end) + end + else + Base.print_array(IOContext(io, :SHOWN_SET => cs), cs) + end +end + +function Base.display(c::Colorant) + show(stdout, MIME("text/plain"), c) + print('\n') +end + +end diff --git a/src/Setup.jl b/src/Setup.jl new file mode 100644 index 0000000..82f508c --- /dev/null +++ b/src/Setup.jl @@ -0,0 +1,80 @@ +module Setup + +import REPL +import Pkg + +export about, @about + +@static if VERSION >= v"1.8" + TERM::Ref{String} = get(ENV, "TERM", "") + IS_SSH::Ref{Bool} = Ref(false) +else + TERM = get(ENV, "TERM", "") + IS_SSH = Ref(false) +end + +include("install.jl") + +include("cmdpuns.jl") +include("termsetup.jl") +include("about.jl") +include("pkgstack.jl") + +include("autoloads.jl") +using .Autoloads +include("autoload_data.jl") + +include("sessions.jl") +using .Sessions + +using .PkgStack + +function ensurepkg(pkg::String) + if isnothing(Base.find_package(pkg)) + @info "Installing $pkg" + Pkg.add(pkg) + end +end + +function theme_term end # For cairomakie.jl + +function __init__() + IS_SSH[] = haskey(ENV, "SSH_CLIENT") || haskey(ENV, "SSH_TTY") + if isinteractive() + TERM[] = termcode() + + Main.Threads.@spawn if TERM[] == "xterm-kitty" + if isnothing(Base.find_package("KittyTerminalImages")) + @info "Installing KittyTerminalImages" + currentproj = Pkg.project().path + try + Pkg.activate() + # Pkg.add("KittyTerminalImages") + finally + Pkg.activate(currentproj) + end + end + termtitle(pwd()) + end + + if ENV["TERM"] != "dumb" + replsetup() + end + end +end + +precompile(termtitle, ()) +precompile(replsetup, ()) +precompile(_termcolors, ()) +precompile(termcolors, ()) +precompile(termtheme, ()) + +precompile(Autoloads.autoload_loadpkg, (Symbol,)) +precompile(Autoloads.autoload_scan!, (Expr,)) +precompile(Autoloads.autoload_scan!, (Symbol,)) +precompile(Autoloads.autoload_scan!, (Any,)) +precompile(Autoloads.read_named_envs!, ()) +precompile(Autoloads.repl_scan_autoloads!, (Expr,)) +precompile(Autoloads.repl_scan_autoloads!, (Nothing,)) + +end diff --git a/src/about.jl b/src/about.jl new file mode 100644 index 0000000..46b164d --- /dev/null +++ b/src/about.jl @@ -0,0 +1,256 @@ +using InteractiveUtils + +const colorcycle = [:light_blue, :light_green, :light_yellow, :light_magenta] + +function humansize(bytes::Integer) + units = ("B", "kB", "MB", "GB") + magnitude = floor(Int, log(1024, 1 + bytes)) + if 10 < bytes < 10*1024^magnitude + round(bytes / 1024^magnitude, digits=1) + else + round(Int, bytes / 1024^magnitude) + end, units[1+magnitude] +end + +function about(obj::Any) + print(Base.summary(obj)) + size, units = humansize(Base.summarysize(obj)) + if ismutable(obj) + print(" (mutable)") + end + print(", ", size, units, "\n\n") + printstyled("Fields:\n", bold=true) + memorylayout(obj) +end + +function about(type::Type) + if isprimitivetype(type) + print("Primitive ") + elseif isconcretetype(type) + print("Concrete ") + if Base.datatype_haspadding(type) + printstyled("(padded) ", color=:light_black) + end + elseif isabstracttype(type) + print("Abstract ") + end + if Base.issingletontype(type) + print("singleton ") + end + print(Base.summary(type)) + print(" defined in ") + printstyled(type.name.module, color=:light_red) + size, units = humansize(Base.summarysize(type)) + selfsize, selfunits = try + st = humansize(sizeof(type)) + print(", ", st...) + st + catch _; (0, "") end + if (size, units) != (selfsize, selfunits) + print(" referencing ", size, units) + end + println("\n ", join(string.(supertypes(type)), " \e[31m<:\e[m ")) + if isstructtype(type) && fieldcount(type) > 0 + println("\nStruct with \e[1m", fieldcount(type), "\e[m fields:") + if type isa DataType + for (i, (fname, ftype)) in enumerate(zip(fieldnames(type), fieldtypes(type))) + color = colorcycle[i % length(colorcycle) + 1] + print(" • ") + printstyled(fname; color) + printstyled("::", string(ftype), color=:light_black) + print('\n') + end + print('\n') + memorylayout(type) + else + println(" • ", + join(string.(fieldnames(type), "\e[90m::", + fieldtypes(type), "\e[m"), + "\n • ")) + end + end +end + +function structinfo(T::DataType) + map(1:fieldcount(T)) do i + size = Int(if i < fieldcount(T) fieldoffset(T, i+1) + else sizeof(T) end - fieldoffset(T, i)) + contentsize=try sizeof(fieldtype(T, i)) catch _ 0 end + (; i, offset=fieldoffset(T, i) |> Int, + size, contentsize, + pointer=contentsize == 0, + name=fieldname(T, i), + type=fieldtype(T, i)) + end +end + +function memorylayout(type::DataType) + cpad(s, n::Integer, p=" ") = rpad(lpad(s,div(n+textwidth(s),2),p),n,p) + si = structinfo(type) + !isempty(si) || return + memstep = memstep = gcd((getfield.(si, :size), getfield.(si, :contentsize)) |> + Iterators.flatten |> collect) + memscale = max(1, floor(Int, 70/(sizeof(type)/memstep))) + print(' ') + for (; i, size, contentsize, pointer) in si + color = colorcycle[i % length(colorcycle) + 1] + contentwidth = memscale * contentsize÷memstep + printstyled('■'^contentwidth; color) + if contentsize < size + paddwidth = memscale * (size - contentsize)÷memstep + printstyled('■'^paddwidth; + color=if pointer; :cyan else :light_black end) + end + end + print("\n ") + for (; i, size, contentsize, pointer) in si + width = memscale * size÷memstep + color = colorcycle[i % length(colorcycle) + 1] + fsize, funits = humansize(size) + if pointer + printstyled(cpad("*", width); color) + elseif contentsize < size + csize, cunits = humansize(contentsize) + psize, punits = humansize(size - contentsize) + csizestr, psizestr = string(csize, cunits), string(psize, punits) + cwidth = div(width + textwidth(csizestr) - textwidth(psizestr) - 1, 2) + pwidth = width - cwidth + printstyled(lpad(csizestr, cwidth); color) + printstyled(rpad('+' * psizestr, pwidth); color=:light_black) + else + printstyled(cpad(string(fsize, funits), width); color) + end + end + if any(getfield.(si, :pointer)) + printstyled("\n\n * = Pointer (8B)", color=:cyan) + end + print('\n') +end + +function memorylayout(val::T) where {T} + si = structinfo(T) + !isempty(si) || return memorylayout(T) + rows = Vector{NamedTuple}() + for (; name, size, contentsize, pointer, type) in si + fsize, funits = humansize(size) + fbits = if isprimitivetype(type) + bitstring(getfield(val, name)) * '-'^(8*(size-contentsize)) + elseif pointer + "Pointer" + else + "" + end + fshow = sprint(show, getfield(val, name), context=:compact => true) + push!(rows, (; fname=string(name), fsize=string(fsize), funits, + fbits, fshow, ftype=string(type))) + end + widths = Dict(c => maximum(length.(getfield.(rows, c))) + for c in fieldnames(typeof(first(rows)))) |> + NamedTuple + for (i, (; fname, fsize, funits, fbits, fshow, ftype)) in enumerate(rows) + color = colorcycle[i % length(colorcycle) + 1] + printstyled(' ', lpad(fname, widths.fname), "::", + rpad(ftype, widths.ftype); color) + printstyled(" ", lpad(fsize, widths.fsize), + rpad(funits, widths.funits), ' '; color) + print(replace(rpad(fbits, widths.fbits), r"0|\-" => s"\e[90m\0\e[m"), ' ') + printstyled(rpad(fshow, widths.fshow); color) + print('\n') + end + print('\n') + memorylayout(T) +end + +function about(fn::Function) + source = Main.InteractiveUtils.which(Main, Symbol(fn)) + methodmodules = getproperty.(methods(fn).ms, :module) + others = setdiff(methodmodules, [source]) + print(Base.summary(fn)) + print("\n\nDefined in ") + printstyled(source, color=:light_red) + if length(others) > 0 + printstyled("(", sum(Ref(source) .=== methodmodules), ")", + color=:light_black) + print(" extended in ") + for (i, oth) in enumerate(others) + printstyled(oth, color=:light_red) + printstyled("(", sum(Ref(oth) .=== methodmodules), ")", + color=:light_black) + if length(others) == 2 && i == 1 + print(" and ") + elseif length(others) > 2 && i < length(others)-1 + print(", ") + elseif length(others) > 2 && i == length(others)-1 + print(", and ") + end + end + end + print(".\n") +end + +function about(fn::Union{Function, Type}, typesig::Type{<:Tuple}) + about(Base.unwrap_unionall(fn)) + print("\n") + matchedmethods = methods(fn, typesig) + printstyled("Matching methods ($(length(matchedmethods.ms))):\n", bold=true) + for method in matchedmethods.ms + println(" ", sprint(show, method, context=IOContext(stdout))) + end + print('\n') + printstyled("Effects:\n", bold=true) + effects = Base.infer_effects(fn, typesig) + trichar(t::UInt8) = + Dict(Core.Compiler.ALWAYS_TRUE => '✔', + # Core.Compiler.TRISTATE_UNKNOWN => '?', + Core.Compiler.ALWAYS_FALSE => '✗')[t] + trichar(b::Bool) = ifelse(b, '✔', '✗') + tricolor(t::UInt8) = + Dict(Core.Compiler.ALWAYS_TRUE => :green, + # Core.Compiler.TRISTATE_UNKNOWN => :yellow, + Core.Compiler.ALWAYS_FALSE => :red)[t] + tricolor(b::Bool) = ifelse(b, :green, :red) + hedge(t::UInt8) = + Dict(Core.Compiler.ALWAYS_TRUE => "guaranteed to", + # Core.Compiler.TRISTATE_UNKNOWN => "might", + Core.Compiler.ALWAYS_FALSE => "not guaranteed to")[t] + hedge(b::Bool) = ifelse(b, "guaranteed to", "not guaranteed to") + for (effect, description) in + [(:consistent, "return or terminate consistently"), + (:effect_free, "be free from externally semantically visible side effects"), + (:nothrow, "never throw an exception"), + (:terminates, "terminate")] + e = getfield(effects, effect) + print(" "); printstyled(trichar(e), ' ', string(effect), color=tricolor(e)) + print(" "); printstyled(hedge(e), ' ', description, color=:light_black); + print('\n') + end + if !effects.nonoverlayed + print(" "); printstyled(trichar(Core.Compiler.ALWAYS_FALSE), " nonoverlayed", + color=tricolor(Core.Compiler.ALWAYS_FALSE)) + print(" "); printstyled("methods within this method are not defined in an overlayed method table", + color=:light_black); + print('\n') + end +end + +macro about(obj::Symbol, types::Union{Symbol, Expr}...) + if Core.eval(Main, obj) isa Function && length(types) == 1 && + types[1] == :(()) + :(about($obj, Tuple{})) + elseif Core.eval(Main, obj) isa Function && length(types) > 0 + :(about($obj, $(Tuple{Core.eval.(Main, types)...}))) + else + :(about($obj)) + end |> esc +end + +macro about(qobj::Union{QuoteNode, Expr}, types::Union{Symbol, Expr}...) + if Core.eval(Main, Core.eval(Main, qobj)) isa Function && length(types) == 1 && + types[1] == :(()) + :(about($(Core.eval(Main, qobj)), Tuple{})) + elseif length(types) > 0 + :(about($(Core.eval(Main, qobj)), $(Tuple{Core.eval.(Main, types)...}))) + else + :(about($(Core.eval(Main, qobj)))) + end |> esc +end diff --git a/src/autoload_data.jl b/src/autoload_data.jl new file mode 100644 index 0000000..26f1c8a --- /dev/null +++ b/src/autoload_data.jl @@ -0,0 +1,258 @@ +foreach(add_autoloads!, [ + PkgAutoloads( + :(Base.Threads), + exported = Bindings( + macros = ["@spawn", "@threads"], + callables = [ + :Atomic, :Event, :SpinLock, :Threads, :atomic_add!, + :atomic_and!, :atomic_cas!, :atomic_fence, :atomic_max!, + :atomic_min!, :atomic_nand!, :atomic_or!, :atomic_sub!, + :atomic_xchg!, :atomic_xor!, :nthreadpools, :nthreads, + :threadid, :threadpool])), + PkgAutoloads(:Base64, exported = Bindings(callables = [ + :Base64DecodePipe, :Base64EncodePipe, :base64decode, :base64encode, + :stringmime])), + PkgAutoloads(:BaseDirs), + PkgAutoloads( + :BenchmarkTools, + exported = Bindings(macros = [ + "@ballocated", "@belapsed", "@benchmark", "@benchmarkable", + "@benchmarkset", "@bprofile", "@btime", "@case", "@tagged"])), + PkgAutoloads(:CRC32c, exported = Bindings(callables = [:crc32c])), + PkgAutoloads(:CSV), + PkgAutoloads(:Cthulhu, exported = Bindings( + macros = ["@decend", "@decend_code_typed", "@decend_code_warntype"], + callables = [:ascend, :descend, :descend_code_typed, :descend_code_warntype])), + PkgAutoloads( + :DataFrames, + exported = Bindings(callables = [ + :AbstractDataFrame, :All, :AsTable, :Between, :ByRow, :Cols, + :DataFrame, :DataFrameRow, :DataFrames, :GroupedDataFrame, + :InvertedIndex, :InvertedIndices, :Missing, :MissingException, + :Missings, :Not, :PrettyTables, :SubDataFrame, :Tables, :aggregate, + :allcombinations, :allowmissing, :allowmissing!, :antijoin, :by, + :coalesce, :colmetadata, :colmetadata!, :colmetadatakeys, + :columnindex, :combine, :completecases, :crossjoin, :delete!, + :deletecolmetadata!, :deletemetadata!, :disallowmissing, + :disallowmissing!, :dropmissing, :dropmissing!, :emptycolmetadata!, + :emptymetadata!, :emptymissing, :fillcombinations, :flatten, + :groupby, :groupcols, :groupindices, :innerjoin, :insertcols, + :insertcols!, :ismissing, :leftjoin, :leftjoin!, :levels, :mapcols, + :mapcols!, :metadata, :metadata!, :metadatakeys, :missing, + :missings, :ncol, :nonmissingtype, :nonunique, :nrow, :order, + :outerjoin, :passmissing, :proprow, :rename, :rename!, :repeat!, + :rightjoin, :rownumber, :select, :select!, :semijoin, :skipmissings, + :subset, :subset!, :transform, :transform!, :unique!, :unstack, + :valuecols])), + PkgAutoloads( + :DataToolkit, + exported = Bindings( + macros = ["@d_str", "@data_cmd", "@import"], + callables = [:DataSet, :DataToolkit, :dataset, :loadcollection!])), + PkgAutoloads( + :Dates, + exported = Bindings( + macros = ["@dateformat_str"], + callables = [ + :Apr, :April, :Aug, :August, :Date, :DateFormat, :DatePeriod, + :DateTime, :Dates, :Day, :Dec, :December, :Feb, :February, :Fri, + :Friday, :Hour, :ISODateFormat, :ISODateTimeFormat, + :ISOTimeFormat, :Jan, :January, :Jul, :July, :Jun, :June, :Mar, + :March, :May, :Microsecond, :Millisecond, :Minute, :Mon, + :Monday, :Month, :Nanosecond, :Nov, :November, :Oct, :October, + :Period, :Quarter, :RFC1123Format, :Sat, :Saturday, :Second, + :Sep, :September, :Sun, :Sunday, :Thu, :Thursday, :Time, + :TimePeriod, :TimeType, :TimeZone, :Tue, :Tuesday, :UTC, :Wed, + :Wednesday, :Week, :Year, :adjust, :canonicalize, + :datetime2julian, :datetime2rata, :datetime2unix, :day, + :dayabbr, :dayname, :dayofmonth, :dayofquarter, :dayofweek, + :dayofweekofmonth, :dayofyear, :daysinmonth, :daysinyear, + :daysofweekinmonth, :firstdayofmonth, :firstdayofquarter, + :firstdayofweek, :firstdayofyear, :hour, :isleapyear, + :julian2datetime, :lastdayofmonth, :lastdayofquarter, + :lastdayofweek, :lastdayofyear, :microsecond, :millisecond, + :minute, :month, :monthabbr, :monthday, :monthname, :nanosecond, + :now, :quarterofyear, :rata2datetime, :second, :today, :tofirst, + :tolast, :tonext, :toprev, :unix2datetime, :week, :year, + :yearmonth, :yearmonthday])), + PkgAutoloads(:Debugger, exported = Bindings(macros = ["@enter", "@run"])), + PkgAutoloads( + :Distributed, + exported = Bindings( + macros = ["@distributed", "@everywhere", "@fetch", "@fetchfrom", "@spawnat"], + callables = [ + :AbstractWorkerPool, :CachingPool, :ClusterManager, :Future, + :ProcessExitedException, :RemoteChannel, :RemoteException, + :WorkerConfig, :WorkerPool, :addprocs, :channel_from_id, + :check_same_host, :clear!, :cluster_cookie, + :default_worker_pool, :init_worker, :interrupt, :launch, + :manage, :myid, :nprocs, :nworkers, :pmap, :process_messages, + :procs, :remote, :remote_do, :remotecall, :remotecall_fetch, + :remotecall_wait, :remoteref_id, :rmprocs, :start_worker, + :worker_id_from_socket, :workers])), + PkgAutoloads( + :Distributions, + exported = Bindings(callables = [ + :AbstractMixtureModel, :AbstractMvNormal, :Arcsine, + :ArrayLikeVariate, :Bernoulli, :BernoulliLogit, :Beta, + :BetaBinomial, :BetaPrime, :Binomial, :Biweight, :Categorical, + :Cauchy, :Chernoff, :Chi, :Chisq, :CholeskyVariate, :Continuous, + :ContinuousDistribution, :ContinuousMatrixDistribution, + :ContinuousMultivariateDistribution, + :ContinuousUnivariateDistribution, :Cosine, :DiagNormal, + :DiagNormalCanon, :Dirac, :Dirichlet, :DirichletMultinomial, + :Discrete, :DiscreteDistribution, :DiscreteMatrixDistribution, + :DiscreteMultivariateDistribution, :DiscreteNonParametric, + :DiscreteUniform, :DiscreteUnivariateDistribution, :Distribution, + :DoubleExponential, :EdgeworthMean, :EdgeworthSum, :EdgeworthZ, + :Epanechnikov, :Erlang, :Estimator, :Exponential, :FDist, + :FisherNoncentralHypergeometric, :Frechet, :FullNormal, + :FullNormalCanon, :Gamma, :GeneralizedExtremeValue, + :GeneralizedPareto, :Geometric, :Gumbel, :Hypergeometric, + :InverseGamma, :InverseGaussian, :InverseWishart, :IsoNormal, + :IsoNormalCanon, :JohnsonSU, :JointOrderStatistics, :KSDist, + :KSOneSided, :Kolmogorov, :Kumaraswamy, :LKJ, :LKJCholesky, + :Laplace, :Levy, :Lindley, :LocationScale, :LogNormal, :LogUniform, + :Logistic, :LogitNormal, :MLEstimator, :MatrixBeta, + :MatrixDistribution, :MatrixFDist, :MatrixNormal, :MatrixReshaped, + :MatrixTDist, :Matrixvariate, :MixtureModel, :Multinomial, + :Multivariate, :MultivariateDistribution, :MultivariateMixture, + :MultivariateNormal, :MvLogNormal, :MvNormal, :MvNormalCanon, + :MvNormalKnownCov, :MvTDist, :NegativeBinomial, + :NonMatrixDistribution, :NoncentralBeta, :NoncentralChisq, + :NoncentralF, :NoncentralHypergeometric, :NoncentralT, :Normal, + :NormalCanon, :NormalInverseGaussian, :OrderStatistic, + :PGeneralizedGaussian, :Pareto, :Poisson, :PoissonBinomial, + :Product, :QQPair, :Rayleigh, :RealInterval, :Rician, :Sampleable, + :Semicircle, :Skellam, :SkewNormal, :SkewedExponentialPower, + :Soliton, :StudentizedRange, :SufficientStats, :SymTriangularDist, + :TDist, :TriangularDist, :Triweight, :Truncated, :TruncatedNormal, + :Uniform, :Univariate, :UnivariateDistribution, :UnivariateGMM, + :UnivariateMixture, :ValueSupport, :VariateForm, :VonMises, + :VonMisesFisher, :WalleniusNoncentralHypergeometric, :Weibull, + :Wishart, :ZeroMeanDiagNormal, :ZeroMeanDiagNormalCanon, + :ZeroMeanFullNormal, :ZeroMeanFullNormalCanon, :ZeroMeanIsoNormal, + :ZeroMeanIsoNormalCanon, :canonform, :ccdf, :cdf, :censored, :cf, + :cgf, :circvar, :component, :components, :componentwise_logpdf, + :componentwise_pdf, :concentration, :convolve, :cquantile, + :estimate, :expected_logdet, :failprob, :fit_mle, :gradlogpdf, + :hasfinitesupport, :insupport, :invcov, :invlogccdf, :invlogcdf, + :invscale, :isbounded, :isleptokurtic, :islowerbounded, + :ismesokurtic, :isplatykurtic, :isprobvec, :isupperbounded, + :location, :location!, :logccdf, :logcdf, :logdetcov, :logdiffcdf, + :logpdf, :logpdf!, :meandir, :meanform, :meanlogx, :mgf, + :ncategories, :ncomponents, :nsamples, :ntrials, :partype, :pdf, + :pdfsquaredL2norm, :probs, :probval, :product_distribution, + :qqbuild, :rate, :sampler, :scale, :scale!, :shape, :sqmahal, + :sqmahal!, :stdlogx, :succprob, :suffstats, :support, :truncated, + :varlogx])), + PkgAutoloads( + :Downloads, + exported = Bindings(callables = [ + :Downloader, :RequestError, :Response, :download, :request])), + PkgAutoloads(:JET, exported = Bindings( + macros = ["@report_call", "@report_opt", "@test_call", "@test_opt"], + callables = [:AnyFrameModule, :LastFrameModule, :report_call, + :report_file, :report_opt, :report_package, :report_text, + :test_call, :test_file, :test_opt, :test_package, + :test_text, :watch_file])), + PkgAutoloads(:JSON3), + PkgAutoloads( + :LinearAlgebra, + exported = Bindings(callables = [ + :Adjoint, :BLAS, :Bidiagonal, :BunchKaufman, :Cholesky, + :CholeskyPivoted, :ColumnNorm, :Diagonal, :Eigen, :Factorization, + :GeneralizedEigen, :GeneralizedSVD, :GeneralizedSchur, :Hermitian, + :Hessenberg, :I, :LAPACK, :LAPACKException, :LDLt, :LQ, :LU, + :LinearAlgebra, :LowerTriangular, :NoPivot, :PosDefException, :QR, + :QRPivoted, :RankDeficientException, :RowMaximum, :RowNonZero, :SVD, + :Schur, :SingularException, :SymTridiagonal, :Symmetric, :Transpose, + :Tridiagonal, :UniformScaling, :UnitLowerTriangular, + :UnitUpperTriangular, :UpperHessenberg, :UpperTriangular, + :ZeroPivotException, :adjoint!, :axpby!, :axpy!, :bunchkaufman, + :bunchkaufman!, :cholesky, :cholesky!, :cond, :condskeel, + :copy_transpose!, :copyto!, :cross, :det, :diag, :diagind, :diagm, + :dot, :eigen, :eigen!, :eigmax, :eigmin, :eigvals, :eigvals!, + :eigvecs, :factorize, :givens, :hessenberg, :hessenberg!, :isdiag, + :ishermitian, :isposdef, :isposdef!, :issuccess, :issymmetric, + :istril, :istriu, :kron, :kron!, :ldiv!, :ldlt, :ldlt!, :lmul!, + :logabsdet, :logdet, :lowrankdowndate, :lowrankdowndate!, + :lowrankupdate, :lowrankupdate!, :lq, :lq!, :lu, :lu!, :lyap, :mul!, + :norm, :normalize, :normalize!, :nullspace, :opnorm, :ordschur, + :ordschur!, :pinv, :qr, :qr!, :rank, :rdiv!, :reflect!, :rmul!, + :rotate!, :schur, :schur!, :svd, :svd!, :svdvals, :svdvals!, + :sylvester, :tr, :transpose, :transpose!, :tril, :tril!, :triu, + :triu!, :×, :⋅])), + PkgAutoloads(:Markdown, exported = + Bindings(macros = ["@doc_str", "@md_str"], + callables = [:html, :latex])), + PkgAutoloads(:Mmap, exported = Bindings(callables = [:mmap])), + PkgAutoloads(:Org, exported = Bindings( + macros = ["@org_str"], + callables = [:Org, :OrgDoc, :org, :parsetree, :tableofcontents, :term])), + PkgAutoloads(:Pkg, exported = Bindings(macros = ["@pkg_str"])), + PkgAutoloads(:Printf, exported = Bindings(macros = ["@printf"])), + PkgAutoloads(:Random, exported = Bindings(callables = [ + :AbstractRNG, :MersenneTwister, :Random, :RandomDevice, :TaskLocalRNG, + :Xoshiro, :bitrand, :rand!, :randcycle, :randcycle!, :randexp, + :randexp!, :randn!, :randperm, :randperm!, :randstring, :randsubseq, + :randsubseq!, :shuffle, :shuffle!])), + PkgAutoloads(:REPL, exported = Bindings(callables = + [:AbstractREPL, :BasicREPL, :LineEditREPL, :StreamREPL])), + PkgAutoloads(:SHA, exported = Bindings(callables = + [:HMAC_CTX, :SHA1_CTX, :SHA224_CTX, :SHA256_CTX, :SHA2_224_CTX, + :SHA2_256_CTX, :SHA2_384_CTX, :SHA2_512_CTX, :SHA384_CTX, :SHA3_224_CTX, + :SHA3_256_CTX, :SHA3_384_CTX, :SHA3_512_CTX, :SHA512_CTX, :digest!, + :hmac_sha1, :hmac_sha224, :hmac_sha256, :hmac_sha2_224, :hmac_sha2_256, + :hmac_sha2_384, :hmac_sha2_512, :hmac_sha384, :hmac_sha3_224, + :hmac_sha3_256, :hmac_sha3_384, :hmac_sha3_512, :hmac_sha512, :sha1, + :sha224, :sha256, :sha2_224, :sha2_256, :sha2_384, :sha2_512, :sha384, + :sha3_224, :sha3_256, :sha3_384, :sha3_512, :sha512, :update!])), + PkgAutoloads(:Serialization, exported = Bindings(callables = [ + :AbstractSerializer, :Serializer, :deserialize, :serialize])), + PkgAutoloads( + :Statistics, + exported = Bindings(callables = [ + :cor, :cov, :mean, :mean!, :median, :median!, :middle, :quantile, + :quantile!, :std, :stdm, :var, :varm])), + PkgAutoloads( + :StatsBase, + exported = Bindings(callables = [ + :AbstractDataTransform, :AbstractHistogram, :AbstractWeights, + :AnalyticWeights, :CoefTable, :ConvergenceException, + :CovarianceEstimator, :CronbachAlpha, :ECDF, :FrequencyWeights, + :Histogram, :L1dist, :L2dist, :Linfdist, :ProbabilityWeights, + :SimpleCovariance, :UnitRangeTransform, :UnitWeights, :Weights, + :ZScoreTransform, :addcounts!, :autocor, :autocor!, :autocov, + :autocov!, :aweights, :competerank, :cor2cov, :corkendall, + :corspearman, :counteq, :countmap, :countne, :counts, :cov2cor, + :cronbachalpha, :crosscor, :crosscor!, :crosscov, :crosscov!, + :crossentropy, :cumulant, :denserank, :ecdf, :entropy, :eweights, + :fweights, :genmean, :genvar, :geomean, :gkldiv, :harmmean, + :indexmap, :indicatormat, :inverse_rle, :iqr, :kldivergence, + :kurtosis, :levelsmap, :mad, :mad!, :maxad, :mean_and_cov, + :mean_and_std, :mean_and_var, :meanad, :midpoints, :mode, + :model_response, :modes, :moment, :msd, :norepeats, :nquantile, + :ordinalrank, :pacf, :pacf!, :partialcor, :percentile, + :percentilerank, :proportionmap, :proportions, :psnr, :pweights, + :quantilerank, :renyientropy, :rle, :rmsd, :sample, :sample!, + :samplepair, :scattermat, :scattermat_zm, :scattermatm, :sem, + :skewness, :span, :sqL2dist, :standardize, :summarystats, :tiedrank, + :totalvar, :trim, :trim!, :trimvar, :uweights, :variation, :winsor, + :winsor!, :wmedian, :wquantile, :wsample, :wsample!, :wsum, :wsum!, + :zscore, :zscore!])), + PkgAutoloads(:TOML), + PkgAutoloads(:Tar), + PkgAutoloads(:Test, exported = + Bindings(macros = ["@inferred", "@test", "@test_broken", + "@test_deprecated", "@test_logs", "@test_nowarn", + "@test_skip", "@test_throws", "@test_warn", + "@testset"], + callables = [:GenericArray, :GenericDict, :GenericOrder, + :GenericSet, :GenericString, :LogRecord, + :TestLogger, :TestSetException, + :detect_ambiguities, :detect_unbound_args])), + PkgAutoloads(:UUIDs, exported = Bindings(callables = [ + :uuid1, :uuid4, :uuid5, :uuid_version])), + PkgAutoloads(:WhyNotEqual, exported = Bindings(callables = [:whynot])), +]) diff --git a/src/autoloads.jl b/src/autoloads.jl new file mode 100644 index 0000000..1493219 --- /dev/null +++ b/src/autoloads.jl @@ -0,0 +1,270 @@ +module Autoloads + +using TOML +using REPL + +export Bindings, AutoDot, PkgAutoloads, add_autoloads! + +function __init__() + if isinteractive() + read_named_envs!() + repl_apply_autoloads!() + end +end + +struct Bindings + macros::Vector{Symbol} + callables::Vector{Symbol} + variables::Vector{Symbol} +end + +struct AutoDot end + +Bindings(; macros::Vector{<:Union{Symbol, String}}=Symbol[], + callables::Vector{Symbol}=Symbol[], variables=Symbol[]) = + Bindings(Symbol.(macros), callables, variables) + +struct PkgAutoloads + pkg::Union{Symbol, Expr} + exported::Bindings + unexported::Union{Bindings, AutoDot} + partial::Bool +end + +PkgAutoloads(pkg::Union{Symbol, Expr}; + exported::Bindings = Bindings(Symbol[], Symbol[], Symbol[]), + unexported::Union{Bindings, AutoDot} = AutoDot(), + partial::Bool = false) = + PkgAutoloads(pkg, exported, unexported, partial) + +function PkgAutoloads(mod::Module) + hasparent(u::Union, m::Module) = + hasparent(u.a, m) && hasparent(u.b, m) + hasparent(x::Any, m::Module) = + parentmodule(x) === m + macros = Symbol[] + callables = Symbol[] + variables = Symbol[] + for symb in names(mod) + obj = getglobal(mod, symb) + if !hasparent(obj, mod) || obj === mod + elseif startswith(String(symb), '@') + push!(macros, symb) + elseif obj isa Function || obj isa Type + push!(callables, symb) + else + push!(variables, symb) + end + end + PkgAutoloads(nameof(mod), + exported = Bindings( + macros, callables, variables)) +end + +const AUTOLOADS = + (sources = Dict{Symbol, PkgAutoloads}(), + pkgs = Dict{Symbol, Expr}(), + exported = ( + macros = Dict{Symbol, Symbol}(), + callables = Dict{Symbol, Symbol}(), + variables = Dict{Symbol, Symbol}()), + bindings = ( + macros = Dict{Symbol, Set{Symbol}}(), + callables = Dict{Symbol, Set{Symbol}}(), + variables = Dict{Symbol, Set{Symbol}}()), + ondotaccess = Set{Symbol}(), + loaded = Set{Symbol}(), + partloaded = Dict{Symbol, Set{Symbol}}()) + +function add_autoloads!(autos::PkgAutoloads) + !haskey(AUTOLOADS.sources, autos.pkg) || + error("Autoloads for $(autos.pkg) have already been registered") + pkgsym, pkg = if autos.pkg isa Symbol + autos.pkg, Expr(:., autos.pkg) + elseif autos.pkg isa Expr && + Meta.isexpr(autos.pkg, :(.)) && + length(autos.pkg.args) == 2 && + autos.pkg.args[1] isa Symbol && + autos.pkg.args[2] isa QuoteNode && + autos.pkg.args[2].value isa Symbol + autos.pkg.args[2].value, + Expr(:., autos.pkg.args[1], autos.pkg.args[2].value) + end + AUTOLOADS.sources[pkgsym] = autos + AUTOLOADS.pkgs[pkgsym] = pkg + autos.partial && (AUTOLOADS.partloaded[pkgsym] = Set{Symbol}()) + AUTOLOADS.exported.variables[pkgsym] = pkgsym + for category in (:macros, :callables, :variables) + autodot = autos.unexported isa AutoDot + bindings = autodot || Set(getfield(autos.unexported, category)) + slots = getfield(AUTOLOADS.exported, category) + for binding in getfield(autos.exported, category) + if haskey(slots, binding) + @warn "An autload for $(String(category)[1:end-1]) $binding already exists" + else + slots[binding] = pkgsym + end + autodot || push!(bindings, binding) + end + if !autodot && !isempty(bindings) + getfield(AUTOLOADS.bindings, category)[pkgsym] = bindings + end + end +end + +const NAMED_ENVS = Vector{Pair{String, Vector{Symbol}}}() + +function read_named_envs!() + empty!(NAMED_ENVS) + for depot in Base.DEPOT_PATH + envdir = joinpath(depot, "environments") + isdir(envdir) || continue + for env in readdir(envdir) + if !isnothing(match(r"^__", env)) + elseif !isnothing(match(r"^v\d+\.\d+$", env)) + elseif isfile(joinpath(envdir, env, "Project.toml")) + proj = open(TOML.parse, joinpath(envdir, env, "Project.toml")) + pkgs = get(proj, "deps", Dict{String, Any}()) |> + keys |> collect |> sort .|> Symbol + push!(NAMED_ENVS, '@' * env => pkgs) + end + end + end +end + +function find_named_env(pkg::Symbol) + for (env, pkgs) in NAMED_ENVS + if pkg in pkgs + return env + end + end +end + +function autoload_loadpkg(name::Symbol, thing::Union{Symbol, Nothing}=nothing) + if name ∉ AUTOLOADS.loaded && haskey(AUTOLOADS.pkgs, name) + AUTOLOADS.sources[name].partial && thing in AUTOLOADS.partloaded[name] && + return + pkg = AUTOLOADS.pkgs[name] + if length(pkg.args) == 1 && isnothing(Base.find_package(String(first(pkg.args)))) + named_env = find_named_env(name) + if !isnothing(named_env) + autoload_loadpkg_namedenv(name, named_env, thing) + else + @warn "Cannot autoload $name, as it is not availible in the current environment stack" + end + return + end + try + if !isnothing(thing) && AUTOLOADS.sources[name].partial + Core.eval(Main, Expr(:import, Expr(:., pkg.args..., thing))) + push!(AUTOLOADS.partloaded[name], thing) + else + Core.eval(Main, Expr(:using, pkg)) + push!(AUTOLOADS.loaded, name) + end + nothing + catch err + @warn "Failed to automatically load $name" err + end + end +end + +function autoload_loadpkg_namedenv(name::Symbol, env::String, thing::Union{Symbol, Nothing}=nothing) + pkg = AUTOLOADS.pkgs[name] + if Base.active_project() == Base.load_path_expand("@v#.#") + pushfirst!(LOAD_PATH, env) + try + if !isnothing(thing) && AUTOLOADS.sources[name].partial + Core.eval(Main, Expr(:import, Expr(:., pkg.args..., thing))) + push!(AUTOLOADS.partloaded[name], thing) + else + Core.eval(Main, Expr(:using, pkg)) + push!(AUTOLOADS.loaded, name) + end + finally + popfirst!(LOAD_PATH) + end + else + @warn "Cannot autoload $name, but it is availible in the $env environment" + end +end + +function autoload_scan!(ast::Expr, localbindings::Vector{Symbol}=Symbol[]) + if Meta.isexpr(ast, :macrocall) + pkg = if first(ast.args) isa Symbol && first(ast.args) ∉ localbindings + get(AUTOLOADS.exported.macros, first(ast.args), nothing) + elseif Meta.isexpr(first(ast.args), :(.)) && + length(first(ast.args).args) == 2 && + (root = first(ast.args).args[1]) isa Symbol && + first(ast.args).args[2] isa QuoteNode && + (child = first(ast.args).args[2].value) isa Symbol && + haskey(AUTOLOADS.bindings.macros, root) && + haskey(AUTOLOADS.bindings.macros[root], child) + AUTOLOADS.bindings.macros[root] + end + if !isnothing(pkg) autoload_loadpkg(pkg, first(ast.args)) end + elseif Meta.isexpr(ast, :call) + pkg = if first(ast.args) isa Symbol && first(ast.args) ∉ localbindings + get(AUTOLOADS.exported.callables, first(ast.args), nothing) + elseif Meta.isexpr(first(ast.args), :(.)) && + (root = first(ast.args).args[1]) isa Symbol && + root ∈ AUTOLOADS.ondotaccess + root + elseif Meta.isexpr(first(ast.args), :(.)) && + length(first(ast.args).args) == 2 && + (root = first(ast.args).args[1]) isa Symbol && + first(ast.args).args[2] isa QuoteNode && + (child = first(ast.args).args[2].value) isa Symbol && + haskey(AUTOLOADS.bindings.callables, root) && + child ∈ AUTOLOADS.bindings.callables[root] + root + end + isnothing(pkg) || autoload_loadpkg(pkg, first(ast.args)) + elseif Meta.isexpr(ast, :(.)) && + length(ast.args) == 2 && + (root = ast.args[1]) isa Symbol && + ast.args[2] isa QuoteNode && + (child = ast.args[2].value) isa Symbol && + (root ∈ AUTOLOADS.ondotaccess || + (haskey(AUTOLOADS.bindings.variables, root) && + child ∈ AUTOLOADS.bindings.variables[root]) || + (haskey(AUTOLOADS.bindings.callables, root) && + child ∈ AUTOLOADS.bindings.callables[root])) + autoload_loadpkg(root) + elseif ast.head ∈ (:using, :import, :quote) + return + elseif Meta.isexpr(ast, :(=)) && first(ast.args) isa Symbol + push!(localbindings, first(ast.args)) + end + for arg in ast.args + autoload_scan!(arg, localbindings) + end +end + +function autoload_scan!(var::Symbol, localbindings::Vector{Symbol}=Symbol[]) + if var ∉ localbindings && var ∉ names(Main) + pkg = @something(get(AUTOLOADS.exported.variables, var, nothing), + get(AUTOLOADS.exported.callables, var, nothing), + Some(nothing)) + if !isnothing(pkg) autoload_loadpkg(pkg, var) end + end +end + +autoload_scan!(::Any, ::Vector{Symbol}=Symbol[]) = nothing + +function repl_scan_autoloads!(ast::Expr) + @static if VERSION < v"1.9" + autoload_scan!(ast) + else + if Base.active_module() == Main + autoload_scan!(ast) + end + end + ast +end +repl_scan_autoloads!(val::Any) = val + +repl_apply_autoloads!() = + pushfirst!(REPL.repl_ast_transforms, repl_scan_autoloads!) + +end diff --git a/src/cmdpuns.jl b/src/cmdpuns.jl new file mode 100644 index 0000000..d01d1d3 --- /dev/null +++ b/src/cmdpuns.jl @@ -0,0 +1,71 @@ +import Base.!, Base.~ + +""" + ~(cmd::AbstractCmd) + +Run `cmd` and return the output as a string. + +```julia-repl +julia> ~ `ls /home` +"doe" +``` +""" +function ~(c::Base.AbstractCmd) + strip(read(c, String), '\n') +end + +""" + !(cmd::AbstractCmd) + +Run `cmd` and print the output to stdout. + +```julia-repl +julia> ! `ls /home` +doe +``` + +This can also be combined with `~ cmd` to print and return +the output by using `!~ cmd`. +```julia-repl +julia> !~ `ls /home` +doe +"doe" +``` +""" +function !(c::Base.AbstractCmd) + println(~c) +end +function !(o::SubString{String}) + println(o) + o +end + +struct PipeCmds <: Base.AbstractCmd + cmds::Vector{<:Base.AbstractCmd} + PipeCmds(left::Base.AbstractCmd, right::Base.AbstractCmd) = new([left, right]) + PipeCmds(left::PipeCmds, right::Base.AbstractCmd) = new([left.cmds; right]) + PipeCmds(left::Base.AbstractCmd, right::PipeCmds) = new([left; right.cmds]) +end + +import Base.| + +""" + cmd1::AbstractCmd | cmd2::AbstractCmd + +Create a pipleine from `cmd1` to `cmd2`. + +```julia-repl +julia> `cat .zshenv` | `wc -l` +PipeCmds(Cmd[`cat .zshenv`, `wc -l`]) + +When `PipeCmds` are passed to `!` or `~`, the commands contained are +expanded inside a `pipeline` call. +``` +""" +function (|)(left::Base.AbstractCmd, right::Base.AbstractCmd) + PipeCmds(left, right) +end + +function ~(c::PipeCmds) + strip(read(pipeline(c.cmds...), String), '\n') +end diff --git a/src/install.jl b/src/install.jl new file mode 100644 index 0000000..1c00c56 --- /dev/null +++ b/src/install.jl @@ -0,0 +1,79 @@ +const SETUP_GIT_URL = "https://git.tecosaur.net/tec/Startup.jl" + +const STARTUP_MANAGED_MESSAGE = + "# !! Managed by the Setup package !!" + +const STARTUP_CONTENT = """ +$STARTUP_MANAGED_MESSAGE +if VERSION < v"1.7" + @warn "Setup disabled as Julia \$VERSION < 1.7 is unsupported" +else + let pkg_id = Base.PkgId(Base.UUID("44cfe95a-1eb2-52ea-b672-e2afdf69b78f"), "Pkg") + Pkg = Base.loaded_modules[pkg_id] + isnothing(Base.find_package("Setup")) && + Pkg.develop(path=joinpath(@__DIR__, "Setup")) + end + + using Setup + + Setup.ensurepkg("Revise") + @async @eval using Revise +end +""" + +function _install_paths() + config = joinpath(first(Base.DEPOT_PATH), "config") + startup = joinpath(config, "startup.jl") + setup = joinpath(config, "Setup") + (; config, startup, setup) +end + +function install() + (; config, startup, setup) = _install_paths() + isdir(config) || mkpath(config) + if isfile(startup) + if readline(startup) == STARTUP_MANAGED_MESSAGE + isdir(setup) && update() + else + @info "Moving original startup file to $(startup).old" + mv(startup, startup * ".old") + end + end + write(startup, STARTUP_CONTENT) + if isdir(setup) && !isdir(joinpath(setup, ".git")) + @warn "Setup folder exists, but it is not a git repository. Re-creating..." + rm(setup, recursive=true) + end + if !isdir(setup) + success(`git clone $SETUP_GIT_URL $setup`) || + @error "Failed to clone $SETUP_GIT_URL" + end +end + +function uninstall() + (; startup, setup) = _install_paths() + isfile(startup) && readline(startup) != STARTUP_MANAGED_MESSAGE && + rm(startup) + isfile(startup * ".old") && !isfile(startup) && mv(startup * ".old", startup) + if isdir(joinpath(setup, ".git")) && !isempty(read(Cmd(`git status --porcelain=v1`, dir=setup))) + @warn "Unstaged changes in $(setup)! Manually fix this and try again." + elseif isdir(setup) + rm(setup, recursive=true, force=true) + end +end + +function update() + (; startup, setup) = _install_paths() + if readline(startup) != STARTUP_MANAGED_MESSAGE + @warn "Startup.jl is not managed by Setup, re-installing" + return install() + else + write(startup, STARTUP_CONTENT) + end + if !ispath(setup) || !isdir(joinpath(setup, ".git")) + ispath(setup) || @warn "Setup missing, re-installing" + return install() + end + success(Cmd(`git pull`, dir=setup)) || + @error "Failed to update Setup" +end diff --git a/src/pkgstack.jl b/src/pkgstack.jl new file mode 100644 index 0000000..8728812 --- /dev/null +++ b/src/pkgstack.jl @@ -0,0 +1,133 @@ +module PkgStack + +import Pkg +import Markdown: @md_str + +function stack(envs) + if isempty(envs) + printstyled(" The current stack:\n", bold=true) + println.(" " .* LOAD_PATH) + else + for env in envs + if env ∉ LOAD_PATH + push!(LOAD_PATH, env) + end + end + end +end + +const STACK_SPEC = Pkg.REPLMode.CommandSpec( + name = "stack", + api = stack, + help = md""" + stack envs... + +Stack another environment. +""", + description = "Stack another environment", + completions = Pkg.REPLMode.complete_activate, + should_splat = false, + arg_count = 0 => Inf) + +function unstack(envs) + if isempty(envs) + printstyled(" The current stack:\n", bold=true) + println.(" " .* LOAD_PATH) + else + deleteat!(LOAD_PATH, sort(filter(!isnothing, indexin(envs, LOAD_PATH)))) + end +end + +const UNSTACK_SPEC = Pkg.REPLMode.CommandSpec( + name = "unstack", + api = unstack, + help = md""" + unstack envs... + +Unstack a previously stacked environment. +""", + description = "Unstack an environment", + completions = (_, partial, _, _) -> + filter(p -> startswith(p, partial), LOAD_PATH), + should_splat = false, + arg_count = 0 => Inf) + +function environments() + envs = String[] + for depot in Base.DEPOT_PATH + envdir = joinpath(depot, "environments") + isdir(envdir) || continue + for env in readdir(envdir) + if !isnothing(match(r"^__", env)) + elseif !isnothing(match(r"^v\d+\.\d+$", env)) + else + push!(envs, '@' * env) + end + end + end + envs = Base.DEFAULT_LOAD_PATH ∪ LOAD_PATH ∪ envs + for env in envs + if env in LOAD_PATH + print(" ", env) + else + printstyled(" ", env, color=:light_black) + if env in Base.DEFAULT_LOAD_PATH + printstyled(" (unloaded)", color=:light_red) + end + end + if env == "@" + printstyled(" [current environment]", color=:light_black) + elseif env == "@v#.#" + printstyled(" [global environment]", color=:light_black) + elseif env == "@stdlib" + printstyled(" [standard library]", color=:light_black) + elseif env in LOAD_PATH + printstyled(" (loaded)", color=:green) + end + print('\n') + end +end + +const ENVS_SPEC = Pkg.REPLMode.CommandSpec( + name = "environments", + short_name = "envs", + api = environments, + help = md""" + environments|envs + +List all known named environments. +""", + description = "List all known named environments", + arg_count = 0 => 0) + +const SPECS = Dict( + "stack" => STACK_SPEC, + "unstack" => UNSTACK_SPEC, + "environments" => ENVS_SPEC, + "envs" => ENVS_SPEC) + +function __init__() + # add the commands to the repl + activate = Pkg.REPLMode.SPECS["package"]["activate"] + let temp = Pkg.REPLMode.OptionSpec("temp", "t", :temp => true, false) + activate.option_specs["temp"] = temp + activate.option_specs["t"] = temp + end + activate_modified = Pkg.REPLMode.CommandSpec( + activate.canonical_name, + "a", # Modified entry, short name + activate.api, + activate.should_splat, + activate.argument_spec, + activate.option_specs, + activate.completions, + activate.description, + activate.help) + SPECS["activate"] = activate_modified + SPECS["a"] = activate_modified + Pkg.REPLMode.SPECS["package"] = merge(Pkg.REPLMode.SPECS["package"], SPECS) + # update the help with the new commands + copy!(Pkg.REPLMode.help.content, Pkg.REPLMode.gen_help().content) +end + +end diff --git a/src/termsetup.jl b/src/termsetup.jl new file mode 100644 index 0000000..5510685 --- /dev/null +++ b/src/termsetup.jl @@ -0,0 +1,153 @@ +import Base.cd + +""" + termtitle(dir) + +Set the terminal title suffix to `dir`. +The home directory is replaced with "~", and trailing slash removed. +""" +function termtitle(dir) + if ENV["TERM"] != "dumb" + print("\e]0;Julia ● " * + replace(replace(dir, homedir() => "~"), r"/$" => "") * + "\a") + end +end + +function replsetup() + atreplinit() do repl + if !isdefined(repl, :interface) + repl.interface = REPL.setup_interface(repl) + end + if Main.InteractiveUtils.editor().exec[1] == "e" + Main.InteractiveUtils.define_editor( + r"\be", wait=false) do cmd, path, line + `$cmd +$line $path` + end + Main.InteractiveUtils.define_editor( + r"\be\b.*\s(-w|--wait|-t|--tty)", wait=true) do cmd, path, line + `$cmd +$line $path` + end + end + if get(ENV, "INSIDE_EMACS", nothing) != "vterm" + print("\e[5 q") + end + @static if VERSION >= v"1.9" + # Reset title every prompt + repl.interface.modes[1].prompt = () -> + (termtitle(pwd()); REPL.contextual_prompt(repl, REPL.JULIA_PROMPT)()) + end + end + @eval function Base.cd(dir::AbstractString) + err = ccall(:uv_chdir, Cint, (Cstring,), dir) + err < 0 && Base.uv_error("cd($(repr(dir)))", err) + termtitle(dir) + return nothing + end +end + +clearterm() = print("\e[2J") + +function termcode() + REPL.Terminals.raw!(REPL.TerminalMenus.terminal, true) + print("\eP+q544e\e\\") + output = @task readuntil(stdin, "\e\\") + schedule(output) + Timer(0.05) do _ + istaskdone(output) || Base.throwto(output, InterruptException()) + end + value = try + fetch(output) + catch + "" + end + REPL.Terminals.raw!(REPL.TerminalMenus.terminal, false) + String(hex2bytes(last(split(value, '=')))) +end + +const TERM_COLORS = Ref(Dict{Symbol, @NamedTuple{r::UInt8, g::UInt8, b::UInt8}}()) + +function termcolors(; refresh::Bool=false) + !refresh && !isempty(TERM_COLORS[]) && return TERM_COLORS[] + REPL.Terminals.raw!(REPL.TerminalMenus.terminal, true) + output = @task _termcolors() + schedule(output) + Timer(0.5) do _ + istaskdone(output) || Base.throwto(output, InterruptException()) + end + colors = try + fetch(output) + catch end + REPL.Terminals.raw!(REPL.TerminalMenus.terminal, false) + if !isnothing(colors) + TERM_COLORS[] = colors + end + TERM_COLORS[] +end + +function termtheme(; refresh::Bool=false) + colors = termcolors(; refresh) + white = get(colors, :white, (r=0x80, g=0x80, b=0x80)) + black = get(colors, :black, (r=0x80, g=0x80, b=0x80)) + fglevel, bglevel = sum(values(white)), sum(values(black)) + if fglevel > bglevel + :dark + elseif fglevel < bglevel + :light + else + :unknown + end +end + +function _termcolors() + RGBTuple = NamedTuple{(:r, :g, :b), NTuple{3, UInt8}} + colors = Dict{Symbol, RGBTuple}() + function readcolor(io::IO, n::Integer) + if read(io, Char) == '\e' + entry = readuntil(io, "\e\\") + m = match(r"^]4;(\d+);rgb:([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)$", entry) + if !isnothing(m) && m.captures[1] == string(n) + r = parse(UInt8, m.captures[2][1:2], base=16) + g = parse(UInt8, m.captures[3][1:2], base=16) + b = parse(UInt8, m.captures[4][1:2], base=16) + RGBTuple((r, g, b)) + end + end + end + colornames = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white, + :light_black, :light_red, :light_green, :light_yellow, + :light_blue, :light_magenta, :light_cyan, :light_white] + for (index, color) in enumerate(colornames) + print(stdout, "\e]4;$(index-1);?\e\\") + flush(stdout) + rgb = readcolor(stdin, index - 1) + if !isnothing(rgb) + colors[color] = rgb + end + end + colors +end + +const TERM_IMAGE_PACKAGES = let pkg(uuid, name) = Base.PkgId(Base.UUID(uuid), name) + kitty = (pkg("b7fa5abe-5c7d-46c6-a1ae-1026d0d509b9", "KittyTerminalImages"), + :(KittyTerminalImages.pushKittyDisplay!())) + # TODO: sixel + Dict("xterm-kitty" => kitty) +end + +const ATTEMPTED_TERMIMAGE_LOAD = Ref(false) + +function load_termimage_pkg() + ATTEMPTED_TERMIMAGE_LOAD[] && return + pkg, pkgsetup = get(TERM_IMAGE_PACKAGES, Setup.TERM[], (nothing, nothing)) + if !isnothing(pkg) && !haskey(Base.loaded_modules, pkg) + if !isnothing(Base.find_package(pkg.name)) + ATTEMPTED_TERMIMAGE_LOAD[] = true + # Don't to this async because it may affect a `display` call + # that's just about to occur. + Core.eval(Main, :(import $(Symbol(pkg.name)); $pkgsetup)) + else + @info "Consider installing $(pkg.name) for better in-terminal image previews" + end + end +end