Skip to content
Closed
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
4 changes: 2 additions & 2 deletions cmd/config/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@ func initShellIntegration() {
}

fmt.Println()
fmt.Printf("Usage: %s\n", commands.SuccessStyle.Render("w"))
fmt.Printf(" %s\n", commands.SuccessStyle.Render("w <workspace-name>"))
fmt.Printf("Usage: %s (select workspace → Claude Code → loop back)\n", commands.SuccessStyle.Render("w"))
fmt.Printf(" %s (go to workspace → Claude Code → loop back)\n", commands.SuccessStyle.Render("w <name>"))
}
77 changes: 67 additions & 10 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

import (
"fmt"
"os"
"os/exec"

"github.com/spf13/cobra"

"github.com/jcleira/workspace/pkg/config"
"github.com/jcleira/workspace/pkg/shell"
"github.com/jcleira/workspace/pkg/ui/commands"
"github.com/jcleira/workspace/pkg/ui/dashboard"
"github.com/jcleira/workspace/pkg/workspace"
Expand Down Expand Up @@ -72,24 +73,80 @@
return
}

selectedPath, err := dashboard.RunDashboard(WorkspaceManager, ConfigManager)
if err != nil {
commands.PrintError(fmt.Sprintf("Dashboard error: %v", err))
if OutputPathOnly {
runOutputPathOnlyMode()
return
}

if selectedPath != "" {
if OutputPathOnly {
fmt.Println(selectedPath)
if !isClaudeInstalled() {
commands.PrintError("'claude' command not found")
fmt.Println("Install: https://claude.ai/code")
return
}

for {
result, err := dashboard.RunDashboard(WorkspaceManager, ConfigManager)
if err != nil {
commands.PrintError(fmt.Sprintf("Dashboard error: %v", err))
return
}

if result.Path == "" {
return
}

if result.LaunchShell {
launchShell(result.Path)
} else {
ws := workspace.Workspace{Path: selectedPath}
shell.NavigateToWorkspace(ws)
launchClaude(result.Path)
}
} else if OutputPathOnly {
}
}

func runOutputPathOnlyMode() {
result, err := dashboard.RunDashboard(WorkspaceManager, ConfigManager)
if err != nil {
commands.PrintError(fmt.Sprintf("Dashboard error: %v", err))
return
}

if result.Path != "" {
fmt.Println(result.Path)
} else {
fmt.Println("quit")
}
}

func isClaudeInstalled() bool {
_, err := exec.LookPath("claude")
return err == nil
}

func launchClaude(workspacePath string) {
cmd := exec.Command("claude", "--continue")
cmd.Dir = workspacePath
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

_ = cmd.Run()

Check failure on line 132 in cmd/root.go

View workflow job for this annotation

GitHub Actions / Lint

Error return value of `cmd.Run` is not checked (errcheck)
}

func launchShell(workspacePath string) {
shellPath := os.Getenv("SHELL")
if shellPath == "" {
shellPath = "/bin/sh"
}

cmd := exec.Command(shellPath)
cmd.Dir = workspacePath
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

_ = cmd.Run()

Check failure on line 147 in cmd/root.go

View workflow job for this annotation

GitHub Actions / Lint

Error return value of `cmd.Run` is not checked (errcheck)
}

// WorkspaceCompletionFunc provides shell completion for workspace names.
func WorkspaceCompletionFunc(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
workspaces, err := WorkspaceManager.GetWorkspaces()
Expand Down
117 changes: 75 additions & 42 deletions pkg/shell/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,35 @@ package shell

// GenerateBashFunction returns a bash shell function for workspace navigation.
func GenerateBashFunction() string {
return `# Workspace navigation function
return `# Workspace navigation function with Claude Code integration
w() {
if [ $# -eq 0 ]; then
# Interactive selection
local result=$(workspace --output-path-only)
if [ -n "$result" ] && [ "$result" != "quit" ]; then
if ! command -v claude &> /dev/null; then
echo "Error: 'claude' command not found"
echo "Install: https://claude.ai/code"
return 1
fi

while true; do
if [ $# -eq 0 ]; then
local result=$(workspace --output-path-only)
if [ -z "$result" ] || [ "$result" = "quit" ]; then
break
fi
cd "$result"
fi
else
# Direct workspace navigation
local workspace_path="${HOME}/Tactic/workspaces/workspace-$1"
if [ -d "$workspace_path" ]; then
cd "$workspace_path"
else
echo "Workspace '$1' not found"
workspace list
local workspace_path="${HOME}/Tactic/workspaces/workspace-$1"
if [ -d "$workspace_path" ]; then
cd "$workspace_path"
else
echo "Workspace '$1' not found"
workspace list
return 1
fi
set --
fi
fi

claude --continue
done
}

# Completion function for w command
Expand All @@ -36,24 +47,35 @@ complete -F _w_complete w`

// GenerateZshFunction returns a zsh shell function for workspace navigation.
func GenerateZshFunction() string {
return `# Workspace navigation function
return `# Workspace navigation function with Claude Code integration
w() {
if [ $# -eq 0 ]; then
# Interactive selection
local result=$(workspace --output-path-only)
if [ -n "$result" ] && [ "$result" != "quit" ]; then
if ! command -v claude &> /dev/null; then
echo "Error: 'claude' command not found"
echo "Install: https://claude.ai/code"
return 1
fi

while true; do
if [ $# -eq 0 ]; then
local result=$(workspace --output-path-only)
if [ -z "$result" ] || [ "$result" = "quit" ]; then
break
fi
cd "$result"
fi
else
# Direct workspace navigation
local workspace_path="${HOME}/Tactic/workspaces/workspace-$1"
if [ -d "$workspace_path" ]; then
cd "$workspace_path"
else
echo "Workspace '$1' not found"
workspace list
local workspace_path="${HOME}/Tactic/workspaces/workspace-$1"
if [ -d "$workspace_path" ]; then
cd "$workspace_path"
else
echo "Workspace '$1' not found"
workspace list
return 1
fi
set --
fi
fi

claude --continue
done
}

# Completion function for w command
Expand All @@ -69,23 +91,34 @@ compdef _w_complete w`

// GenerateFishFunction returns a fish shell function for workspace navigation.
func GenerateFishFunction() string {
return `# Workspace navigation function
function w --description "Navigate to workspace"
if test (count $argv) -eq 0
# Interactive selection
set result (workspace --output-path-only)
if test -n "$result" -a "$result" != "quit"
return `# Workspace navigation function with Claude Code integration
function w --description "Navigate to workspace and launch Claude"
if not command -v claude &> /dev/null
echo "Error: 'claude' command not found"
echo "Install: https://claude.ai/code"
return 1
end

while true
if test (count $argv) -eq 0
set result (workspace --output-path-only)
if test -z "$result" -o "$result" = "quit"
break
end
cd "$result"
end
else
# Direct workspace navigation
set workspace_path "$HOME/Tactic/workspaces/workspace-$argv[1]"
if test -d "$workspace_path"
cd "$workspace_path"
else
echo "Workspace '$argv[1]' not found"
workspace list
set workspace_path "$HOME/Tactic/workspaces/workspace-$argv[1]"
if test -d "$workspace_path"
cd "$workspace_path"
else
echo "Workspace '$argv[1]' not found"
workspace list
return 1
end
set -e argv
end

claude --continue
end
end

Expand Down
36 changes: 31 additions & 5 deletions pkg/ui/dashboard/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type DashboardModel struct {
statusMessage string

selectedPath string
launchShell bool
quitting bool
}

Expand Down Expand Up @@ -211,6 +212,16 @@ func (m DashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, nil

case key.Matches(msg, m.keys.Shell):
ws := m.workspaceList.SelectedWorkspace()
if ws != nil {
m.selectedPath = ws.Path
m.launchShell = true
m.quitting = true
return m, tea.Quit
}
return m, nil

case key.Matches(msg, m.keys.Refresh):
return m, m.refreshSelectedStatus()

Expand Down Expand Up @@ -552,7 +563,8 @@ func (m DashboardModel) renderFooter() string {
keys := []string{
helpKeyStyle.Render("↑/↓ j/k") + helpDescStyle.Render(" navigate"),
helpKeyStyle.Render("←/→ h/l") + helpDescStyle.Render(" panels"),
helpKeyStyle.Render("Enter") + helpDescStyle.Render(" select"),
helpKeyStyle.Render("Enter") + helpDescStyle.Render(" claude"),
helpKeyStyle.Render("s") + helpDescStyle.Render(" shell"),
helpKeyStyle.Render("f") + helpDescStyle.Render(" fetch"),
helpKeyStyle.Render("p") + helpDescStyle.Render(" pull"),
helpKeyStyle.Render("G") + helpDescStyle.Render(" staged diff"),
Expand All @@ -578,16 +590,30 @@ func (m DashboardModel) SelectedPath() string {
return m.selectedPath
}

// RunDashboard runs the dashboard and returns the selected workspace path.
func RunDashboard(wm *workspace.Manager, cm *config.ConfigManager) (string, error) {
// LaunchShell returns true if the user wants to launch a shell instead of Claude.
func (m DashboardModel) LaunchShell() bool {
return m.launchShell
}

// DashboardResult contains the result of running the dashboard.
type DashboardResult struct {
Path string
LaunchShell bool
}

// RunDashboard runs the dashboard and returns the selected workspace path and action.
func RunDashboard(wm *workspace.Manager, cm *config.ConfigManager) (DashboardResult, error) {
m := NewDashboard(wm, cm)
p := tea.NewProgram(m, tea.WithAltScreen())

finalModel, err := p.Run()
if err != nil {
return "", err
return DashboardResult{}, err
}

dm := finalModel.(DashboardModel)
return dm.SelectedPath(), nil
return DashboardResult{
Path: dm.SelectedPath(),
LaunchShell: dm.LaunchShell(),
}, nil
}
7 changes: 6 additions & 1 deletion pkg/ui/dashboard/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type KeyMap struct {
Left key.Binding
Right key.Binding
Select key.Binding
Shell key.Binding
Fetch key.Binding
Pull key.Binding
Delete key.Binding
Expand Down Expand Up @@ -44,7 +45,11 @@ func DefaultKeyMap() KeyMap {
),
Select: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "switch workspace"),
key.WithHelp("enter", "claude"),
),
Shell: key.NewBinding(
key.WithKeys("s"),
key.WithHelp("s", "shell"),
),
Fetch: key.NewBinding(
key.WithKeys("f"),
Expand Down
Loading