mirror of
https://github.com/chenasraf/tx.git
synced 2026-05-18 09:39:05 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e6ded3040 | ||
| 627fde8b01 | |||
|
|
53c0c4654b | ||
| 54fa59be46 | |||
|
|
022b4de720 | ||
| 7a9ea1b41d | |||
|
|
779940ea3d | ||
| d067a6e964 | |||
|
|
c5e21a5897 | ||
| 296e13549e |
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,5 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## [2.4.1](https://github.com/chenasraf/tx/compare/v2.4.0...v2.4.1) (2026-04-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use -B for background flag to prevent flags conflict ([627fde8](https://github.com/chenasraf/tx/commit/627fde8b01f685ff8ba0705fd2735b89fd92b356))
|
||||
|
||||
## [2.4.0](https://github.com/chenasraf/tx/compare/v2.3.0...v2.4.0) (2026-04-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add background flag ([54fa59b](https://github.com/chenasraf/tx/commit/54fa59be464c226e9da0510b8d2a290023782d4a))
|
||||
|
||||
## [2.3.0](https://github.com/chenasraf/tx/compare/v2.2.0...v2.3.0) (2026-04-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ls sessions as table ([7a9ea1b](https://github.com/chenasraf/tx/commit/7a9ea1b41d1fff87cbd160409453502b56dad29d))
|
||||
|
||||
## [2.2.0](https://github.com/chenasraf/tx/compare/v2.1.0...v2.2.0) (2026-03-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow pane size config ([d067a6e](https://github.com/chenasraf/tx/commit/d067a6e964f3ccd6c7e1cf322aa3997e943381c5))
|
||||
|
||||
## [2.1.0](https://github.com/chenasraf/tx/compare/v2.0.0...v2.1.0) (2026-03-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* accept multiple session names for rm/kill ([296e135](https://github.com/chenasraf/tx/commit/296e13549e2e962c4b2287f214ca03ecc9359d85))
|
||||
|
||||
## [2.0.0](https://github.com/chenasraf/tx/compare/v1.5.0...v2.0.0) (2026-03-20)
|
||||
|
||||
|
||||
|
||||
95
README.md
95
README.md
@@ -82,24 +82,32 @@ tx create -S # save only (don't create)
|
||||
tx prj [name]
|
||||
tx prj -s # save to config
|
||||
|
||||
# Create session in background (don't switch to it)
|
||||
tx -b my-session
|
||||
tx create -b
|
||||
tx prj -b myproject
|
||||
|
||||
# Attach to existing session
|
||||
tx attach [name]
|
||||
|
||||
# Remove a configuration
|
||||
# Remove configurations
|
||||
tx rm <name>
|
||||
tx rm foo bar baz # remove multiple at once
|
||||
tx rm <name> -c ~/local.yaml # remove from a specific config file
|
||||
|
||||
# Kill a running session
|
||||
# Kill running sessions
|
||||
tx kill # kill current session
|
||||
tx kill <name> # kill specific session
|
||||
tx kill foo bar baz # kill multiple sessions
|
||||
```
|
||||
|
||||
### Global Flags
|
||||
|
||||
| Flag | Description |
|
||||
| --------------- | ----------------------------------------- |
|
||||
| `-v, --verbose` | Verbose logging |
|
||||
| `-d, --dry` | Dry run (show commands without executing) |
|
||||
| Flag | Description |
|
||||
| ------------------ | ---------------------------------------------------- |
|
||||
| `-V, --verbose` | Verbose logging |
|
||||
| `-d, --dry` | Dry run (show commands without executing) |
|
||||
| `-b, --background` | Create session in background without switching to it |
|
||||
|
||||
---
|
||||
|
||||
@@ -202,7 +210,27 @@ windows:
|
||||
|
||||
### Layout Configuration
|
||||
|
||||
Layouts define pane splits and commands:
|
||||
Layouts define pane splits and commands. When no layout is specified, the following default is used:
|
||||
|
||||
```yaml
|
||||
# Default layout - horizontal split with vertical sub-split and clock
|
||||
layout:
|
||||
cwd: .
|
||||
split:
|
||||
direction: h
|
||||
child:
|
||||
cwd: .
|
||||
split:
|
||||
direction: v
|
||||
child:
|
||||
cwd: .
|
||||
clock: true
|
||||
```
|
||||
|
||||
You can customize the default for all windows via [`default_layout`](#default-layout) in global
|
||||
settings, or override per window.
|
||||
|
||||
#### Layout Formats
|
||||
|
||||
**String** - just a directory:
|
||||
|
||||
@@ -229,16 +257,36 @@ layout:
|
||||
clock: false # show tmux clock mode
|
||||
split:
|
||||
direction: h # h (horizontal) or v (vertical)
|
||||
size: 30 # percentage of space for the child pane (1-100, default: 50)
|
||||
child:
|
||||
cwd: ./other
|
||||
cmd: npm test
|
||||
split: # nested splits
|
||||
direction: v
|
||||
size: 40
|
||||
child:
|
||||
cwd: .
|
||||
clock: true # show clock in this pane
|
||||
```
|
||||
|
||||
#### Pane Options
|
||||
|
||||
| Setting | Description |
|
||||
| ------- | ---------------------------------------- |
|
||||
| `cwd` | Working directory (defaults to window's) |
|
||||
| `cmd` | Command to run in the pane |
|
||||
| `zoom` | Zoom this pane (default: false) |
|
||||
| `clock` | Show tmux clock mode (default: false) |
|
||||
| `split` | Split this pane (see below) |
|
||||
|
||||
#### Split Options
|
||||
|
||||
| Setting | Description |
|
||||
| ----------- | ----------------------------------------------------------- |
|
||||
| `direction` | `h` (horizontal / side-by-side) or `v` (vertical / stacked) |
|
||||
| `size` | Percentage of space for the child pane (1-100, default: 50) |
|
||||
| `child` | Pane configuration for the new pane created by the split |
|
||||
|
||||
### Global Settings
|
||||
|
||||
The special `.config` key is reserved for global settings and won't be treated as a session:
|
||||
@@ -266,17 +314,8 @@ myproject:
|
||||
|
||||
#### Default Layout
|
||||
|
||||
The `default_layout` setting configures the default pane arrangement for windows. Each pane can
|
||||
have:
|
||||
|
||||
| Setting | Description |
|
||||
| ------- | ------------------------------------------------------------- |
|
||||
| `cwd` | Working directory (defaults to window's directory) |
|
||||
| `cmd` | Command to run (defaults to none) |
|
||||
| `clock` | Show tmux clock mode (defaults to false) |
|
||||
| `split` | Create a split with direction (`h` or `v`) and a `child` pane |
|
||||
|
||||
Example - single pane with clock:
|
||||
The `default_layout` setting overrides the built-in default pane arrangement for all windows. It
|
||||
accepts the same [pane options](#pane-options) as a regular layout.
|
||||
|
||||
```yaml
|
||||
.config:
|
||||
@@ -285,23 +324,6 @@ Example - single pane with clock:
|
||||
clock: true
|
||||
```
|
||||
|
||||
Example - horizontal split with vertical sub-split (default):
|
||||
|
||||
```yaml
|
||||
.config:
|
||||
default_layout:
|
||||
cwd: .
|
||||
split:
|
||||
direction: h
|
||||
child:
|
||||
cwd: .
|
||||
split:
|
||||
direction: v
|
||||
child:
|
||||
cwd: .
|
||||
clock: true
|
||||
```
|
||||
|
||||
#### Named Layouts
|
||||
|
||||
Define reusable layouts that can be referenced by name in session configurations:
|
||||
@@ -430,6 +452,9 @@ tx create -r ~/myproject -w src -w lib -w test
|
||||
|
||||
# Create and save to config
|
||||
tx create -r ~/myproject -s
|
||||
|
||||
# Create in background (don't switch to it)
|
||||
tx create -b -r ~/myproject
|
||||
```
|
||||
|
||||
### Project Workflow
|
||||
|
||||
@@ -75,6 +75,10 @@ func runCreate(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Check if session exists
|
||||
if tmux.SessionExists(opts, parsed.Name) {
|
||||
if background {
|
||||
exec.Log(opts, "Session already exists (background mode, not attaching)")
|
||||
return nil
|
||||
}
|
||||
exec.Log(opts, "Session already exists, attaching")
|
||||
return tmux.AttachToSession(opts, parsed.Name)
|
||||
}
|
||||
@@ -92,5 +96,5 @@ func runCreate(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Create the session
|
||||
return tmux.CreateFromConfig(opts, parsed)
|
||||
return tmux.CreateFromConfig(opts, parsed, background)
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
)
|
||||
|
||||
var killCmd = &cobra.Command{
|
||||
Use: "kill [session]",
|
||||
Use: "kill [session...]",
|
||||
Aliases: []string{"k"},
|
||||
Short: "Kill a running tmux session (current session if no arg)",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Short: "Kill running tmux sessions (current session if no arg)",
|
||||
RunE: runKill,
|
||||
ValidArgsFunction: completeRunningSessions,
|
||||
}
|
||||
@@ -19,24 +18,26 @@ func runKill(cmd *cobra.Command, args []string) error {
|
||||
opts := GetOpts()
|
||||
|
||||
if len(args) > 0 {
|
||||
sessionName := args[0]
|
||||
// Check if session exists
|
||||
if !tmux.SessionExists(opts, sessionName) {
|
||||
return NewUserError("tmux session '" + sessionName + "' does not exist")
|
||||
var errs []error
|
||||
for _, sessionName := range args {
|
||||
if !tmux.SessionExists(opts, sessionName) {
|
||||
errs = append(errs, NewUserError("tmux session '"+sessionName+"' does not exist"))
|
||||
continue
|
||||
}
|
||||
if err := tmux.KillSession(opts, sessionName); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return tmux.KillSession(opts, sessionName)
|
||||
return joinErrors(errs)
|
||||
}
|
||||
|
||||
// No arg - kill current session
|
||||
return exec.RunCommand(opts, "tmux kill-session")
|
||||
}
|
||||
|
||||
// completeRunningSessions returns running session names for shell completion
|
||||
// completeRunningSessions returns running session names for shell completion,
|
||||
// excluding sessions already provided as arguments.
|
||||
func completeRunningSessions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// Don't complete if we already have an argument
|
||||
if len(args) > 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
return tmux.GetSessionNames(), cobra.ShellCompDirectiveNoFileComp
|
||||
all := tmux.GetSessionNames()
|
||||
return filterUsed(all, args), cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/chenasraf/tx/internal/config"
|
||||
"github.com/chenasraf/tx/internal/table"
|
||||
"github.com/chenasraf/tx/internal/tmux"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -61,13 +63,7 @@ func runList(cmd *cobra.Command, args []string) error {
|
||||
sessionsOutput, err := tmux.ListSessions(opts)
|
||||
sessionsStr := ""
|
||||
if err == nil && sessionsOutput != "" {
|
||||
// Format sessions output
|
||||
lines := strings.Split(strings.TrimSpace(sessionsOutput), "\n")
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
sessionsStr += " " + line + "\n"
|
||||
}
|
||||
}
|
||||
sessionsStr = formatSessionsTable(sessionsOutput, " ")
|
||||
} else {
|
||||
sessionsStr = " No tmux sessions\n"
|
||||
}
|
||||
@@ -107,3 +103,28 @@ func runList(cmd *cobra.Command, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var sessionLineRe = regexp.MustCompile(`^([^:]+):\s*(\d+)\s+windows?\s*\(created\s+([^)]+)\)\s*(.*)$`)
|
||||
|
||||
// formatSessionsTable parses `tmux ls` output and renders it as a bordered
|
||||
// table with headers. Each output line is prefixed with indent.
|
||||
func formatSessionsTable(raw, indent string) string {
|
||||
lines := strings.Split(strings.TrimSpace(raw), "\n")
|
||||
rows := make([][]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
m := sessionLineRe.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
// Fallback: dump the whole line into the Name column
|
||||
rows = append(rows, []string{line, "", "", ""})
|
||||
continue
|
||||
}
|
||||
status := strings.TrimSpace(m[4])
|
||||
status = strings.TrimPrefix(status, "(")
|
||||
status = strings.TrimSuffix(status, ")")
|
||||
rows = append(rows, []string{m[1], m[2], m[3], status})
|
||||
}
|
||||
return table.Render([]string{"Name", "Windows", "Created", "Status"}, rows, indent)
|
||||
}
|
||||
|
||||
@@ -53,10 +53,14 @@ func runMain(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Check if session exists
|
||||
if tmux.SessionExists(opts, parsed.Name) {
|
||||
if background {
|
||||
exec.Log(opts, "Session exists (background mode, not attaching)")
|
||||
return nil
|
||||
}
|
||||
exec.Log(opts, "Session exists, attaching...")
|
||||
return tmux.AttachToSession(opts, parsed.Name)
|
||||
}
|
||||
|
||||
// Create session
|
||||
return tmux.CreateFromConfig(opts, parsed)
|
||||
return tmux.CreateFromConfig(opts, parsed, background)
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func runPrj(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Create session
|
||||
return tmux.CreateFromConfig(opts, parsed)
|
||||
return tmux.CreateFromConfig(opts, parsed, background)
|
||||
}
|
||||
|
||||
// getProjects returns directory names in the given path
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
var removeConfigFile string
|
||||
|
||||
var removeCmd = &cobra.Command{
|
||||
Use: "remove <key>",
|
||||
Use: "remove <key...>",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Remove a tmux workspace from the config file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Remove tmux workspaces from the config file",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: runRemove,
|
||||
ValidArgsFunction: completeSessionNames,
|
||||
ValidArgsFunction: completeSessionNamesMulti,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -25,30 +25,32 @@ func init() {
|
||||
|
||||
func runRemove(cmd *cobra.Command, args []string) error {
|
||||
opts := GetOpts()
|
||||
key := args[0]
|
||||
|
||||
// Verify the key exists
|
||||
allConfig, err := config.GetTmuxConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, actualKey, exists := allConfig.Get(key)
|
||||
if !exists {
|
||||
return NewUserError("tmux config item '" + key + "' not found")
|
||||
var errs []error
|
||||
for _, key := range args {
|
||||
_, actualKey, exists := allConfig.Get(key)
|
||||
if !exists {
|
||||
errs = append(errs, NewUserError("tmux config item '"+key+"' not found"))
|
||||
continue
|
||||
}
|
||||
|
||||
err = config.RemoveConfigFromFile(actualKey, removeConfigFile, opts.Dry)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !opts.Dry {
|
||||
fmt.Printf("Removed tmux config item '%s'\n", key)
|
||||
}
|
||||
|
||||
exec.Log(opts, "Removed config item:", key)
|
||||
}
|
||||
|
||||
err = config.RemoveConfigFromFile(actualKey, removeConfigFile, opts.Dry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.Dry {
|
||||
fmt.Printf("Removed tmux config item '%s'\n", key)
|
||||
}
|
||||
|
||||
// Log action in verbose/dry mode
|
||||
exec.Log(opts, "Removed config item:", key)
|
||||
|
||||
return nil
|
||||
return joinErrors(errs)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ func TestRemoveCmd_Exists(t *testing.T) {
|
||||
t.Error("expected removeCmd to not be nil")
|
||||
}
|
||||
|
||||
if removeCmd.Use != "remove <key>" {
|
||||
if removeCmd.Use != "remove <key...>" {
|
||||
t.Errorf("unexpected Use: %q", removeCmd.Use)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -15,8 +16,9 @@ var (
|
||||
Version string
|
||||
|
||||
// Global flags
|
||||
verbose bool
|
||||
dry bool
|
||||
verbose bool
|
||||
dry bool
|
||||
background bool
|
||||
)
|
||||
|
||||
// GetOpts returns the current execution options
|
||||
@@ -41,6 +43,11 @@ func NewUserError(message string) *UserError {
|
||||
return &UserError{Message: message}
|
||||
}
|
||||
|
||||
// joinErrors combines multiple errors into one, returning nil if there are none.
|
||||
func joinErrors(errs []error) error {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// rootCmd represents the base command
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "tx [session]",
|
||||
@@ -79,6 +86,39 @@ func completeSessionNames(cmd *cobra.Command, args []string, toComplete string)
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
// completeSessionNamesMulti returns session names excluding already-provided args.
|
||||
func completeSessionNamesMulti(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
cfg, err := config.GetTmuxConfig()
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
var names []string
|
||||
for name, item := range cfg {
|
||||
if name != config.ConfigKey {
|
||||
names = append(names, name)
|
||||
names = append(names, item.Aliases...)
|
||||
}
|
||||
}
|
||||
|
||||
return filterUsed(names, args), cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
// filterUsed returns items from candidates that are not already in used.
|
||||
func filterUsed(candidates []string, used []string) []string {
|
||||
seen := make(map[string]bool, len(used))
|
||||
for _, u := range used {
|
||||
seen[u] = true
|
||||
}
|
||||
var result []string
|
||||
for _, c := range candidates {
|
||||
if !seen[c] {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// buildFzfItems creates fzf items from a config file
|
||||
func buildFzfItems(cfg config.ConfigFile) []fzf.Item {
|
||||
items := make([]fzf.Item, 0, len(cfg))
|
||||
@@ -127,6 +167,7 @@ func init() {
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "V", false, "Verbose logging")
|
||||
rootCmd.PersistentFlags().BoolVarP(&dry, "dry", "d", false, "Dry run (log commands, don't execute)")
|
||||
rootCmd.PersistentFlags().BoolVarP(&background, "background", "B", false, "Create session in background without attaching")
|
||||
rootCmd.Flags().BoolP("version", "v", false, "Print version")
|
||||
|
||||
// Add subcommands
|
||||
|
||||
@@ -188,6 +188,7 @@ func parsePaneLayout(pane *TmuxPaneLayout, root string) TmuxPaneLayout {
|
||||
if pane.Split != nil {
|
||||
result.Split = &TmuxSplitLayout{
|
||||
Direction: pane.Split.Direction,
|
||||
Size: pane.Split.Size,
|
||||
}
|
||||
if result.Split.Direction == "" {
|
||||
result.Split.Direction = "h"
|
||||
@@ -208,6 +209,7 @@ func copyTmuxSplitLayout(split *TmuxSplitLayout, root string) *TmuxSplitLayout {
|
||||
}
|
||||
result := &TmuxSplitLayout{
|
||||
Direction: split.Direction,
|
||||
Size: split.Size,
|
||||
}
|
||||
if split.Child != nil {
|
||||
child := TmuxPaneLayout{
|
||||
|
||||
@@ -141,7 +141,8 @@ type TmuxPaneLayout struct {
|
||||
|
||||
// TmuxSplitLayout represents a split configuration
|
||||
type TmuxSplitLayout struct {
|
||||
Direction string `yaml:"direction"` // "h" or "v"
|
||||
Direction string `yaml:"direction"` // "h" or "v"
|
||||
Size int `yaml:"size,omitempty"` // percentage (1-100) of the split given to the child pane
|
||||
Child *TmuxPaneLayout `yaml:"child"`
|
||||
}
|
||||
|
||||
|
||||
57
internal/table/table.go
Normal file
57
internal/table/table.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Package table renders simple bordered text tables with headers.
|
||||
package table
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Render returns a bordered table string with the given headers and rows.
|
||||
// Every line in the output is prefixed with indent. Column widths auto-size
|
||||
// to the widest cell (header included).
|
||||
func Render(headers []string, rows [][]string, indent string) string {
|
||||
widths := make([]int, len(headers))
|
||||
for i, h := range headers {
|
||||
widths[i] = utf8.RuneCountInString(h)
|
||||
}
|
||||
for _, row := range rows {
|
||||
for i, cell := range row {
|
||||
if i >= len(widths) {
|
||||
break
|
||||
}
|
||||
if w := utf8.RuneCountInString(cell); w > widths[i] {
|
||||
widths[i] = w
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
border := func(left, mid, right string) string {
|
||||
parts := make([]string, len(widths))
|
||||
for i, w := range widths {
|
||||
parts[i] = strings.Repeat("─", w+2)
|
||||
}
|
||||
return left + strings.Join(parts, mid) + right
|
||||
}
|
||||
rowLine := func(cells []string) string {
|
||||
parts := make([]string, len(widths))
|
||||
for i := range widths {
|
||||
cell := ""
|
||||
if i < len(cells) {
|
||||
cell = cells[i]
|
||||
}
|
||||
pad := max(widths[i]-utf8.RuneCountInString(cell), 0)
|
||||
parts[i] = " " + cell + strings.Repeat(" ", pad) + " "
|
||||
}
|
||||
return "│" + strings.Join(parts, "│") + "│"
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(indent + border("┌", "┬", "┐") + "\n")
|
||||
b.WriteString(indent + rowLine(headers) + "\n")
|
||||
b.WriteString(indent + border("├", "┼", "┤") + "\n")
|
||||
for _, row := range rows {
|
||||
b.WriteString(indent + rowLine(row) + "\n")
|
||||
}
|
||||
b.WriteString(indent + border("└", "┴", "┘") + "\n")
|
||||
return b.String()
|
||||
}
|
||||
@@ -8,8 +8,9 @@ import (
|
||||
"github.com/chenasraf/tx/internal/exec"
|
||||
)
|
||||
|
||||
// CreateFromConfig creates a tmux session from a parsed config
|
||||
func CreateFromConfig(opts exec.Opts, tmuxConfig config.ParsedTmuxConfigItem) error {
|
||||
// CreateFromConfig creates a tmux session from a parsed config.
|
||||
// If background is true, the session is created but not attached to.
|
||||
func CreateFromConfig(opts exec.Opts, tmuxConfig config.ParsedTmuxConfigItem, background bool) error {
|
||||
root := tmuxConfig.Root
|
||||
windows := tmuxConfig.Windows
|
||||
sessionName := config.NameFix(tmuxConfig.Name)
|
||||
@@ -19,6 +20,10 @@ func CreateFromConfig(opts exec.Opts, tmuxConfig config.ParsedTmuxConfigItem) er
|
||||
|
||||
// Check if session already exists
|
||||
if SessionExists(opts, sessionName) {
|
||||
if background {
|
||||
exec.Log(opts, fmt.Sprintf("tmux session %s already exists (background mode, not attaching)", sessionName))
|
||||
return nil
|
||||
}
|
||||
exec.Log(opts, fmt.Sprintf("tmux session %s already exists, attaching...", sessionName))
|
||||
return AttachToSession(opts, sessionName)
|
||||
}
|
||||
@@ -73,7 +78,10 @@ func CreateFromConfig(opts exec.Opts, tmuxConfig config.ParsedTmuxConfigItem) er
|
||||
}
|
||||
}
|
||||
|
||||
// Attach to the session
|
||||
// Attach to the session unless background mode
|
||||
if background {
|
||||
return nil
|
||||
}
|
||||
return AttachToSession(opts, sessionName)
|
||||
}
|
||||
|
||||
@@ -119,10 +127,14 @@ func getPaneCommands(opts exec.Opts, pane config.TmuxPaneLayout, sessionName, wi
|
||||
// Increment pane index for the new pane created by split
|
||||
*paneIndex++
|
||||
|
||||
commands = append(commands, fmt.Sprintf(
|
||||
splitCmd := fmt.Sprintf(
|
||||
"tmux split-window -%s -t %s:%s -c %s",
|
||||
direction, sessionName, windowName, cwd,
|
||||
))
|
||||
)
|
||||
if pane.Split.Size > 0 && pane.Split.Size <= 100 {
|
||||
splitCmd += fmt.Sprintf(" -p %d", pane.Split.Size)
|
||||
}
|
||||
commands = append(commands, splitCmd)
|
||||
|
||||
// Handle child pane
|
||||
if pane.Split.Child != nil {
|
||||
|
||||
@@ -251,7 +251,7 @@ func TestCreateFromConfig_DryRun(t *testing.T) {
|
||||
}
|
||||
|
||||
// In dry mode, this should succeed without actually running tmux
|
||||
err := CreateFromConfig(opts, tmuxConfig)
|
||||
err := CreateFromConfig(opts, tmuxConfig, false)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error in dry mode, got %v", err)
|
||||
}
|
||||
@@ -281,7 +281,7 @@ func TestCreateFromConfig_MultipleWindows(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
err := CreateFromConfig(opts, tmuxConfig)
|
||||
err := CreateFromConfig(opts, tmuxConfig, false)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error in dry mode, got %v", err)
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.0.0
|
||||
2.4.1
|
||||
|
||||
Reference in New Issue
Block a user