Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name = "IJulia"
uuid = "7073ff75-c697-5162-941a-fcdaad2a7d2a"
version = "1.32.0"

[apps]
ijulia = {}

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d"
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,24 @@ Note that `IJulia` should generally be installed in Julia's global package envir
install a custom kernel that specifies a particular environment.

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

**With Julia 1.12+**, you can launch IJulia directly from the command line. First install the app entry point:
```julia
pkg> app add IJulia
```
Then run:
```bash
ijulia --help
```

**With Julia 1.10-1.11**, type the following in Julia, at the `julia>` prompt:
```julia
using IJulia
notebook()
```

to launch the IJulia notebook in your browser.
The first time you run `notebook()`, it will prompt you
The first time you run `notebook()` or `ijulia`, it will prompt you
for whether it should install Jupyter. Hit enter to
have it use the [Conda.jl](https://github.com/Luthaf/Conda.jl)
package to install a minimal Python+Jupyter distribution (via
Expand Down
41 changes: 41 additions & 0 deletions docs/src/manual/running.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
# Running IJulia


## Command-Line Launcher

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 (see [Julia app documentation](https://pkgdocs.julialang.org/v1/apps/)) by running in the Julia REPL:

```julia
pkg> app add IJulia
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think IJulia is one of the few packages that still has a build step 😅 Shall we just install the app automatically when building? My only concern is that from reading the docs it seems that an app is somehow hardcoded to a specific Julia binary? That's a bit unfortunate because we recently added a juliaup integration that reduced the need for rebuilding IJulia to once per minor release (#1201). And perhaps if it's that experimental we should leave it out by default, can always enable it later.

Copy link
Member Author

@KristofferC KristofferC Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think IJulia is one of the few packages that still has a build step 😅

I thought about this and I even have a started branch that removes the build step.
Moves the kernel installation into the package and auto-installs on first kernel launch and uses a scratch space to keep the preference (https://github.com/JuliaPackaging/Scratch.jl).

What do you think about that? Build scripts are not that great...

But anyway, once I've fixed a bug, app installation should run the build step like normally.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds ok, as long as it still installs the same kernels as now. @stevengj, what do you think?

```

You may need to add `~/.julia/bin` to your PATH if it's not already there.

Then you can use the `ijulia` command from your terminal:

```bash
ijulia
```

This provides a convenient way to launch Jupyter without starting a Julia REPL session first. The launcher supports the following options:

- `ijulia` or `ijulia notebook` - Launch Jupyter Notebook (default)
- `ijulia lab` - Launch JupyterLab
- `--dir=PATH` - Launch in the specified directory (default: home directory)
- `--port=N` - Open on the specified port number
- `--detached` - Run in detached mode (continues after Julia exits)
- `--verbose` - Enable verbose output from Jupyter
- `--help, -h` - Show help message

Any additional arguments are passed directly to the jupyter command.

**Examples:**
```bash
# Launch notebook in a specific directory
ijulia --dir=/path/to/project

# Launch JupyterLab on a specific port
ijulia lab --port=8888 --detached

# Pass additional arguments to Jupyter
ijulia --no-browser
```


## Running the IJulia Notebook

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:
Expand Down
86 changes: 86 additions & 0 deletions src/IJulia.jl
Original file line number Diff line number Diff line change
Expand Up @@ -584,4 +584,90 @@ include("inline.jl")
include("kernel.jl")
include("precompile.jl")

#######################################################################
# App entry point for `ijulia` command

@static if VERSION >= v"1.11"
function (@main)(ARGS; notebook_cmd::Function = notebook, jupyterlab_cmd::Function = jupyterlab)
# Show help if help flag
if !isempty(ARGS) && ARGS[1] in ("--help", "-h", "help")
println("""
IJulia Launcher

Usage: ijulia [command] [OPTIONS] [JUPYTER_ARGS...]

Commands:
notebook Launch Jupyter notebook (default)
lab Launch JupyterLab

Options:
--dir=PATH Launch in the specified directory (default: home directory)
--port=N Open on the specified port number
--detached Run in detached mode (continues after Julia exits)
--verbose Enable verbose output from Jupyter
--help, -h Show this help message

Any additional arguments are passed directly to the jupyter command.

Examples:
ijulia --dir=/path/to/project --port=8888
ijulia notebook --dir=/path/to/project --port=8888
ijulia lab --detached --verbose
ijulia --no-browser
""")
return 0
end

# Parse subcommand (default to "lab")
subcommand = "lab"
args_start = 1
if !isempty(ARGS)
first_arg = ARGS[1]
if first_arg in ("notebook", "lab")
subcommand = first_arg
args_start = 2
elseif !startswith(first_arg, "--")
# First arg looks like a subcommand but isn't valid
@error "Unknown subcommand: $first_arg. Use 'notebook' or 'lab'."
return 1
end
end

# Parse options
dir = homedir()
port = nothing
detached = false
verbose = false
extra_args = String[]

for arg in ARGS[args_start:end]
if startswith(arg, "--dir=")
dir = arg[7:end]
elseif startswith(arg, "--port=")
port = parse(Int, arg[8:end])
elseif arg == "--detached"
detached = true
elseif arg == "--verbose"
verbose = true
else
push!(extra_args, arg)
end
end

# Launch the appropriate command
launch_func = subcommand == "notebook" ? notebook_cmd : jupyterlab_cmd
try
launch_func(Cmd(extra_args); dir, detached, port, verbose)
return 0
catch e
if e isa InterruptException
return 0
else
@error "Failed to launch $subcommand" exception=(e, catch_backtrace())
return 1
end
end
end
end # if VERSION

end # IJulia
102 changes: 102 additions & 0 deletions test/main.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using Test
import IJulia

@testset "@main app entry point" begin
# Test help output
@testset "help" begin
# Capture stdout using a pipe
old_stdout = stdout
rd, wr = redirect_stdout()

ret = IJulia.main(["--help"])

# Restore stdout and read the captured output
redirect_stdout(old_stdout)
close(wr)
help_text = read(rd, String)
close(rd)

@test ret == 0
@test occursin("IJulia Launcher", help_text)
@test occursin("notebook", help_text)
@test occursin("lab", help_text)
@test occursin("--dir=PATH", help_text)
@test occursin("--port=N", help_text)
@test occursin("--detached", help_text)
@test occursin("--verbose", help_text)
end

# Test invalid subcommand
@testset "invalid subcommand" begin
ret = @test_logs (:error, r"Unknown subcommand.*Use 'notebook' or 'lab'") IJulia.main(["invalid"])
@test ret == 1
end

# Test argument parsing without actually launching
@testset "argument parsing" begin
# Test default subcommand (lab)
called_with = nothing
mock_jupyterlab = function(args=``; dir=homedir(), detached=false, port=nothing, verbose=false)
called_with = (; args, dir, detached, port, verbose)
return nothing
end

ret = IJulia.main(["--dir=/test", "--port=9999", "--detached", "--verbose", "--no-browser"]; jupyterlab_cmd=mock_jupyterlab)
@test ret == 0
@test !isnothing(called_with)
@test called_with.dir == "/test"
@test called_with.port == 9999
@test called_with.detached == true
@test called_with.verbose == true
@test "--no-browser" in called_with.args

# Test explicit notebook subcommand
called_with = nothing
mock_notebook = function(args=``; dir=homedir(), detached=false, port=nothing, verbose=false)
called_with = (; args, dir, detached, port, verbose)
return nothing
end

ret = IJulia.main(["notebook", "--dir=/test", "--port=9999", "--detached", "--verbose", "--no-browser"]; notebook_cmd=mock_notebook)
@test ret == 0
@test !isnothing(called_with)
@test called_with.dir == "/test"
@test called_with.port == 9999
@test called_with.detached == true
@test called_with.verbose == true
@test "--no-browser" in called_with.args

# Test lab subcommand
called_with = nothing
mock_jupyterlab = function(args=``; dir=homedir(), detached=false, port=nothing, verbose=false)
called_with = (; args, dir, detached, port, verbose)
return nothing
end

ret = IJulia.main(["lab", "--dir=/lab", "--verbose"]; jupyterlab_cmd=mock_jupyterlab)
@test ret == 0
@test !isnothing(called_with)
@test called_with.dir == "/lab"
@test called_with.verbose == true
end

# Test InterruptException handling
@testset "interrupt handling" begin
mock_notebook = function(args=``; kwargs...)
throw(InterruptException())
end

ret = IJulia.main(["notebook"]; notebook_cmd=mock_notebook)
@test ret == 0 # InterruptException should return 0
end

# Test error handling
@testset "error handling" begin
mock_notebook = function(args=``; kwargs...)
error("Test error")
end

ret = @test_logs (:error, r"Failed to launch") IJulia.main(["notebook"]; notebook_cmd=mock_notebook)
@test ret == 1 # Errors should return 1
end
end
4 changes: 4 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const TEST_FILES = [
"inline.jl", "completion.jl", "jsonx.jl"
]

if VERSION >= v"1.11"
push!(TEST_FILES, "main.jl")
end

for file in TEST_FILES
println(file)
include(file)
Expand Down
Loading