mirror of
https://github.com/chenasraf/dotfiles.git
synced 2026-05-17 17:28:07 +00:00
feat(tx): remove old tx code
This commit is contained in:
@@ -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: '"
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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 })
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 })
|
||||
@@ -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',
|
||||
})
|
||||
@@ -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 })
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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 })
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user