Skip to content

Commit

Permalink
feat(launcher): add edit mode
Browse files Browse the repository at this point in the history
  • Loading branch information
CyanSalt committed Jun 13, 2022
1 parent 1040550 commit f665be1
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 53 deletions.
85 changes: 85 additions & 0 deletions addons/launcher/src/main/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import type { LauncherInfo } from '../../typings/launcher'

async function readJSONFile<T = any>(file: string) {
try {
const content = await fs.promises.readFile(file, 'utf8')
return JSON.parse(content) as T
} catch {
return undefined
}
}

export function getNodePackageManager(pkg: any, files: string[]) {
const supportedManagers = ['npm', 'yarn', 'pnpm']
// Corepack style
const packageManager: string | undefined = pkg.packageManager
if (packageManager) {
const name = packageManager.match(/^\w+(?=@|$)/)?.[0]
if (name && supportedManagers.includes(name)) {
return name
}
}
// Lerna style
const npmClient: string | undefined = pkg.npmClient
if (npmClient && supportedManagers.includes(npmClient)) {
return npmClient
}
if (files.includes('yarn.lock')) return 'yarn'
if (files.includes('pnpm-lock.yaml')) return 'pnpm'
return 'npm'
}

function getNodePackageCommand(pkg: any, files) {
const packageManager = getNodePackageManager(pkg, files)
const scripts: Record<string, string> = pkg.scripts ?? {}
const builtinScripts = ['pack', 'rebuild', 'start', 'test']
const runScripts = ['serve', 'build', 'dev']
const builtinScript = builtinScripts.find(key => scripts[key])
if (builtinScript) {
return `${packageManager} ${builtinScript}`
}
const runScript = runScripts.find(key => scripts[key])
if (runScript) {
return `${packageManager}${packageManager === 'yarn' ? '' : ' run'} ${builtinScript}`
}
return `${packageManager}${packageManager === 'yarn' ? '' : ' install'}`
}

async function createLauncher(entry: string): Promise<LauncherInfo> {
const info = await fs.promises.stat(entry)
const directory = info.isDirectory() ? entry : path.dirname(entry)
const files = await fs.promises.readdir(directory)
const homedir = os.homedir()
if (info.isDirectory()) {
if (files.includes('package.json')) {
return createLauncher(path.join(entry, 'package.json'))
}
}
const parsed = path.parse(entry)
const isUserPath = process.platform !== 'win32' && parsed.dir.startsWith(homedir + path.sep)
if (parsed.base === 'package.json') {
const data = await readJSONFile(entry)
const command = getNodePackageCommand(data, files)
return {
name: data.productName ?? data.name ?? path.basename(parsed.dir),
command,
directory: isUserPath
? '~' + parsed.dir.slice(homedir.length)
: parsed.dir,
}
}
return {
name: path.basename(parsed.dir),
command: entry,
directory: isUserPath
? os.homedir()
: parsed.dir,
}
}

export {
createLauncher,
}
15 changes: 15 additions & 0 deletions addons/launcher/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as commas from 'commas:api/main'
import { BrowserWindow, dialog } from 'electron'
import type { Launcher } from '../../typings/launcher'
import { createLauncher } from './create'
import { useLaunchers } from './launcher'

declare module '../../../../src/typings/settings' {
Expand All @@ -12,6 +15,18 @@ export default () => {
const launchersRef = useLaunchers()
commas.ipcMain.provide('launchers', launchersRef)

commas.ipcMain.handle('create-launcher', async (event) => {
const frame = BrowserWindow.fromWebContents(event.sender)
if (!frame) return
const result = await dialog.showOpenDialog(frame, {
properties: process.platform === 'darwin'
? ['openFile', 'openDirectory', 'multiSelections', 'createDirectory']
: ['openFile', 'multiSelections', 'dontAddToRecent'],
})
const launchers = await Promise.all(result.filePaths.map(entry => createLauncher(entry))) as Launcher[]
launchersRef.value = [...launchersRef.value, ...launchers]
})

commas.settings.addSettingsSpecsFile('settings.spec.json')

commas.i18n.addTranslationDirectory('locales')
Expand Down
21 changes: 13 additions & 8 deletions addons/launcher/src/main/launcher.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { computed, unref } from '@vue/reactivity'
import * as commas from 'commas:api/main'
import type { Launcher } from '../../typings/launcher'
import type { Launcher, LauncherInfo } from '../../typings/launcher'

const generateID = commas.helperMain.createIDGenerator()

function fillLauncherIDs(launchers: Omit<Launcher, 'id'>[], old: Launcher[] | null) {
function fillLauncherIDs(launchers: LauncherInfo[], old: Launcher[] | null) {
const oldValues = old ? [...old] : []
return launchers.map<Launcher>(launcher => {
const index = oldValues.findIndex(item => {
Expand All @@ -24,17 +24,22 @@ function fillLauncherIDs(launchers: Omit<Launcher, 'id'>[], old: Launcher[] | nu
})
}

const rawLaunchersRef = commas.file.useYAMLFile<Omit<Launcher, 'id'>[]>(
const rawLaunchersRef = commas.file.useYAMLFile<LauncherInfo[]>(
commas.file.userFile('launchers.yaml'),
[],
)

let oldValue: Launcher[]
const launchersRef = computed(() => {
const value = unref(rawLaunchersRef)
const launchers = fillLauncherIDs(value, oldValue)
oldValue = launchers
return launchers
const launchersRef = computed({
get: () => {
const value = unref(rawLaunchersRef)
const launchers = fillLauncherIDs(value, oldValue)
oldValue = launchers
return launchers
},
set: value => {
rawLaunchersRef.value = value.map(({ id, ...launcher }) => launcher)
},
})

function useLaunchers() {
Expand Down
126 changes: 95 additions & 31 deletions addons/launcher/src/renderer/LauncherList.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<script lang="ts" setup>
import * as commas from 'commas:api/renderer'
import { ipcRenderer } from 'electron'
import { nextTick } from 'vue'
import type { Launcher } from '../../typings/launcher'
import {
createLauncherGroup,
getTerminalTabByLauncher,
moveLauncher,
openLauncher,
removeLauncher,
startLauncher,
startLauncherExternally,
useLaunchers,
Expand All @@ -18,34 +20,50 @@ const launchers = $(useLaunchers())
let searcher = $ref<HTMLInputElement>()
let isCollapsed: boolean = $ref(false)
let isFinding: boolean = $ref(false)
let isActionsVisible: boolean = $ref(false)
let isEditing: boolean = $ref(false)
let keyword = $ref('')
const keywords = $computed(() => {
return keyword.trim().toLowerCase().split(/\s+/)
.map(item => item.trim())
.filter(Boolean)
})
const filteredLaunchers = $computed(() => {
if (!isFinding) return launchers
const keywords = keyword.toLowerCase().split(/\s+/)
return launchers.filter(
launcher => keywords.every(
item => Object.values(launcher).join(' ').toLowerCase().includes(item),
),
)
return launchers.filter(launcher => {
if (isCollapsed) {
if (!getTerminalTabByLauncher(launcher)) return false
}
if (keywords.length) {
const matched = keywords.every(
item => Object.values(launcher).join(' ').toLowerCase().includes(item),
)
if (!matched) return false
}
return true
})
})
const isLauncherSortingDisabled = $computed(() => {
return isCollapsed || isFinding
return filteredLaunchers.length !== launchers.length
})
const isAnyActionEnabled = $computed(() => {
if (isActionsVisible) return true
return Boolean(keywords.length) || isEditing
})
function toggleCollapsing() {
isCollapsed = !isCollapsed
}
async function toggleFinding() {
isFinding = !isFinding
if (isFinding) {
async function toggleActions() {
isActionsVisible = !isActionsVisible
if (isActionsVisible) {
await nextTick()
searcher.focus()
} else {
keyword = ''
searcher.blur()
}
}
Expand All @@ -54,6 +72,27 @@ function sortLaunchers(from: number, to: number) {
moveLauncher(from, to)
}
function toggleEditing() {
isEditing = !isEditing
}
function createLauncher() {
ipcRenderer.invoke('create-launcher')
}
function closeLauncher(launcher: Launcher) {
const tab = getTerminalTabByLauncher(launcher)
if (isEditing) {
const index = launchers.findIndex(item => item.id === launcher.id)
removeLauncher(index)
if (tab) {
delete tab.group
}
} else if (tab) {
commas.workspace.closeTerminalTab(tab)
}
}
function showLauncherScripts(launcher: Launcher, event: MouseEvent) {
const scripts = launcher.scripts ?? []
commas.ui.openContextMenu([
Expand Down Expand Up @@ -84,13 +123,13 @@ function showLauncherScripts(launcher: Launcher, event: MouseEvent) {
</div>
<div class="buttons" @click.stop>
<div
:class="['button', 'find', { active: isFinding }]"
@click="toggleFinding"
:class="['button', 'more', { active: isAnyActionEnabled }]"
@click="toggleActions"
>
<span class="feather-icon icon-filter"></span>
<span class="feather-icon icon-more-vertical"></span>
</div>
</div>
<div v-show="isFinding" class="find-launcher" @click.stop>
<div v-show="isActionsVisible" class="launcher-actions" @click.stop>
<input
ref="searcher"
v-model="keyword"
Expand All @@ -99,8 +138,11 @@ function showLauncherScripts(launcher: Launcher, event: MouseEvent) {
class="keyword"
placeholder="Find#!terminal.5"
autofocus
@keyup.esc="toggleFinding"
@keyup.esc="toggleActions"
>
<span :class="['button', 'edit', { active: isEditing }]" @click="toggleEditing">
<span class="feather-icon icon-edit-3"></span>
</span>
</div>
</div>
<SortableList
Expand All @@ -112,12 +154,13 @@ function showLauncherScripts(launcher: Launcher, event: MouseEvent) {
@change="sortLaunchers"
>
<TabItem
v-show="!isCollapsed || getTerminalTabByLauncher(launcher)"
:tab="getTerminalTabByLauncher(launcher)"
:group="createLauncherGroup(launcher)"
closable
@click="openLauncher(launcher)"
@close="closeLauncher(launcher)"
>
<template #operations>
<template v-if="!isEditing" #operations>
<div
class="button launch"
@click.stop="startLauncher(launcher)"
Expand All @@ -134,6 +177,9 @@ function showLauncherScripts(launcher: Launcher, event: MouseEvent) {
</template>
</TabItem>
</SortableList>
<div v-if="isEditing" class="new-launcher" @click="createLauncher">
<span class="feather-icon icon-plus"></span>
</div>
</div>
</template>

Expand All @@ -145,6 +191,12 @@ function showLauncherScripts(launcher: Launcher, event: MouseEvent) {
padding: 8px 16px;
line-height: 16px;
cursor: pointer;
.button {
opacity: 0.5;
&:hover {
opacity: 1;
}
}
}
.buttons {
display: flex;
Expand All @@ -156,15 +208,10 @@ function showLauncherScripts(launcher: Launcher, event: MouseEvent) {
transition: opacity 0.2s, color 0.2s;
cursor: pointer;
}
.find {
opacity: 0.5;
&:hover {
opacity: 1;
}
&.active {
color: rgb(var(--system-yellow));
opacity: 1;
}
.more.active,
.edit.active {
color: rgb(var(--system-yellow));
opacity: 1;
}
.group-name {
flex: auto;
Expand All @@ -185,12 +232,16 @@ function showLauncherScripts(launcher: Launcher, event: MouseEvent) {
transform: rotate(-90deg);
}
}
.find-launcher {
.launcher-actions {
display: flex;
flex-basis: 100%;
align-items: center;
margin-top: 8px;
}
.keyword {
width: 100%;
flex: 1;
width: 0;
margin-right: 6px;
padding: 2px 6px;
border: none;
color: inherit;
Expand All @@ -210,4 +261,17 @@ function showLauncherScripts(launcher: Launcher, event: MouseEvent) {
.launch-externally:hover {
color: rgb(var(--system-blue));
}
.new-launcher {
height: var(--tab-height);
padding: 8px 16px 0;
font-size: 21px;
line-height: var(--tab-height);
text-align: center;
opacity: 0.5;
transition: opacity 0.2s;
cursor: pointer;
&:hover {
opacity: 1;
}
}
</style>
Loading

0 comments on commit f665be1

Please sign in to comment.