Skip to content

Commit

Permalink
Merge pull request #7 from kid-icarus/rk/configurable-config-path
Browse files Browse the repository at this point in the history
Allow for configurable config files.
  • Loading branch information
kid-icarus committed May 21, 2021
2 parents a1290b4 + 3af6129 commit c61b284
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 76 deletions.
45 changes: 39 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
# Focus Me

A configurable CLI-based timer with a plugin system meant to automate various
tasks when the timer starts, stops, or ticks. The following plugins are
included:
A configurable timer with a plugin system meant to automate various
tasks when the timer starts, stops, or ticks.

- [applciation-manager](src/plugins/application-manager/README.md) - Closes
## Project Goals

The goal of this project is to help maintain focus. Start a timer and remove distraction seamlessly. When the timer ends, take a break on revisit the
distracting world for a bit before setting another timer. We accomplish the goal via:

- OS/GUI automations (applescript, powershell, etc)
- Useful 3rd party integrations (slack, rescuetime, arbitrary http/s requests)
- 1st party Persistence and analytics (see how focused you are, track trends over time)

To this end, most of the functionality of the Focus Me lies within the various plugins.

## Plugins

- [applcation-manager](src/plugins/application-manager/README.md) - Closes
distracting applications before the timer starts, and opens them back up when
the timer ends.
- [bell](src/plugins/bell/README.md) - Plays a nice sounding bell at the start
Expand All @@ -27,10 +39,13 @@ included:

`npm install -g @focus-me/focus-cli`

macOS is the only supported platform at the moment, PRs welcome!

## Configuration

`focus` will look in `~/.timerrc.json` to try to load configuration, and if
cannot find it, it will supply a default.
`focus` will look in a platform specific folder to try to load configuration, and if
cannot find it will throw. You can specify an alternative config with using the `--config` command line option or
`config` environment variable.

- `config.time` - The amount of time, in minutes, to count down.
- `config.plugins` - An object of plugins to configure. Each key is the name of
Expand All @@ -56,4 +71,22 @@ If you need to cancel the timer, you can kill the process with a `SIGINT`.
Canceling the timer will not execute any plugins' stop methods that should only
be run upon _completion_ of the timer.

### CLI options

- `--help` - Display the list of options
- `--version` - Display the version of FocusMe
- `--config` - Specify an alternative path to a FocusMe config file.

## Creating your own plugin

If you specify an additional plugin in `config.plugins`, we attempt to import that module. The module must implement
the [plugin interface](https://github.com/kid-icarus/focus-me/blob/a51d6cbd05a03354137046e454df69a9832e9ed3/src/util/load-plugins.ts#L4-L8)

## Files

Installing this module will create files in:

- `~/Library/Application Support/FocusMe/preferences.json`: Your FocusMe preferences/config.
- `~/Library/Script Libraries/FocusMe/util.scpt`:
A compiled applescript that serves as a library for other applescripts to import. It is responsible for
reading and parsing the user's preferences.
32 changes: 32 additions & 0 deletions default-preferences.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"time": 25,
"plugins": {
"application-manager": {
"enabled": false
},
"bell": {
"enabled": false
},
"logger": {
"enabled": true
},
"rain": {
"enabled": false
},
"rescue-time": {
"enabled": false
},
"slack": {
"enabled": false
},
"spotify": {
"enabled": false
},
"tracker": {
"enabled": false
},
"webhooks": {
"enabled": false
}
}
}
26 changes: 23 additions & 3 deletions install.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
const fs = require('fs')
const {join} = require('path')
const APP_NAME = 'FocusMe'

const scriptLibrariesDir = join(process.env.HOME, 'Library', 'Script Libraries');
const focusMeDir = join(scriptLibrariesDir, 'focus-me-applescripts');
const applicationSupportDir = join(process.env.HOME, 'Library', 'Application Support', APP_NAME);
const configFile = join(process.env.HOME, 'Library', 'Application Support', APP_NAME, 'preferences.json');
const focusMeDir = join(scriptLibrariesDir, APP_NAME);

const createSymlink = () => {
/**
* Initial post-install script to install default applescripts and configuration.
*/
const install = () => {
if (!fs.existsSync(scriptLibrariesDir)) {
console.log('Applescript script libraries directory does not exist, creating it.')
fs.mkdirSync(scriptLibrariesDir)
}

if (!fs.existsSync(applicationSupportDir)) {
console.log('FocusMe application support directory does not exist, creating it.')
fs.mkdirSync(applicationSupportDir)
}

if (!fs.existsSync(configFile)) {
console.log('FocusMe config does not exist, creating it.')
fs.copyFile(join(__dirname, 'default-preferences.json'), configFile, (err) => {
if (err) {
console.error('Error saving default config: ', e)
}
})
}

if (!fs.existsSync(focusMeDir)) {
console.log('Symlinking compiled applescript libraries.')
fs.symlinkSync(join(__dirname, 'dist/util/focus-me-applescripts'), focusMeDir);
}
}

createSymlink();
install();
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@focus-me/focus-cli",
"version": "2.0.0",
"version": "3.0.0",
"author": "Ryan Kois <ryan.kois@gmail.com>",
"repository": {
"type": "git",
Expand All @@ -21,7 +21,8 @@
"dist",
"assets",
"bin",
"install.js"
"install.js",
"default-preferences.json"
],
"license": "MIT",
"dependencies": {
Expand Down
13 changes: 12 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@
import { Timer } from './timer';
import { loadPlugins } from './util/load-plugins';
import { loadConfig } from './util/load-config';
import yargs from 'yargs';

const argv = yargs(process.argv.slice(2))
.options({
config: {
type: 'string',
description: 'An alternate path to a config file',
},
})
.help().argv;

const init = async (): Promise<void> => {
const config = await loadConfig();
const config = await loadConfig(argv.config);
config.path = argv.config;
const plugins = loadPlugins(config);
const timer = new Timer(config, plugins);

Expand Down
17 changes: 3 additions & 14 deletions src/plugins/application-manager/applescripts/close-app.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
const util = Library('focus-me-applescripts/util');
const util = Library('FocusMe/util');
const config = util.getConfig();

config.plugins['application-manager'].close.forEach(({ name }) => {
const app = Application(name);
const realName = app.name();
const displayedName = app.displayedName();
const id = app.id();
try {
if (app.running()) app.quit();
if (app && app.running()) app.quit();
} catch (e) {
// Sometimes some MailQuickLookExtension process makes Mail falsely report itself as running.
console.log(
`Could not quit ${name}:
found ${realName} with:
id: ${id}
name: ${name}
displayedName: ${displayedName}
`,
);
console.log(e);
console.log(`Could not close ${name}: `, e);
}
});
8 changes: 2 additions & 6 deletions src/plugins/application-manager/applescripts/open-app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
const util = Library('focus-me-applescripts/util');
const util = Library('FocusMe/util');
const config = util.getConfig();

config.plugins['application-manager'].open.forEach(
({ name, bounds, delayTime }) => {
const app = Application(name);
const realName = app.name();
const id = app.id();
app.activate();
if (delayTime) delay(delayTime);
// eslint-disable-next-line no-param-reassign
Expand All @@ -14,9 +12,7 @@ config.plugins['application-manager'].open.forEach(
try {
window.bounds = bounds;
} catch (e) {
console.log(
`open-app: could not set bounds on ${name}, found ${realName} with ID ${id}`,
);
console.log(`open-app: could not set bounds on ${name}`);
}
}
},
Expand Down
42 changes: 12 additions & 30 deletions src/plugins/application-manager/application-manager.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,22 @@
import { Plugin } from '../../util/load-plugins';
import { exec } from 'child_process';
import { script } from '../../util/exec-script';
import { execAppleScript } from '../../util/exec-script';

const plugin: Plugin = {
async start(): Promise<void> {
return new Promise<void>((res, rej) => {
exec(
`osascript -l JavaScript ${script(
'application-manager',
'close-app.js',
)}`,
(err, stdout, stderr) => {
console.log(stderr);
console.log(stdout);
if (err) return rej(err);
res();
},
);
});
async start(config: any): Promise<void> {
try {
await execAppleScript('application-manager', 'close-app.js', config.path);
} catch (e) {
console.error(e);
}
},

async stop(config: any, completed: boolean): Promise<void> {
if (!completed) return;
const p = new Promise<void>((res, rej) => {
exec(
`osascript -l JavaScript ${script(
'application-manager',
'open-app.js',
)}`,
err => {
if (err) return rej(err);
res();
},
);
});
return p.catch(err => console.log(err));
try {
await execAppleScript('application-manager', 'open-app.js', config.path);
} catch (e) {
console.error(e);
}
},
};

Expand Down
2 changes: 2 additions & 0 deletions src/plugins/tracker/current-app-master.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const watchApps = (): ChildProcess => {
const proc = spawn('osascript', ['-l', 'JavaScript', script], {});
procRef.proc = proc;

// This process lives on for the duration of the timer, and every second logs the currently focused application,
// which is added to the accumulator here
proc.stderr.on('data', data => {
const app = data.toString().trim();
apps[app] = apps[app] ? apps[app] + 1 : 1;
Expand Down
22 changes: 21 additions & 1 deletion src/util/exec-script.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import { join } from 'path';
import { ExecException } from 'child_process';
import { exec, ExecException } from 'child_process';

export const script = (plugin: string, file: string) =>
join(__dirname, '..', 'plugins', plugin, 'applescripts', file);

export const log = (error: ExecException | null): void => {
if (error) console.error(error);
};

export const execAppleScript = (
plugin: string,
file: string,
configPath: string,
): Promise<{ stdout: string; stderr: string }> =>
new Promise((res, rej) => {
exec(
`osascript -l JavaScript ${script(plugin, file)}`,
{ env: { config: configPath } },
(err, stdout, stderr) => {
if (err) return rej(err);

res({
stderr,
stdout,
});
},
);
});
13 changes: 11 additions & 2 deletions src/util/focus-me-applescripts/util.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
function getConfig(path) {
let env = $.NSProcessInfo.processInfo.environment;
env = ObjC.unwrap(env);
const configPath = ObjC.unwrap(env.config);

function getConfig() {
const app = Application.currentApplication();
app.includeStandardAdditions = true;
const file = app.openForAccess(`${app.pathTo('home folder')}/.timerrc.json`);
const file = app.openForAccess(
configPath ||
`${app.pathTo('application support', {
from: 'user domain',
})}/FocusMe/timerrc.json`,
);
const contents = app.read(file);
return JSON.parse(contents);
}
33 changes: 22 additions & 11 deletions src/util/load-config.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
import * as path from 'path';
import * as fs from 'fs';
import { promisify } from 'util';
import * as os from 'os';
const readFile = promisify(fs.readFile);

export interface TimerConfig {
path?: string;
time: number;
plugins?: Record<string, any>;
}

export const loadConfig = async (): Promise<TimerConfig> => {
const defaultConfig: Partial<Record<NodeJS.Platform, string>> = {
darwin: path.join(
os.homedir(),
'Library/Application Support/FocusMe/preferences.json',
),
};

const platform = os.platform();
const defaultConfigPath = defaultConfig[platform];
if (!defaultConfigPath) {
const keys = Object.keys(defaultConfig).join(', ');
throw new Error(
`unsupported platform, currently supported platforms: ${keys}`,
);
}

export const loadConfig = async (
configPath: string = defaultConfigPath,
): Promise<TimerConfig> => {
const defaultConfig: TimerConfig = {
time: 25,
};
let config: TimerConfig;

if (!process.env.HOME) {
console.error(
'No $HOME environment variable set, returning default config',
);
return defaultConfig;
}

try {
config = JSON.parse(
await readFile(path.join(process.env.HOME, '.timerrc.json'), 'utf8'),
);
config = JSON.parse(await readFile(configPath, 'utf8'));
} catch (e) {
console.error('Error reading configuration, returning default config: ', e);
return defaultConfig;
Expand Down

0 comments on commit c61b284

Please sign in to comment.