Skip to content

Commit 54861c4

Browse files
committed
make an ijulia "app" entrypoint
this makes ijulia installable via the Pkg app interface: https://pkgdocs.julialang.org/v1/apps/ and allows one to do e.g. ``` pkg> app add IJulia ``` and then run e.g. ``` ijulia lab ``` in the terminal to start an ijulia jupyterlab notebook without having to launch julia itself and load IJulia explicitly.
1 parent 656ed86 commit 54861c4

File tree

5 files changed

+268
-2
lines changed

5 files changed

+268
-2
lines changed

Project.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ name = "IJulia"
22
uuid = "7073ff75-c697-5162-941a-fcdaad2a7d2a"
33
version = "1.32.0"
44

5+
[apps]
6+
ijulia = {}
7+
58
[deps]
69
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
710
Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d"

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,24 @@ Note that `IJulia` should generally be installed in Julia's global package envir
4040
install a custom kernel that specifies a particular environment.
4141

4242
Alternatively, you can have IJulia create and manage its own Python/Jupyter installation.
43-
To do this, type the following in Julia, at the `julia>` prompt:
4443

44+
**With Julia 1.12+**, you can launch IJulia directly from the command line. First install the app entry point:
45+
```julia
46+
pkg> app add IJulia
47+
```
48+
Then run:
49+
```bash
50+
ijulia --help
51+
```
52+
53+
**With Julia 1.10-1.11**, type the following in Julia, at the `julia>` prompt:
4554
```julia
4655
using IJulia
4756
notebook()
4857
```
4958

5059
to launch the IJulia notebook in your browser.
51-
The first time you run `notebook()`, it will prompt you
60+
The first time you run `notebook()` or `ijulia`, it will prompt you
5261
for whether it should install Jupyter. Hit enter to
5362
have it use the [Conda.jl](https://github.com/Luthaf/Conda.jl)
5463
package to install a minimal Python+Jupyter distribution (via

docs/src/manual/running.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
11
# Running IJulia
22

33

4+
## Command-Line Launcher
5+
6+
Starting with Julia 1.12, you can launch IJulia directly from the command line using the `ijulia` command. First, you need to install the app entry point by running in the Julia REPL:
7+
8+
```julia
9+
pkg> app add IJulia
10+
```
11+
12+
Then you can use the `ijulia` command from your terminal:
13+
14+
```bash
15+
ijulia
16+
```
17+
18+
This provides a convenient way to launch Jupyter without starting a Julia REPL session first. The launcher supports the following options:
19+
20+
- `ijulia` or `ijulia notebook` - Launch Jupyter Notebook (default)
21+
- `ijulia lab` - Launch JupyterLab
22+
- `--dir=PATH` - Launch in the specified directory (default: home directory)
23+
- `--port=N` - Open on the specified port number
24+
- `--detached` - Run in detached mode (continues after Julia exits)
25+
- `--verbose` - Enable verbose output from Jupyter
26+
- `--help, -h` - Show help message
27+
28+
Any additional arguments are passed directly to the jupyter command.
29+
30+
**Examples:**
31+
```bash
32+
# Launch notebook in a specific directory
33+
ijulia --dir=/path/to/project
34+
35+
# Launch JupyterLab on a specific port
36+
ijulia lab --port=8888 --detached
37+
38+
# Pass additional arguments to Jupyter
39+
ijulia --no-browser
40+
```
41+
42+
443
## Running the IJulia Notebook
544

645
If you are comfortable managing your own Python/Jupyter installation, you can just run `jupyter notebook` yourself in a terminal. To simplify installation, however, you can alternatively type the following in Julia, at the `julia>` prompt:

src/IJulia.jl

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,4 +584,92 @@ include("inline.jl")
584584
include("kernel.jl")
585585
include("precompile.jl")
586586

587+
#######################################################################
588+
# App entry point for `ijulia` command
589+
590+
# Mutable function references for testing - allows tests to mock these functions
591+
notebook_cmd::Function = notebook
592+
jupyterlab_cmd::Function = jupyterlab
593+
594+
function (@main)(ARGS)
595+
# Show help if help flag
596+
if !isempty(ARGS) && ARGS[1] in ("--help", "-h", "help")
597+
println("""
598+
IJulia Launcher
599+
600+
Usage: ijulia [command] [OPTIONS] [JUPYTER_ARGS...]
601+
602+
Commands:
603+
notebook Launch Jupyter notebook (default)
604+
lab Launch JupyterLab
605+
606+
Options:
607+
--dir=PATH Launch in the specified directory (default: home directory)
608+
--port=N Open on the specified port number
609+
--detached Run in detached mode (continues after Julia exits)
610+
--verbose Enable verbose output from Jupyter
611+
--help, -h Show this help message
612+
613+
Any additional arguments are passed directly to the jupyter command.
614+
615+
Examples:
616+
ijulia --dir=/path/to/project --port=8888
617+
ijulia notebook --dir=/path/to/project --port=8888
618+
ijulia lab --detached --verbose
619+
ijulia --no-browser
620+
""")
621+
return 0
622+
end
623+
624+
# Parse subcommand (default to "notebook")
625+
subcommand = "notebook"
626+
args_start = 1
627+
if !isempty(ARGS)
628+
first_arg = ARGS[1]
629+
if first_arg in ("notebook", "lab")
630+
subcommand = first_arg
631+
args_start = 2
632+
elseif !startswith(first_arg, "--")
633+
# First arg looks like a subcommand but isn't valid
634+
@error "Unknown subcommand: $first_arg. Use 'notebook' or 'lab'."
635+
return 1
636+
end
637+
end
638+
639+
# Parse options
640+
dir = homedir()
641+
port = nothing
642+
detached = false
643+
verbose = false
644+
extra_args = String[]
645+
646+
for arg in ARGS[args_start:end]
647+
if startswith(arg, "--dir=")
648+
dir = arg[7:end]
649+
elseif startswith(arg, "--port=")
650+
port = parse(Int, arg[8:end])
651+
elseif arg == "--detached"
652+
detached = true
653+
elseif arg == "--verbose"
654+
verbose = true
655+
else
656+
push!(extra_args, arg)
657+
end
658+
end
659+
660+
# Launch the appropriate command
661+
launch_func = subcommand == "notebook" ? notebook_cmd : jupyterlab_cmd
662+
try
663+
launch_func(Cmd(extra_args); dir=dir, detached=detached, port=port, verbose=verbose)
664+
return 0
665+
catch e
666+
if e isa InterruptException
667+
return 0
668+
else
669+
@error "Failed to launch $subcommand" exception=(e, catch_backtrace())
670+
return 1
671+
end
672+
end
673+
end
674+
587675
end # IJulia

test/main.jl

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using Test
2+
import IJulia
3+
4+
@testset "@main app entry point" begin
5+
# Test help output
6+
@testset "help" begin
7+
# Capture stdout using a pipe
8+
old_stdout = stdout
9+
rd, wr = redirect_stdout()
10+
11+
ret = IJulia.main(["--help"])
12+
13+
# Restore stdout and read the captured output
14+
redirect_stdout(old_stdout)
15+
close(wr)
16+
help_text = read(rd, String)
17+
close(rd)
18+
19+
@test ret == 0
20+
@test occursin("IJulia Launcher", help_text)
21+
@test occursin("notebook", help_text)
22+
@test occursin("lab", help_text)
23+
@test occursin("--dir=PATH", help_text)
24+
@test occursin("--port=N", help_text)
25+
@test occursin("--detached", help_text)
26+
@test occursin("--verbose", help_text)
27+
end
28+
29+
# Test invalid subcommand
30+
@testset "invalid subcommand" begin
31+
ret = @test_logs (:error, r"Unknown subcommand.*Use 'notebook' or 'lab'") IJulia.main(["invalid"])
32+
@test ret == 1
33+
end
34+
35+
# Test argument parsing without actually launching
36+
# We use mutable function references (notebook_cmd/jupyterlab_cmd) to test parsing
37+
@testset "argument parsing" begin
38+
# Save original functions
39+
orig_notebook = IJulia.notebook_cmd
40+
orig_jupyterlab = IJulia.jupyterlab_cmd
41+
42+
# Test notebook (default)
43+
called_with = nothing
44+
IJulia.notebook_cmd = function(args=``; dir=homedir(), detached=false, port=nothing, verbose=false)
45+
called_with = (; args, dir, detached, port, verbose)
46+
return nothing
47+
end
48+
49+
try
50+
ret = IJulia.main(["--dir=/test", "--port=9999", "--detached", "--verbose", "--no-browser"])
51+
@test ret == 0
52+
@test !isnothing(called_with)
53+
@test called_with.dir == "/test"
54+
@test called_with.port == 9999
55+
@test called_with.detached == true
56+
@test called_with.verbose == true
57+
@test "--no-browser" in called_with.args
58+
finally
59+
IJulia.notebook_cmd = orig_notebook
60+
end
61+
62+
# Test explicit notebook subcommand
63+
called_with = nothing
64+
IJulia.notebook_cmd = function(args=``; dir=homedir(), detached=false, port=nothing, verbose=false)
65+
called_with = (; args, dir, detached, port, verbose)
66+
return nothing
67+
end
68+
69+
try
70+
ret = IJulia.main(["notebook", "--port=8888"])
71+
@test ret == 0
72+
@test !isnothing(called_with)
73+
@test called_with.port == 8888
74+
@test called_with.dir == homedir()
75+
@test called_with.detached == false
76+
finally
77+
IJulia.notebook_cmd = orig_notebook
78+
end
79+
80+
# Test lab subcommand
81+
called_with = nothing
82+
IJulia.jupyterlab_cmd = function(args=``; dir=homedir(), detached=false, port=nothing, verbose=false)
83+
called_with = (; args, dir, detached, port, verbose)
84+
return nothing
85+
end
86+
87+
try
88+
ret = IJulia.main(["lab", "--dir=/lab", "--verbose"])
89+
@test ret == 0
90+
@test !isnothing(called_with)
91+
@test called_with.dir == "/lab"
92+
@test called_with.verbose == true
93+
finally
94+
IJulia.jupyterlab_cmd = orig_jupyterlab
95+
end
96+
end
97+
98+
# Test InterruptException handling
99+
@testset "interrupt handling" begin
100+
orig_notebook = IJulia.notebook_cmd
101+
IJulia.notebook_cmd = function(args=``; kwargs...)
102+
throw(InterruptException())
103+
end
104+
105+
try
106+
ret = IJulia.main([])
107+
@test ret == 0 # InterruptException should return 0
108+
finally
109+
IJulia.notebook_cmd = orig_notebook
110+
end
111+
end
112+
113+
# Test error handling
114+
@testset "error handling" begin
115+
orig_notebook = IJulia.notebook_cmd
116+
IJulia.notebook_cmd = function(args=``; kwargs...)
117+
error("Test error")
118+
end
119+
120+
try
121+
ret = @test_logs (:error, r"Failed to launch") IJulia.main([])
122+
@test ret == 1 # Errors should return 1
123+
finally
124+
IJulia.notebook_cmd = orig_notebook
125+
end
126+
end
127+
end

0 commit comments

Comments
 (0)