Skip to content

Commit

Permalink
add support for themes and profiles
Browse files Browse the repository at this point in the history
  • Loading branch information
pomdtr committed Dec 1, 2023
1 parent 0ec104c commit 6075477
Show file tree
Hide file tree
Showing 17 changed files with 387 additions and 280 deletions.
15 changes: 15 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"args": [
"serve"
]
}
],
}
13 changes: 13 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build Extension",
"type": "shell",
"command": "npm run build",
"options": {
"cwd": "${workspaceFolder}/extension"
}
}
]
}
47 changes: 38 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

An integrated terminal for your browser.

![screenshot](./medias/demo.png)

Check out a demo of the extension running from the arc browser here: <https://www.capster.io/video/zRgddaTPilyn>.

## Installation
Expand All @@ -25,20 +23,51 @@ Download the extension from the [releases page](https://github.com/pomdtr/popcor
Then go to the `chrome://extensions` page, activate the Developer mode and click on the `Load unpacked` button.
You will need to select the `dist` folder you just extracted using the file picker.

![Extension Page](./medias/extensions.png)

Once you have installed the extension, copy the extension id, and run the following command:

```bash
popcorn init --browser chrome --extension-id <extension-id>
popcorn init <extension-id>
```

Alternatively, you can right click on the extension icon and select `Copy Installation Command`.

## Usage

## CLI
Click on the extension icon to open your default profile in a new tab.

Right click on the extension icon to open a new tab with a specific profile.
Each profile gets it's own unique url, so you can bookmark them.

You can use the wesh cli to control your browser from the command line.
It will only work from a terminal started from the extension.
From the terminal tab, you can manipulate your browser tabs, windows, bookmarks, etc...

```sh
popcorn tab list
```

## Configuration

The configuration file is located in `~/.config/popcorn/popcorn.json` or `~/.config/popcorn/popcorn.jsonc` if you want to use comments.

You can customize the configuration dir by setting the `XDG_CONFIG_HOME` environment variable.

Example Config:

```json
{
"theme": "Tomorrow",
"themeDark": "Tomorrow Night",
"defaultProfile": "zsh",
"profiles": {
"zsh": {
"command": "zsh",
"args": ["-l"]
},
"htop": {
"command": "/opt/homebrew/bin/htop"
}
}
}
```

## How does it work?

Expand All @@ -50,7 +79,7 @@ popcorn is composed of two parts:
When the chrome extension is loaded, it will use the native messaging API to communicate with the host binary.
An instance of an HTTP server will be started on a random free http port.

When the popup is opened, the embedded terminal (xterm.js) will connect to the HTTP server and will be able to send and receive data through a websocket.
The embedded terminal (xterm.js) will connect to the HTTP server and will be able to send and receive data through a websocket.

When you use the popcorn cli, the message is sent to the http server, and then piped to the chrome extension.

Expand Down
182 changes: 99 additions & 83 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Config } from "./config";

type Message = {
id: string;
payload?: {
Expand All @@ -8,59 +10,71 @@ type Message = {
};

enum ContextMenuID {
OPEN_TERMINAL_TAB = "open-terminal-tab",
COPY_EXTENSION_ID = "copy-extension-id",
OPEN_PROFILE_DEFAUlT = "open-profile-default",
OPEN_PROFILE = "open-profile",
COPY_INSTALLATION_COMMAND = "copy-installation-command",
}

// activate when installed or updated
chrome.runtime.onInstalled.addListener(() => {
console.log("Extension installed or updated");
chrome.contextMenus.create({
id: ContextMenuID.OPEN_TERMINAL_TAB,
title: "Open Terminal in New Tab",
title: "Copy Installation Command",
id: ContextMenuID.COPY_INSTALLATION_COMMAND,
contexts: ["action"],
});
chrome.contextMenus.create({
title: "Copy Extension ID",
id: ContextMenuID.COPY_EXTENSION_ID,
contexts: ["action"],
});
});

// activate when chrome starts
chrome.runtime.onStartup.addListener(() => {
console.log("Browser started");
});


const nativePort = chrome.runtime.connectNative("com.pomdtr.popcorn");
nativePort.onMessage.addListener(async (msg: Message) => {
console.log("Received message", msg);
try {
const res = await handleMessage(msg.payload);
nativePort.postMessage({
id: msg.id,
payload: res,
});
} catch (e: any) {
nativePort.postMessage({
id: msg.id,
error: e.message,
});
}
});

chrome.storage.session.setAccessLevel({
accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS",
});
let nativePort: chrome.runtime.Port;
try {
nativePort = chrome.runtime.connectNative("com.pomdtr.popcorn");
nativePort.onMessage.addListener(async (msg: Message) => {
console.log("Received message", msg);
try {
const res = await handleMessage(msg.payload);
nativePort.postMessage({
id: msg.id,
payload: res,
});
} catch (e: any) {
nativePort.postMessage({
id: msg.id,
error: e.message,
});
}
});
} catch (e) {
console.log(`Native messaging host not found: ${e}`);
}

async function handleMessage(payload: any): Promise<any> {
switch (payload.command) {
case "init": {
const { port, token, config } = payload as { port: number, token: string, config: Config }
await chrome.storage.session.set({
port: payload.port,
token: payload.token,
port, token, config
});

chrome.contextMenus.remove(ContextMenuID.COPY_INSTALLATION_COMMAND)
chrome.contextMenus.create({
id: ContextMenuID.OPEN_PROFILE_DEFAUlT,
title: "Open Default Profile",
contexts: ["action"],
});

chrome.contextMenus.create({
id: ContextMenuID.OPEN_PROFILE,
title: "Open Profile",
contexts: ["action"],
});
for (const profile of Object.keys(config.profiles)) {
chrome.contextMenus.create({
id: `${ContextMenuID.OPEN_PROFILE}:${profile}`,
parentId: ContextMenuID.OPEN_PROFILE,
title: profile,
contexts: ["action"],
});
}

return "ok";
}
case "tab.list": {
Expand Down Expand Up @@ -294,60 +308,62 @@ async function getActiveTabId() {

chrome.contextMenus.onClicked.addListener(async (info) => {
const mainPage = "/src/terminal.html";
switch (info.menuItemId) {
case ContextMenuID.OPEN_TERMINAL_TAB: {
await chrome.tabs.create({ url: mainPage });
break;
}
case ContextMenuID.COPY_EXTENSION_ID: {
await addToClipboard(chrome.runtime.id);
break;
}
default: {
throw new Error(`Unknown menu item: ${info.menuItemId}`);
}

const menuItemID = info.menuItemId;
if (typeof menuItemID !== "string") {
throw new Error(`Unknown menu item: ${menuItemID}`);
}
});

chrome.commands.onCommand.addListener(async (command) => {
switch (command) {
case "open-terminal-tab": {
const tab = await chrome.tabs.create({ url: "/src/terminal.html" });
await chrome.windows.update(tab.windowId, { focused: true });
break;
}
default: {
throw new Error(`Unknown command: ${command}`);
if (menuItemID == ContextMenuID.OPEN_PROFILE_DEFAUlT) {
await chrome.tabs.create({ url: mainPage });
} else if (typeof menuItemID.startsWith(ContextMenuID.OPEN_PROFILE)) {
const profile = menuItemID.split(":")[1];
if (!profile) {
throw new Error(`Unknown menu item: ${menuItemID}`);
}
await chrome.tabs.create({
url: `${mainPage}?profile=${profile}`,
});
} else if (menuItemID == ContextMenuID.COPY_INSTALLATION_COMMAND) {
throw new Error(`Unknown menu item: ${menuItemID}`);
} else {
await addToClipboard(`popcorn init ${chrome.runtime.id}`);
}
})

chrome.omnibox.onInputStarted.addListener(async () => {
chrome.omnibox.setDefaultSuggestion({
description: "Run command",
});
});

chrome.omnibox.onInputChanged.addListener(async (text) => {
chrome.omnibox.setDefaultSuggestion({
description: `Run: ${text}`,
});
});

chrome.omnibox.onInputEntered.addListener(async (disposition) => {
const url = `/src/terminal.html`;
switch (disposition) {
case "currentTab":
await chrome.tabs.update({ url });
break;
case "newForegroundTab":
await chrome.tabs.create({ url });
break;
case "newBackgroundTab":
await chrome.tabs.create({ url, active: false });
chrome.action.onClicked.addListener(async () => {
if (nativePort === undefined) {
return;
}
await chrome.tabs.create({ url: "/src/terminal.html" });
});

// chrome.omnibox.onInputStarted.addListener(async () => {
// chrome.omnibox.setDefaultSuggestion({
// description: "Run command",
// });
// });

// chrome.omnibox.onInputChanged.addListener(async (text) => {
// chrome.omnibox.setDefaultSuggestion({
// description: `Run: ${text}`,
// });
// });

// chrome.omnibox.onInputEntered.addListener(async (disposition) => {
// const url = `/src/terminal.html`;
// switch (disposition) {
// case "currentTab":
// await chrome.tabs.update({ url });
// break;
// case "newForegroundTab":
// await chrome.tabs.create({ url });
// break;
// case "newBackgroundTab":
// await chrome.tabs.create({ url, active: false });
// }
// });

async function addToClipboard(value: string) {
await chrome.offscreen.createDocument({
url: 'src/offscreen.html',
Expand Down
11 changes: 11 additions & 0 deletions extension/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type Config = {
theme?: string
themeDark?: string
env: Record<string, string>
defaultProfile: string
profiles: Record<string, {
command: string
args: string[]
env: Record<string, string>
}>
}
12 changes: 2 additions & 10 deletions extension/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const manifest: chrome.runtime.ManifestV3 = {
default_icon: {
48: "icons/48.png",
},
default_popup: "src/popup.html#popup",
default_title: "Open Terminal",
},
background: {
Expand All @@ -21,18 +20,11 @@ export const manifest: chrome.runtime.ManifestV3 = {
keyword: "tty",
},
commands: {
"_execute_action": {
description: "Show terminal popup",
suggested_key: {
default: "Ctrl+E",
mac: "Command+E",
}
},
"open-terminal-tab": {
description: "Create a new terminal tab",
suggested_key: {
default: "Ctrl+Shift+E",
mac: "Command+Shift+E",
default: "Ctrl+E",
mac: "Command+E",
}
}
},
Expand Down
25 changes: 0 additions & 25 deletions extension/src/popup.html

This file was deleted.

Loading

0 comments on commit 6075477

Please sign in to comment.