Skip to content

Commit

Permalink
Fancier application manager.
Browse files Browse the repository at this point in the history
  • Loading branch information
kid-icarus committed May 16, 2021
1 parent f8be673 commit 313375f
Show file tree
Hide file tree
Showing 14 changed files with 365 additions and 123 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
"sourceType": "module",
"project": "./tsconfig.json",
ecmaFeatures: {
"jsx": true
"jsx": true,
}
},
extends: [
Expand Down
22 changes: 22 additions & 0 deletions getApplication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const initialApp = Application('System Events').applicationProcesses.where({
frontmost: true,
})

const initialName = initialApp.name().toString()

while (true) {
const currentApp = Application('System Events').applicationProcesses.where({
frontmost: true,
})
const currentAppName = currentApp.name().toString()
if (initialName !== currentAppName) {
console.log(
JSON.stringify({
name: currentAppName,
displayedName: currentApp.displayedName(),
})
)
break
}
delay(0.5)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"webpack-cli": "^4.6.0"
},
"dependencies": {
"@focus-me/focus-cli": "^1.3.0",
"@focus-me/focus-cli": "^2.0.0",
"@types/react-dom": "^17.0.3",
"@types/styled-components": "^5.1.9",
"electron": "^12.0.2",
Expand Down
22 changes: 22 additions & 0 deletions src/applescripts/getApplication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const initialApp = Application('System Events').applicationProcesses.where({
frontmost: true,
})

const initialName = initialApp.name().toString()

while (true) {
const currentApp = Application('System Events').applicationProcesses.where({
frontmost: true,
})
const currentAppName = currentApp.name().toString()
if (initialName !== currentAppName) {
console.log(
JSON.stringify({
name: currentAppName,
displayedName: currentApp.displayedName(),
})
)
break
}
delay(0.5)
}
13 changes: 13 additions & 0 deletions src/applescripts/getRunningApps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const apps = Application('System Events').applicationProcesses()

console.log(
JSON.stringify(
Object.values(apps)
.filter((x) => !x.backgroundOnly())
.map((x) => ({
id: x.id(),
name: x.name(),
displayedName: x.displayedName(),
}))
)
)
117 changes: 57 additions & 60 deletions src/components/Plugins/ApplicationManager/ApplicationManager.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,79 @@
import React, { KeyboardEventHandler } from 'react'
import { Button, FormField, Heading, Paragraph, Text, TextInput } from 'grommet'
import React, {
ChangeEvent,
EventHandler,
KeyboardEventHandler,
useEffect,
useState,
} from 'react'
import {
Box,
Button,
FormField,
Heading,
Layer,
Paragraph,
TextInput,
TextInputProps,
} from 'grommet'
import { PluginProps } from '../index'
import { FormClose } from 'grommet-icons'
import { FormClose, Target } from 'grommet-icons'
import ApplicationSelector, { ApplicationType } from './ApplicationSelector'

interface Suggestion {
label: string
value: string
}

interface App {
name: string
displayedName: string
}

const ApplicationManager: React.FC<PluginProps> = ({
config,
updatePluginConfig,
}) => {
const pluginConfig: ApplicationManagerConfig =
config.plugins['application-manager']
const [runningApps, setRunningApps] = useState<
{
label: string
value: string
}[]
>([])
const appTypes: ApplicationType[] = ['close', 'open']

const addPluginToClose: KeyboardEventHandler<HTMLInputElement> = async (
e
) => {
if (e.key === 'Enter') {
await updatePluginConfig('application-manager', {
...pluginConfig,
close: [...pluginConfig.close, e.currentTarget.value],
useEffect(() => {
window.electron
.getRunningApps()
.then((apps) => {
console.log(apps)
return apps.map((x): { label: string; value: string } => ({
label: x.displayedName ?? x.name,
value: x.name,
}))
})
}
}

const addPluginToOpen: KeyboardEventHandler<HTMLInputElement> = async (e) => {
if (e.key === 'Enter') {
await updatePluginConfig('application-manager', {
...pluginConfig,
open: [...pluginConfig.open, { name: e.currentTarget.value }],
})
}
}

const onRemoveAppClick = async (appName: string) => {
const index = pluginConfig.close.indexOf(appName)
pluginConfig.close.splice(index, 1)
await updatePluginConfig('application-manager', {
...pluginConfig,
})
}
.then(setRunningApps)
.catch((e) => console.log(e))
}, [])

return (
<div>
<Box>
<Heading>Application Manager</Heading>
<Paragraph margin={{ top: 'small', bottom: 'medium' }}>
Configure applications to be closed while focusing, and optionally
choose applications to open once the timer ends.
</Paragraph>
<FormField label="Close the following applications when timer starts:">
<TextInput onKeyPress={addPluginToClose} />
</FormField>
{pluginConfig.close.map((x) => (
<Button
onClick={() => onRemoveAppClick(x)}
key={x}
icon={<FormClose />}
label={x}
size="small"
margin="xsmall"
primary
/>
))}
<FormField
label="Open the following applications when timer stops:"
margin={{ top: 'large' }}
>
<TextInput onKeyPress={addPluginToOpen} />
</FormField>
{pluginConfig.open.map((x) => (
<Button
onClick={() => onRemoveAppClick(x.name)}
key={x.name}
icon={<FormClose />}
label={x.name}
primary
size="small"
margin="xsmall"
{appTypes.map((type) => (
<ApplicationSelector
key={type}
config={config}
updatePluginConfig={updatePluginConfig}
suggestions={runningApps}
type={type}
/>
))}
</div>
</Box>
)
}

Expand Down
135 changes: 135 additions & 0 deletions src/components/Plugins/ApplicationManager/ApplicationSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { ChangeEvent, EventHandler, useEffect, useState } from 'react'
import {
Box,
Button,
FormField,
Layer,
Paragraph,
TextInput,
TextInputProps,
} from 'grommet'
import { PluginProps } from '../index'
import { FormClose, Target } from 'grommet-icons'

interface Suggestion {
label: string
value: string
}

export type ApplicationType = 'open' | 'close'

interface ApplicationSelectorProps {
type: ApplicationType
suggestions: Suggestion[]
}

const ApplicationSelector: React.FC<PluginProps & ApplicationSelectorProps> = ({
type,
config,
updatePluginConfig,
suggestions,
}) => {
const pluginConfig: ApplicationManagerConfig =
config.plugins['application-manager']
const [showModal, setShowModal] = useState<boolean>(false)
const [selection, setSelection] = useState<string>('')
const [runningApps, setRunningApps] = useState<
{
label: string
value: string
}[]
>(suggestions)

useEffect(() => {
if (!suggestions) return
setRunningApps(suggestions)
}, [suggestions])

const addApp = async (app: Application): Promise<void> => {
if (pluginConfig[type].some((x) => x.name === app.name)) return

return updatePluginConfig('application-manager', {
...pluginConfig,
[type]: [...pluginConfig[type], app],
})
}

const onRemoveAppClick = async (appName: string) => {
const index = pluginConfig[type].findIndex((app) => app.name === appName)
pluginConfig[type].splice(index, 1)
await updatePluginConfig('application-manager', {
...pluginConfig,
})
}

const selectApplication = async () => {
setShowModal(true)
const app = await window.electron.selectApplication()
await addApp(app)
setShowModal(false)
}

const onSuggestionSelect: TextInputProps['onSuggestionSelect'] = async (e: {
suggestion: Suggestion
}) => {
setSelection('')
await addApp({
name: e.suggestion.value,
displayedName: e.suggestion.label,
})
}

const onAppChange: EventHandler<ChangeEvent<HTMLInputElement>> = (e) => {
const nextValue = e.target.value
setSelection(nextValue)
if (!nextValue) {
console.log('no next value, resetting suggestions')
setRunningApps(suggestions)
} else {
const regexp = new RegExp(`^${nextValue}`, 'i')
setRunningApps(suggestions.filter((s) => regexp.test(s.label)))
}
}

return (
<Box margin={{ bottom: 'large' }}>
{showModal && (
<Layer>
<Box pad="large">
<Paragraph>
Select the application you'd like by focusing its window.
</Paragraph>
</Box>
</Layer>
)}
<FormField label={type === 'close' ? 'Close' : 'Open'}>
<Box direction="row">
<Button icon={<Target />} onClick={selectApplication} />
<TextInput
defaultSuggestion={0}
value={selection}
suggestions={runningApps}
onSuggestionSelect={onSuggestionSelect}
onChange={onAppChange}
plain
/>
</Box>
</FormField>
<Box direction="row" wrap>
{pluginConfig[type].map((app) => (
<Button
onClick={() => onRemoveAppClick(app.name)}
key={app.name}
icon={<FormClose />}
label={app.displayedName ?? app.name}
size="small"
margin="xsmall"
primary
/>
))}
</Box>
</Box>
)
}

export default ApplicationSelector
1 change: 1 addition & 0 deletions src/preferences.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
}
:root, html, body {
height: 100%;
font-family: "Helvetica Neue"
}
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
Expand Down
Loading

0 comments on commit 313375f

Please sign in to comment.