diff --git a/package-lock.json b/package-lock.json index 685b0bd..440b7fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pebble-vscode", - "version": "0.0.3", + "version": "0.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pebble-vscode", - "version": "0.0.3", + "version": "0.0.6", "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "20.x", diff --git a/package.json b/package.json index 72305bb..4d29502 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,14 @@ "type": "tree", "contextualTitle": "Pebble Projects", "icon": "$(file-code)" + }, + { + "id": "pebble.emuActionsTreeView", + "name": "Pebble emulator actions", + "type": "tree", + "contextualTitle": "Pebble Emulator Actions", + "icon": "$(file-code)", + "initialSize": 0 } ], "explorer": [ @@ -151,6 +159,36 @@ "command": "pebble.downloadZip", "title": "Download ZIP", "category": "Pebble" + }, + { + "command": "pebble.openEmulatorAppConfig", + "title": "Emulator - Open emulator app configuration in a browser", + "category": "Pebble" + }, + { + "command": "pebble.emuBatteryState", + "title": "Emulator - Change Battery Level & Charging State", + "category": "Pebble" + }, + { + "command": "pebble.emuBluetoothState", + "title": "Emulator - Change Bluetooth Connection State", + "category": "Pebble" + }, + { + "command": "pebble.emuTap", + "title": "Emulator - Emulates Tap", + "category": "Pebble" + }, + { + "command": "pebble.emuTimeFormat", + "title": "Emulator - Sets Time Format (12h or 24h)", + "category": "Pebble" + }, + { + "command": "pebble.emuTimelineQuickView", + "title": "Emulator - Change Timeline Quick View State", + "category": "Pebble" } ], "walkthroughs": [ @@ -227,6 +265,30 @@ { "command": "pebble.showEditorPreview", "when": "pebbleProject" + }, + { + "command": "pebble.openEmulatorAppConfig", + "when": "pebbleProject" + }, + { + "command": "pebble.emuBatteryState", + "when": "pebbleProject" + }, + { + "command": "pebble.emuBluetoothState", + "when": "pebbleProject" + }, + { + "command": "pebble.emuTap", + "when": "pebbleProject" + }, + { + "command": "pebble.emuTimeFormat", + "when": "pebbleProject" + }, + { + "command": "pebble.emuTimelineQuickView", + "when": "pebbleProject" } ] } diff --git a/src/emulatorControl.ts b/src/emulatorControl.ts new file mode 100644 index 0000000..1746be0 --- /dev/null +++ b/src/emulatorControl.ts @@ -0,0 +1,276 @@ +import * as vscode from 'vscode'; +import { getWorkspacePath, getPebbleVersionInfo, isVersionBelow, upgradePebbleTool, isDevContainer, execAsync } from './utils'; +import { getEmulatorPlatform } from './run'; + +export async function openEmulatorAppConfig() { + console.log('openEmulatorAppConfig called'); + + const workspacePath = getWorkspacePath(); + if (!workspacePath) { + vscode.window.showErrorMessage('No workspace folder is open. Please open a workspace folder and run the project.'); + return; + } + + const platform = await getEmulatorPlatform(); + if (!platform) { + console.log('No platform selected, returning early'); + return; + } + console.log('Platform selected:', platform); + + // Checking if the emulator is currently running : TODO ? + let terminal = vscode.window.terminals.find(t => t.name === `Pebble Run`); + if (!terminal) { + vscode.window.showErrorMessage('Please first run the project and keep the terminal open'); + return; + } + + let codespaceArgs = ''; + + if (isDevContainer()) { + let fullUri = await vscode.env.asExternalUri( + vscode.Uri.parse("http://localhost:6443/") + ); + + if (process.env.CODESPACES === 'true' && process.env.CODESPACE_NAME) { + try { + const { stdout, stderr } = await execAsync(`gh codespace ports visibility 6443:public -c ${process.env.CODESPACE_NAME}`); + if (stderr) { + throw stderr; + } else { + console.debug('(emu-app-config) codespace port visibility 6443 public OK'); + } + } catch (error: any) { + console.error(`(emu-app-config) codespace port visibility 6443 public: ${error}`); + vscode.window.showErrorMessage(`Failed to make port 6443 public: ${error}`); + } + + if (fullUri.authority.startsWith('localhost') || fullUri.authority.startsWith('127.0.0.1')) { + fullUri = await vscode.env.asExternalUri( + vscode.Uri.parse("http://localhost:6443/") + ); + } + } + + codespaceArgs = ` --port 6443 --address ${fullUri}`; + console.debug(`Using codespace arguments = '${codespaceArgs}'`); + } + + terminal.show(); + terminal.sendText('\x03'); // Send Ctrl+C + terminal.sendText(`pebble emu-app-config --emulator ${platform} --vnc${codespaceArgs}`, true); +} + +export async function emulatorBatterySetState() { + console.log('emulatorBatterySetState called'); + + const platform = await getEmulatorPlatform(); + if (!platform) { + console.log('No platform selected, returning early'); + return; + } + console.log('Platform selected:', platform); + + const batteryStateStr = await vscode.window.showInputBox({ + placeHolder: 'Enter a battery percentage between 0 and 100.', + }); + if (!batteryStateStr) { + return; + } + const batteryState = Number(batteryStateStr).valueOf(); + if (isNaN(batteryState) || batteryState < 0 || batteryState > 100) { + vscode.window.showErrorMessage('Please enter an integer between 0 and 100'); + return; + } + + let batteryStateStrSubstring = batteryStateStr; + if (batteryState < 10) { + batteryStateStrSubstring = batteryStateStr.substring(0, 1); + } else if (batteryState < 100) { + batteryStateStrSubstring = batteryStateStr.substring(0, 2); + } else { + batteryStateStrSubstring = batteryStateStr.substring(0, 3); + } + + const chargingMap: {[key: string] : boolean } = { + 'Yes': true, + 'No': false + }; + + const chargingChoice = await vscode.window.showQuickPick(Object.keys(chargingMap), { + placeHolder: 'Is the battery charging?', + canPickMany: false, + }); + + if (!chargingChoice) { + return; + } + + const charging = chargingMap[chargingChoice]; + + // Checking if the emulator is currently running : TODO ? + let terminal = vscode.window.terminals.find(t => t.name === `Pebble Run`); + if (!terminal) { + vscode.window.showErrorMessage('Please first run the project and keep the terminal open'); + return; + } + + terminal.show(); + + terminal.sendText(`pebble emu-battery --emulator ${platform} --vnc --percent ${batteryStateStrSubstring} ${charging ? '--charging' : ''}`); +} + +async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function emulatorBluetoothSetState() { + console.log('emulatorBluetoothSetState called'); + + const platform = await getEmulatorPlatform(); + if (!platform) { + console.log('No platform selected, returning early'); + return; + } + console.log('Platform selected:', platform); + + const connectedMap: {[key: string] : boolean } = { + 'Active': true, + 'Inactive': false + }; + + const connectedChoice = await vscode.window.showQuickPick(Object.keys(connectedMap), { + placeHolder: 'Set the bluetooth connection', + canPickMany: false, + }); + + if (!connectedChoice) { + return; + } + + const connected = connectedMap[connectedChoice]; + + // Checking if the emulator is currently running : TODO ? + let terminal = vscode.window.terminals.find(t => t.name === `Pebble Run`); + if (!terminal) { + vscode.window.showErrorMessage('Please first run the project and keep the terminal open'); + return; + } + + terminal.show(); + + terminal.sendText(`pebble emu-bt-connection --emulator ${platform} --vnc --connected ${connected ? 'yes' : 'no'}`); + + if (!connected) { + // Display a progress notification to match the debounce disconnection behavior in firmware + const waitSec = 25; + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Disconnected. App will be notified in ${waitSec} seconds.`, + cancellable: true + }, async (progress, cancelToken) => { + for (let index = 0; index < (waitSec + 1); index++) { + if (cancelToken.isCancellationRequested) { + terminal.sendText('\x03'); // Send Ctrl+C + terminal.sendText(`pebble emu-bt-connection --emulator ${platform} --vnc --connected yes`); + break; + } + await sleep(1000); + progress.report({ + increment: 100 / (waitSec + 1) + }); + } + }); + } +} + +export async function emulatorAccelTapTrigger() { + console.log('emulatorAccelTapTrigger called'); + + const platform = await getEmulatorPlatform(); + if (!platform) { + console.log('No platform selected, returning early'); + return; + } + console.log('Platform selected:', platform); + + // Checking if the emulator is currently running : TODO ? + let terminal = vscode.window.terminals.find(t => t.name === `Pebble Run`); + if (!terminal) { + vscode.window.showErrorMessage('Please first run the project and keep the terminal open'); + return; + } + + terminal.show(); + + terminal.sendText(`pebble emu-tap --emulator ${platform} --vnc --direction y+`); +} + +export async function emulatorSetTimeFormat() { + console.log('emulatorSetTimeFormat called'); + + const platform = await getEmulatorPlatform(); + if (!platform) { + console.log('No platform selected, returning early'); + return; + } + console.log('Platform selected:', platform); + + const timeFormatChoice = await vscode.window.showQuickPick(['12h', '24h'], { + placeHolder: 'Select Time Format', + canPickMany: false, + }); + + if (!timeFormatChoice) { + return; + } + + // Checking if the emulator is currently running : TODO ? + let terminal = vscode.window.terminals.find(t => t.name === `Pebble Run`); + if (!terminal) { + vscode.window.showErrorMessage('Please first run the project and keep the terminal open'); + return; + } + + terminal.show(); + + terminal.sendText(`pebble emu-time-format --emulator ${platform} --vnc --format ${timeFormatChoice}`); +} + +export async function emulatorTimelineQuickViewSet() { + console.log('emulatorTimelineQuickViewSet called'); + + const platform = await getEmulatorPlatform(); + if (!platform) { + console.log('No platform selected, returning early'); + return; + } + console.log('Platform selected:', platform); + + const quickViewStateMap: {[key: string] : boolean } = { + 'Active': true, + 'Inactive': false + }; + + const quickViewStateChoice = await vscode.window.showQuickPick(Object.keys(quickViewStateMap), { + placeHolder: 'Set the timeline quick view state', + canPickMany: false, + }); + + if (!quickViewStateChoice) { + return; + } + + const quickViewState = quickViewStateMap[quickViewStateChoice]; + + // Checking if the emulator is currently running : TODO ? + let terminal = vscode.window.terminals.find(t => t.name === `Pebble Run`); + if (!terminal) { + vscode.window.showErrorMessage('Please first run the project and keep the terminal open'); + return; + } + + terminal.show(); + + terminal.sendText(`pebble emu-set-timeline-quick-view --emulator ${platform} --vnc ${quickViewState ? 'on' : 'off'}`); +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 29804f8..4c4da27 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,7 +2,9 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import { PebbleTreeProvider } from './pebbleTreeProvider'; +import { PebbleEmulationActionsTreeProvider } from './pebbleEmulationActionsTreeProvider'; import { requestEmulatorPlatform, runOnEmulatorWithArgs, requestPhoneIp, runOnPhoneWithArgs, wipeEmulator } from './run'; +import { openEmulatorAppConfig, emulatorBatterySetState, emulatorBluetoothSetState, emulatorAccelTapTrigger, emulatorSetTimeFormat, emulatorTimelineQuickViewSet } from './emulatorControl'; import { createProject, openProject } from './project'; import { isPebbleProject } from './utils'; @@ -50,7 +52,10 @@ async function getWebviewContent() { const { exec } = require('child_process'); exec(`gh codespace ports visibility 6080:public -c ${process.env.CODESPACE_NAME}`, (error: any) => { if (error) { + console.error(`(webview) codespace port visibility 6080 public: ${error}`); vscode.window.showErrorMessage(`Failed to make port 6080 public: ${error}`); + } else { + console.debug('(webview) codespace port visibility 6080 public OK'); } }); } @@ -59,6 +64,8 @@ async function getWebviewContent() { vscode.Uri.parse("http://localhost:6080/") ); + console.debug(`Using WebviewContent URI = '${fullUri}'`); + // Convert to WebSocket URL const wsUrl = fullUri.toString() .replace(/^https:/, 'wss:') @@ -569,12 +576,41 @@ export async function activate(context: vscode.ExtensionContext) { }); }); + const openEmuAppConfig = vscode.commands.registerCommand('pebble.openEmulatorAppConfig', async () => { + openEmulatorAppConfig(); + }); + + const emuBatteryState = vscode.commands.registerCommand('pebble.emuBatteryState', async () => { + emulatorBatterySetState(); + }); + + const emuBluetoothState = vscode.commands.registerCommand('pebble.emuBluetoothState', async () => { + emulatorBluetoothSetState(); + }); + + const emuAccelTap = vscode.commands.registerCommand('pebble.emuTap', async () => { + emulatorAccelTapTrigger(); + }); + + const emuTimeFormat = vscode.commands.registerCommand('pebble.emuTimeFormat', async () => { + emulatorSetTimeFormat(); + }); + + const emuTimelineQuickView = vscode.commands.registerCommand('pebble.emuTimelineQuickView', async () => { + emulatorTimelineQuickViewSet(); + }); + const treeDataProvider = new PebbleTreeProvider(); const treeView = vscode.window.createTreeView('backgroundTreeView', { treeDataProvider: treeDataProvider }); - context.subscriptions.push(newProjectDisposable, openProjectDisposable, run, runWithLogs, setDefaultPlatform, runOnPhone, runOnPhoneWithLogs, setPhoneIp, wipeEmulatorCommand, downloadPbwCommand, downloadZipCommand, treeView, showSidebarPreview, showEditorPreviewCommand); + const emuActionsTreeDataProvider = new PebbleEmulationActionsTreeProvider(); + const emuActionsTreeView = vscode.window.createTreeView('pebble.emuActionsTreeView', { + treeDataProvider: emuActionsTreeDataProvider + }); + + context.subscriptions.push(newProjectDisposable, openProjectDisposable, run, runWithLogs, setDefaultPlatform, runOnPhone, runOnPhoneWithLogs, setPhoneIp, wipeEmulatorCommand, downloadPbwCommand, downloadZipCommand, treeView, showSidebarPreview, showEditorPreviewCommand, emuActionsTreeView, openEmuAppConfig, emuBatteryState, emuBluetoothState, emuAccelTap, emuTimeFormat, emuTimelineQuickView); } export function deactivate() {} \ No newline at end of file diff --git a/src/pebbleEmulationActionsTreeProvider.ts b/src/pebbleEmulationActionsTreeProvider.ts new file mode 100644 index 0000000..8c360d5 --- /dev/null +++ b/src/pebbleEmulationActionsTreeProvider.ts @@ -0,0 +1,53 @@ +import * as vscode from 'vscode'; +export class PebbleEmulationActionsTreeProvider implements vscode.TreeDataProvider { + + actions: vscode.TreeItem[]; + + private createItem(label: string, commandId: string, themeIconId: string): vscode.TreeItem { + let item = new vscode.TreeItem(label); + item.command = { + command: commandId, + title: label + }; + item.iconPath = new vscode.ThemeIcon(themeIconId); + return item; + } + + constructor() { + this.actions = [ + this.createItem('Open App Config in Browser', 'pebble.openEmulatorAppConfig', 'settings'), + this.createItem('Change Battery Level & Charging State', 'pebble.emuBatteryState', 'percentage'), + this.createItem('Change Bluetooth Connection State', 'pebble.emuBluetoothState', 'broadcast'), + this.createItem('Emulates Tap', 'pebble.emuTap', 'move'), + this.createItem('Sets Time Format (12h or 24h)', 'pebble.emuTimeFormat', 'calendar'), + this.createItem('Change Timeline Quick View State', 'pebble.emuTimelineQuickView', 'bell-dot'), + ]; + } + + getChildren(element?: vscode.TreeItem | undefined): vscode.ProviderResult { + if (!element) { + // Return the root items + return this.actions; + } + + // No children + return []; + } + + getParent(element: vscode.TreeItem): vscode.ProviderResult { + // Return the parent of the given element + // For this example, we assume all items are root items, so return undefined + return undefined; + } + + getTreeItem(element: vscode.TreeItem): vscode.TreeItem | Thenable { + // Return the TreeItem for the given element + return element; + } + + resolveTreeItem(item: vscode.TreeItem, element: vscode.TreeItem, token: vscode.CancellationToken): vscode.ProviderResult { + // Resolve the TreeItem if needed + // For this example, we do not need to resolve anything + return item; + } +} \ No newline at end of file diff --git a/src/run.ts b/src/run.ts index 93fd290..38cc493 100644 --- a/src/run.ts +++ b/src/run.ts @@ -174,4 +174,4 @@ export async function wipeEmulator() { vscode.window.showInformationMessage('Emulator data wiped successfully.'); }); -} \ No newline at end of file +} diff --git a/src/utils.ts b/src/utils.ts index e244cef..a1e43e7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -55,7 +55,7 @@ export async function isPebbleProject() : Promise { } } -const execAsync = promisify(exec); +export const execAsync = promisify(exec); export interface PebbleVersionInfo { toolVersion: string | null; @@ -133,4 +133,4 @@ export async function upgradePebbleTool(): Promise { vscode.window.showErrorMessage('Failed to upgrade Pebble Tool. Please run "uv tool upgrade pebble-tool" manually.'); return false; } -} +} \ No newline at end of file