Skip to content

Instantly share code, notes, and snippets.

@tarcisiomiranda
Last active June 8, 2025 15:03
Show Gist options
  • Save tarcisiomiranda/c9059e4071dcdaafa8a16eb6ae049d33 to your computer and use it in GitHub Desktop.
Save tarcisiomiranda/c9059e4071dcdaafa8a16eb6ae049d33 to your computer and use it in GitHub Desktop.
Install Cursor IDE on any Linux distribution. This Python script installs or removes the Cursor AI IDE for the current user on Linux. It always downloads the latest stable version, sets up the AppImage, icon, desktop launcher, and a bash alias. The script uses a Firefox User-Agent header to avoid HTTP 403 errors during download.
#!/usr/bin/env python3
"""
cursor_installer.py – installs or removes the Cursor AI IDE for the current user only.
✔ AppImage → ~/.local/bin/cursor.appimage
✔ Icon → ~/.local/share/icons/cursor.png
✔ Launcher → ~/.local/share/applications/cursor.desktop
Usage:
python3 cursor_installer.py install
python3 cursor_installer.py uninstall
Notes:
This Python script installs or removes the Cursor AI IDE for the current user on Linux,
always downloading the latest stable version. It sets up the AppImage, icon, desktop launcher,
and bash alias, using a Firefox User-Agent header to avoid HTTP 403 errors.
* The latest AppImage is obtained from the official Cursor endpoint
`https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable`.
* Some servers return **HTTP 403** if the *User-Agent* header is missing.
This script now sends a generic User-Agent to avoid blocks.
"""
import argparse
import json
import os
import stat
import sys
import urllib.request
import subprocess
from pathlib import Path
from urllib.error import HTTPError
try:
import distro
except ImportError:
print("[ERROR] Missing dependency: 'distro'. Please run 'pip install distro' and try again.")
sys.exit(1)
API_URL = "https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable"
ICON_URL = (
"https://us1.discourse-cdn.com/flex020/uploads/cursor1/original/2X/a/a4f78589d63edd61a2843306f8e11bad9590f0ca.png"
)
HOME = Path.home()
APPIMAGE_PATH = HOME / ".local" / "bin" / "cursor.appimage"
ICON_PATH = HOME / ".local" / "share" / "icons" / "cursor.png"
DATA_HOME = Path(os.environ.get("XDG_DATA_HOME", HOME / ".local" / "share"))
DESKTOP_ENTRY_PATH = DATA_HOME / "applications" / "cursor.desktop"
BASHRC_ALIAS_COMMENT = "# Cursor alias"
HEADERS = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
}
def check_fuse_dependency():
distro_id = distro.id()
print(f"[INFO] Detected Linux distribution: {distro_id}")
pkg_installed = False
msg_install = ""
if distro_id in ["ubuntu", "debian", "linuxmint", "pop"]:
try:
result = subprocess.run(["dpkg", "-s", "libfuse2"], capture_output=True, text=True)
if "Status: install ok installed" in result.stdout:
print("✔ 'libfuse2' is already installed.")
pkg_installed = True
else:
msg_install = "sudo apt install libfuse2"
except Exception as e:
print(f"Could not check for libfuse2: {e}")
msg_install = "sudo apt install libfuse2"
elif distro_id in ["arch", "manjaro", "endeavouros", "cachyos"]:
try:
result = subprocess.run(["pacman", "-Qs", "fuse2"], capture_output=True, text=True)
if "local/fuse2" in result.stdout:
print("✔ 'fuse2' is already installed.")
pkg_installed = True
else:
msg_install = "sudo pacman -S fuse2"
except Exception as e:
print(f"Could not check for fuse2: {e}")
msg_install = "sudo pacman -S fuse2"
elif distro_id in ["fedora"]:
try:
result = subprocess.run(["rpm", "-q", "fuse"], capture_output=True, text=True)
if "is not installed" not in result.stdout:
print("✔ 'fuse' is already installed.")
pkg_installed = True
else:
msg_install = "sudo dnf install fuse"
except Exception as e:
print(f"Could not check for fuse: {e}")
msg_install = "sudo dnf install fuse"
elif distro_id in ["opensuse-leap", "opensuse-tumbleweed"]:
try:
result = subprocess.run(["rpm", "-q", "fuse"], capture_output=True, text=True)
if "is not installed" not in result.stdout:
print("✔ 'fuse' is already installed.")
pkg_installed = True
else:
msg_install = "sudo zypper install fuse"
except Exception as e:
print(f"Could not check for fuse: {e}")
msg_install = "sudo zypper install fuse"
else:
print(f"[WARNING] Unrecognized distribution: {distro_id}. Please make sure FUSE 2 is installed on your system to run AppImages.")
if not pkg_installed and msg_install:
print("\n[ER] To run AppImages on your system, the FUSE 2 library is required.")
print(f"Please install it using the following command:\n {msg_install}\n")
input("Press Enter once you have installed the library (or to continue anyway)...")
def _ensure_dirs():
for d in (
APPIMAGE_PATH.parent,
ICON_PATH.parent,
DESKTOP_ENTRY_PATH.parent,
):
d.mkdir(parents=True, exist_ok=True)
def _download(url: str, dest: Path):
"""Downloads a file (supports JSON wrapper) and adds User-Agent."""
print(f"Downloading {url} → {dest}")
try:
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req) as resp:
ctype = resp.headers.get("Content-Type", "")
if "application/json" in ctype:
data = json.loads(resp.read().decode())
final_url = data.get("url") or data.get("downloadUrl")
if not final_url:
print("[ERROR] Unexpected download JSON.")
sys.exit(1)
return _download(final_url, dest)
with open(dest, "wb") as fp:
while True:
chunk = resp.read(8192)
if not chunk:
break
fp.write(chunk)
except HTTPError as e:
print(f"[ERROR] Download failed: {e}")
sys.exit(1)
def _add_exec_permission(path: Path):
path.chmod(path.stat().st_mode | stat.S_IXUSR)
def _write_desktop_entry():
entry = f"""[Desktop Entry]
Name=Cursor AI IDE
Exec={APPIMAGE_PATH} --no-sandbox
Icon={ICON_PATH}
Type=Application
Categories=Development;
"""
DESKTOP_ENTRY_PATH.write_text(entry)
print(f"Launcher created at {DESKTOP_ENTRY_PATH}")
def _add_alias_to_bashrc():
bashrc = HOME / ".bashrc"
if bashrc.exists() and BASHRC_ALIAS_COMMENT in bashrc.read_text():
return
alias_block = f"""
{BASHRC_ALIAS_COMMENT}
cursor() {{
nohup {APPIMAGE_PATH} --no-sandbox "$@" > /dev/null 2>&1 &
printf ""
}}
"""
with bashrc.open("a") as fp:
fp.write(alias_block)
print("Alias added to ~/.bashrc (reopen shell).")
def _remove_alias_from_bashrc():
bashrc = HOME / ".bashrc"
if not bashrc.exists():
return
lines = bashrc.read_text().splitlines()
new_lines = []
skip = False
for line in lines:
if line.strip() == BASHRC_ALIAS_COMMENT:
skip = True
continue
if skip and line.startswith("}"):
skip = False
continue
if not skip:
new_lines.append(line)
bashrc.write_text("\n".join(new_lines))
def install():
check_fuse_dependency()
if APPIMAGE_PATH.exists():
print("Cursor AI IDE is already installed.")
return
print("Installing Cursor AI IDE…")
_ensure_dirs()
_download(API_URL, APPIMAGE_PATH)
_add_exec_permission(APPIMAGE_PATH)
_download(ICON_URL, ICON_PATH)
_write_desktop_entry()
_add_alias_to_bashrc()
print("Installation complete! It will appear in the applications menu.")
def uninstall():
print("Removing Cursor AI IDE…")
for p in (APPIMAGE_PATH, ICON_PATH, DESKTOP_ENTRY_PATH):
if p.exists():
print(f"Deleting {p}")
p.unlink()
_remove_alias_from_bashrc()
for d in (
APPIMAGE_PATH.parent,
ICON_PATH.parent,
DESKTOP_ENTRY_PATH.parent,
):
try:
d.rmdir()
except OSError:
pass
print("Uninstallation complete.")
def main():
parser = argparse.ArgumentParser(description="Installs or removes the Cursor AI IDE for the current user only.")
parser.add_argument("action", choices=["install", "uninstall"], help="Desired action")
args = parser.parse_args()
if args.action == "install":
install()
elif args.action == "uninstall":
uninstall()
else:
print("Invalid action. Please use 'install' or 'uninstall'.")
sys.exit(1)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment