diff --git a/README.md b/README.md index da54e0e..0387693 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ npm install -g tmux-tui tmuxtui ``` -Keyboard shortcuts: +Keyboard shortcuts (all customizable via config file): | Key | Action | |-----|--------| @@ -56,10 +56,57 @@ Keyboard shortcuts: | `x` | Detach session | | `d` | Kill session (with confirm) | | `c` | Config — window management | +| `i` | Session details (windows & panes) | +| `/` | Search sessions | +| `s` | Toggle favorite | +| `f` | Filter favorites only | +| `R` | Refresh session list | +| `Tab` | Mark / unmark session | +| `X` | Batch detach marked sessions | +| `D` | Batch kill marked sessions | +| `h` | Help | | `q` | Quit | When creating a new session, press `Tab` to switch between the name and path fields. +### Configuration + +Create `~/.config/tmuxtui/config.json` to customize behavior: + +```json +{ + "defaultSort": "lastAttached", + "confirmBeforeKill": true, + "autoAttachOnCreate": false, + "refreshInterval": 0, + "keybindings": { + "quit": "escape", + "new": "n", + "kill": "d" + }, + "ui": { + "showPath": true, + "showSessionId": false, + "sessionNameWidth": 20 + } +} +``` + +#### Config options + +| Option | Default | Description | +|--------|---------|-------------| +| `defaultSort` | `"lastAttached"` | Sort mode: `"lastAttached"`, `"name"`, or `"created"` | +| `confirmBeforeKill` | `true` | Show confirmation before killing sessions | +| `autoAttachOnCreate` | `false` | Auto-attach after creating a new session | +| `refreshInterval` | `0` | Auto-refresh interval in seconds (0 = disabled) | +| `ui.showPath` | `true` | Show session working directory | +| `ui.showSessionId` | `false` | Show tmux session ID | +| `ui.sessionNameWidth` | `20` | Session name column width (10-60) | +| `keybindings.*` | see defaults | Remap any keyboard shortcut | + +All keybinding names: `select-up`, `select-down`, `attach`, `new`, `rename`, `kill`, `detach`, `search`, `favorite`, `favorites-filter`, `refresh`, `help`, `quit`, `batch-mark`, `batch-detach`, `batch-kill`, `detail`, `config`. + ### Config mode (window management) Press `c` on any session to enter config mode. This lets you manage windows within the selected session. diff --git a/README_zh.md b/README_zh.md index 10b9917..5400c54 100644 --- a/README_zh.md +++ b/README_zh.md @@ -45,7 +45,7 @@ npm install -g tmux-tui tmuxtui ``` -快捷键: +快捷键(全部可通过配置文件自定义): | 按键 | 操作 | |------|------| @@ -56,10 +56,57 @@ tmuxtui | `x` | 分离会话 | | `d` | 销毁会话(需确认) | | `c` | 配置 — 窗口管理 | +| `i` | 查看会话详情(窗口和 pane) | +| `/` | 搜索会话 | +| `s` | 收藏/取消收藏 | +| `f` | 仅显示收藏会话 | +| `R` | 刷新会话列表 | +| `Tab` | 标记/取消标记会话 | +| `X` | 批量分离已标记会话 | +| `D` | 批量销毁已标记会话 | +| `h` | 帮助 | | `q` | 退出 | 创建新会话时,按 `Tab` 在名称和路径输入框之间切换。 +### 配置文件 + +创建 `~/.config/tmuxtui/config.json` 自定义行为: + +```json +{ + "defaultSort": "lastAttached", + "confirmBeforeKill": true, + "autoAttachOnCreate": false, + "refreshInterval": 0, + "keybindings": { + "quit": "escape", + "new": "n", + "kill": "d" + }, + "ui": { + "showPath": true, + "showSessionId": false, + "sessionNameWidth": 20 + } +} +``` + +#### 配置项说明 + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| `defaultSort` | `"lastAttached"` | 排序方式:`"lastAttached"`、`"name"` 或 `"created"` | +| `confirmBeforeKill` | `true` | 销毁会话前是否需要确认 | +| `autoAttachOnCreate` | `false` | 创建会话后是否自动 attach | +| `refreshInterval` | `0` | 自动刷新间隔(秒),0 = 关闭 | +| `ui.showPath` | `true` | 显示会话工作目录 | +| `ui.showSessionId` | `false` | 显示 tmux 会话 ID | +| `ui.sessionNameWidth` | `20` | 会话名列宽度(10-60) | +| `keybindings.*` | 见默认值 | 自定义任意快捷键 | + +所有可配置的键绑定名称:`select-up`、`select-down`、`attach`、`new`、`rename`、`kill`、`detach`、`search`、`favorite`、`favorites-filter`、`refresh`、`help`、`quit`、`batch-mark`、`batch-detach`、`batch-kill`、`detail`、`config`。 + ### 配置模式(窗口管理) 在任意会话上按 `c` 进入配置模式,管理该会话下的窗口。 diff --git a/package.json b/package.json index e13829c..f190800 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tmux-tui", - "version": "1.3.3", + "version": "1.4.0", "description": "Terminal UI for tmux session management", "type": "module", "bin": { diff --git a/src/components/App.tsx b/src/components/App.tsx index a77e259..1e59f19 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2,6 +2,8 @@ import React, { useState } from 'react'; import { Box, Text, useApp, useInput } from 'ink'; import { listSessions, listWindows, newWindow, renameWindow, killWindow, initPanes, PANE_LAYOUTS, formatTime, getSessionDetail, moveWindow } from '../services/tmuxService.js'; import { loadFavorites, toggleFavorite } from '../services/favoritesService.js'; +import { loadConfig, matchesKey, sortSessions } from '../services/configService.js'; +import type { ResolvedConfig } from '../services/configService.js'; import type { TmuxSession, TmuxWindow } from '../types.js'; type Mode = 'list' | 'new' | 'rename' | 'confirm-kill' | 'config' | 'search' | 'detail' | 'confirm-batch-kill' | 'help'; @@ -9,6 +11,7 @@ type ConfigSubMode = 'list' | 'new' | 'rename' | 'confirm-delete' | 'init-panes' interface SessionViewProps { interactive: boolean; + config: ResolvedConfig; onSelect?: (session: TmuxSession) => void; onCreate?: (name: string, path: string) => void; onKill?: (name: string) => void; @@ -16,8 +19,9 @@ interface SessionViewProps { onDetach?: (name: string) => void; } -function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, onRename, onDetach }: SessionViewProps & { favoritesOnly?: boolean }) { +function SessionView({ interactive, config, favoritesOnly, onSelect, onCreate, onKill, onRename, onDetach }: SessionViewProps & { favoritesOnly?: boolean }) { const { exit } = useApp(); + const cfg = config; function fuzzyMatch(query: string, text: string): boolean { const q = query.toLowerCase(); @@ -28,7 +32,7 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o } return qi === q.length; } - const [sessions, setSessions] = useState(listSessions); + const [sessions, setSessions] = useState(() => sortSessions(listSessions(), cfg.defaultSort)); const [selected, setSelected] = useState(0); const [mode, setMode] = useState('list'); const [inputValue, setinputValue] = useState(''); @@ -63,7 +67,7 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o const [marked, setMarked] = useState>(new Set()); const refresh = () => { - const updated = listSessions(); + const updated = sortSessions(listSessions(), cfg.defaultSort); setSessions(updated); setSelected((i) => Math.min(i, Math.max(0, updated.length - 1))); }; @@ -101,7 +105,15 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o } const expandedPath = (inputValue2 || process.cwd()).replace(/^~/, process.env.HOME || '~'); onCreate?.(inputValue.trim(), expandedPath); - exit(); + if (cfg.autoAttachOnCreate) { + exit(); + } else { + refresh(); + setMode('list'); + setinputValue(''); + setinputValue2(''); + setError(''); + } return; } if (key.backspace || key.delete) { @@ -438,18 +450,19 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o } // ── list mode ── - if (input === 'q') { exit(); return; } - if (input === 'R') { refresh(); return; } - if (input === 's' && displayedSessions.length > 0) { + const bindings = cfg.keybindings; + if (matchesKey(bindings['quit'], input, key)) { exit(); return; } + if (matchesKey(bindings['refresh'], input, key)) { refresh(); return; } + if (matchesKey(bindings['favorite'], input, key) && displayedSessions.length > 0) { setFavorites(toggleFavorite(displayedSessions[selected].name, favorites)); return; } - if (input === 'f') { + if (matchesKey(bindings['favorites-filter'], input, key)) { setShowFavoritesOnly((v) => !v); setSelected(0); return; } - if (key.tab && displayedSessions.length > 0) { + if (matchesKey(bindings['batch-mark'], input, key) && displayedSessions.length > 0) { setMarked((prev) => { const next = new Set(prev); const name = displayedSessions[selected].name; @@ -459,23 +472,29 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o setSelected((i) => (i + 1) % displayedSessions.length); return; } - if (input === 'X' && marked.size > 0) { + if (matchesKey(bindings['batch-detach'], input, key) && marked.size > 0) { for (const name of marked) { onDetach?.(name); } setMarked(new Set()); refresh(); return; } - if (input === 'D' && marked.size > 0) { - setMode('confirm-batch-kill'); + if (matchesKey(bindings['batch-kill'], input, key) && marked.size > 0) { + if (cfg.confirmBeforeKill) { + setMode('confirm-batch-kill'); + } else { + for (const name of marked) { onKill?.(name); } + setMarked(new Set()); + refresh(); + } return; } - if (input === '/') { + if (matchesKey(bindings['search'], input, key)) { setMode('search'); setSearchQuery(''); setSelected(0); return; } - if (input === 'i' && displayedSessions.length > 0) { + if (matchesKey(bindings['detail'], input, key) && displayedSessions.length > 0) { const s = displayedSessions[selected]; setDetailSession(s); setDetailWindows(getSessionDetail(s.name)); @@ -483,11 +502,11 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o setMode('detail'); return; } - if (input === 'h') { + if (matchesKey(bindings['help'], input, key)) { setMode('help'); return; } - if (input === 'n') { + if (matchesKey(bindings['new'], input, key)) { setMode('new'); setFocusField('name'); setinputValue(''); @@ -495,22 +514,27 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o setError(''); return; } - if (input === 'r' && displayedSessions.length > 0) { + if (matchesKey(bindings['rename'], input, key) && displayedSessions.length > 0) { setMode('rename'); setinputValue(displayedSessions[selected].name); setError(''); return; } - if (input === 'x' && displayedSessions.length > 0) { + if (matchesKey(bindings['detach'], input, key) && displayedSessions.length > 0) { onDetach?.(displayedSessions[selected].name); refresh(); return; } - if (input === 'd' && displayedSessions.length > 0) { - setMode('confirm-kill'); + if (matchesKey(bindings['kill'], input, key) && displayedSessions.length > 0) { + if (cfg.confirmBeforeKill) { + setMode('confirm-kill'); + } else { + onKill?.(displayedSessions[selected].name); + refresh(); + } return; } - if (input === 'c' && displayedSessions.length > 0) { + if (matchesKey(bindings['config'], input, key) && displayedSessions.length > 0) { const s = displayedSessions[selected]; setConfigSession(s); setConfigWindows(listWindows(s.name)); @@ -520,11 +544,11 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o setMode('config'); return; } - if (key.upArrow) { + if (matchesKey(bindings['select-up'], input, key)) { setSelected((i) => (i - 1 + displayedSessions.length) % displayedSessions.length); - } else if (key.downArrow) { + } else if (matchesKey(bindings['select-down'], input, key)) { setSelected((i) => (i + 1) % displayedSessions.length); - } else if (key.return && displayedSessions.length > 0) { + } else if (matchesKey(bindings['attach'], input, key) && displayedSessions.length > 0) { onSelect?.(displayedSessions[selected]); exit(); } @@ -639,7 +663,7 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o {' '} - {'NAME'.padEnd(22)} + {'NAME'.padEnd(cfg.ui.sessionNameWidth + 2)} {'WINS'.padEnd(7)} {'STATUS'.padEnd(8)} {' LAST USED'} @@ -649,7 +673,7 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o {i === displaySelected ? '▸ ' : ' '} - {(s.name.length > 20 ? s.name.slice(0, 17) + '...' : s.name).padEnd(22)} + {(s.name.length > cfg.ui.sessionNameWidth ? s.name.slice(0, cfg.ui.sessionNameWidth - 3) + '...' : s.name).padEnd(cfg.ui.sessionNameWidth + 2)} {`${s.windows} win${s.windows !== 1 ? 's' : ''}`.padEnd(7)} @@ -721,6 +745,7 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o // ── Render: help ── if (mode === 'help') { + const kb = cfg.keybindings; return ( @@ -732,33 +757,33 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o NAVIGATION - {' '}↑↓ Select session + {' '}{kb['select-up']}/{kb['select-down']} Select session SESSION MANAGEMENT - {' '} Attach to selected session - {' '}n Create new session - {' '}r Rename selected session - {' '}x Detach selected session - {' '}d Delete selected session + {' '}{kb['attach']} Attach to selected session + {' '}{kb['new']} Create new session + {' '}{kb['rename']} Rename selected session + {' '}{kb['detach']} Detach selected session + {' '}{kb['kill']} Delete selected session SEARCH & FILTER - {' '}/ Search sessions by name - {' '}s Toggle favorite on selected session - {' '}f Filter list to favorites only + {' '}{kb['search']} Search sessions by name + {' '}{kb['favorite']} Toggle favorite on selected session + {' '}{kb['favorites-filter']} Filter list to favorites only ADVANCED - {' '}c Config — manage windows for selected session - {' '}i Show session details (windows & panes) - {' '}R Refresh session list + {' '}{kb['config']} Config — manage windows for selected session + {' '}{kb['detail']} Show session details (windows & panes) + {' '}{kb['refresh']} Refresh session list BATCH OPERATIONS - {' '}Tab Mark / unmark session - {' '}X Batch detach all marked sessions - {' '}D Batch delete all marked sessions + {' '}{kb['batch-mark']} Mark / unmark session + {' '}{kb['batch-detach']} Batch detach all marked sessions + {' '}{kb['batch-kill']} Batch delete all marked sessions OTHER - {' '}q Quit tmuxtui - {' '}h Show this help screen + {' '}{kb['quit']} Quit tmuxtui + {' '}{kb['help']} Show this help screen PERSISTENCE {' Sessions live in tmux server memory and do NOT survive reboots.'} @@ -965,7 +990,7 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o {' '} - {'NAME'.padEnd(22)} + {'NAME'.padEnd(cfg.ui.sessionNameWidth + 2)} {'WINS'.padEnd(7)} {'STATUS'.padEnd(8)} {' LAST USED'} @@ -977,15 +1002,16 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o {favorites.has(s.name) ? '★' : ' '} {i === safeSelected ? '▸' : ' '} - {(s.name.length > 20 ? s.name.slice(0, 17) + '...' : s.name).padEnd(22)} + {(s.name.length > cfg.ui.sessionNameWidth ? s.name.slice(0, cfg.ui.sessionNameWidth - 3) + '...' : s.name).padEnd(cfg.ui.sessionNameWidth + 2)} {`${s.windows} win${s.windows !== 1 ? 's' : ''}`.padEnd(7)} {s.attached ? 'attached' : ' '} {' '}{formatTime(s.lastAttached)} + {cfg.ui.showSessionId && {' '}{s.sessionId}} - {s.path && ( + {s.path && cfg.ui.showPath && ( {' '}{s.path.replace(process.env.HOME || '', '~')} )} @@ -996,26 +1022,26 @@ function SessionView({ interactive, favoritesOnly, onSelect, onCreate, onKill, o {interactive && ( - ↑↓ select | ↵ attach | - n new | - r rename | - c config | - q quit | - h help + {cfg.keybindings['select-up']}/{cfg.keybindings['select-down']} select | {cfg.keybindings['attach']} attach | + {cfg.keybindings['new']} new | + {cfg.keybindings['rename']} rename | + {cfg.keybindings['config']} config | + {cfg.keybindings['quit']} quit | + {cfg.keybindings['help']} help - / search | - s star | - f filter | - R refresh | - x detach | - d delete | - i info + {cfg.keybindings['search']} search | + {cfg.keybindings['favorite']} star | + {cfg.keybindings['favorites-filter']} filter | + {cfg.keybindings['refresh']} refresh | + {cfg.keybindings['detach']} detach | + {cfg.keybindings['kill']} delete | + {cfg.keybindings['detail']} info - Tab mark | - X batch detach | - D batch kill + {cfg.keybindings['batch-mark']} mark | + {cfg.keybindings['batch-detach']} batch detach | + {cfg.keybindings['batch-kill']} batch kill )} @@ -1030,6 +1056,7 @@ export default function App({ onRename, onDetach, favoritesOnly, + config, }: { onSelect?: (session: TmuxSession) => void; onCreate?: (name: string, path: string) => void; @@ -1037,10 +1064,13 @@ export default function App({ onRename?: (oldName: string, newName: string) => void; onDetach?: (name: string) => void; favoritesOnly?: boolean; + config?: ResolvedConfig; }) { + const resolvedConfig = config ?? loadConfig(); return ( a !== '--favorites' && a !== '-F'); @@ -155,6 +158,7 @@ const instance = render( onRename: (oldName: string, newName: string) => { renameSession(oldName, newName); }, onDetach: (name: string) => { detachSession(name); }, favoritesOnly, + config, }), ); @@ -163,7 +167,7 @@ await instance.waitUntilExit(); try { if (state.session) { attachToSession(state.session.name); - } else if (state.newSession) { + } else if (state.newSession && config.autoAttachOnCreate) { attachToSession(state.newSession); } } catch { diff --git a/src/services/configService.ts b/src/services/configService.ts new file mode 100644 index 0000000..d2d6e63 --- /dev/null +++ b/src/services/configService.ts @@ -0,0 +1,173 @@ +import { readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { homedir } from 'os'; +import type { TmuxSession } from '../types.js'; + +const CONFIG_DIR = join(homedir(), '.config', 'tmuxtui'); +const FILE = join(CONFIG_DIR, 'config.json'); + +type SortMode = 'lastAttached' | 'name' | 'created'; +type KeyAction = + | 'select-up' | 'select-down' + | 'attach' | 'new' | 'rename' | 'kill' + | 'detach' | 'search' | 'favorite' + | 'favorites-filter' | 'refresh' | 'help' | 'quit' + | 'batch-mark' | 'batch-detach' | 'batch-kill' + | 'detail' | 'config'; + +export interface UserConfig { + defaultSort?: SortMode; + confirmBeforeKill?: boolean; + autoAttachOnCreate?: boolean; + refreshInterval?: number; + keybindings?: Partial>; + ui?: { + showPath?: boolean; + showSessionId?: boolean; + sessionNameWidth?: number; + }; +} + +const DEFAULT_KEYBINDINGS: Record = { + 'select-up': 'up', + 'select-down': 'down', + 'attach': 'return', + 'new': 'n', + 'rename': 'r', + 'kill': 'd', + 'detach': 'x', + 'search': '/', + 'favorite': 's', + 'favorites-filter': 'f', + 'refresh': 'R', + 'help': 'h', + 'quit': 'q', + 'batch-mark': 'tab', + 'batch-detach': 'X', + 'batch-kill': 'D', + 'detail': 'i', + 'config': 'c', +}; + +export type ResolvedConfig = { + defaultSort: SortMode; + confirmBeforeKill: boolean; + autoAttachOnCreate: boolean; + refreshInterval: number; + keybindings: Record; + ui: { + showPath: boolean; + showSessionId: boolean; + sessionNameWidth: number; + }; +}; + +const DEFAULT_CONFIG: ResolvedConfig = { + defaultSort: 'lastAttached', + confirmBeforeKill: true, + autoAttachOnCreate: false, + refreshInterval: 0, + keybindings: DEFAULT_KEYBINDINGS, + ui: { + showPath: true, + showSessionId: false, + sessionNameWidth: 20, + }, +}; + +const VALID_SORTS = new Set(['lastAttached', 'name', 'created']); + +function validateConfig(raw: unknown): Partial { + if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}; + const obj = raw as Record; + const result: Partial = {}; + + if (VALID_SORTS.has(obj.defaultSort as string)) { + result.defaultSort = obj.defaultSort as SortMode; + } + if (typeof obj.confirmBeforeKill === 'boolean') { + result.confirmBeforeKill = obj.confirmBeforeKill; + } + if (typeof obj.autoAttachOnCreate === 'boolean') { + result.autoAttachOnCreate = obj.autoAttachOnCreate; + } + if (typeof obj.refreshInterval === 'number' && obj.refreshInterval >= 0) { + result.refreshInterval = Math.min(obj.refreshInterval, 300); + } + + if (typeof obj.keybindings === 'object' && obj.keybindings !== null && !Array.isArray(obj.keybindings)) { + const kb: Partial> = {}; + for (const [k, v] of Object.entries(obj.keybindings)) { + if (typeof v === 'string') { + kb[k as KeyAction] = v; + } + } + result.keybindings = kb; + } + + if (typeof obj.ui === 'object' && obj.ui !== null && !Array.isArray(obj.ui)) { + const uiObj = obj.ui as Record; + const ui: UserConfig['ui'] = {}; + if (typeof uiObj.showPath === 'boolean') ui.showPath = uiObj.showPath; + if (typeof uiObj.showSessionId === 'boolean') ui.showSessionId = uiObj.showSessionId; + if (typeof uiObj.sessionNameWidth === 'number') { + ui.sessionNameWidth = Math.max(10, Math.min(60, Math.round(uiObj.sessionNameWidth))); + } + result.ui = ui; + } + + return result; +} + +function mergeConfig(partial: Partial): ResolvedConfig { + return { + defaultSort: partial.defaultSort ?? DEFAULT_CONFIG.defaultSort, + confirmBeforeKill: partial.confirmBeforeKill ?? DEFAULT_CONFIG.confirmBeforeKill, + autoAttachOnCreate: partial.autoAttachOnCreate ?? DEFAULT_CONFIG.autoAttachOnCreate, + refreshInterval: partial.refreshInterval ?? DEFAULT_CONFIG.refreshInterval, + keybindings: { ...DEFAULT_KEYBINDINGS, ...(partial.keybindings ?? {}) }, + ui: { ...DEFAULT_CONFIG.ui, ...(partial.ui ?? {}) }, + }; +} + +export function loadConfig(): ResolvedConfig { + try { + const raw = JSON.parse(readFileSync(FILE, 'utf-8')); + const partial = validateConfig(raw); + return mergeConfig(partial); + } catch { + return { ...DEFAULT_CONFIG, keybindings: { ...DEFAULT_KEYBINDINGS }, ui: { ...DEFAULT_CONFIG.ui } }; + } +} + +export function saveConfig(config: ResolvedConfig): void { + mkdirSync(CONFIG_DIR, { recursive: true }); + writeFileSync(FILE, JSON.stringify(config, null, 2)); +} + +const SPECIAL_KEYS: Record = { + up: 'upArrow', + down: 'downArrow', + return: 'return', + escape: 'escape', + tab: 'tab', + backspace: 'backspace', + delete: 'delete', +}; + +export function matchesKey(binding: string, input: string, key: Record): boolean { + const inkProp = SPECIAL_KEYS[binding]; + if (inkProp) return !!key[inkProp]; + return input === binding; +} + +export function sortSessions(sessions: TmuxSession[], sortBy: SortMode): TmuxSession[] { + return [...sessions].sort((a, b) => { + if (a.attached !== b.attached) return a.attached ? -1 : 1; + switch (sortBy) { + case 'name': return a.name.localeCompare(b.name); + case 'created': return b.created - a.created; + default: return b.lastAttached - a.lastAttached; + } + }); +} diff --git a/src/services/tmuxService.ts b/src/services/tmuxService.ts index 10087c6..07bfaa1 100644 --- a/src/services/tmuxService.ts +++ b/src/services/tmuxService.ts @@ -25,10 +25,7 @@ export function listSessions(): TmuxSession[] { }; }); - return sessions.sort((a, b) => { - if (a.attached !== b.attached) return a.attached ? -1 : 1; - return b.lastAttached - a.lastAttached; - }); + return sessions; } catch { return []; }