Last active
June 8, 2025 15:03
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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