Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an 'open' utility to Phoenix #344

Closed
wants to merge 2 commits into from
Closed
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
173 changes: 88 additions & 85 deletions packages/backend/src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,9 @@ function seconds_to_string(seconds) {
async function suggest_app_for_fsentry(fsentry, options){
const monitor = PerformanceMonitor.createContext("suggest_app_for_fsentry");
const suggested_apps = [];
const add_suggested_app = (app) => {
if (app) suggested_apps.push(app);
};

let content_type = mime.contentType(fsentry.name);
if(content_type === null || content_type === undefined || content_type === false)
Expand All @@ -1574,127 +1577,127 @@ async function suggest_app_for_fsentry(fsentry, options){
// Code
//---------------------------------------------
if(
fsname.endsWith('.asm') ||
fsname.endsWith('.asp') ||
fsname.endsWith('.aspx') ||
fsname.endsWith('.bash') ||
fsname.endsWith('.c') ||
fsname.endsWith('.cpp') ||
fsname.endsWith('.css') ||
fsname.endsWith('.csv') ||
fsname.endsWith('.dhtml') ||
fsname.endsWith('.f') ||
fsname.endsWith('.go') ||
fsname.endsWith('.h') ||
fsname.endsWith('.htm') ||
fsname.endsWith('.html') ||
fsname.endsWith('.html5') ||
fsname.endsWith('.java') ||
fsname.endsWith('.jl') ||
fsname.endsWith('.js') ||
fsname.endsWith('.jsa') ||
fsname.endsWith('.json') ||
fsname.endsWith('.jsonld') ||
fsname.endsWith('.jsf') ||
fsname.endsWith('.jsp') ||
fsname.endsWith('.kt') ||
fsname.endsWith('.log') ||
fsname.endsWith('.lock') ||
fsname.endsWith('.lua') ||
fsname.endsWith('.md') ||
fsname.endsWith('.perl') ||
fsname.endsWith('.phar') ||
fsname.endsWith('.php') ||
fsname.endsWith('.pl') ||
fsname.endsWith('.py') ||
fsname.endsWith('.r') ||
fsname.endsWith('.rb') ||
fsname.endsWith('.rdata') ||
fsname.endsWith('.rda') ||
fsname.endsWith('.rdf') ||
fsname.endsWith('.rds') ||
fsname.endsWith('.rs') ||
fsname.endsWith('.rlib') ||
fsname.endsWith('.rpy') ||
fsname.endsWith('.scala') ||
fsname.endsWith('.sc') ||
fsname.endsWith('.scm') ||
fsname.endsWith('.sh') ||
fsname.endsWith('.sol') ||
fsname.endsWith('.sql') ||
fsname.endsWith('.ss') ||
fsname.endsWith('.svg') ||
fsname.endsWith('.swift') ||
fsname.endsWith('.toml') ||
fsname.endsWith('.ts') ||
fsname.endsWith('.wasm') ||
fsname.endsWith('.xhtml') ||
fsname.endsWith('.xml') ||
fsname.endsWith('.yaml') ||
file_extension === '.asm' ||
file_extension === '.asp' ||
file_extension === '.aspx' ||
file_extension === '.bash' ||
file_extension === '.c' ||
file_extension === '.cpp' ||
file_extension === '.css' ||
file_extension === '.csv' ||
file_extension === '.dhtml' ||
file_extension === '.f' ||
file_extension === '.go' ||
file_extension === '.h' ||
file_extension === '.htm' ||
file_extension === '.html' ||
file_extension === '.html5' ||
file_extension === '.java' ||
file_extension === '.jl' ||
file_extension === '.js' ||
file_extension === '.jsa' ||
file_extension === '.json' ||
file_extension === '.jsonld' ||
file_extension === '.jsf' ||
file_extension === '.jsp' ||
file_extension === '.kt' ||
file_extension === '.log' ||
file_extension === '.lock' ||
file_extension === '.lua' ||
file_extension === '.md' ||
file_extension === '.perl' ||
file_extension === '.phar' ||
file_extension === '.php' ||
file_extension === '.pl' ||
file_extension === '.py' ||
file_extension === '.r' ||
file_extension === '.rb' ||
file_extension === '.rdata' ||
file_extension === '.rda' ||
file_extension === '.rdf' ||
file_extension === '.rds' ||
file_extension === '.rs' ||
file_extension === '.rlib' ||
file_extension === '.rpy' ||
file_extension === '.scala' ||
file_extension === '.sc' ||
file_extension === '.scm' ||
file_extension === '.sh' ||
file_extension === '.sol' ||
file_extension === '.sql' ||
file_extension === '.ss' ||
file_extension === '.svg' ||
file_extension === '.swift' ||
file_extension === '.toml' ||
file_extension === '.ts' ||
file_extension === '.wasm' ||
file_extension === '.xhtml' ||
file_extension === '.xml' ||
file_extension === '.yaml' ||
// files with no extension
!fsname.includes('.')
file_extension === ''
){
suggested_apps.push(await get_app({name: 'code'}))
suggested_apps.push(await get_app({name: 'editor'}))
add_suggested_app(await get_app({name: 'code'}))
add_suggested_app(await get_app({name: 'editor'}))
}
//---------------------------------------------
// Editor
//---------------------------------------------
if(
fsname.endsWith('.txt') ||
file_extension === '.txt' ||
// files with no extension
!fsname.includes('.')
file_extension === ''
){
suggested_apps.push(await get_app({name: 'editor'}))
suggested_apps.push(await get_app({name: 'code'}))
add_suggested_app(await get_app({name: 'editor'}))
add_suggested_app(await get_app({name: 'code'}))
}
//---------------------------------------------
// Markus
//---------------------------------------------
if(fsname.endsWith('.md')){
suggested_apps.push(await get_app({name: 'markus'}))
if(file_extension === '.md'){
add_suggested_app(await get_app({name: 'markus'}))
}
//---------------------------------------------
// Viewer
//---------------------------------------------
if(
fsname.endsWith('.jpg') ||
fsname.endsWith('.png') ||
fsname.endsWith('.webp') ||
fsname.endsWith('.svg') ||
fsname.endsWith('.bmp') ||
fsname.endsWith('.jpeg')
file_extension === '.jpg' ||
file_extension === '.png' ||
file_extension === '.webp' ||
file_extension === '.svg' ||
file_extension === '.bmp' ||
file_extension === '.jpeg'
){
suggested_apps.push(await get_app({name: 'viewer'}));
add_suggested_app(await get_app({name: 'viewer'}));
}
//---------------------------------------------
// Draw
//---------------------------------------------
if(
fsname.endsWith('.bmp') ||
file_extension === '.bmp' ||
content_type.startsWith('image/')
){
suggested_apps.push(await get_app({name: 'draw'}));
add_suggested_app(await get_app({name: 'draw'}));
}
//---------------------------------------------
// PDF
//---------------------------------------------
if(fsname.endsWith('.pdf')){
suggested_apps.push(await get_app({name: 'pdf'}));
if(file_extension === '.pdf'){
add_suggested_app(await get_app({name: 'pdf'}));
}
//---------------------------------------------
// Player
//---------------------------------------------
if(
fsname.endsWith('.mp4') ||
fsname.endsWith('.webm') ||
fsname.endsWith('.mpg') ||
fsname.endsWith('.mpv') ||
fsname.endsWith('.mp3') ||
fsname.endsWith('.m4a') ||
fsname.endsWith('.ogg')
file_extension === '.mp4' ||
file_extension === '.webm' ||
file_extension === '.mpg' ||
file_extension === '.mpv' ||
file_extension === '.mp3' ||
file_extension === '.m4a' ||
file_extension === '.ogg'
){
suggested_apps.push(await get_app({name: 'player'}));
add_suggested_app(await get_app({name: 'player'}));
}

//---------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions packages/phoenix/src/puter-shell/coreutils/__exports__.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import module_man from './man.js'
import module_mkdir from './mkdir.js'
import module_mv from './mv.js'
import module_neofetch from './neofetch.js'
import module_open from './open.js'
import module_printf from './printf.js'
import module_printhist from './printhist.js'
import module_pwd from './pwd.js'
Expand Down Expand Up @@ -88,6 +89,7 @@ export default {
"mkdir": module_mkdir,
"mv": module_mv,
"neofetch": module_neofetch,
"open": module_open,
"printf": module_printf,
"printhist": module_printhist,
"pwd": module_pwd,
Expand Down
132 changes: 132 additions & 0 deletions packages/phoenix/src/puter-shell/coreutils/open.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Phoenix Shell.
*
* Phoenix Shell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Exit } from './coreutil_lib/exit.js';
import { resolveRelativePath } from '../../util/path.js';
import { ErrorCodes } from '@heyputer/puter-js-common/src/PosixError.js';

export default {
name: 'open',
usage: 'open FILE',
description: "Opens FILE in the user's preferred application",
args: {
$: 'simple-parser',
allowPositionals: true
},
execute: async ctx => {
const { out, err } = ctx.externs;
const { positionals } = ctx.locals;
const { filesystem } = ctx.platform;

if (positionals.length !== 1) {
await err.write('open: Please provide exactly one FILE parameter\n');
throw new Exit(1);
}

const path = positionals[0];
if (ctx.platform.name === 'node') {
// On Node, best option is to use whichever utility the OS provides.
const { platform } = await import('node:process');
const system_open_command = (() => {
switch (platform) {
case 'darwin': return 'open';
case 'win32': return 'start';

// For all the others, we'll assume xdg-open is available.
case 'aix':
case 'android':
case 'freebsd':
case 'linux':
case 'openbsd':
case 'sunos':
default:
return 'xdg-open';
}
})();

// TODO: Extract app-launching code from PathCommandProvider and use that here.
// (But make it a background process.)
const { spawn } = await import('node:child_process');
spawn(system_open_command, [ path ]);
return;
}

// ------------------------- //
// Otherwise, we're on Puter //
// ------------------------- //

// Open URLs in a browser
try {
new URL(path);
// Parsing succeeded -> it's a URL
// TODO: Launch in the user's preferred browser app on Puter, once that's queryable.
window.open(path);
return;
} catch (e) {
// Not a URL!
}

// Check if the file exists
const abs_path = resolveRelativePath(ctx.vars, path);
let stat;
try {
stat = await filesystem.stat(abs_path);
} catch (e) {
if (e.posixCode === ErrorCodes.ENOENT) {
await err.write(`open: File or directory "${abs_path}" does not exist\n`);
throw new Exit(2);
}
throw e;
}

let app_name = '';
if (stat.is_dir) {
// Directories should open in explorer.
app_name = 'explorer';
} else {
// Query Puter for the preferred application
const request = await fetch(`${puter.APIOrigin}/open_item`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({ uid: stat.uid, path: abs_path }),
"method": "POST",
});
const response = await request.json();

if (!response.suggested_apps) {
await err.write(`open: ${response.message}\n`);
throw new Exit(1);
}

const app_info = response.suggested_apps[0];
app_name = app_info.name;
}

// Launch it
// TODO: Extract app-launching code from PuterAppCommandProvider and use that here.
// (But make it a background process.)
// TODO: Implement passing a list of files to open in `launchApp()`
await out.write(`Launching ${app_name}...\n` +
`Please note that this will not open the file. (Yet!)`);
puter.ui.launchApp(app_name);

return;
}
};