Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions pkg/cmd/tmux/popup/kill/kill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package kill

import (
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/zkhvan/z/pkg/cmdutil"
"github.com/zkhvan/z/pkg/tmux"
)

type Options struct {
Name string
All bool
Zombies bool
}

func NewCmdKill(_ *cmdutil.Factory) *cobra.Command {
opts := &Options{}

cmd := &cobra.Command{
Use: "kill [name]",
Short: "Kill popup sessions",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
opts.Name = args[0]
}
if opts.Name == "" && !opts.All && !opts.Zombies {
return fmt.Errorf("provide a popup name or use --all/--zombies")
}
return opts.Run(cmd.Context())
},
}

cmd.Flags().BoolVar(&opts.All, "all", false, "Kill all popup sessions for the current session")
cmd.Flags().BoolVar(&opts.Zombies, "zombies", false, "Kill orphaned popup sessions whose parent no longer exists")

return cmd
}

func (opts *Options) Run(ctx context.Context) error {
if opts.Zombies {
return opts.killZombies(ctx)
}

parentName, err := tmux.CurrentSessionName(ctx)
if err != nil {
return err
}

if !opts.All {
popupSessionName := tmux.ToPopupSessionName(parentName, opts.Name)
if !tmux.HasSession(ctx, popupSessionName) {
return fmt.Errorf("popup session %q not found", opts.Name)
}
return tmux.KillSession(ctx, tmux.Session{Name: popupSessionName})
}

sessions, err := tmux.ListSessions(ctx, nil)
if err != nil {
return err
}

for _, session := range sessions {
if _, ok := tmux.ExtractPopupName(session.Name, parentName); ok {
if err := tmux.KillSession(ctx, session); err != nil {
return err
}
}
}

return nil
}

func (opts *Options) killZombies(ctx context.Context) error {
sessions, err := tmux.ListSessions(ctx, nil)
if err != nil {
return err
}

// Build a set of non-popup session names
alive := make(map[string]struct{})
for _, s := range sessions {
if !tmux.IsPopupSession(s.Name) {
alive[s.Name] = struct{}{}
}
}

// Kill popups whose parent is no longer alive
for _, s := range sessions {
parent, ok := tmux.ExtractPopupParent(s.Name)
if !ok {
continue
}
if _, exists := alive[parent]; !exists {
if err := tmux.KillSession(ctx, s); err != nil {
return err
}
}
}

return nil
}
52 changes: 52 additions & 0 deletions pkg/cmd/tmux/popup/list/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package list

import (
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/zkhvan/z/pkg/cmdutil"
"github.com/zkhvan/z/pkg/iolib"
"github.com/zkhvan/z/pkg/tmux"
)

type Options struct {
io *iolib.IOStreams
}

func NewCmdList(f *cmdutil.Factory) *cobra.Command {
opts := &Options{
io: f.IOStreams,
}

cmd := &cobra.Command{
Use: "list",
Short: "List popup sessions",
RunE: func(cmd *cobra.Command, _ []string) error {
return opts.Run(cmd.Context())
},
}

return cmd
}

func (opts *Options) Run(ctx context.Context) error {
parentName, err := tmux.CurrentSessionName(ctx)
if err != nil {
return err
}

sessions, err := tmux.ListSessions(ctx, nil)
if err != nil {
return err
}

for _, session := range sessions {
if name, ok := tmux.ExtractPopupName(session.Name, parentName); ok {
fmt.Fprintln(opts.io.Out, name)
}
}

return nil
}
23 changes: 23 additions & 0 deletions pkg/cmd/tmux/popup/popup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package popup

import (
"github.com/spf13/cobra"

killCmd "github.com/zkhvan/z/pkg/cmd/tmux/popup/kill"
listCmd "github.com/zkhvan/z/pkg/cmd/tmux/popup/list"
useCmd "github.com/zkhvan/z/pkg/cmd/tmux/popup/use"
"github.com/zkhvan/z/pkg/cmdutil"
)

func NewCmdPopup(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "popup",
Short: "Manage tmux popup sessions",
}

cmd.AddCommand(killCmd.NewCmdKill(f))
cmd.AddCommand(listCmd.NewCmdList(f))
cmd.AddCommand(useCmd.NewCmdUse(f))

return cmd
}
80 changes: 80 additions & 0 deletions pkg/cmd/tmux/popup/use/use.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package use

import (
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/zkhvan/z/pkg/cmdutil"
"github.com/zkhvan/z/pkg/tmux"
)

type Options struct {
Name string
Width string
Height string
}

func NewCmdUse(_ *cmdutil.Factory) *cobra.Command {
opts := &Options{}

cmd := &cobra.Command{
Use: "use <name>",
Short: "Open a popup session",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.Name = args[0]
return opts.Run(cmd.Context())
},
}

cmd.Flags().StringVar(&opts.Width, "width", "80%", "Popup width")
cmd.Flags().StringVar(&opts.Height, "height", "80%", "Popup height")

return cmd
}

func (opts *Options) Run(ctx context.Context) error {
parentName, err := tmux.CurrentSessionName(ctx)
if err != nil {
return err
}

popupSessionName := tmux.ToPopupSessionName(parentName, opts.Name)

if !tmux.HasSession(ctx, popupSessionName) {
session, err := tmux.NewSessionDetached(ctx, &tmux.NewOptions{
Name: popupSessionName,
})
if err != nil {
return err
}

if err := tmux.SetSessionOption(ctx, session.ID, "status", "off"); err != nil {
return err
}
if err := tmux.SetSessionOption(ctx, session.ID, "prefix", "None"); err != nil {
return err
}
// Popup keybindings — bound in root table with a conditional so they
// only take effect in popup sessions (name starts with _popup_).
popupPattern := fmt.Sprintf("#{m:%s*,#{session_name}}", tmux.PopupPrefix)
if err := tmux.BindKey(ctx, "root", "S-F1",
"if-shell", "-F", popupPattern,
"detach-client", "send-keys S-F1"); err != nil {
return err
}
if err := tmux.BindKey(ctx, "root", "M-[",
"if-shell", "-F", popupPattern,
"copy-mode", "send-keys M-["); err != nil {
return err
}
}

return tmux.DisplayPopup(ctx, &tmux.DisplayPopupOptions{
Width: opts.Width,
Height: opts.Height,
ShellCommand: fmt.Sprintf("tmux attach-session -t '=%s'", popupSessionName),
})
}
13 changes: 11 additions & 2 deletions pkg/cmd/tmux/session/kill/kill.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kill

import (
"context"
"errors"
"sort"

"github.com/MakeNowJust/heredoc/v2"
Expand Down Expand Up @@ -41,10 +42,17 @@ func (o *Options) Run(ctx context.Context) error {
if err != nil {
return err
}
currentSessionName, err := tmux.CurrentSessionName(ctx)
if err != nil {
return err
}
defer func() {
if err == nil {
// If we successfully switch to another session, kill the current one
err = tmux.KillSession(ctx, tmux.Session{ID: currentSessionID})
err = errors.Join(
// If we successfully switch to another session, kill the current one
tmux.KillSession(ctx, tmux.Session{ID: currentSessionID}),
tmux.KillPopups(ctx, currentSessionName),
)
}
}()

Expand All @@ -56,6 +64,7 @@ func (o *Options) Run(ctx context.Context) error {
// Get all sessions except current one
sessions, err := tmux.ListSessions(ctx, &tmux.ListOptions{
ExcludeCurrentSession: true,
ExcludePopupSessions: true,
})
if err != nil {
return err
Expand Down
4 changes: 3 additions & 1 deletion pkg/cmd/tmux/session/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
}

func (opts *Options) Run(ctx context.Context) error {
sessions, err := tmux.ListSessions(ctx, nil)
sessions, err := tmux.ListSessions(ctx, &tmux.ListOptions{
ExcludePopupSessions: true,
})
if err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/tmux/session/use/use.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func NewCmdUse(_ *cmdutil.Factory) *cobra.Command {
func (opts *Options) Run(ctx context.Context) error {
sessions, err := tmux.ListSessions(ctx, &tmux.ListOptions{
ExcludeCurrentSession: true,
ExcludePopupSessions: true,
})
if err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions pkg/cmd/tmux/tmux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tmux
import (
"github.com/spf13/cobra"

popupCmd "github.com/zkhvan/z/pkg/cmd/tmux/popup"
sessionCmd "github.com/zkhvan/z/pkg/cmd/tmux/session"
"github.com/zkhvan/z/pkg/cmdutil"
)
Expand All @@ -13,6 +14,7 @@ func NewCmdTmux(f *cmdutil.Factory) *cobra.Command {
Short: "Manage tmux",
}

cmd.AddCommand(popupCmd.NewCmdPopup(f))
cmd.AddCommand(sessionCmd.NewCmdSession(f))

return cmd
Expand Down
61 changes: 61 additions & 0 deletions pkg/tmux/popup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package tmux

import (
"context"
"errors"
"strings"
)

const (
PopupPrefix = "_popup_"
PopupSeparator = "__"
)

func ToPopupSessionName(parentSession, popupName string) string {
return PopupPrefix + parentSession + PopupSeparator + popupName
}

func IsPopupSession(sessionName string) bool {
return strings.HasPrefix(sessionName, PopupPrefix)
}

// ExtractPopupName extracts the popup name from a full popup session name,
// given the parent session name. Returns the popup name and true if the
// session belongs to the parent, or empty string and false otherwise.
func ExtractPopupName(sessionName, parentSession string) (string, bool) {
prefix := PopupPrefix + parentSession + PopupSeparator
if !strings.HasPrefix(sessionName, prefix) {
return "", false
}
return strings.TrimPrefix(sessionName, prefix), true
}

// ExtractPopupParent extracts the parent session name from a popup session
// name by splitting on the double-underscore separator. Returns the parent
// name and true, or empty string and false if not a popup session.
func ExtractPopupParent(sessionName string) (string, bool) {
if !IsPopupSession(sessionName) {
return "", false
}
rest := strings.TrimPrefix(sessionName, PopupPrefix)
idx := strings.LastIndex(rest, PopupSeparator)
if idx < 0 {
return "", false
}
return rest[:idx], true
}

// KillPopups kills all popup sessions associated with the given parent session.
func KillPopups(ctx context.Context, parentSessionName string) error {
sessions, err := ListSessions(ctx, nil)
if err != nil {
return err
}
var errs []error
for _, session := range sessions {
if _, ok := ExtractPopupName(session.Name, parentSessionName); ok {
errs = append(errs, KillSession(ctx, session))
}
}
return errors.Join(errs...)
}
Loading