feat(tx): remove old tx code

This commit is contained in:
2026-01-29 11:51:37 +02:00
parent 9e96893ee1
commit a237d898d2
13 changed files with 1 additions and 1011 deletions

View File

@@ -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: '"

View File

@@ -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/",

View File

@@ -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<Opts>({
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 })

View File

@@ -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<Opts>({
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] <tmux session name>', { color: 'green' }),
].join(' '),
[format('tmux', { color: 'yellow' }), format('<command> [options]', { color: 'gray' })].join(
' ',
),
),
})
mainCmd
.command(listCmd)
.command(showCmd)
.command(editCmd)
.command(rmCmd)
.command(createCmd)
.command(attachCmd)
.command(prjCmd)
.parse()

View File

@@ -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)
}

View File

@@ -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<CreateOpts>({
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 })

View File

@@ -1,24 +0,0 @@
import { MassargCommand } from 'massarg/command'
import { Opts, runCommand } from '../common'
import { getTmuxConfigFileInfo, throwNoConfigFound } from './utils'
export const editCmd = new MassargCommand<Opts & { local?: boolean }>({
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',
})

View File

@@ -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<Opts & { bare?: boolean; sessions?: boolean }>({
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 })

View File

@@ -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<CreateOpts>({
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
}

View File

@@ -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<ConfigFileOpts>({
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,
})

View File

@@ -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<ShowOpts>({
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 })

View File

@@ -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)
}

View File

@@ -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<string, TmuxConfigItemInput>
export type ParsedTmuxConfigItem = Omit<TmuxConfigItemInput, 'windows'> & {
windows: ParsedTmuxWindow[]
}
export type ParsedTmuxWindow = Omit<TmuxWindow, 'layout'> & { 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<string, string> = {
' ': '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<Exclude<ConfigType, 'merged'>, CosmiconfigResult> & {
merged: NonNullable<CosmiconfigResult>
}
> {
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<ConfigFile> {
const files = await getTmuxConfigFileInfo()
return files.merged.config
}
export async function sessionExists(opts: Opts, sessionName: string): Promise<boolean> {
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<string> {
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<void> {
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)
}