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
8 changes: 5 additions & 3 deletions DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ internal/tui/select_test.go
| `/Users/uzulla/プロジェクト/api` | `______.api-f10f1a6e` (非ASCIIは1文字あたり `_` + suffix) |
| `/srv/app-v1.2.3/api` | `srv.app-v1.2.3.api-597dbea5` |

ホームディレクトリそのもの(相対パスが空になる)やファイルシステムのルートでは相対パス由来の名前が空になる。この場合は `session.FallbackName()` が `$HOME` を取り除かないフルパスから名前を生成する(例: `/home/developer` → `home.developer`)。フルパスでも空になるルート `/` のみ `shell` に固定フォールバックする。`shp` (引数なし) / `shp -f` / `shp --print-name` はいずれも `session.FromCwdOrFallback()` を通すので、ホーム/ルートでも空にならず一貫して起動・出力できる。

MVP ではこのルールは固定。将来は `~/work` や `~/src` を root として相対化するなどの設定を追加できる設計にしてある (`session.FromPath(path, home)` で home を引数化済み)。

## attach の実行方式
Expand All @@ -118,7 +120,7 @@ attach には常に `--dir .` を渡す。既存セッションへの attach で
- 空行を除外
- `NAME STATUS` のヘッダ行を除外
- 各行の先頭カラムをセッション名として扱う
- 候補が0件なら標準エラーに `No shpool sessions found.` を表示して終了
- 既存セッションが0件でも、cwd 由来名(空ならフルパス由来のフォールバック名)を既定候補として picker を表示し、`shp` がそのまま起動できるようにする

## TUI

Expand Down Expand Up @@ -147,8 +149,8 @@ MVP では fuzzy / negative / 正規表現 / 複数選択 / preview pane / sort
| 状況 | 出力 | 終了コード |
| --- | --- | --- |
| `shpool` が PATH に無い | `Error: shpool command not found in PATH. Please install shpool first` | 1 |
| `shpool list` で一覧取得不可 | cwd 候補だけを表示。cwd 名も空なら `No shpool sessions found.` | 0 |
| `shpool list` で一覧取得不可 | cwd 候補(空ならフォールバック名)だけを表示 | 0 |
| `shpool list` 失敗 (一覧取得不可として扱わないもの) | `Error: shpool list failed: <stderr>` | 1 |
| セッション0件 | `No shpool sessions found.` (stderr) | 0 |
| セッション0件 | cwd 候補(空ならフォールバック名)だけの picker を表示 | 0 |
| TUI でキャンセル | (何も出さない) | 0 |
| 不正な引数 | `Error: too many arguments` 等 + usage | 1 |
24 changes: 12 additions & 12 deletions cmd/shp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ Usage:
shp -h | --help Show this help.

New sessions are created in the current directory.
In the home directory (or filesystem root), where no name can be derived from
a relative path, the session name falls back to one derived from the full path
(e.g. "home.<user>"), so shp still launches instead of dead-ending.
shpool must be installed and on PATH.
`

Expand Down Expand Up @@ -64,7 +67,7 @@ func run(args []string) error {
if len(rest) > 0 {
return fmt.Errorf("--print-name takes no arguments")
}
name, err := session.FromCwd()
name, err := session.FromCwdOrFallback()
if err != nil {
return err
}
Expand All @@ -81,15 +84,13 @@ func run(args []string) error {
return fmt.Errorf("too many arguments")
}

// `shp -f` without a name: force-attach directly to the cwd-derived name.
// `shp -f` without a name: force-attach directly to the cwd-derived name,
// or the full-path fallback when the cwd yields none (home / root).
if force {
name, err := session.FromCwd()
name, err := session.FromCwdOrFallback()
if err != nil {
return err
}
if name == "" {
return fmt.Errorf("could not derive a session name from the current directory")
}
return shpool.Attach(name, true)
}

Expand All @@ -98,7 +99,11 @@ func run(args []string) error {
}

func runPicker() error {
cwdName, err := session.FromCwd()
// Use the full-path fallback when the cwd yields no name (the home
// directory itself or the filesystem root) so the picker always has a
// default create-entry, just like any other directory, instead of
// dead-ending with "no sessions".
cwdName, err := session.FromCwdOrFallback()
if err != nil {
return err
}
Expand All @@ -111,11 +116,6 @@ func runPicker() error {
sessions = nil
}

if cwdName == "" && len(sessions) == 0 {
fmt.Fprintln(os.Stderr, "No shpool sessions found.")
return nil
}

picked, err := tui.SelectWithDefault(sessions, cwdName)
if err != nil {
if errors.Is(err, tui.ErrCancelled) {
Expand Down
34 changes: 34 additions & 0 deletions internal/session/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,40 @@ func FromCwd() (string, error) {
return FromPath(cwd, home), nil
}

// FromCwdOrFallback returns the cwd-derived session name, falling back to
// FallbackName when the cwd yields no name (the home directory itself or the
// filesystem root). The result is always non-empty, so callers can attach or
// print a name without dead-ending.
func FromCwdOrFallback() (string, error) {
name, err := FromCwd()
if err != nil {
return "", err
}
if name != "" {
return name, nil
}
return FallbackName()
}

// FallbackName returns a session name to use when FromCwd yields no name,
// which happens in the home directory itself or at the filesystem root. It
// derives the name from the full absolute path (without stripping the home
// prefix), so the home directory maps to e.g. "home.developer" rather than
// dead-ending. This keeps the name unique and consistent with FromPath's
// naming rules. It falls back to "shell" only when even the full path yields
// nothing (the filesystem root).
func FallbackName() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("get cwd: %w", err)
}
name := FromPath(cwd, "")
if name == "" {
name = "shell"
}
return name, nil
}

// FromPath converts an absolute path to a session name.
// If home is non-empty and path is under home, the home prefix is stripped first.
func FromPath(path, home string) string {
Expand Down
80 changes: 79 additions & 1 deletion internal/session/name_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package session

import "testing"
import (
"os"
"testing"
)

func TestFromPath(t *testing.T) {
cases := []struct {
Expand Down Expand Up @@ -89,6 +92,81 @@ func TestFromPath_JapaneseLen(t *testing.T) {
}
}

func TestFallbackName_DerivesFromFullPath(t *testing.T) {
t.Chdir(t.TempDir())

got, err := FallbackName()
if err != nil {
t.Fatalf("FallbackName() error: %v", err)
}
// The full (home-prefix-unstripped) path is used, so the name must match
// FromPath with no home, never be empty, and reflect the directory.
// Resolve the working directory via Getwd (TempDir may contain symlinks).
dir, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd error: %v", err)
}
want := FromPath(dir, "")
if got != want {
t.Errorf("FallbackName() = %q, want %q", got, want)
}
if got == "" {
t.Errorf("FallbackName() returned empty name for %q", dir)
}
}

func TestFallbackName_RootFallsBackToShell(t *testing.T) {
t.Chdir("/")

got, err := FallbackName()
if err != nil {
t.Fatalf("FallbackName() error: %v", err)
}
if got != "shell" {
t.Errorf("FallbackName() at root = %q, want %q", got, "shell")
}
}

func TestFromCwdOrFallback_AtHomeUsesFallback(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Skipf("no home directory: %v", err)
}
t.Chdir(home)

// The real trigger for the fallback: at home, FromCwd derives nothing.
if got, _ := FromCwd(); got != "" {
t.Fatalf("FromCwd() at home = %q, want empty (precondition for fallback)", got)
}

got, err := FromCwdOrFallback()
if err != nil {
t.Fatalf("FromCwdOrFallback() error: %v", err)
}
if got == "" {
t.Errorf("FromCwdOrFallback() at home = %q, want non-empty", got)
}
if want, _ := FallbackName(); got != want {
t.Errorf("FromCwdOrFallback() = %q, want FallbackName() = %q", got, want)
}
}

func TestFromCwdOrFallback_OutsideHomeUsesCwdName(t *testing.T) {
t.Chdir(t.TempDir())

cwd, _ := FromCwd()
if cwd == "" {
t.Skip("temp dir derived an empty name; cannot assert passthrough")
}
got, err := FromCwdOrFallback()
if err != nil {
t.Fatalf("FromCwdOrFallback() error: %v", err)
}
if got != cwd {
t.Errorf("FromCwdOrFallback() = %q, want FromCwd() = %q", got, cwd)
}
}

func TestFromPath_DisambiguatesSanitizedCollisions(t *testing.T) {
gotSpace := FromPath("/Users/u/foo bar", "/Users/u")
gotUnderscore := FromPath("/Users/u/foo_bar", "/Users/u")
Expand Down
Loading