diff --git a/aliases.zsh b/aliases.zsh index 52420307..174755f2 100755 --- a/aliases.zsh +++ b/aliases.zsh @@ -116,7 +116,7 @@ alias tk="trm" alias tks="tmux kill-server" alias txp="tx p" alias txa="tx c -s -l -r" -alias tls='command -v node >/dev/null || eval "$(fnm env)"; tx ls -s' +alias tls="tx ls -s" # network/ip alias ip4="curl -4 simpip.com --max-time 2 --proto-default https --silent | prepend 'ipv4: '" diff --git a/utils/package.json b/utils/package.json index f4c40fb0..e5146d58 100644 --- a/utils/package.json +++ b/utils/package.json @@ -5,12 +5,10 @@ "description": "", "main": "index.js", "bin": { - "tx": "tmux/cmd.js", "home": "home/home.js", "tblf": "tblf.js" }, "scripts": { - "tx": "ts-node src/tmux/cmd.ts", "h": "ts-node src/home/home.ts", "tblf": "ts-node src/tblf.ts", "build": "tsc && cp package.json build/", diff --git a/utils/src/tmux/attach_cmd.ts b/utils/src/tmux/attach_cmd.ts deleted file mode 100644 index 004de29b..00000000 --- a/utils/src/tmux/attach_cmd.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Opts, UserError, log, runCommand } from '../common' -import { MassargCommand } from 'massarg/command' -import { attachToSession, getTmuxConfig, parseConfig, sessionExists } from './utils' - -export const attachCmd = new MassargCommand({ - name: 'attach', - aliases: ['a'], - description: 'Attach to a tmux session', - run: async (opts) => { - const { key } = opts - - if (key) { - const allConfigs = await getTmuxConfig() - if (!allConfigs[key]) { - throw new UserError(`tmux config item '${key}' not found`) - } - const config = parseConfig(key, allConfigs[key]) - if (!(await sessionExists(opts, config.name))) { - throw new UserError(`tmux session '${config.name}' does not exist`) - } - return attachToSession(opts, config.name) - } - - if (process.env.TMUX) { - log(opts, 'Already in tmux and no key specified, not attaching') - return - } - - await runCommand(opts, `tmux attach`) - }, -}) - .option({ - name: 'key', - aliases: ['k'], - description: 'The tmux session to attach to', - isDefault: true, - }) - .help({ bindOption: true, bindCommand: true }) diff --git a/utils/src/tmux/cmd.ts b/utils/src/tmux/cmd.ts deleted file mode 100644 index cfe36f3f..00000000 --- a/utils/src/tmux/cmd.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { massarg } from 'massarg' -import { Opts } from '../common' -import { strConcat } from 'massarg/utils' -import { format } from 'massarg/style' -import { main } from './tmux' -import { createCmd } from './create_cmd' -import { listCmd } from './list_cmd' -import { showCmd } from './show_cmd' -import { editCmd } from './edit_cmd' -import { rmCmd } from './rm_cmd' -import { attachCmd } from './attach_cmd' -import { prjCmd } from './prj_cmd' - -// ================================================================================ -// Commands -// ================================================================================ -// TODO move to tmux.ts -const mainCmd = massarg({ - name: 'tmux', - description: 'Generate layouts for tmux using presets or on-the-fly args.', -}) - .main(main) - .flag({ - name: 'verbose', - aliases: ['v'], - description: 'Verbose logs', - }) - .flag({ - name: 'dry', - aliases: ['d'], - description: 'Dry run', - }) - .option({ - name: 'key', - aliases: ['k'], - description: 'The tmux session to open', - isDefault: true, - }) - .help({ - bindOption: true, - bindCommand: true, - usageText: strConcat( - [ - format('tmux', { color: 'yellow' }), - format('[options]', { color: 'gray' }), - format('[-k] ', { color: 'green' }), - ].join(' '), - [format('tmux', { color: 'yellow' }), format(' [options]', { color: 'gray' })].join( - ' ', - ), - ), - }) - -mainCmd - .command(listCmd) - .command(showCmd) - .command(editCmd) - .command(rmCmd) - .command(createCmd) - .command(attachCmd) - .command(prjCmd) - .parse() diff --git a/utils/src/tmux/command_builder.ts b/utils/src/tmux/command_builder.ts deleted file mode 100644 index 6336f7c1..00000000 --- a/utils/src/tmux/command_builder.ts +++ /dev/null @@ -1,146 +0,0 @@ -import * as path from 'node:path' -import * as fs from 'node:fs/promises' -import * as os from 'node:os' -import { Opts, UserError, log, runCommand } from '../common' -import { - ParsedTmuxConfigItem, - TmuxPaneLayout, - attachToSession, - getTmuxConfig, - getTmuxConfigFileInfo, - nameFix, - sessionExists, - transformCmdToTmuxKeys, -} from './utils' -import { CreateOpts } from './create_cmd' - -export async function createFromConfig(opts: Opts, tmuxConfig: ParsedTmuxConfigItem) { - const { root, windows: windows } = tmuxConfig - log(opts, 'Config:', tmuxConfig) - - const sessionName = nameFix(tmuxConfig.name) - - log(opts, 'Session name:', sessionName) - - if (await sessionExists(opts, sessionName)) { - log(opts, `tmux session ${sessionName} already exists, attaching...`) - await attachToSession(opts, sessionName) - return - } - - log(opts, `tmux session ${sessionName} does not exist, creating...`) - - const commands: string[] = [] - commands.push( - `tmux -f ~/.config/tmux/conf.tmux new-session -d -s ${sessionName} -n general -c ${root}; sleep 1`, - ) - - // Create all windows - for (let i = 0; i < windows.length; i++) { - const window = windows[i] - const dir = window.cwd - const windowName = window.name || nameFix(path.basename(dir)) - log(opts, 'Creating window:', windowName) - commands.push(`tmux new-window -t ${sessionName}:${i + 1} -n ${windowName} -c ${dir}`) - const paneCommands: string[] = getPaneCommands(opts, window.layout, { - rootDir: root, - windowName, - sessionName, - }) - commands.push(...paneCommands) - commands.push(`tmux clock-mode -t ${sessionName}:${windowName}`) - commands.push(`tmux select-pane -t ${sessionName}.0`) - } - - commands.push(`tmux select-window -t ${sessionName}:1`) - - for (const command of commands) { - await runCommand(opts, command) - } - - await attachToSession(opts, sessionName) -} - -function getPaneCommands( - opts: Opts, - pane: TmuxPaneLayout, - { - windowName, - sessionName, - rootDir, - }: { windowName: string; sessionName: string; rootDir: string }, -): string[] { - const commands: string[] = [] - const cmd = pane.cmd ? transformCmdToTmuxKeys(pane.cmd) : '' - if (cmd) { - log(opts, 'Sending keys:', JSON.stringify(cmd)) - commands.push(`tmux send-keys -t ${sessionName}:${windowName} ${cmd} Enter`) - } - if (pane.split) { - log( - opts, - 'Splitting pane:', - pane.split, - 'with cmd:', - cmd, - 'in session:window:', - `${sessionName}:${windowName}`, - 'direction:', - pane.split.direction || 'h', - ) - commands.push( - `tmux split-window -${pane.split.direction || 'h'} ` + - ` -t ${sessionName}:${windowName} -c ${pane.cwd || rootDir}`.trim(), - ) - - if (pane.split.child) { - log(opts, 'Handling child pane:', pane.split.child) - // commands.push(`tmux select-pane -t ${sessionName}:${windowName}.0`) - commands.push( - ...getPaneCommands(opts, pane.split.child, { - windowName, - sessionName, - rootDir, - }), - ) - } - if (pane.zoom) { - commands.push(`tmux resize-pane -t ${sessionName}:${windowName} -Z`) - } - } - return commands -} - -export async function addSimpleConfigToFile(opts: CreateOpts, config: ParsedTmuxConfigItem) { - const files = await getTmuxConfigFileInfo() - const file = opts.local ? files.local : files.global - if (!file) { - throw new UserError('tmux config file not found') - } - const { filepath } = file - const existingConfig = await getTmuxConfig() - if (existingConfig[config.name] && !opts.dry) { - throw new UserError(`tmux config item '${config.name}' already exists`) - } - - const dirFix = (dir: string) => dir.replace(config.root, './').replace(os.homedir(), '~') - - // dump config as yaml - const contents = ` -${config.name}: - root: ${config.root} - windows: -${config.windows.map((w) => ` - ${dirFix(w.cwd)}`).join('\n')} -` - if (opts.dry) { - if (existingConfig[config.name]) { - log(opts, 'Config item already exists, not saving') - } - log(opts, 'Dry run, not saving config') - log(opts, 'Would have saved config to', filepath) - log(opts, 'Contents:') - log(opts, contents) - return - } - await fs.appendFile(filepath, contents) -} diff --git a/utils/src/tmux/create_cmd.ts b/utils/src/tmux/create_cmd.ts deleted file mode 100644 index 68dedf30..00000000 --- a/utils/src/tmux/create_cmd.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as path from 'node:path' -import { Opts, log } from '../common' -import { MassargCommand } from 'massarg/command' -import { attachToSession, nameFix, parseConfig, sessionExists } from './utils' -import { addSimpleConfigToFile, createFromConfig } from './command_builder' - -export type CreateOpts = Opts & { - rootDir?: string - window?: string[] - save?: boolean - saveOnly?: boolean - local?: boolean -} - -export const createCmd = new MassargCommand({ - name: 'create', - aliases: ['c'], - description: 'Create a new tmux session (temporary)', - run: async (opts) => { - log(opts, 'Options:', opts) - const name = nameFix(path.basename(opts.rootDir ?? process.cwd())) - const config = parseConfig(name, { - name, - root: opts.rootDir ?? process.cwd(), - windows: opts.window ?? ['.'], - }) - - if (await sessionExists(opts, config.name)) { - log(opts, 'Session already exists, attaching') - return attachToSession(opts, config.name) - } - - if (opts.save || opts.saveOnly) { - addSimpleConfigToFile(opts, config) - } - if (opts.saveOnly) { - return - } - createFromConfig(opts, config) - }, -}) - .option({ - name: 'root-dir', - aliases: ['r'], - description: 'The root directory to create the tmux session in', - }) - .option({ - name: 'window', - aliases: ['w'], - description: 'Add a window with the given directory, relative to root', - array: true, - }) - .flag({ - name: 'save', - aliases: ['s'], - description: 'Save the tmux session to the config file', - }) - .flag({ - name: 'save-only', - aliases: ['S'], - description: 'Save the tmux session to the config file without creating it', - }) - .flag({ - name: 'verbose', - aliases: ['v'], - description: 'Verbose logs', - }) - .flag({ - name: 'dry', - aliases: ['d'], - description: 'Dry run', - }) - .flag({ - name: 'local', - aliases: ['l'], - description: 'Save the tmux session to the local config file', - }) - .help({ bindOption: true, bindCommand: true }) diff --git a/utils/src/tmux/edit_cmd.ts b/utils/src/tmux/edit_cmd.ts deleted file mode 100644 index 4f299eea..00000000 --- a/utils/src/tmux/edit_cmd.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MassargCommand } from 'massarg/command' -import { Opts, runCommand } from '../common' -import { getTmuxConfigFileInfo, throwNoConfigFound } from './utils' - -export const editCmd = new MassargCommand({ - name: 'edit', - aliases: ['e'], - description: 'Edit the tmux configuration file', - run: async (opts) => { - const configs = await getTmuxConfigFileInfo() - const config = opts.local ? configs.local : configs.global - if (!config) { - throwNoConfigFound() - return - } - const { filepath } = config - const editor = process.env.EDITOR || 'vim' - await runCommand(opts, `${editor} ${filepath}`) - }, -}).flag({ - name: 'local', - aliases: ['l'], - description: 'Edit the local tmux config file', -}) diff --git a/utils/src/tmux/list_cmd.ts b/utils/src/tmux/list_cmd.ts deleted file mode 100644 index a9e8897e..00000000 --- a/utils/src/tmux/list_cmd.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Opts, getCommandOutput } from '../common' -import { indent } from 'massarg/utils' -import { MassargCommand } from 'massarg/command' -import { getTmuxConfig, getTmuxConfigFileInfo, parseConfig } from './utils' - -export const listCmd = new MassargCommand({ - name: 'list', - aliases: ['ls'], - description: 'List all tmux configurations and sessions', - run: async (opts) => { - const configs = await getTmuxConfigFileInfo() - const rawConfig = await getTmuxConfig() - const config = Object.fromEntries( - Object.entries(rawConfig) - .map(([key, item]) => [key, parseConfig(key, item)]) - .sort(([a], [b]) => (a as string).localeCompare(b as string)), - ) - const keys = Object.keys(config).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) - if (opts.bare) { - console.log(keys.join('\n')) - return - } - let sessionsOutput: { output: string; code: number } | null = null - try { - sessionsOutput = await getCommandOutput(opts, 'tmux ls') - } catch (e) { - sessionsOutput = { output: '', code: 1 } - } - let sessions = sessionsOutput.output.replace(/\(created ([^)]+)\)/g, '$1') - sessions = sessions - .split('\n') - .map((line) => line.replace(/^([^:]+):/, '$1').trim()) - .join('\n') - const tbl = sessions - ? await getCommandOutput( - opts, - `echo ${JSON.stringify( - sessions, - )} | tblf -th "Name # Windows DDD MMM DD HH:MM:SS YYYY Status"`, - ) - : { output: 'No tmux sessions\n', code: 0 } - if (opts.sessions) { - console.log(tbl.output) - return - } - console.log('tmux sessions:\n') - console.log(indent(tbl.output)) - console.log('tmux config files:\n') - console.log( - ' - ' + - Object.entries(configs) - .map(([key, config]) => - config && key !== 'merged' ? key + ': ' + config.filepath : undefined, - ) - .filter(Boolean) - .join('\n - ') + - '\n', - ) - console.log('tmux configurations:\n') - console.log(' - ' + keys.join('\n - ')) - }, -}) - .flag({ - name: 'verbose', - aliases: ['v'], - description: 'Verbose logs', - }) - .flag({ - name: 'bare', - aliases: ['b'], - description: - 'Show only the tmux configuration names, without the sessions or formatting (useful for scripting)', - }) - .flag({ - name: 'sessions', - aliases: ['s'], - description: 'Show only the tmux sessions', - }) - .help({ bindOption: true, bindCommand: true }) diff --git a/utils/src/tmux/prj_cmd.ts b/utils/src/tmux/prj_cmd.ts deleted file mode 100644 index 50022ae6..00000000 --- a/utils/src/tmux/prj_cmd.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as path from 'node:path' -import * as os from 'node:os' -import * as fs from 'node:fs/promises' -import { Opts, UserError, log } from '../common' -import { MassargCommand } from 'massarg/command' -import { fzf, isDirectory, nameFix, parseConfig, pathExists } from './utils' -import { addSimpleConfigToFile, createFromConfig } from './command_builder' - -export type CreateOpts = Opts & { - name?: string - save?: boolean - local?: boolean -} - -export const prjCmd = new MassargCommand({ - name: 'prj', - aliases: ['p'], - description: 'Create a new tmux session (temporary) from project folder', - run: async (opts) => { - const devProjects = await getProjects(opts) - const output = opts.name || (await fzf(opts, devProjects, { allowCustom: true })) - if (!output) { - throw new UserError('No selection') - } - const projectDir = path.join(os.homedir(), 'Dev', output) - const exists = await pathExists(projectDir) - if (!exists) { - log(opts, `Creating dir: ${projectDir}`) - await fs.mkdir(projectDir, { recursive: true }) - } - const config = parseConfig(output, { - name: nameFix(output), - root: projectDir, - windows: ['.'], - }) - if (opts.save) { - addSimpleConfigToFile(opts, config) - } - - return createFromConfig(opts, config) - }, -}) - .option({ - name: 'name', - aliases: ['n'], - description: 'Name of the directory to open as session', - isDefault: true, - }) - .flag({ - name: 'verbose', - aliases: ['v'], - description: 'Verbose logs', - }) - .flag({ - name: 'dry', - aliases: ['d'], - description: 'Dry run', - }) - .flag({ - name: 'save', - aliases: ['s'], - description: 'Save the session in config file', - }) - .flag({ - name: 'local', - aliases: ['l'], - description: 'Save the session in local config file', - }) - .help({ bindOption: true, bindCommand: true }) - -async function getProjects(_opts: Opts) { - const devDir = path.join(os.homedir(), 'Dev') - - const devFiles = await fs.readdir(devDir) - const devProjects = [] as string[] - - for (const file of devFiles) { - if (await isDirectory(path.join(devDir, file))) { - devProjects.push(file) - } - } - return devProjects -} diff --git a/utils/src/tmux/rm_cmd.ts b/utils/src/tmux/rm_cmd.ts deleted file mode 100644 index 5ce63bcc..00000000 --- a/utils/src/tmux/rm_cmd.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as fs from 'node:fs/promises' -import { Opts, UserError, log } from '../common' -import { MassargCommand } from 'massarg/command' -import { getTmuxConfig, getTmuxConfigFileInfo } from './utils' - -export type ConfigFileOpts = Opts & { - local?: boolean -} -export const rmCmd = new MassargCommand({ - name: 'remove', - aliases: ['rm'], - description: 'Remove a tmux workspace from the config file', - run: async (opts) => { - const { key } = opts - const configFiles = await getTmuxConfigFileInfo() - const allConfigs = await getTmuxConfig() - const configFile = opts.local ? configFiles.local : configFiles.global - if (!configFile) { - throw new UserError('tmux config file not found') - } - if (!allConfigs[key]) { - throw new UserError(`tmux config item '${key}' not found`) - } - const strContents = await fs.readFile(configFile.filepath, 'utf-8') - const contents = strContents.split('\n') - const index = contents.findIndex((line) => line.startsWith(key + ':')) - log(opts, 'Index:', index) - if (index === -1) { - throw new UserError(`tmux config item '${key}' not found in config file`) - } - let endIndex = contents.slice(index + 1).findIndex((line) => line.match(/^\S/)) - log(opts, 'End index:', endIndex) - if (endIndex === -1) { - endIndex = contents.length - index - log(opts, 'End index set to end:', endIndex) - } - - const newContents = contents - .slice(0, index) - .concat(contents.slice(index + endIndex)) - .join('\n') - .trimEnd() - - log(opts, 'New contents:', newContents) - log(opts, 'Filepath:', configFile.filepath) - - await fs.writeFile(configFile.filepath, newContents) - console.log(`Removed tmux config item ${key}`) - }, -}) - .option({ - name: 'key', - aliases: ['k'], - description: 'The tmux session to remove', - isDefault: true, - required: true, - }) - .option({ - name: 'verbose', - aliases: ['v'], - description: 'Verbose logs', - }) - .flag({ - name: 'local', - aliases: ['l'], - description: 'Remove from the local tmux config file', - }) - .help({ - bindOption: true, - bindCommand: true, - }) diff --git a/utils/src/tmux/show_cmd.ts b/utils/src/tmux/show_cmd.ts deleted file mode 100644 index f84d9b85..00000000 --- a/utils/src/tmux/show_cmd.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as util from 'node:util' -import { Opts, UserError } from '../common' -import { MassargCommand } from 'massarg/command' -import { fzf, getTmuxConfig, parseConfig } from './utils' - -type ShowOpts = Opts & { json?: boolean } - -export const showCmd = new MassargCommand({ - name: 'show', - aliases: ['s'], - description: 'Show the tmux configuration file for a specific key', - run: async (opts) => { - const config = await getTmuxConfig() - let { key } = opts - if (!key) { - key = await fzf(opts, Object.keys(config)) - } - if (!config[key]) { - throw new UserError(`tmux config item '${key}' not found`) - } - const item = parseConfig(key, config[key]) - if (opts.json) { - console.log(JSON.stringify(item)) - } else { - console.log(util.inspect(item, { depth: Infinity, colors: true })) - } - }, -}) - .option({ - name: 'key', - aliases: ['k'], - description: 'The tmux session to show', - isDefault: true, - }) - .flag({ - name: 'json', - aliases: ['j'], - description: 'Output as JSON', - }) - .help({ bindOption: true, bindCommand: true }) diff --git a/utils/src/tmux/tmux.ts b/utils/src/tmux/tmux.ts deleted file mode 100644 index 4c7a5246..00000000 --- a/utils/src/tmux/tmux.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Opts, UserError } from '../common' -import { - attachToSession, - fzf, - getTmuxConfig, - getTmuxConfigFileInfo, - parseConfig, - sessionExists, -} from './utils' -import { createFromConfig } from './command_builder' - -export async function main(opts: Opts) { - let { key } = opts - if (!key) { - const { - merged: { config }, - } = await getTmuxConfigFileInfo() - const output = await fzf(opts, Object.keys(config)) - if (!output || !(output in config)) { - throw new UserError(`tmux config item '${output || '(none)'}' not found`) - } - key = output - } - const config = await getTmuxConfig() - const item = config[key] - if (!item) { - throw new UserError(`tmux config item '${key}' not found`) - } - - const parsed = parseConfig(key, item) - if (await sessionExists(opts, parsed.name)) { - return attachToSession(opts, parsed.name) - } - return createFromConfig(opts, parsed) -} diff --git a/utils/src/tmux/utils.ts b/utils/src/tmux/utils.ts deleted file mode 100644 index 87666d7f..00000000 --- a/utils/src/tmux/utils.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { CosmiconfigResult, cosmiconfig } from 'cosmiconfig' -import * as path from 'node:path' -import * as os from 'node:os' -import * as fs from 'node:fs/promises' -import { Opts, UserError, getCommandOutput, runCommand } from '../common' -import { spawn } from 'node:child_process' - -const searchDirs = [ - process.cwd(), - __dirname, - os.homedir(), - path.join(os.homedir(), '.dotfiles'), - process.env.APPDATA, -].filter(Boolean) as string[] - -function searchPatterns(name: string) { - return [`.${name}.yaml`, `.${name}.yml`, `.config/.${name}.yaml`, `.config/.${name}.yml`] -} - -const globalExplorer = cosmiconfig('tmux', { searchPlaces: searchPatterns('tmux') }) -const localExplorer = cosmiconfig('tmux_local', { searchPlaces: searchPatterns('tmux_local') }) - -export type TmuxConfigItemInput = { - root: string - name: string - blank_window?: boolean - windows: TmuxWindowInput[] -} - -export type TmuxWindowInput = string | TmuxWindow - -export type TmuxWindow = { - name: string - cwd: string - layout?: TmuxLayoutInput -} - -export type TmuxLayoutInput = string | string[] | TmuxPaneLayout - -export type TmuxPane = { - dir: string - cmd?: string -} - -export type ConfigFile = Record - -export type ParsedTmuxConfigItem = Omit & { - windows: ParsedTmuxWindow[] -} - -export type ParsedTmuxWindow = Omit & { layout: TmuxPaneLayout } - -export type TmuxWindowLayout = { - name: string - cwd: string - panes: TmuxPaneLayout[] -} - -type TmuxSplitLayout = { - direction: 'h' | 'v' - child: TmuxPaneLayout -} - -export type TmuxPaneLayout = { - cwd: string - cmd?: string - zoom?: boolean - split?: TmuxSplitLayout -} - -const defaultEmptyPane: TmuxPaneLayout = { - cwd: '.', - cmd: '', -} - -const defaultEmptyLayout: TmuxPaneLayout = { - ...defaultEmptyPane, - zoom: false, - split: { - direction: 'h', - child: { - cwd: '.', - split: { - direction: 'v', - child: { - cwd: '.', - }, - }, - }, - }, -} - -export function transformCmdToTmuxKeys(cmd: string): string { - if (!cmd.trim()) return '' - let string = '' - const map: Record = { - ' ': 'Space', - '\n': 'Enter', - } - for (const letter of cmd.split('')) { - string += map[letter] ? ` ${map[letter]} ` : letter - } - return string.toString() -} - -export function parseConfig(key: string, item: TmuxConfigItemInput): ParsedTmuxConfigItem { - const dirFix = (dir: string) => dir.replace('~', os.homedir()) - const root = dirFix(item.root) - const name = item.name || key || path.basename(root) - const _windows = item.windows || [] - if (!_windows.length || item.blank_window) { - _windows.unshift({ ...defaultEmptyLayout, name: name, cwd: root }) - } - const windows = _windows.map((w): ParsedTmuxWindow => { - if (typeof w === 'string') { - return { - name: nameFix(path.basename(path.resolve(root, w))), - cwd: dirFix(path.resolve(root, w)), - layout: { - ...parseLayout(defaultEmptyLayout, dirFix(path.resolve(root, w))), - cwd: dirFix(path.resolve(root, w)), - }, - } - } - return { - name: w.name || nameFix(dirFix(path.basename(path.resolve(root, w.cwd)))), - cwd: dirFix(path.resolve(root, w.cwd)), - layout: parseLayout(w.layout, dirFix(path.resolve(root, w.cwd))), - } - }) - const tmuxConfig = { - name, - root, - windows, - } - return tmuxConfig -} - -export function nameFix(name: string) { - return (name || '').includes('.') ? name.split('.').filter(Boolean)[0] : name -} - -export type ConfigType = 'local' | 'global' | 'merged' - -function mergeConfigs(...configs: ConfigFile[]): ConfigFile { - const out: ConfigFile = {} - for (const config of configs) { - for (const key in config) { - if (!out[key]) { - out[key] = config[key] - } else { - out[key] = { - ...out[key], - ...config[key], - } - } - } - } - return out -} - -export async function getTmuxConfigFileInfo(): Promise< - Record, CosmiconfigResult> & { - merged: NonNullable - } -> { - const out: Record<'local' | 'global', CosmiconfigResult> = { - local: null, - global: null, - } - - for (const dir of searchDirs) { - const result = await globalExplorer.search(dir) - if (result) { - out.global = result - break - } - } - - for (const dir of searchDirs) { - const result = await localExplorer.search(dir) - if (result) { - out.local = result - break - } - } - - const merged = mergeConfigs(out.global?.config, out.local?.config) - if (!out.global && !out.local) { - throwNoConfigFound() - } - return { - ...out, - merged: { - config: merged, - filepath: 'merged', - }, - } - // return results -} - -export function throwNoConfigFound(): never { - throw new UserError( - [ - 'tmux config file not found, searched in:', - '\t' + - searchDirs - .map((x) => - searchPatterns('tmux') - .map((y) => path.join(x, y)) - .join('\n\t'), - ) - .join('\n\t'), - '\t' + - searchDirs - .map((x) => - searchPatterns('tmux_local') - .map((y) => path.join(x, y)) - .join('\n\t'), - ) - .join('\n\t'), - ].join('\n'), - ) -} - -export async function getTmuxConfig(): Promise { - const files = await getTmuxConfigFileInfo() - return files.merged.config -} - -export async function sessionExists(opts: Opts, sessionName: string): Promise { - try { - const { code } = await getCommandOutput( - { ...opts, dry: false }, - `tmux has-session -t ${sessionName}`, - ) - return code === 0 - } catch (error) { - return false - } -} - -export type FzfOptions = { - allowCustom?: boolean -} - -export async function fzf( - _opts: Opts, - inputs: string[], - fzfOpts: FzfOptions = {}, -): Promise { - let cmd = `echo "${inputs.join('\n')}" | fzf` - if (fzfOpts.allowCustom) { - cmd += ' --print-query | tail -1' - } - const fzf = spawn(cmd, { - stdio: ['inherit', 'pipe', 'inherit'], - shell: true, - }) - - fzf.stdout.setEncoding('utf-8') - - return new Promise((resolve, reject) => { - fzf.stdout.on('readable', function () { - const value = fzf.stdout.read() - - if (value !== null) { - resolve(value.toString().trim()) - return - } - reject(new UserError('Selection cancelled')) - }) - - fzf.on('exit', (code) => { - if (code === 1) { - reject(new UserError('Selection cancelled')) - } - }) - }) -} - -export async function attachToSession(opts: Opts, sessionName: string): Promise { - if (process.env.TMUX) { - await runCommand(opts, `tmux switch-client -t ${sessionName}`) - return - } - await runCommand(opts, `tmux attach -t ${sessionName}`) - return -} - -function parseLayout(layoutInput: TmuxLayoutInput | undefined, root: string): TmuxPaneLayout { - const layout = layoutInput as TmuxPaneLayout - if (!layout) { - return { - ...defaultEmptyLayout, - cwd: path.resolve(root, '.'), - } - } - if (typeof layoutInput === 'string') { - return { - ...defaultEmptyPane, - cwd: path.resolve(root, layoutInput), - } - } - if (Array.isArray(layoutInput)) { - return { - ...parseLayout(defaultEmptyLayout, root), - split: layoutInput.reduceRight( - (acc, cwd) => { - return { - direction: 'h', - child: { - cwd: path.resolve(root, cwd), - split: acc, - }, - } - }, - undefined as unknown as TmuxSplitLayout, - ), - } - } - return { - cwd: path.resolve(root, layout.cwd), - cmd: layout.cmd, - zoom: layout.zoom, - split: layout.split - ? ({ - direction: - typeof layout.split === 'string' ? layout.split : layout.split.direction || 'h', - child: parseLayout(layout.split.child, path.resolve(root, layout.cwd)), - } as TmuxSplitLayout) - : undefined, - } -} - -export async function pathExists(path: string) { - return fs.stat(path).catch(() => false) -} - -export async function isDirectory(path: string) { - return fs - .stat(path) - .then((stat) => stat.isDirectory()) - .catch(() => false) -} - -export async function isFile(path: string) { - return fs - .stat(path) - .then((stat) => stat.isFile()) - .catch(() => false) -}