Skip to content
Merged
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
52 changes: 52 additions & 0 deletions src/components/terminal/terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,58 @@ export default class TerminalComponent {

// Handle copy/paste keybindings
this.setupCopyPasteHandlers();

// Handle custom OSC 7777 for acode CLI commands
this.setupOscHandler();
}

/**
* Setup custom OSC handler for acode CLI integration
* OSC 7777 format: \e]7777;command;arg1;arg2;...\a
*/
setupOscHandler() {
// Register custom OSC handler for ID 7777
// Format: command;arg1;arg2;... where arg2 (path) may contain semicolons
this.terminal.parser.registerOscHandler(7777, (data) => {
const firstSemi = data.indexOf(";");
if (firstSemi === -1) {
console.warn("Invalid OSC 7777 format:", data);
return true;
}

const command = data.substring(0, firstSemi);
const rest = data.substring(firstSemi + 1);

switch (command) {
case "open": {
// Format: open;type;path (path may contain semicolons)
const secondSemi = rest.indexOf(";");
if (secondSemi === -1) {
console.warn("Invalid OSC 7777 open format:", data);
return true;
}
const type = rest.substring(0, secondSemi);
const path = rest.substring(secondSemi + 1);
this.handleOscOpen(type, path);
break;
}
default:
console.warn("Unknown OSC 7777 command:", command);
}
return true;
});
}

/**
* Handle OSC open command from acode CLI
* @param {string} type - "file" or "folder"
* @param {string} path - Path to open
*/
handleOscOpen(type, path) {
if (!path) return;

// Emit event for the app to handle
this.onOscOpen?.(type, path);
}

/**
Expand Down
60 changes: 60 additions & 0 deletions src/components/terminal/terminalManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import "@xterm/xterm/css/xterm.css";
import quickTools from "components/quickTools";
import toast from "components/toast";
import confirm from "dialogs/confirm";
import openFile from "lib/openFile";
import openFolder from "lib/openFolder";
import appSettings from "lib/settings";
import helpers from "utils/helpers";

Expand Down Expand Up @@ -577,6 +579,30 @@ class TerminalManager {
toast(message);
};

// Handle acode CLI open commands (OSC 7777)
terminalComponent.onOscOpen = async (type, path) => {
if (!path) return;

// Convert proot path
const fileUri = this.convertProotPath(path);
// Extract folder/file name from path
const name = path.split("/").filter(Boolean).pop() || "folder";

try {
if (type === "folder") {
// Open folder in sidebar
await openFolder(fileUri, { name, saveState: true, listFiles: true });
toast(`Opened folder: ${name}`);
} else {
// Open file in editor
await openFile(fileUri, { render: true });
}
} catch (error) {
console.error("Failed to open from terminal:", error);
toast(`Failed to open: ${path}`);
}
};

// Store references for cleanup
terminalFile._terminalId = terminalId;
terminalFile.terminalComponent = terminalComponent;
Expand Down Expand Up @@ -791,6 +817,40 @@ class TerminalManager {
});
}

/**
* Convert proot internal path to app-accessible path
* @param {string} prootPath - Path from inside proot environment
* @returns {string} App filesystem path
*/
convertProotPath(prootPath) {
if (!prootPath) return prootPath;

const packageName = window.BuildInfo?.packageName || "com.foxdebug.acode";
const dataDir = `/data/user/0/${packageName}`;
const alpineRoot = `${dataDir}/files/alpine`;

let convertedPath;

if (prootPath.startsWith("/public")) {
// /public -> /data/user/0/com.foxdebug.acode/files/public
convertedPath = `file://${dataDir}/files${prootPath}`;
} else if (
prootPath.startsWith("/sdcard") ||
prootPath.startsWith("/storage") ||
prootPath.startsWith("/data")
) {
convertedPath = `file://${prootPath}`;
} else if (prootPath.startsWith("/")) {
// Everything else is relative to alpine root
convertedPath = `file://${alpineRoot}${prootPath}`;
} else {
convertedPath = prootPath;
}

//console.log(`Path conversion: ${prootPath} -> ${convertedPath}`);
return convertedPath;
}

shouldConfirmTerminalClose() {
const settings = appSettings?.value?.terminalSettings;
if (settings && settings.confirmTabClose === false) {
Expand Down
69 changes: 69 additions & 0 deletions src/plugins/terminal/scripts/init-alpine.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,75 @@ Working with packages:
EOF
fi

# Create acode CLI tool
if [ ! -e "$PREFIX/alpine/usr/local/bin/acode" ]; then
mkdir -p "$PREFIX/alpine/usr/local/bin"
cat <<'ACODE_CLI' > "$PREFIX/alpine/usr/local/bin/acode"
#!/bin/bash
# acode - Open files/folders in Acode editor
# Uses OSC escape sequences to communicate with the Acode terminal

usage() {
echo "Usage: acode [file/folder...]"
echo ""
echo "Open files or folders in Acode editor."
echo ""
echo "Examples:"
echo " acode file.txt # Open a file"
echo " acode . # Open current folder"
echo " acode ~/project # Open a folder"
echo " acode -h, --help # Show this help"
}

get_abs_path() {
local path="$1"
local abs_path
abs_path=$(realpath -- "$path" 2>/dev/null)
if [[ $? -ne 0 ]]; then
if [[ "$path" == /* ]]; then
abs_path="$path"
else
abs_path="$PWD/$path"
fi
fi
echo "$abs_path"
}

open_in_acode() {
local path=$(get_abs_path "$1")
local type="file"
[[ -d "$path" ]] && type="folder"

# Send OSC 7777 escape sequence: \e]7777;cmd;type;path\a
# The terminal component will intercept and handle this
printf '\e]7777;open;%s;%s\a' "$type" "$path"
}

if [[ $# -eq 0 ]]; then
open_in_acode "."
exit 0
fi

for arg in "$@"; do
case "$arg" in
-h|--help)
usage
exit 0
;;
*)
if [[ -e "$arg" ]]; then
open_in_acode "$arg"
else
echo "Error: '$arg' does not exist" >&2
exit 1
fi
;;
esac
done
ACODE_CLI
chmod +x "$PREFIX/alpine/usr/local/bin/acode"
fi

# Create initrc if it doesn't exist
#initrc runs in bash so we can use bash features
if [ ! -e "$PREFIX/alpine/initrc" ]; then
Expand Down