diff --git a/README.md b/README.md index f1ee258..23af5b3 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ # Singular -Install web apps as native apps. +Install web apps as native apps, or just open a URL in a "temporary" app that +closes and disappears forever on close. -WARNING: This is not production software yet, just scratching an itch at this point. +WARNING: This is not production software yet, just scratching an itch and exploring +alternative ways to use the web. ## Why I missed Nativeifer. + Desktop PWAs are not nearly sufficient as replacement (and are their own tire fire). I looked at the Nativefier code, and it's... massive and ancient. @@ -18,7 +21,7 @@ Current status: - Only tested on macOS -## Usage +## Install an app - Open the app - Enter an app name (your choice) @@ -29,15 +32,31 @@ An app is generated and zipped up, and you are prompted to download the zip file In the zip file is the generated app. Install it in whatever way your OS requires, or you prefer. +## Open a URL with an ephemeral browser + + + ## TODO +priority features +- app icons + build/deploy -- set up local build -- set up remote build -- set up auto release +- local build +- remote build +- auto release + +runtime +- auto-update generated apps (eg electron version) +- or tauri + webview, eg Pake? + +horizontality +- registry +- un/install +- cross-app comms layer -future -- look into auto-update of generated apps +extensibility +- web-extensions... ## Thanks diff --git a/electron-base/main.js b/electron-base/main.js index 5f088d7..497b323 100644 --- a/electron-base/main.js +++ b/electron-base/main.js @@ -1,8 +1,10 @@ // Modules to control application life and create native browser window -import { app, BrowserWindow } from 'electron' -import path from 'node:path' +import { app, BrowserWindow, Notification } from 'electron'; +import path from 'node:path'; +import process from 'node:process'; +import { execFileSync } from 'child_process'; -const __dirname = import.meta.dirname +const __dirname = import.meta.dirname; import * as fs from 'fs'; const str = fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'); @@ -45,5 +47,46 @@ app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) +const deleteMyself = () => { + console.log('Deleting myself'); + + const appPath = process.platform === 'win32' + ? path.resolve('.', 'singular.exe') + : process.execPath.replace(/\.app.*$/, '.app'); + console.log('appPath', appPath); + + const script = `tell application "Finder" + move (application file of application process "fooble" as alias) to trash +end tell`; + + const humanReadableOutput = true; + const outputArguments = humanReadableOutput ? [] : ['-ss']; + const stdout = execFileSync('osascript', ['-e', script, ...outputArguments], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + timeout: 5000, + }); + console.log('stdout', stdout.trim()); + + /* + //fs.unlinkSync(appPath, (err) => { + fs.unlink(appPath, (err) => { + if (err) { + + new Notification({ + title: 'ERROR', + body: err.message, + }).show(); + + console.error('Error removing app file:', err); + } else { + console.log('App file removed successfully'); + } + }); + */ +}; + +//app.on('quit', deleteMyself); + // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. diff --git a/main.js b/main.js index 48b6120..a5ff993 100644 --- a/main.js +++ b/main.js @@ -2,6 +2,7 @@ import { app, BrowserWindow, dialog, ipcMain, session } from 'electron' import path from 'node:path' import process from 'node:process' +import { spawn } from 'node:child_process'; import packager from '@electron/packager' import * as fs from 'fs'; import zip from 'cross-zip'; @@ -15,7 +16,18 @@ process.noAsar = true; const __dirname = import.meta.dirname -function createWindow () { +// if spawned copy for url opening, log to file so we can debug +if (app.commandLine.hasSwitch('url2')) { + const tempDir = app.getPath('temp'); + const access = fs.createWriteStream(path.join(tempDir, 'singular.log')); + process.stdout.write = process.stderr.write = access.write.bind(access); +} + +process.on('uncaughtException', function(err) { + console.error((err && err.stack) ? err.stack : err); +}); + +function createWindow() { // Create the browser window. const mainWindow = new BrowserWindow({ width: WIN_WIDTH, @@ -32,6 +44,23 @@ function createWindow () { //mainWindow.webContents.openDevTools() } +function openURL(url) { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + // and load the URL of the website. + mainWindow.loadURL(url) + + // Open the DevTools. + // mainWindow.webContents.openDevTools() +} + const validURL = str => { try { return new URL(str); @@ -132,17 +161,123 @@ const generate = async opts => { ipcMain.on('generate', (e, msg) => generate(msg)); +const initTempProfile = () => { + const PROFILE = `p${Date.now()}`; + + console.log('PROFILE', PROFILE); + + // Profile dirs are subdir of userData dir + // ..................................... ↓ we set this per profile + // + // {home} / {appData} / {userData} / {profileDir} + // + // Chromium's data in a subfolder of profile folder + // + // ................................................. ↓ we set this per profile + // + // {home} / {appData} / {userData} / {profileDir} / {sessionData} + + + // specify various app data paths and make if not exist + const appDataPath = app.getPath('appData'); + const userDataPath = app.getPath('userData'); + //const profileDataPath = path.join(userDataPath, PROFILE); + //const sessionDataPath = path.join(profileDataPath, 'chromium'); + + console.log('adp', app.getPath('appData')); + console.log('udp', app.getPath('userData')); + + const tempDir = app.getPath('temp'); + console.log('tempDir', tempDir); + + const tempAppDataPath = path.join(tempDir, PROFILE); + app.setPath('appData', tempAppDataPath); + + console.log('adp', app.getPath('appData')); + console.log('udp', app.getPath('userData')); + //console.log('pdp', profileDataPath); + //console.log('sdp', sessionDataPath); +}; + +const openSelf = url => { + const appName = 'singular'; + const appPath = process.platform === 'win32' + ? path.resolve('.', `${appName}.exe`) + : process.execPath.replace(/\.app.*$/, '.app'); + console.log('appPath', appPath); + + //const cmd = appPath; + const cmd = 'electron'; + + const args = [ + '.', + //path.join(process.cwd(), 'electron-base'), + `--url2=${url}` + ]; + + const cp = spawn(cmd, args, { + detached: true, + stdio:'ignore', + env: { + NODE_ENV: 'electron', + PATH: process.env.PATH + } + }).on('error', (err) => { + console.error(err); + process.exit(2); + }).on('spawn', () => { + cp.unref(); + console.log('spawn complete, exiting process'); + process.exit(0); + }); +}; + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(() => { - createWindow() + console.log('ready'); - app.on('activate', function () { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) createWindow() - }) + // We're initial process to open a URL + if (app.commandLine.hasSwitch('url')) { + console.log('url switch found'); + + const url = app.commandLine.getSwitchValue('url'); + console.log('url', url); + + if (!validURL(url)) { + console.error('Bad URL'); + } + else { + openSelf(url); + console.log('self opened'); + + // exit + app.quit() + console.log('app quit'); + } + } + // We're a temporary spawn to open a URL + else if (app.commandLine.hasSwitch('url2')) { + // If we're opening a temporary URL then we need to + // create a temporary profile for the data + initTempProfile(); + console.log('temp profile created'); + + const url = app.commandLine.getSwitchValue('url2'); + console.log('url2', url); + openURL(url); + } + // Window for generating and installing URL as app + else { + createWindow(); + + app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }); + } }); // Quit when all windows are closed, except on macOS. There, it's common @@ -151,3 +286,4 @@ app.whenReady().then(() => { app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }); +