diff --git a/.claude/plans/plan.md b/.claude/plans/plan.md index 6d0b444..2a6d77d 100644 --- a/.claude/plans/plan.md +++ b/.claude/plans/plan.md @@ -1,7 +1,13 @@ -# Plan: 検索マッチのハイライト表示 +# Plan: UI を gocui から Bubble Tea へ移行 ## タスク -- [x] 1. `highlightMatch(name, query string) string` ヘルパー関数を追加 -- [x] 2. `renderEntries`で検索クエリがある場合にハイライト適用 -- [x] 3. テスト作成(`highlightMatch`のユニットテスト) -- [x] 4. ビルド・テスト・フォーマット確認 +- [x] 1. ブランチ作成(feat/migrate-bubbletea) +- [x] 2. go get(bubbletea / lipgloss / bubbles) +- [x] 3. gomarkdoc で docs/lib/{bubbletea,lipgloss,textinput}.md 生成 +- [x] 4. App を tea.Model 化(Init/Update/View)— 通常モードのペイン描画・移動 +- [x] 5. 入力モード(textinput)・確認モード・検索/フィルタ・作成/リネーム移植 +- [x] 6. セレクタポップアップ移植(中央モーダル) +- [x] 7. gocui 依存コード削除(SetGui/Layout/render*/keybinding登録/inputEditor/selector View群) +- [x] 8. main.go 置換、flake.nix vendorHash 更新、docs/lib/gocui.md 削除、design.md/CLAUDE.md 更新 +- [x] 9. gofmt → golangci-lint → go test ./... → nix build +- [ ] 10. PR 作成 diff --git a/CLAUDE.md b/CLAUDE.md index 3aa8314..60eb366 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ Go ### ライブラリ -jroimartin/gocui +charmbracelet/bubbletea(描画: charmbracelet/lipgloss、入力欄: charmbracelet/bubbles の textinput) ## 規約 - ソースコード内のコメントは日本語で記述すること。 diff --git a/docs/design.md b/docs/design.md index a694b28..f40f092 100644 --- a/docs/design.md +++ b/docs/design.md @@ -31,7 +31,7 @@ ghqでリポジトリを管理しているCLIユーザー。 ### 技術スタック - 言語: Go -- TUIライブラリ: jroimartin/gocui +- TUIライブラリ: charmbracelet/bubbletea(描画は charmbracelet/lipgloss、入力欄は charmbracelet/bubbles の textinput を利用) ### 操作コンセプト @@ -58,9 +58,13 @@ graftx — 「接ぎ木(graft)」から。複数のソースから必要な graftx/ ├── main.go # エントリポイント ├── internal/ -│ ├── ui/ # GUI管理(レイアウト、View生成、キーバインド登録) -│ │ ├── layout.go -│ │ └── keybinding.go +│ ├── ui/ # TUI管理(tea.Model実装、描画、キー処理) +│ │ ├── layout.go # App構造体(tea.Model)と状態・共通ロジック +│ │ ├── model.go # Init/Update/View とキー入力のルーティング +│ │ ├── render.go # lipgloss によるペイン・ステータス・モーダルの描画 +│ │ ├── selector.go # リポジトリ選択ポップアップ +│ │ ├── keybinding.go # 各キー操作のハンドラ +│ │ └── icon.go # ファイル種別アイコン │ ├── pane/ # ペインの状態管理(カーソル位置、選択状態、現在のディレクトリ) │ │ └── pane.go │ ├── fs/ # ファイルシステム操作(一覧取得、コピー、削除) @@ -70,7 +74,9 @@ graftx/ ├── docs/ │ ├── design.md │ └── lib/ -│ └── gocui.md +│ ├── bubbletea.md +│ ├── lipgloss.md +│ └── textinput.md └── go.mod ``` @@ -78,11 +84,11 @@ graftx/ | パッケージ | 責務 | |-----------|------| -| `main` | gocui.Guiの初期化、Managerの登録、MainLoopの起動。MainLoopの再起動ループを管理する | -| `internal/ui` | gocui.ManagerインターフェースのLayout実装、View生成・配置、キーバインドの登録。`App`構造体を保持し、アプリケーション全体の状態を管理する。gocuiに直接依存する唯一のパッケージ | -| `internal/pane` | ペインの状態管理。現在のディレクトリパス、エントリ一覧、カーソル位置、選択中ファイル一覧、`YankBuffer`を保持。gocuiに依存しない | -| `internal/fs` | ファイルシステム操作。`FileSystem`インターフェースの定義と実装。ディレクトリのエントリ一覧取得、ファイル/ディレクトリのコピー、削除を提供。`Entry`構造体もここで定義する。gocuiに依存しない | -| `internal/selector` | `ghq list | fzf` の実行とリポジトリパスの取得。`CommandRunner`インターフェースの定義と実装。gocuiに依存しない | +| `main` | `tea.Program` の生成と起動(`tea.WithAltScreen()`)| +| `internal/ui` | `tea.Model`(`App`構造体)の実装。`Update`でキー入力を処理し、`View`で画面を描画する。アプリケーション全体の状態を保持する。TUIライブラリに直接依存する唯一のパッケージ | +| `internal/pane` | ペインの状態管理。現在のディレクトリパス、エントリ一覧、カーソル位置、選択中ファイル一覧、`YankBuffer`を保持。TUIライブラリに依存しない | +| `internal/fs` | ファイルシステム操作。`FileSystem`インターフェースの定義と実装。ディレクトリのエントリ一覧取得、ファイル/ディレクトリのコピー、削除を提供。`Entry`構造体もここで定義する。TUIライブラリに依存しない | +| `internal/selector` | `ghq list | fzf` の実行とリポジトリパスの取得。`CommandRunner`インターフェースの定義と実装。TUIライブラリに依存しない | ### コンポーネント間の依存関係 @@ -94,66 +100,26 @@ main └── selector ``` -- `main` → `ui`: GUIの初期化と起動を委譲 -- `ui` → `pane`: ペインの状態を参照・更新し、Viewに描画 -- `ui` → `selector`: コピー元リポジトリの選択を委譲 +- `main` → `ui`: TUIの初期化と起動を委譲 +- `ui` → `pane`: ペインの状態を参照・更新し、`View`で描画 +- `ui` → `selector`: コピー元リポジトリ一覧の取得を委譲 - `pane` → `fs`: ディレクトリ一覧取得やコピー操作を委譲 -- `fs`, `selector`, `pane` は `gocui` に依存しない(テスト容易性の確保) +- `fs`, `selector`, `pane` はTUIライブラリに依存しない(テスト容易性の確保) -### gocuiとの統合方針 +### Bubble Teaとの統合方針 -- `ui` パッケージが `gocui.Manager` インターフェースを実装し、`Layout` メソッドでViewの生成・更新を行う -- キーバインドのハンドラは `ui` パッケージ内で定義し、`pane` や `selector` のメソッドを呼び出す -- Viewの更新はgocuiのイベントループ内(Layout関数またはキーバインドコールバック内)でのみ行う - -### MainLoopの再起動パターン - -gocuiはターミナルを排他的に制御するため、fzfのような対話的な外部プロセスと共存できない。そのため、`main.go`でMainLoopの再起動ループを管理する。 - -```go -// main.go の概念的な構造 -func main() { - app := ui.NewApp() - - for { - g, err := gocui.NewGui(gocui.OutputNormal) - if err != nil { - log.Fatal(err) - } - - app.SetGui(g) // Layout・キーバインドを登録 - - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - log.Fatal(err) - } - g.Close() - - // MainLoop終了理由の判別 - switch app.ExitReason { - case ui.ExitReasonQuit: - return // アプリケーション終了 - case ui.ExitReasonSelectRepo: - // fzfを起動してリポジトリ選択 - repoPath, err := app.Selector.SelectRepository() - if err == nil && repoPath != "" { - app.SourcePane.ChangeDir(repoPath) - } - app.ExitReason = ui.ExitReasonNone - // ループ先頭に戻り、gocuiを再初期化 - } - } -} -``` - -- `App.ExitReason` フィールドでMainLoop終了理由を判別する(`q`キーによる終了 vs `s`キーによるfzf起動) -- gocui再初期化時、`App`構造体がループをまたいで生存するため、ペインの状態(ディレクトリ、カーソル位置、選択状態)は自動的に復元される -- `Layout`関数が`App`構造体の状態に基づいてViewを再描画するため、追加の復元処理は不要 +- `ui` パッケージの `App` 構造体が `tea.Model` インターフェース(`Init`/`Update`/`View`)を実装する +- `Update` はメッセージ(`tea.KeyMsg`・`tea.WindowSizeMsg`)を受け取り、現在のモード(通常/テキスト入力/確認/リポジトリ選択)に応じてキー入力をルーティングする +- 各キー操作のハンドラは `App` の状態と `pane`・`selector` を更新する。副作用が必要な場合は `tea.Cmd` を返す +- `View` は `App` の状態から画面文字列を生成する(純粋関数的)。lipgloss で左右ペイン・ステータスバー・リポジトリ選択モーダルを描画する +- リポジトリ選択は `s` キーでアプリ内のモーダル(`InputModeSelectRepo`)として表示する。`Selector.ListRepositories()` で取得した一覧を bubbles の textinput でフィルタし、Enter で確定する(外部プロセスでターミナルを奪わないため、イベントループの再起動は不要) +- 端末サイズは `tea.WindowSizeMsg` で受け取り `App.width`/`App.height` に保持する ### エラーハンドリング方針 - 画面下部にステータスバー用のViewを設け、エラーメッセージをユーザーに表示する - ファイル操作のエラー(権限不足、ディスク容量不足等)はステータスバーに表示し、操作を中断する -- 致命的エラー(gocui初期化失敗等)はプログラムを終了し、標準エラー出力にメッセージを出力する +- 致命的エラー(`tea.Program` の起動失敗等)はプログラムを終了し、標準エラー出力にメッセージを出力する ### テスト方針 @@ -162,7 +128,7 @@ func main() { | `internal/pane` | カーソル移動、選択状態管理、ディレクトリ遷移 | ユニットテスト。fsパッケージをインターフェースで注入しモック可能にする | | `internal/fs` | ディレクトリ一覧取得、コピー、削除 | ユニットテスト。一時ディレクトリを使用 | | `internal/selector` | ghq + fzf の呼び出しとパス解析 | ユニットテスト。コマンド実行をインターフェースで注入しモック可能にする | -| `internal/ui` | gocuiに直接依存するため | テスト対象外(ロジックは他パッケージに委譲済み) | +| `internal/ui` | キー入力のルーティング、状態遷移、`View`描画 | ユニットテスト。`Update`を直接呼び出して状態を検証し、`View`の出力文字列をアサートする(実端末不要) | ## 3. UIレイアウト @@ -218,20 +184,19 @@ func main() { ### カーソル・選択のハイライト -- カーソル行: gocuiの `Highlight` 機能で背景色を反転して表示 +- カーソル行: フォーカス中のペインで lipgloss の背景色(シアン)/前景色(黒)を適用して反転表示する - 選択済みファイル: 行頭に `*` マークを表示 ### フォーカス中ペインの視覚的区別 -- フォーカス中のペインはgocuiの `SelBgColor` / `SelFgColor` で枠線の色を変更する -- フォーカスしていないペインは通常の枠線色 +- フォーカス中のペインは lipgloss の枠線色(緑)で強調する +- フォーカスしていないペインはグレーの枠線色 ### ウィンドウリサイズ時の挙動 -- Layout関数で毎回 `g.Size()` を取得し、ペイン幅を再計算する -- 左ペイン: `x0=0, x1=maxX/2-1` -- 右ペイン: `x0=maxX/2, x1=maxX-1` -- ステータスバー: `x0=0, x1=maxX-1, y0=maxY-2, y1=maxY` +- `tea.WindowSizeMsg` で受け取った幅・高さを保持し、`View` でペイン幅を再計算する +- 左ペイン幅: `width/2`、右ペイン幅: `width - width/2` +- ステータスバー: 最下段の1行 ## 4. キーバインド仕様 @@ -359,12 +324,11 @@ func main() { `s`キーでコピー元リポジトリを選択する。 **動作フロー:** -1. gocuiのMainLoopを一時停止する(`gocui.ErrQuit`を返して一旦終了) -2. `ghq list --full-path | fzf` を外部プロセスとして実行し、ターミナルの制御を渡す -3. ユーザーがfzfでリポジトリを選択すると、選択されたパスを取得 -4. gocuiを再初期化してMainLoopを再開 -5. 左ペインに選択されたリポジトリのルートディレクトリを表示 -6. ユーザーがfzfをキャンセルした場合(Esc/Ctrl+C)は、元の状態に戻る +1. `s`キーで `Selector.ListRepositories()`(`ghq list --full-path`)を実行し、リポジトリ一覧を取得する +2. アプリ内に中央モーダル(`InputModeSelectRepo`)を表示する +3. bubbles の textinput でインクリメンタルにフィルタし、`Ctrl+j/k`(または `Ctrl+n/p`)でカーソルを移動する +4. Enter で確定すると、選択されたリポジトリのルートディレクトリを左ペインに表示する +5. Esc/Ctrl+C でキャンセルした場合は、元の状態に戻る ### 5.5 複数ファイル選択とコピー @@ -471,25 +435,26 @@ YankBuffer (internal/pane) type ExitReason int const ( - ExitReasonNone ExitReason = iota - ExitReasonQuit // qキーによる終了 - ExitReasonSelectRepo // sキーによるリポジトリ選択 + ExitReasonNone ExitReason = iota + ExitReasonQuit // qキーによる終了 ) +// App は tea.Model を実装する。リポジトリ選択はアプリ内モーダル(InputModeSelectRepo)で行う。 type App struct { - SourcePane *pane.Pane // 左ペイン(コピー元) - DestPane *pane.Pane // 右ペイン(コピー先) - YankBuffer *pane.YankBuffer // ヤンクバッファ + SourcePane *pane.Pane // 左ペイン(コピー元) + DestPane *pane.Pane // 右ペイン(コピー先) + YankBuf *pane.YankBuffer // ヤンクバッファ Selector selector.CommandRunner // リポジトリ選択 - FocusLeft bool // true: 左ペインにフォーカス, false: 右ペイン - Status string // ステータスバーに表示するメッセージ - ExitReason ExitReason // MainLoop終了理由 + FocusLeft bool // true: 左ペインにフォーカス, false: 右ペイン + Status string // ステータスバーに表示するメッセージ + ExitReason ExitReason // 終了理由 + // ほかに入力モード・検索クエリ・端末サイズ・textinput等の内部状態を保持する } ``` ### Pane(`internal/pane` パッケージ) -各ペインの状態を管理する構造体。gocuiに依存しない。 +各ペインの状態を管理する構造体。TUIライブラリに依存しない。 ```go type Pane struct { diff --git a/docs/lib/bubbletea.md b/docs/lib/bubbletea.md new file mode 100644 index 0000000..18d9972 --- /dev/null +++ b/docs/lib/bubbletea.md @@ -0,0 +1,1586 @@ + + +# tea + +```go +import "github.com/charmbracelet/bubbletea" +``` + +Package tea provides a framework for building rich terminal user interfaces based on the paradigms of The Elm Architecture. It's well\-suited for simple and complex terminal applications, either inline, full\-window, or a mix of both. It's been battle\-tested in several large projects and is production\-ready. + +A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/master/tutorials + +Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/master/examples + +## Index + +- [Variables](<#variables>) +- [func LogToFile\(path string, prefix string\) \(\*os.File, error\)](<#LogToFile>) +- [func LogToFileWith\(path string, prefix string, log LogOptionsSetter\) \(\*os.File, error\)](<#LogToFileWith>) +- [type BatchMsg](<#BatchMsg>) +- [type BlurMsg](<#BlurMsg>) +- [type Cmd](<#Cmd>) + - [func Batch\(cmds ...Cmd\) Cmd](<#Batch>) + - [func Every\(duration time.Duration, fn func\(time.Time\) Msg\) Cmd](<#Every>) + - [func Exec\(c ExecCommand, fn ExecCallback\) Cmd](<#Exec>) + - [func ExecProcess\(c \*exec.Cmd, fn ExecCallback\) Cmd](<#ExecProcess>) + - [func Printf\(template string, args ...interface\{\}\) Cmd](<#Printf>) + - [func Println\(args ...interface\{\}\) Cmd](<#Println>) + - [func ScrollDown\(newLines \[\]string, topBoundary, bottomBoundary int\) Cmd](<#ScrollDown>) + - [func ScrollUp\(newLines \[\]string, topBoundary, bottomBoundary int\) Cmd](<#ScrollUp>) + - [func Sequence\(cmds ...Cmd\) Cmd](<#Sequence>) + - [func Sequentially\(cmds ...Cmd\) Cmd](<#Sequentially>) + - [func SetWindowTitle\(title string\) Cmd](<#SetWindowTitle>) + - [func SyncScrollArea\(lines \[\]string, topBoundary int, bottomBoundary int\) Cmd](<#SyncScrollArea>) + - [func Tick\(d time.Duration, fn func\(time.Time\) Msg\) Cmd](<#Tick>) + - [func WindowSize\(\) Cmd](<#WindowSize>) +- [type ExecCallback](<#ExecCallback>) +- [type ExecCommand](<#ExecCommand>) +- [type FocusMsg](<#FocusMsg>) +- [type InterruptMsg](<#InterruptMsg>) +- [type Key](<#Key>) + - [func \(k Key\) String\(\) \(str string\)](<#Key.String>) +- [type KeyMsg](<#KeyMsg>) + - [func \(k KeyMsg\) String\(\) \(str string\)](<#KeyMsg.String>) +- [type KeyType](<#KeyType>) + - [func \(k KeyType\) String\(\) \(str string\)](<#KeyType.String>) +- [type LogOptionsSetter](<#LogOptionsSetter>) +- [type Model](<#Model>) +- [type MouseAction](<#MouseAction>) +- [type MouseButton](<#MouseButton>) +- [type MouseEvent](<#MouseEvent>) + - [func \(m MouseEvent\) IsWheel\(\) bool](<#MouseEvent.IsWheel>) + - [func \(m MouseEvent\) String\(\) \(s string\)](<#MouseEvent.String>) +- [type MouseEventType](<#MouseEventType>) +- [type MouseMsg](<#MouseMsg>) + - [func \(m MouseMsg\) String\(\) string](<#MouseMsg.String>) +- [type Msg](<#Msg>) + - [func ClearScreen\(\) Msg](<#ClearScreen>) + - [func ClearScrollArea\(\) Msg](<#ClearScrollArea>) + - [func DisableBracketedPaste\(\) Msg](<#DisableBracketedPaste>) + - [func DisableMouse\(\) Msg](<#DisableMouse>) + - [func DisableReportFocus\(\) Msg](<#DisableReportFocus>) + - [func EnableBracketedPaste\(\) Msg](<#EnableBracketedPaste>) + - [func EnableMouseAllMotion\(\) Msg](<#EnableMouseAllMotion>) + - [func EnableMouseCellMotion\(\) Msg](<#EnableMouseCellMotion>) + - [func EnableReportFocus\(\) Msg](<#EnableReportFocus>) + - [func EnterAltScreen\(\) Msg](<#EnterAltScreen>) + - [func ExitAltScreen\(\) Msg](<#ExitAltScreen>) + - [func HideCursor\(\) Msg](<#HideCursor>) + - [func Interrupt\(\) Msg](<#Interrupt>) + - [func Quit\(\) Msg](<#Quit>) + - [func ShowCursor\(\) Msg](<#ShowCursor>) + - [func Suspend\(\) Msg](<#Suspend>) +- [type Program](<#Program>) + - [func NewProgram\(model Model, opts ...ProgramOption\) \*Program](<#NewProgram>) + - [func \(p \*Program\) DisableMouseAllMotion\(\)](<#Program.DisableMouseAllMotion>) + - [func \(p \*Program\) DisableMouseCellMotion\(\)](<#Program.DisableMouseCellMotion>) + - [func \(p \*Program\) EnableMouseAllMotion\(\)](<#Program.EnableMouseAllMotion>) + - [func \(p \*Program\) EnableMouseCellMotion\(\)](<#Program.EnableMouseCellMotion>) + - [func \(p \*Program\) EnterAltScreen\(\)](<#Program.EnterAltScreen>) + - [func \(p \*Program\) ExitAltScreen\(\)](<#Program.ExitAltScreen>) + - [func \(p \*Program\) Kill\(\)](<#Program.Kill>) + - [func \(p \*Program\) Printf\(template string, args ...interface\{\}\)](<#Program.Printf>) + - [func \(p \*Program\) Println\(args ...interface\{\}\)](<#Program.Println>) + - [func \(p \*Program\) Quit\(\)](<#Program.Quit>) + - [func \(p \*Program\) ReleaseTerminal\(\) error](<#Program.ReleaseTerminal>) + - [func \(p \*Program\) RestoreTerminal\(\) error](<#Program.RestoreTerminal>) + - [func \(p \*Program\) Run\(\) \(returnModel Model, returnErr error\)](<#Program.Run>) + - [func \(p \*Program\) Send\(msg Msg\)](<#Program.Send>) + - [func \(p \*Program\) SetWindowTitle\(title string\)](<#Program.SetWindowTitle>) + - [func \(p \*Program\) Start\(\) error](<#Program.Start>) + - [func \(p \*Program\) StartReturningModel\(\) \(Model, error\)](<#Program.StartReturningModel>) + - [func \(p \*Program\) Wait\(\)](<#Program.Wait>) +- [type ProgramOption](<#ProgramOption>) + - [func WithANSICompressor\(\) ProgramOption](<#WithANSICompressor>) + - [func WithAltScreen\(\) ProgramOption](<#WithAltScreen>) + - [func WithContext\(ctx context.Context\) ProgramOption](<#WithContext>) + - [func WithEnvironment\(env \[\]string\) ProgramOption](<#WithEnvironment>) + - [func WithFPS\(fps int\) ProgramOption](<#WithFPS>) + - [func WithFilter\(filter func\(Model, Msg\) Msg\) ProgramOption](<#WithFilter>) + - [func WithInput\(input io.Reader\) ProgramOption](<#WithInput>) + - [func WithInputTTY\(\) ProgramOption](<#WithInputTTY>) + - [func WithMouseAllMotion\(\) ProgramOption](<#WithMouseAllMotion>) + - [func WithMouseCellMotion\(\) ProgramOption](<#WithMouseCellMotion>) + - [func WithOutput\(output io.Writer\) ProgramOption](<#WithOutput>) + - [func WithReportFocus\(\) ProgramOption](<#WithReportFocus>) + - [func WithoutBracketedPaste\(\) ProgramOption](<#WithoutBracketedPaste>) + - [func WithoutCatchPanics\(\) ProgramOption](<#WithoutCatchPanics>) + - [func WithoutRenderer\(\) ProgramOption](<#WithoutRenderer>) + - [func WithoutSignalHandler\(\) ProgramOption](<#WithoutSignalHandler>) + - [func WithoutSignals\(\) ProgramOption](<#WithoutSignals>) +- [type QuitMsg](<#QuitMsg>) +- [type ResumeMsg](<#ResumeMsg>) +- [type SuspendMsg](<#SuspendMsg>) +- [type WindowSizeMsg](<#WindowSizeMsg>) + + +## Variables + +ErrInterrupted is returned by [Program.Run](<#Program.Run>) when the program get a SIGINT signal, or when it receives a [InterruptMsg](<#InterruptMsg>). + +```go +var ErrInterrupted = errors.New("program was interrupted") +``` + +ErrProgramKilled is returned by [Program.Run](<#Program.Run>) when the program gets killed. + +```go +var ErrProgramKilled = errors.New("program was killed") +``` + +ErrProgramPanic is returned by [Program.Run](<#Program.Run>) when the program recovers from a panic. + +```go +var ErrProgramPanic = errors.New("program experienced a panic") +``` + + +## func LogToFile + +```go +func LogToFile(path string, prefix string) (*os.File, error) +``` + +LogToFile sets up default logging to log to a file. This is helpful as we can't print to the terminal since our TUI is occupying it. If the file doesn't exist it will be created. + +Don't forget to close the file when you're done with it. + +``` +f, err := LogToFile("debug.log", "debug") + if err != nil { + fmt.Println("fatal:", err) + os.Exit(1) + } + defer f.Close() +``` + + +## func LogToFileWith + +```go +func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) +``` + +LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter. + + +## type BatchMsg + +BatchMsg is a message used to perform a bunch of commands concurrently with no ordering guarantees. You can send a BatchMsg with Batch. + +```go +type BatchMsg []Cmd +``` + + +## type BlurMsg + +BlurMsg represents a terminal blur message. This occurs when the terminal loses focus. + +```go +type BlurMsg struct{} +``` + + +## type Cmd + +Cmd is an IO operation that returns a message when it's complete. If it's nil it's considered a no\-op. Use it for things like HTTP requests, timers, saving and loading from disk, and so on. + +Note that there's almost never a reason to use a command to send a message to another part of your program. That can almost always be done in the update function. + +```go +type Cmd func() Msg +``` + + +### func Batch + +```go +func Batch(cmds ...Cmd) Cmd +``` + +Batch performs a bunch of commands concurrently with no ordering guarantees about the results. Use a Batch to return several commands. + +Example: + +``` +func (m model) Init() Cmd { + return tea.Batch(someCommand, someOtherCommand) + } +``` + + +### func Every + +```go +func Every(duration time.Duration, fn func(time.Time) Msg) Cmd +``` + +Every is a command that ticks in sync with the system clock. So, if you wanted to tick with the system clock every second, minute or hour you could use this. It's also handy for having different things tick in sync. + +Because we're ticking with the system clock the tick will likely not run for the entire specified duration. For example, if we're ticking for one minute and the clock is at 12:34:20 then the next tick will happen at 12:35:00, 40 seconds later. + +To produce the command, pass a duration and a function which returns a message containing the time at which the tick occurred. + +``` +type TickMsg time.Time + +cmd := Every(time.Second, func(t time.Time) Msg { + return TickMsg(t) +}) +``` + +Beginners' note: Every sends a single message and won't automatically dispatch messages at an interval. To do that, you'll want to return another Every command after receiving your tick message. For example: + +``` +type TickMsg time.Time + +// Send a message every second. +func tickEvery() Cmd { + return Every(time.Second, func(t time.Time) Msg { + return TickMsg(t) + }) +} + +func (m model) Init() Cmd { + // Start ticking. + return tickEvery() +} + +func (m model) Update(msg Msg) (Model, Cmd) { + switch msg.(type) { + case TickMsg: + // Return your Every command again to loop. + return m, tickEvery() + } + return m, nil +} +``` + +Every is analogous to Tick in the Elm Architecture. + + +### func Exec + +```go +func Exec(c ExecCommand, fn ExecCallback) Cmd +``` + +Exec is used to perform arbitrary I/O in a blocking fashion, effectively pausing the Program while execution is running and resuming it when execution has completed. + +Most of the time you'll want to use ExecProcess, which runs an exec.Cmd. + +For non\-interactive i/o you should use a Cmd \(that is, a tea.Cmd\). + + +### func ExecProcess + +```go +func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd +``` + +ExecProcess runs the given \*exec.Cmd in a blocking fashion, effectively pausing the Program while the command is running. After the \*exec.Cmd exists the Program resumes. It's useful for spawning other interactive applications such as editors and shells from within a Program. + +To produce the command, pass an \*exec.Cmd and a function which returns a message containing the error which may have occurred when running the ExecCommand. + +``` +type VimFinishedMsg struct { err error } + +c := exec.Command("vim", "file.txt") + +cmd := ExecProcess(c, func(err error) Msg { + return VimFinishedMsg{err: err} +}) +``` + +Or, if you don't care about errors, you could simply: + +``` +cmd := ExecProcess(exec.Command("vim", "file.txt"), nil) +``` + +For non\-interactive i/o you should use a Cmd \(that is, a tea.Cmd\). + + +### func Printf + +```go +func Printf(template string, args ...interface{}) Cmd +``` + +Printf prints above the Program. It takes a format template followed by values similar to fmt.Printf. This output is unmanaged by the program and will persist across renders by the Program. + +Unlike fmt.Printf \(but similar to log.Printf\) the message will be print on its own line. + +If the altscreen is active no output will be printed. + + +### func Println + +```go +func Println(args ...interface{}) Cmd +``` + +Println prints above the Program. This output is unmanaged by the program and will persist across renders by the Program. + +Unlike fmt.Println \(but similar to log.Println\) the message will be print on its own line. + +If the altscreen is active no output will be printed. + + +### func ScrollDown + +```go +func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd +``` + +ScrollDown adds lines to the bottom of the scrollable region, pushing existing lines above up. Lines that are pushed out of the scrollable region disappear from view. + +For high\-performance, scroll\-based rendering only. + +Deprecated: This option will be removed in a future version of this package. + + +### func ScrollUp + +```go +func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd +``` + +ScrollUp adds lines to the top of the scrollable region, pushing existing lines below down. Lines that are pushed out the scrollable region disappear from view. + +For high\-performance, scroll\-based rendering only. + +Deprecated: This option will be removed in a future version of this package. + + +### func Sequence + +```go +func Sequence(cmds ...Cmd) Cmd +``` + +Sequence runs the given commands one at a time, in order. Contrast this with Batch, which runs commands concurrently. + + +### func Sequentially + +```go +func Sequentially(cmds ...Cmd) Cmd +``` + +Sequentially produces a command that sequentially executes the given commands. The Msg returned is the first non\-nil message returned by a Cmd. + +``` +func saveStateCmd() Msg { + if err := save(); err != nil { + return errMsg{err} + } + return nil +} + +cmd := Sequentially(saveStateCmd, Quit) +``` + +Deprecated: use Sequence instead. + + +### func SetWindowTitle + +```go +func SetWindowTitle(title string) Cmd +``` + +SetWindowTitle produces a command that sets the terminal title. + +For example: + +``` +func (m model) Init() Cmd { + // Set title. + return tea.SetWindowTitle("My App") +} +``` + + +### func SyncScrollArea + +```go +func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd +``` + +SyncScrollArea performs a paint of the entire region designated to be the scrollable area. This is required to initialize the scrollable region and should also be called on resize \(WindowSizeMsg\). + +For high\-performance, scroll\-based rendering only. + +Deprecated: This option will be removed in a future version of this package. + + +### func Tick + +```go +func Tick(d time.Duration, fn func(time.Time) Msg) Cmd +``` + +Tick produces a command at an interval independent of the system clock at the given duration. That is, the timer begins precisely when invoked, and runs for its entire duration. + +To produce the command, pass a duration and a function which returns a message containing the time at which the tick occurred. + +``` +type TickMsg time.Time + +cmd := Tick(time.Second, func(t time.Time) Msg { + return TickMsg(t) +}) +``` + +Beginners' note: Tick sends a single message and won't automatically dispatch messages at an interval. To do that, you'll want to return another Tick command after receiving your tick message. For example: + +``` +type TickMsg time.Time + +func doTick() Cmd { + return Tick(time.Second, func(t time.Time) Msg { + return TickMsg(t) + }) +} + +func (m model) Init() Cmd { + // Start ticking. + return doTick() +} + +func (m model) Update(msg Msg) (Model, Cmd) { + switch msg.(type) { + case TickMsg: + // Return your Tick command again to loop. + return m, doTick() + } + return m, nil +} +``` + + +### func WindowSize + +```go +func WindowSize() Cmd +``` + +WindowSize is a command that queries the terminal for its current size. It delivers the results to Update via a [WindowSizeMsg](<#WindowSizeMsg>). Keep in mind that WindowSizeMsgs will automatically be delivered to Update when the [Program](<#Program>) starts and when the window dimensions change so in many cases you will not need to explicitly invoke this command. + + +## type ExecCallback + +ExecCallback is used when executing an \*exec.Command to return a message with an error, which may or may not be nil. + +```go +type ExecCallback func(error) Msg +``` + + +## type ExecCommand + +ExecCommand can be implemented to execute things in a blocking fashion in the current terminal. + +```go +type ExecCommand interface { + Run() error + SetStdin(io.Reader) + SetStdout(io.Writer) + SetStderr(io.Writer) +} +``` + + +## type FocusMsg + +FocusMsg represents a terminal focus message. This occurs when the terminal gains focus. + +```go +type FocusMsg struct{} +``` + + +## type InterruptMsg + +InterruptMsg signals the program should suspend. This usually happens when ctrl\+c is pressed on common programs, but since bubbletea puts the terminal in raw mode, we need to handle it in a per\-program basis. + +You can send this message with \[Interrupt\(\)\]. + +```go +type InterruptMsg struct{} +``` + + +## type Key + +Key contains information about a keypress. + +```go +type Key struct { + Type KeyType + Runes []rune + Alt bool + Paste bool +} +``` + + +### func \(Key\) String + +```go +func (k Key) String() (str string) +``` + +String returns a friendly string representation for a key. It's safe \(and encouraged\) for use in key comparison. + +``` +k := Key{Type: KeyEnter} +fmt.Println(k) +// Output: enter +``` + + +## type KeyMsg + +KeyMsg contains information about a keypress. KeyMsgs are always sent to the program's update function. There are a couple general patterns you could use to check for keypresses: + +``` +// Switch on the string representation of the key (shorter) +switch msg := msg.(type) { +case KeyMsg: + switch msg.String() { + case "enter": + fmt.Println("you pressed enter!") + case "a": + fmt.Println("you pressed a!") + } +} + +// Switch on the key type (more foolproof) +switch msg := msg.(type) { +case KeyMsg: + switch msg.Type { + case KeyEnter: + fmt.Println("you pressed enter!") + case KeyRunes: + switch string(msg.Runes) { + case "a": + fmt.Println("you pressed a!") + } + } +} +``` + +Note that Key.Runes will always contain at least one character, so you can always safely call Key.Runes\[0\]. In most cases Key.Runes will only contain one character, though certain input method editors \(most notably Chinese IMEs\) can input multiple runes at once. + +```go +type KeyMsg Key +``` + + +### func \(KeyMsg\) String + +```go +func (k KeyMsg) String() (str string) +``` + +String returns a string representation for a key message. It's safe \(and encouraged\) for use in key comparison. + + +## type KeyType + +KeyType indicates the key pressed, such as KeyEnter or KeyBreak or KeyCtrlC. All other keys will be type KeyRunes. To get the rune value, check the Rune method on a Key struct, or use the Key.String\(\) method: + +``` +k := Key{Type: KeyRunes, Runes: []rune{'a'}, Alt: true} +if k.Type == KeyRunes { + + fmt.Println(k.Runes) + // Output: a + + fmt.Println(k.String()) + // Output: alt+a + +} +``` + +```go +type KeyType int +``` + +Control key aliases. + +```go +const ( + KeyNull KeyType = keyNUL + KeyBreak KeyType = keyETX + KeyEnter KeyType = keyCR + KeyBackspace KeyType = keyDEL + KeyTab KeyType = keyHT + KeyEsc KeyType = keyESC + KeyEscape KeyType = keyESC + + KeyCtrlAt KeyType = keyNUL // ctrl+@ + KeyCtrlA KeyType = keySOH + KeyCtrlB KeyType = keySTX + KeyCtrlC KeyType = keyETX + KeyCtrlD KeyType = keyEOT + KeyCtrlE KeyType = keyENQ + KeyCtrlF KeyType = keyACK + KeyCtrlG KeyType = keyBEL + KeyCtrlH KeyType = keyBS + KeyCtrlI KeyType = keyHT + KeyCtrlJ KeyType = keyLF + KeyCtrlK KeyType = keyVT + KeyCtrlL KeyType = keyFF + KeyCtrlM KeyType = keyCR + KeyCtrlN KeyType = keySO + KeyCtrlO KeyType = keySI + KeyCtrlP KeyType = keyDLE + KeyCtrlQ KeyType = keyDC1 + KeyCtrlR KeyType = keyDC2 + KeyCtrlS KeyType = keyDC3 + KeyCtrlT KeyType = keyDC4 + KeyCtrlU KeyType = keyNAK + KeyCtrlV KeyType = keySYN + KeyCtrlW KeyType = keyETB + KeyCtrlX KeyType = keyCAN + KeyCtrlY KeyType = keyEM + KeyCtrlZ KeyType = keySUB + KeyCtrlOpenBracket KeyType = keyESC // ctrl+[ + KeyCtrlBackslash KeyType = keyFS // ctrl+\ + KeyCtrlCloseBracket KeyType = keyGS // ctrl+] + KeyCtrlCaret KeyType = keyRS // ctrl+^ + KeyCtrlUnderscore KeyType = keyUS // ctrl+_ + KeyCtrlQuestionMark KeyType = keyDEL // ctrl+? +) +``` + +Other keys. + +```go +const ( + KeyRunes KeyType = -(iota + 1) + KeyUp + KeyDown + KeyRight + KeyLeft + KeyShiftTab + KeyHome + KeyEnd + KeyPgUp + KeyPgDown + KeyCtrlPgUp + KeyCtrlPgDown + KeyDelete + KeyInsert + KeySpace + KeyCtrlUp + KeyCtrlDown + KeyCtrlRight + KeyCtrlLeft + KeyCtrlHome + KeyCtrlEnd + KeyShiftUp + KeyShiftDown + KeyShiftRight + KeyShiftLeft + KeyShiftHome + KeyShiftEnd + KeyCtrlShiftUp + KeyCtrlShiftDown + KeyCtrlShiftLeft + KeyCtrlShiftRight + KeyCtrlShiftHome + KeyCtrlShiftEnd + KeyF1 + KeyF2 + KeyF3 + KeyF4 + KeyF5 + KeyF6 + KeyF7 + KeyF8 + KeyF9 + KeyF10 + KeyF11 + KeyF12 + KeyF13 + KeyF14 + KeyF15 + KeyF16 + KeyF17 + KeyF18 + KeyF19 + KeyF20 +) +``` + + +### func \(KeyType\) String + +```go +func (k KeyType) String() (str string) +``` + + + + +## type LogOptionsSetter + +LogOptionsSetter is an interface implemented by stdlib's log and charm's log libraries. + +```go +type LogOptionsSetter interface { + SetOutput(io.Writer) + SetPrefix(string) +} +``` + + +## type Model + +Model contains the program's state as well as its core functions. + +```go +type Model interface { + // Init is the first function that will be called. It returns an optional + // initial command. To not perform an initial command return nil. + Init() Cmd + + // Update is called when a message is received. Use it to inspect messages + // and, in response, update the model and/or send a command. + Update(Msg) (Model, Cmd) + + // View renders the program's UI, which is just a string. The view is + // rendered after every Update. + View() string +} +``` + + +## type MouseAction + +MouseAction represents the action that occurred during a mouse event. + +```go +type MouseAction int +``` + +Mouse event actions. + +```go +const ( + MouseActionPress MouseAction = iota + MouseActionRelease + MouseActionMotion +) +``` + + +## type MouseButton + +MouseButton represents the button that was pressed during a mouse event. + +```go +type MouseButton int +``` + +Mouse event buttons + +This is based on X11 mouse button codes. + +``` +1 = left button +2 = middle button (pressing the scroll wheel) +3 = right button +4 = turn scroll wheel up +5 = turn scroll wheel down +6 = push scroll wheel left +7 = push scroll wheel right +8 = 4th button (aka browser backward button) +9 = 5th button (aka browser forward button) +10 +11 +``` + +Other buttons are not supported. + +```go +const ( + MouseButtonNone MouseButton = iota + MouseButtonLeft + MouseButtonMiddle + MouseButtonRight + MouseButtonWheelUp + MouseButtonWheelDown + MouseButtonWheelLeft + MouseButtonWheelRight + MouseButtonBackward + MouseButtonForward + MouseButton10 + MouseButton11 +) +``` + + +## type MouseEvent + +MouseEvent represents a mouse event, which could be a click, a scroll wheel movement, a cursor movement, or a combination. + +```go +type MouseEvent struct { + X int + Y int + Shift bool + Alt bool + Ctrl bool + Action MouseAction + Button MouseButton + + // Deprecated: Use MouseAction & MouseButton instead. + Type MouseEventType +} +``` + + +### func \(MouseEvent\) IsWheel + +```go +func (m MouseEvent) IsWheel() bool +``` + +IsWheel returns true if the mouse event is a wheel event. + + +### func \(MouseEvent\) String + +```go +func (m MouseEvent) String() (s string) +``` + +String returns a string representation of a mouse event. + + +## type MouseEventType + +MouseEventType indicates the type of mouse event occurring. + +Deprecated: Use MouseAction & MouseButton instead. + +```go +type MouseEventType int +``` + +Mouse event types. + +Deprecated: Use MouseAction & MouseButton instead. + +```go +const ( + MouseUnknown MouseEventType = iota + MouseLeft + MouseRight + MouseMiddle + MouseRelease // mouse button release (X10 only) + MouseWheelUp + MouseWheelDown + MouseWheelLeft + MouseWheelRight + MouseBackward + MouseForward + MouseMotion +) +``` + + +## type MouseMsg + +MouseMsg contains information about a mouse event and are sent to a programs update function when mouse activity occurs. Note that the mouse must first be enabled in order for the mouse events to be received. + +```go +type MouseMsg MouseEvent +``` + + +### func \(MouseMsg\) String + +```go +func (m MouseMsg) String() string +``` + +String returns a string representation of a mouse event. + + +## type Msg + +Msg contain data from the result of a IO operation. Msgs trigger the update function and, henceforth, the UI. + +```go +type Msg interface{} +``` + + +### func ClearScreen + +```go +func ClearScreen() Msg +``` + +ClearScreen is a special command that tells the program to clear the screen before the next update. This can be used to move the cursor to the top left of the screen and clear visual clutter when the alt screen is not in use. + +Note that it should never be necessary to call ClearScreen\(\) for regular redraws. + + +### func ClearScrollArea + +```go +func ClearScrollArea() Msg +``` + +ClearScrollArea deallocates the scrollable region and returns the control of those lines to the main rendering routine. + +For high\-performance, scroll\-based rendering only. + +Deprecated: This option will be removed in a future version of this package. + + +### func DisableBracketedPaste + +```go +func DisableBracketedPaste() Msg +``` + +DisableBracketedPaste is a special command that tells the Bubble Tea program to stop processing bracketed paste input. + +Note that bracketed paste will be automatically disabled when the program quits. + + +### func DisableMouse + +```go +func DisableMouse() Msg +``` + +DisableMouse is a special command that stops listening for mouse events. + + +### func DisableReportFocus + +```go +func DisableReportFocus() Msg +``` + +DisableReportFocus is a special command that tells the Bubble Tea program to stop reporting focus events to the program. + + +### func EnableBracketedPaste + +```go +func EnableBracketedPaste() Msg +``` + +EnableBracketedPaste is a special command that tells the Bubble Tea program to accept bracketed paste input. + +Note that bracketed paste will be automatically disabled when the program quits. + + +### func EnableMouseAllMotion + +```go +func EnableMouseAllMotion() Msg +``` + +EnableMouseAllMotion is a special command that enables mouse click, release, wheel, and motion events, which are delivered regardless of whether a mouse button is pressed, effectively enabling support for hover interactions. + +Many modern terminals support this, but not all. If in doubt, use EnableMouseCellMotion instead. + +Because commands run asynchronously, this command should not be used in your model's Init function. Use the WithMouseAllMotion ProgramOption instead. + + +### func EnableMouseCellMotion + +```go +func EnableMouseCellMotion() Msg +``` + +EnableMouseCellMotion is a special command that enables mouse click, release, and wheel events. Mouse movement events are also captured if a mouse button is pressed \(i.e., drag events\). + +Because commands run asynchronously, this command should not be used in your model's Init function. Use the WithMouseCellMotion ProgramOption instead. + + +### func EnableReportFocus + +```go +func EnableReportFocus() Msg +``` + +EnableReportFocus is a special command that tells the Bubble Tea program to report focus events to the program. + + +### func EnterAltScreen + +```go +func EnterAltScreen() Msg +``` + +EnterAltScreen is a special command that tells the Bubble Tea program to enter the alternate screen buffer. + +Because commands run asynchronously, this command should not be used in your model's Init function. To initialize your program with the altscreen enabled use the WithAltScreen ProgramOption instead. + + +### func ExitAltScreen + +```go +func ExitAltScreen() Msg +``` + +ExitAltScreen is a special command that tells the Bubble Tea program to exit the alternate screen buffer. This command should be used to exit the alternate screen buffer while the program is running. + +Note that the alternate screen buffer will be automatically exited when the program quits. + + +### func HideCursor + +```go +func HideCursor() Msg +``` + +HideCursor is a special command for manually instructing Bubble Tea to hide the cursor. In some rare cases, certain operations will cause the terminal to show the cursor, which is normally hidden for the duration of a Bubble Tea program's lifetime. You will most likely not need to use this command. + + +### func Interrupt + +```go +func Interrupt() Msg +``` + +Interrupt is a special command that tells the Bubble Tea program to interrupt. + + +### func Quit + +```go +func Quit() Msg +``` + +Quit is a special command that tells the Bubble Tea program to exit. + + +### func ShowCursor + +```go +func ShowCursor() Msg +``` + +ShowCursor is a special command for manually instructing Bubble Tea to show the cursor. + + +### func Suspend + +```go +func Suspend() Msg +``` + +Suspend is a special command that tells the Bubble Tea program to suspend. + + +## type Program + +Program is a terminal user interface. + +```go +type Program struct { + // contains filtered or unexported fields +} +``` + + +### func NewProgram + +```go +func NewProgram(model Model, opts ...ProgramOption) *Program +``` + +NewProgram creates a new Program. + + +### func \(\*Program\) DisableMouseAllMotion + +```go +func (p *Program) DisableMouseAllMotion() +``` + +DisableMouseAllMotion disables All Motion mouse tracking. This will be called automatically when exiting a Bubble Tea program. + +Deprecated: The mouse will automatically be disabled when the program exits. + + +### func \(\*Program\) DisableMouseCellMotion + +```go +func (p *Program) DisableMouseCellMotion() +``` + +DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be called automatically when exiting a Bubble Tea program. + +Deprecated: The mouse will automatically be disabled when the program exits. + + +### func \(\*Program\) EnableMouseAllMotion + +```go +func (p *Program) EnableMouseAllMotion() +``` + +EnableMouseAllMotion enables mouse click, release, wheel and motion events, regardless of whether a mouse button is pressed. Many modern terminals support this, but not all. + +Deprecated: Use the WithMouseAllMotion ProgramOption instead. + + +### func \(\*Program\) EnableMouseCellMotion + +```go +func (p *Program) EnableMouseCellMotion() +``` + +EnableMouseCellMotion enables mouse click, release, wheel and motion events if a mouse button is pressed \(i.e., drag events\). + +Deprecated: Use the WithMouseCellMotion ProgramOption instead. + + +### func \(\*Program\) EnterAltScreen + +```go +func (p *Program) EnterAltScreen() +``` + +EnterAltScreen enters the alternate screen buffer, which consumes the entire terminal window. ExitAltScreen will return the terminal to its former state. + +Deprecated: Use the WithAltScreen ProgramOption instead. + + +### func \(\*Program\) ExitAltScreen + +```go +func (p *Program) ExitAltScreen() +``` + +ExitAltScreen exits the alternate screen buffer. + +Deprecated: The altscreen will exited automatically when the program exits. + + +### func \(\*Program\) Kill + +```go +func (p *Program) Kill() +``` + +Kill signals the program to stop immediately and restore the former terminal state. The final render that you would normally see when quitting will be skipped. \[program.Run\] returns a [ErrProgramKilled](<#ErrProgramKilled>) error. + + +### func \(\*Program\) Printf + +```go +func (p *Program) Printf(template string, args ...interface{}) +``` + +Printf prints above the Program. It takes a format template followed by values similar to fmt.Printf. This output is unmanaged by the program and will persist across renders by the Program. + +Unlike fmt.Printf \(but similar to log.Printf\) the message will be print on its own line. + +If the altscreen is active no output will be printed. + + +### func \(\*Program\) Println + +```go +func (p *Program) Println(args ...interface{}) +``` + +Println prints above the Program. This output is unmanaged by the program and will persist across renders by the Program. + +If the altscreen is active no output will be printed. + + +### func \(\*Program\) Quit + +```go +func (p *Program) Quit() +``` + +Quit is a convenience function for quitting Bubble Tea programs. Use it when you need to shut down a Bubble Tea program from the outside. + +If you wish to quit from within a Bubble Tea program use the Quit command. + +If the program is not running this will be a no\-op, so it's safe to call if the program is unstarted or has already exited. + + +### func \(\*Program\) ReleaseTerminal + +```go +func (p *Program) ReleaseTerminal() error +``` + +ReleaseTerminal restores the original terminal state and cancels the input reader. You can return control to the Program with RestoreTerminal. + + +### func \(\*Program\) RestoreTerminal + +```go +func (p *Program) RestoreTerminal() error +``` + +RestoreTerminal reinitializes the Program's input reader, restores the terminal to the former state when the program was running, and repaints. Use it to reinitialize a Program after running ReleaseTerminal. + + +### func \(\*Program\) Run + +```go +func (p *Program) Run() (returnModel Model, returnErr error) +``` + +Run initializes the program and runs its event loops, blocking until it gets terminated by either [Program.Quit](<#Program.Quit>), [Program.Kill](<#Program.Kill>), or its signal handler. Returns the final model. + + +### func \(\*Program\) Send + +```go +func (p *Program) Send(msg Msg) +``` + +Send sends a message to the main update function, effectively allowing messages to be injected from outside the program for interoperability purposes. + +If the program hasn't started yet this will be a blocking operation. If the program has already been terminated this will be a no\-op, so it's safe to send messages after the program has exited. + + +### func \(\*Program\) SetWindowTitle + +```go +func (p *Program) SetWindowTitle(title string) +``` + +SetWindowTitle sets the terminal window title. + +Deprecated: Use the SetWindowTitle command instead. + + +### func \(\*Program\) Start + +```go +func (p *Program) Start() error +``` + +Start initializes the program and runs its event loops, blocking until it gets terminated by either [Program.Quit](<#Program.Quit>), [Program.Kill](<#Program.Kill>), or its signal handler. + +Deprecated: please use [Program.Run](<#Program.Run>) instead. + + +### func \(\*Program\) StartReturningModel + +```go +func (p *Program) StartReturningModel() (Model, error) +``` + +StartReturningModel initializes the program and runs its event loops, blocking until it gets terminated by either [Program.Quit](<#Program.Quit>), [Program.Kill](<#Program.Kill>), or its signal handler. Returns the final model. + +Deprecated: please use [Program.Run](<#Program.Run>) instead. + + +### func \(\*Program\) Wait + +```go +func (p *Program) Wait() +``` + +Wait waits/blocks until the underlying Program finished shutting down. + + +## type ProgramOption + +ProgramOption is used to set options when initializing a Program. Program can accept a variable number of options. + +Example usage: + +``` +p := NewProgram(model, WithInput(someInput), WithOutput(someOutput)) +``` + +```go +type ProgramOption func(*Program) +``` + + +### func WithANSICompressor + +```go +func WithANSICompressor() ProgramOption +``` + +WithANSICompressor removes redundant ANSI sequences to produce potentially smaller output, at the cost of some processing overhead. + +This feature is provisional, and may be changed or removed in a future version of this package. + +Deprecated: this incurs a noticeable performance hit. A future release will optimize ANSI automatically without the performance penalty. + + +### func WithAltScreen + +```go +func WithAltScreen() ProgramOption +``` + +WithAltScreen starts the program with the alternate screen buffer enabled \(i.e. the program starts in full window mode\). Note that the altscreen will be automatically exited when the program quits. + +Example: + +``` +p := tea.NewProgram(Model{}, tea.WithAltScreen()) +if _, err := p.Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) +} +``` + +To enter the altscreen once the program has already started running use the EnterAltScreen command. + + +### func WithContext + +```go +func WithContext(ctx context.Context) ProgramOption +``` + +WithContext lets you specify a context in which to run the Program. This is useful if you want to cancel the execution from outside. When a Program gets cancelled it will exit with an error ErrProgramKilled. + + +### func WithEnvironment + +```go +func WithEnvironment(env []string) ProgramOption +``` + +WithEnvironment sets the environment variables that the program will use. This useful when the program is running in a remote session \(e.g. SSH\) and you want to pass the environment variables from the remote session to the program. + +Example: + +``` +var sess ssh.Session // ssh.Session is a type from the github.com/charmbracelet/ssh package +pty, _, _ := sess.Pty() +environ := append(sess.Environ(), "TERM="+pty.Term) +p := tea.NewProgram(model, tea.WithEnvironment(environ) +``` + + +### func WithFPS + +```go +func WithFPS(fps int) ProgramOption +``` + +WithFPS sets a custom maximum FPS at which the renderer should run. If less than 1, the default value of 60 will be used. If over 120, the FPS will be capped at 120. + + +### func WithFilter + +```go +func WithFilter(filter func(Model, Msg) Msg) ProgramOption +``` + +WithFilter supplies an event filter that will be invoked before Bubble Tea processes a tea.Msg. The event filter can return any tea.Msg which will then get handled by Bubble Tea instead of the original event. If the event filter returns nil, the event will be ignored and Bubble Tea will not process it. + +As an example, this could be used to prevent a program from shutting down if there are unsaved changes. + +Example: + +``` +func filter(m tea.Model, msg tea.Msg) tea.Msg { + if _, ok := msg.(tea.QuitMsg); !ok { + return msg + } + + model := m.(myModel) + if model.hasChanges { + return nil + } + + return msg +} + +p := tea.NewProgram(Model{}, tea.WithFilter(filter)); + +if _,err := p.Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) +} +``` + + +### func WithInput + +```go +func WithInput(input io.Reader) ProgramOption +``` + +WithInput sets the input which, by default, is stdin. In most cases you won't need to use this. To disable input entirely pass nil. + +``` +p := NewProgram(model, WithInput(nil)) +``` + + +### func WithInputTTY + +```go +func WithInputTTY() ProgramOption +``` + +WithInputTTY opens a new TTY for input \(or console input device on Windows\). + + +### func WithMouseAllMotion + +```go +func WithMouseAllMotion() ProgramOption +``` + +WithMouseAllMotion starts the program with the mouse enabled in "all motion" mode. + +EnableMouseAllMotion is a special command that enables mouse click, release, wheel, and motion events, which are delivered regardless of whether a mouse button is pressed, effectively enabling support for hover interactions. + +This will try to enable the mouse in extended mode \(SGR\), if that is not supported by the terminal it will fall back to normal mode \(X10\). + +Many modern terminals support this, but not all. If in doubt, use EnableMouseCellMotion instead. + +To enable the mouse once the program has already started running use the EnableMouseAllMotion command. To disable the mouse when the program is running use the DisableMouse command. + +The mouse will be automatically disabled when the program exits. + + +### func WithMouseCellMotion + +```go +func WithMouseCellMotion() ProgramOption +``` + +WithMouseCellMotion starts the program with the mouse enabled in "cell motion" mode. + +Cell motion mode enables mouse click, release, and wheel events. Mouse movement events are also captured if a mouse button is pressed \(i.e., drag events\). Cell motion mode is better supported than all motion mode. + +This will try to enable the mouse in extended mode \(SGR\), if that is not supported by the terminal it will fall back to normal mode \(X10\). + +To enable mouse cell motion once the program has already started running use the EnableMouseCellMotion command. To disable the mouse when the program is running use the DisableMouse command. + +The mouse will be automatically disabled when the program exits. + + +### func WithOutput + +```go +func WithOutput(output io.Writer) ProgramOption +``` + +WithOutput sets the output which, by default, is stdout. In most cases you won't need to use this. + + +### func WithReportFocus + +```go +func WithReportFocus() ProgramOption +``` + +WithReportFocus enables reporting when the terminal gains and loses focus. When this is enabled [FocusMsg](<#FocusMsg>) and [BlurMsg](<#BlurMsg>) messages will be sent to your Update method. + +Note that while most terminals and multiplexers support focus reporting, some do not. Also note that tmux needs to be configured to report focus events. + + +### func WithoutBracketedPaste + +```go +func WithoutBracketedPaste() ProgramOption +``` + +WithoutBracketedPaste starts the program with bracketed paste disabled. + + +### func WithoutCatchPanics + +```go +func WithoutCatchPanics() ProgramOption +``` + +WithoutCatchPanics disables the panic catching that Bubble Tea does by default. If panic catching is disabled the terminal will be in a fairly unusable state after a panic because Bubble Tea will not perform its usual cleanup on exit. + + +### func WithoutRenderer + +```go +func WithoutRenderer() ProgramOption +``` + +WithoutRenderer disables the renderer. When this is set output and log statements will be plainly sent to stdout \(or another output if one is set\) without any rendering and redrawing logic. In other words, printing and logging will behave the same way it would in a non\-TUI commandline tool. This can be useful if you want to use the Bubble Tea framework for a non\-TUI application, or to provide an additional non\-TUI mode to your Bubble Tea programs. For example, your program could behave like a daemon if output is not a TTY. + + +### func WithoutSignalHandler + +```go +func WithoutSignalHandler() ProgramOption +``` + +WithoutSignalHandler disables the signal handler that Bubble Tea sets up for Programs. This is useful if you want to handle signals yourself. + + +### func WithoutSignals + +```go +func WithoutSignals() ProgramOption +``` + +WithoutSignals will ignore OS signals. This is mainly useful for testing. + + +## type QuitMsg + +QuitMsg signals that the program should quit. You can send a [QuitMsg](<#QuitMsg>) with [Quit](<#Quit>). + +```go +type QuitMsg struct{} +``` + + +## type ResumeMsg + +ResumeMsg can be listen to do something once a program is resumed back from a suspend state. + +```go +type ResumeMsg struct{} +``` + + +## type SuspendMsg + +SuspendMsg signals the program should suspend. This usually happens when ctrl\+z is pressed on common programs, but since bubbletea puts the terminal in raw mode, we need to handle it in a per\-program basis. + +You can send this message with \[Suspend\(\)\]. + +```go +type SuspendMsg struct{} +``` + + +## type WindowSizeMsg + +WindowSizeMsg is used to report the terminal size. It's sent to Update once initially and then on every terminal resize. Note that Windows does not have support for reporting when resizes occur as it does not support the SIGWINCH signal. + +```go +type WindowSizeMsg struct { + Width int + Height int +} +``` + +Generated by [gomarkdoc]() diff --git a/docs/lib/gocui.md b/docs/lib/gocui.md deleted file mode 100644 index 3b8f8ab..0000000 --- a/docs/lib/gocui.md +++ /dev/null @@ -1,914 +0,0 @@ - - -# gocui - -```go -import "github.com/jroimartin/gocui" -``` - -Package gocui allows to create console user interfaces. - -Create a new GUI: - -``` -g, err := gocui.NewGui(gocui.OutputNormal) -if err != nil { - // handle error -} -defer g.Close() - -// Set GUI managers and key bindings -// ... - -if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - // handle error -} -``` - -Set GUI managers: - -``` -g.SetManager(mgr1, mgr2) -``` - -Managers are in charge of GUI's layout and can be used to build widgets. On each iteration of the GUI's main loop, the Layout function of each configured manager is executed. Managers are used to set\-up and update the application's main views, being possible to freely change them during execution. Also, it is important to mention that a main loop iteration is executed on each reported event \(key\-press, mouse event, window resize, etc\). - -GUIs are composed by Views, you can think of it as buffers. Views implement the io.ReadWriter interface, so you can just write to them if you want to modify their content. The same is valid for reading. - -Create and initialize a view with absolute coordinates: - -``` -if v, err := g.SetView("viewname", 2, 2, 22, 7); err != nil { - if err != gocui.ErrUnknownView { - // handle error - } - fmt.Fprintln(v, "This is a new view") - // ... -} -``` - -Views can also be created using relative coordinates: - -``` -maxX, maxY := g.Size() -if v, err := g.SetView("viewname", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil { - // ... -} -``` - -Configure keybindings: - -``` -if err := g.SetKeybinding("viewname", gocui.KeyEnter, gocui.ModNone, fcn); err != nil { - // handle error -} -``` - -gocui implements full mouse support that can be enabled with: - -``` -g.Mouse = true -``` - -Mouse events are handled like any other keybinding: - -``` -if err := g.SetKeybinding("viewname", gocui.MouseLeft, gocui.ModNone, fcn); err != nil { - // handle error -} -``` - -IMPORTANT: Views can only be created, destroyed or updated in three ways: from the Layout function within managers, from keybinding callbacks or via \*Gui.Update\(\). The reason for this is that it allows gocui to be concurrent\-safe. So, if you want to update your GUI from a goroutine, you must use \*Gui.Update\(\). For example: - -``` -g.Update(func(g *gocui.Gui) error { - v, err := g.View("viewname") - if err != nil { - // handle error - } - v.Clear() - fmt.Fprintln(v, "Writing from different goroutines") - return nil -}) -``` - -By default, gocui provides a basic edition mode. This mode can be extended and customized creating a new Editor and assigning it to \*View.Editor: - -``` -type Editor interface { - Edit(v *View, key Key, ch rune, mod Modifier) -} -``` - -DefaultEditor can be taken as example to create your own custom Editor: - -``` -var DefaultEditor Editor = EditorFunc(simpleEditor) - -func simpleEditor(v *View, key Key, ch rune, mod Modifier) { - switch { - case ch != 0 && mod == 0: - v.EditWrite(ch) - case key == KeySpace: - v.EditWrite(' ') - case key == KeyBackspace || key == KeyBackspace2: - v.EditDelete(true) - // ... - } -} -``` - -Colored text: - -Views allow to add colored text using ANSI colors. For example: - -``` -fmt.Fprintln(v, "\x1b[0;31mHello world") -``` - -For more information, see the examples in folder "\_examples/". - -## Index - -- [Constants](<#constants>) -- [Variables](<#variables>) -- [type Attribute](<#Attribute>) -- [type Editor](<#Editor>) -- [type EditorFunc](<#EditorFunc>) - - [func \(f EditorFunc\) Edit\(v \*View, key Key, ch rune, mod Modifier\)](<#EditorFunc.Edit>) -- [type Gui](<#Gui>) - - [func NewGui\(mode OutputMode\) \(\*Gui, error\)](<#NewGui>) - - [func \(g \*Gui\) Close\(\)](<#Gui.Close>) - - [func \(g \*Gui\) CurrentView\(\) \*View](<#Gui.CurrentView>) - - [func \(g \*Gui\) DeleteKeybinding\(viewname string, key interface\{\}, mod Modifier\) error](<#Gui.DeleteKeybinding>) - - [func \(g \*Gui\) DeleteKeybindings\(viewname string\)](<#Gui.DeleteKeybindings>) - - [func \(g \*Gui\) DeleteView\(name string\) error](<#Gui.DeleteView>) - - [func \(g \*Gui\) MainLoop\(\) error](<#Gui.MainLoop>) - - [func \(g \*Gui\) Rune\(x, y int\) \(rune, error\)](<#Gui.Rune>) - - [func \(g \*Gui\) SetCurrentView\(name string\) \(\*View, error\)](<#Gui.SetCurrentView>) - - [func \(g \*Gui\) SetKeybinding\(viewname string, key interface\{\}, mod Modifier, handler func\(\*Gui, \*View\) error\) error](<#Gui.SetKeybinding>) - - [func \(g \*Gui\) SetManager\(managers ...Manager\)](<#Gui.SetManager>) - - [func \(g \*Gui\) SetManagerFunc\(manager func\(\*Gui\) error\)](<#Gui.SetManagerFunc>) - - [func \(g \*Gui\) SetRune\(x, y int, ch rune, fgColor, bgColor Attribute\) error](<#Gui.SetRune>) - - [func \(g \*Gui\) SetView\(name string, x0, y0, x1, y1 int\) \(\*View, error\)](<#Gui.SetView>) - - [func \(g \*Gui\) SetViewOnBottom\(name string\) \(\*View, error\)](<#Gui.SetViewOnBottom>) - - [func \(g \*Gui\) SetViewOnTop\(name string\) \(\*View, error\)](<#Gui.SetViewOnTop>) - - [func \(g \*Gui\) Size\(\) \(x, y int\)](<#Gui.Size>) - - [func \(g \*Gui\) Update\(f func\(\*Gui\) error\)](<#Gui.Update>) - - [func \(g \*Gui\) View\(name string\) \(\*View, error\)](<#Gui.View>) - - [func \(g \*Gui\) ViewByPosition\(x, y int\) \(\*View, error\)](<#Gui.ViewByPosition>) - - [func \(g \*Gui\) ViewPosition\(name string\) \(x0, y0, x1, y1 int, err error\)](<#Gui.ViewPosition>) - - [func \(g \*Gui\) Views\(\) \[\]\*View](<#Gui.Views>) -- [type Key](<#Key>) -- [type Manager](<#Manager>) -- [type ManagerFunc](<#ManagerFunc>) - - [func \(f ManagerFunc\) Layout\(g \*Gui\) error](<#ManagerFunc.Layout>) -- [type Modifier](<#Modifier>) -- [type OutputMode](<#OutputMode>) -- [type View](<#View>) - - [func \(v \*View\) Buffer\(\) string](<#View.Buffer>) - - [func \(v \*View\) BufferLines\(\) \[\]string](<#View.BufferLines>) - - [func \(v \*View\) Clear\(\)](<#View.Clear>) - - [func \(v \*View\) Cursor\(\) \(x, y int\)](<#View.Cursor>) - - [func \(v \*View\) EditDelete\(back bool\)](<#View.EditDelete>) - - [func \(v \*View\) EditNewLine\(\)](<#View.EditNewLine>) - - [func \(v \*View\) EditWrite\(ch rune\)](<#View.EditWrite>) - - [func \(v \*View\) Line\(y int\) \(string, error\)](<#View.Line>) - - [func \(v \*View\) MoveCursor\(dx, dy int, writeMode bool\)](<#View.MoveCursor>) - - [func \(v \*View\) Name\(\) string](<#View.Name>) - - [func \(v \*View\) Origin\(\) \(x, y int\)](<#View.Origin>) - - [func \(v \*View\) Read\(p \[\]byte\) \(n int, err error\)](<#View.Read>) - - [func \(v \*View\) Rewind\(\)](<#View.Rewind>) - - [func \(v \*View\) SetCursor\(x, y int\) error](<#View.SetCursor>) - - [func \(v \*View\) SetOrigin\(x, y int\) error](<#View.SetOrigin>) - - [func \(v \*View\) Size\(\) \(x, y int\)](<#View.Size>) - - [func \(v \*View\) ViewBuffer\(\) string](<#View.ViewBuffer>) - - [func \(v \*View\) ViewBufferLines\(\) \[\]string](<#View.ViewBufferLines>) - - [func \(v \*View\) Word\(x, y int\) \(string, error\)](<#View.Word>) - - [func \(v \*View\) Write\(p \[\]byte\) \(n int, err error\)](<#View.Write>) - - -## Constants - -Color attributes. - -```go -const ( - ColorDefault Attribute = Attribute(termbox.ColorDefault) - ColorBlack = Attribute(termbox.ColorBlack) - ColorRed = Attribute(termbox.ColorRed) - ColorGreen = Attribute(termbox.ColorGreen) - ColorYellow = Attribute(termbox.ColorYellow) - ColorBlue = Attribute(termbox.ColorBlue) - ColorMagenta = Attribute(termbox.ColorMagenta) - ColorCyan = Attribute(termbox.ColorCyan) - ColorWhite = Attribute(termbox.ColorWhite) -) -``` - -Text style attributes. - -```go -const ( - AttrBold Attribute = Attribute(termbox.AttrBold) - AttrUnderline = Attribute(termbox.AttrUnderline) - AttrReverse = Attribute(termbox.AttrReverse) -) -``` - - - -```go -const ( - // OutputNormal provides 8-colors terminal mode. - OutputNormal = OutputMode(termbox.OutputNormal) - - // Output256 provides 256-colors terminal mode. - Output256 = OutputMode(termbox.Output256) -) -``` - -Special keys. - -```go -const ( - KeyF1 Key = Key(termbox.KeyF1) - KeyF2 = Key(termbox.KeyF2) - KeyF3 = Key(termbox.KeyF3) - KeyF4 = Key(termbox.KeyF4) - KeyF5 = Key(termbox.KeyF5) - KeyF6 = Key(termbox.KeyF6) - KeyF7 = Key(termbox.KeyF7) - KeyF8 = Key(termbox.KeyF8) - KeyF9 = Key(termbox.KeyF9) - KeyF10 = Key(termbox.KeyF10) - KeyF11 = Key(termbox.KeyF11) - KeyF12 = Key(termbox.KeyF12) - KeyInsert = Key(termbox.KeyInsert) - KeyDelete = Key(termbox.KeyDelete) - KeyHome = Key(termbox.KeyHome) - KeyEnd = Key(termbox.KeyEnd) - KeyPgup = Key(termbox.KeyPgup) - KeyPgdn = Key(termbox.KeyPgdn) - KeyArrowUp = Key(termbox.KeyArrowUp) - KeyArrowDown = Key(termbox.KeyArrowDown) - KeyArrowLeft = Key(termbox.KeyArrowLeft) - KeyArrowRight = Key(termbox.KeyArrowRight) - - MouseLeft = Key(termbox.MouseLeft) - MouseMiddle = Key(termbox.MouseMiddle) - MouseRight = Key(termbox.MouseRight) - MouseRelease = Key(termbox.MouseRelease) - MouseWheelUp = Key(termbox.MouseWheelUp) - MouseWheelDown = Key(termbox.MouseWheelDown) -) -``` - -Keys combinations. - -```go -const ( - KeyCtrlTilde Key = Key(termbox.KeyCtrlTilde) - KeyCtrl2 = Key(termbox.KeyCtrl2) - KeyCtrlSpace = Key(termbox.KeyCtrlSpace) - KeyCtrlA = Key(termbox.KeyCtrlA) - KeyCtrlB = Key(termbox.KeyCtrlB) - KeyCtrlC = Key(termbox.KeyCtrlC) - KeyCtrlD = Key(termbox.KeyCtrlD) - KeyCtrlE = Key(termbox.KeyCtrlE) - KeyCtrlF = Key(termbox.KeyCtrlF) - KeyCtrlG = Key(termbox.KeyCtrlG) - KeyBackspace = Key(termbox.KeyBackspace) - KeyCtrlH = Key(termbox.KeyCtrlH) - KeyTab = Key(termbox.KeyTab) - KeyCtrlI = Key(termbox.KeyCtrlI) - KeyCtrlJ = Key(termbox.KeyCtrlJ) - KeyCtrlK = Key(termbox.KeyCtrlK) - KeyCtrlL = Key(termbox.KeyCtrlL) - KeyEnter = Key(termbox.KeyEnter) - KeyCtrlM = Key(termbox.KeyCtrlM) - KeyCtrlN = Key(termbox.KeyCtrlN) - KeyCtrlO = Key(termbox.KeyCtrlO) - KeyCtrlP = Key(termbox.KeyCtrlP) - KeyCtrlQ = Key(termbox.KeyCtrlQ) - KeyCtrlR = Key(termbox.KeyCtrlR) - KeyCtrlS = Key(termbox.KeyCtrlS) - KeyCtrlT = Key(termbox.KeyCtrlT) - KeyCtrlU = Key(termbox.KeyCtrlU) - KeyCtrlV = Key(termbox.KeyCtrlV) - KeyCtrlW = Key(termbox.KeyCtrlW) - KeyCtrlX = Key(termbox.KeyCtrlX) - KeyCtrlY = Key(termbox.KeyCtrlY) - KeyCtrlZ = Key(termbox.KeyCtrlZ) - KeyEsc = Key(termbox.KeyEsc) - KeyCtrlLsqBracket = Key(termbox.KeyCtrlLsqBracket) - KeyCtrl3 = Key(termbox.KeyCtrl3) - KeyCtrl4 = Key(termbox.KeyCtrl4) - KeyCtrlBackslash = Key(termbox.KeyCtrlBackslash) - KeyCtrl5 = Key(termbox.KeyCtrl5) - KeyCtrlRsqBracket = Key(termbox.KeyCtrlRsqBracket) - KeyCtrl6 = Key(termbox.KeyCtrl6) - KeyCtrl7 = Key(termbox.KeyCtrl7) - KeyCtrlSlash = Key(termbox.KeyCtrlSlash) - KeyCtrlUnderscore = Key(termbox.KeyCtrlUnderscore) - KeySpace = Key(termbox.KeySpace) - KeyBackspace2 = Key(termbox.KeyBackspace2) - KeyCtrl8 = Key(termbox.KeyCtrl8) -) -``` - -## Variables - - - -```go -var ( - // ErrQuit is used to decide if the MainLoop finished successfully. - ErrQuit = errors.New("quit") - - // ErrUnknownView allows to assert if a View must be initialized. - ErrUnknownView = errors.New("unknown view") -) -``` - - -## type Attribute - -Attribute represents a terminal attribute, like color, font style, etc. They can be combined using bitwise OR \(|\). Note that it is not possible to combine multiple color attributes. - -```go -type Attribute termbox.Attribute -``` - - -## type Editor - -Editor interface must be satisfied by gocui editors. - -```go -type Editor interface { - Edit(v *View, key Key, ch rune, mod Modifier) -} -``` - -DefaultEditor is the default editor. - -```go -var DefaultEditor Editor = EditorFunc(simpleEditor) -``` - - -## type EditorFunc - -The EditorFunc type is an adapter to allow the use of ordinary functions as Editors. If f is a function with the appropriate signature, EditorFunc\(f\) is an Editor object that calls f. - -```go -type EditorFunc func(v *View, key Key, ch rune, mod Modifier) -``` - - -### func \(EditorFunc\) Edit - -```go -func (f EditorFunc) Edit(v *View, key Key, ch rune, mod Modifier) -``` - -Edit calls f\(v, key, ch, mod\) - - -## type Gui - -Gui represents the whole User Interface, including the views, layouts and keybindings. - -```go -type Gui struct { - - // BgColor and FgColor allow to configure the background and foreground - // colors of the GUI. - BgColor, FgColor Attribute - - // SelBgColor and SelFgColor allow to configure the background and - // foreground colors of the frame of the current view. - SelBgColor, SelFgColor Attribute - - // If Highlight is true, Sel{Bg,Fg}Colors will be used to draw the - // frame of the current view. - Highlight bool - - // If Cursor is true then the cursor is enabled. - Cursor bool - - // If Mouse is true then mouse events will be enabled. - Mouse bool - - // If InputEsc is true, when ESC sequence is in the buffer and it doesn't - // match any known sequence, ESC means KeyEsc. - InputEsc bool - - // If ASCII is true then use ASCII instead of unicode to draw the - // interface. Using ASCII is more portable. - ASCII bool - // contains filtered or unexported fields -} -``` - - -### func NewGui - -```go -func NewGui(mode OutputMode) (*Gui, error) -``` - -NewGui returns a new Gui object with a given output mode. - - -### func \(\*Gui\) Close - -```go -func (g *Gui) Close() -``` - -Close finalizes the library. It should be called after a successful initialization and when gocui is not needed anymore. - - -### func \(\*Gui\) CurrentView - -```go -func (g *Gui) CurrentView() *View -``` - -CurrentView returns the currently focused view, or nil if no view owns the focus. - - -### func \(\*Gui\) DeleteKeybinding - -```go -func (g *Gui) DeleteKeybinding(viewname string, key interface{}, mod Modifier) error -``` - -DeleteKeybinding deletes a keybinding. - - -### func \(\*Gui\) DeleteKeybindings - -```go -func (g *Gui) DeleteKeybindings(viewname string) -``` - -DeleteKeybindings deletes all keybindings of view. - - -### func \(\*Gui\) DeleteView - -```go -func (g *Gui) DeleteView(name string) error -``` - -DeleteView deletes a view by name. - - -### func \(\*Gui\) MainLoop - -```go -func (g *Gui) MainLoop() error -``` - -MainLoop runs the main loop until an error is returned. A successful finish should return ErrQuit. - - -### func \(\*Gui\) Rune - -```go -func (g *Gui) Rune(x, y int) (rune, error) -``` - -Rune returns the rune contained in the cell at the given position. It checks if the position is valid. - - -### func \(\*Gui\) SetCurrentView - -```go -func (g *Gui) SetCurrentView(name string) (*View, error) -``` - -SetCurrentView gives the focus to a given view. - - -### func \(\*Gui\) SetKeybinding - -```go -func (g *Gui) SetKeybinding(viewname string, key interface{}, mod Modifier, handler func(*Gui, *View) error) error -``` - -SetKeybinding creates a new keybinding. If viewname equals to "" \(empty string\) then the keybinding will apply to all views. key must be a rune or a Key. - - -### func \(\*Gui\) SetManager - -```go -func (g *Gui) SetManager(managers ...Manager) -``` - -SetManager sets the given GUI managers. It deletes all views and keybindings. - - -### func \(\*Gui\) SetManagerFunc - -```go -func (g *Gui) SetManagerFunc(manager func(*Gui) error) -``` - -SetManagerFunc sets the given manager function. It deletes all views and keybindings. - - -### func \(\*Gui\) SetRune - -```go -func (g *Gui) SetRune(x, y int, ch rune, fgColor, bgColor Attribute) error -``` - -SetRune writes a rune at the given point, relative to the top\-left corner of the terminal. It checks if the position is valid and applies the given colors. - - -### func \(\*Gui\) SetView - -```go -func (g *Gui) SetView(name string, x0, y0, x1, y1 int) (*View, error) -``` - -SetView creates a new view with its top\-left corner at \(x0, y0\) and the bottom\-right one at \(x1, y1\). If a view with the same name already exists, its dimensions are updated; otherwise, the error ErrUnknownView is returned, which allows to assert if the View must be initialized. It checks if the position is valid. - - -### func \(\*Gui\) SetViewOnBottom - -```go -func (g *Gui) SetViewOnBottom(name string) (*View, error) -``` - -SetViewOnBottom sets the given view on bottom of the existing ones. - - -### func \(\*Gui\) SetViewOnTop - -```go -func (g *Gui) SetViewOnTop(name string) (*View, error) -``` - -SetViewOnTop sets the given view on top of the existing ones. - - -### func \(\*Gui\) Size - -```go -func (g *Gui) Size() (x, y int) -``` - -Size returns the terminal's size. - - -### func \(\*Gui\) Update - -```go -func (g *Gui) Update(f func(*Gui) error) -``` - -Update executes the passed function. This method can be called safely from a goroutine in order to update the GUI. It is important to note that the passed function won't be executed immediately, instead it will be added to the user events queue. Given that Update spawns a goroutine, the order in which the user events will be handled is not guaranteed. - - -### func \(\*Gui\) View - -```go -func (g *Gui) View(name string) (*View, error) -``` - -View returns a pointer to the view with the given name, or error ErrUnknownView if a view with that name does not exist. - - -### func \(\*Gui\) ViewByPosition - -```go -func (g *Gui) ViewByPosition(x, y int) (*View, error) -``` - -ViewByPosition returns a pointer to a view matching the given position, or error ErrUnknownView if a view in that position does not exist. - - -### func \(\*Gui\) ViewPosition - -```go -func (g *Gui) ViewPosition(name string) (x0, y0, x1, y1 int, err error) -``` - -ViewPosition returns the coordinates of the view with the given name, or error ErrUnknownView if a view with that name does not exist. - - -### func \(\*Gui\) Views - -```go -func (g *Gui) Views() []*View -``` - -Views returns all the views in the GUI. - - -## type Key - -Key represents special keys or keys combinations. - -```go -type Key termbox.Key -``` - - -## type Manager - -A Manager is in charge of GUI's layout and can be used to build widgets. - -```go -type Manager interface { - // Layout is called every time the GUI is redrawn, it must contain the - // base views and its initializations. - Layout(*Gui) error -} -``` - - -## type ManagerFunc - -The ManagerFunc type is an adapter to allow the use of ordinary functions as Managers. If f is a function with the appropriate signature, ManagerFunc\(f\) is an Manager object that calls f. - -```go -type ManagerFunc func(*Gui) error -``` - - -### func \(ManagerFunc\) Layout - -```go -func (f ManagerFunc) Layout(g *Gui) error -``` - -Layout calls f\(g\) - - -## type Modifier - -Modifier allows to define special keys combinations. They can be used in combination with Keys or Runes when a new keybinding is defined. - -```go -type Modifier termbox.Modifier -``` - -Modifiers. - -```go -const ( - ModNone Modifier = Modifier(0) - ModAlt = Modifier(termbox.ModAlt) -) -``` - - -## type OutputMode - -OutputMode represents the terminal's output mode \(8 or 256 colors\). - -```go -type OutputMode termbox.OutputMode -``` - - -## type View - -A View is a window. It maintains its own internal buffer and cursor position. - -```go -type View struct { - - // BgColor and FgColor allow to configure the background and foreground - // colors of the View. - BgColor, FgColor Attribute - - // SelBgColor and SelFgColor are used to configure the background and - // foreground colors of the selected line, when it is highlighted. - SelBgColor, SelFgColor Attribute - - // If Editable is true, keystrokes will be added to the view's internal - // buffer at the cursor position. - Editable bool - - // Editor allows to define the editor that manages the edition mode, - // including keybindings or cursor behaviour. DefaultEditor is used by - // default. - Editor Editor - - // Overwrite enables or disables the overwrite mode of the view. - Overwrite bool - - // If Highlight is true, Sel{Bg,Fg}Colors will be used - // for the line under the cursor position. - Highlight bool - - // If Frame is true, a border will be drawn around the view. - Frame bool - - // If Wrap is true, the content that is written to this View is - // automatically wrapped when it is longer than its width. If true the - // view's x-origin will be ignored. - Wrap bool - - // If Autoscroll is true, the View will automatically scroll down when the - // text overflows. If true the view's y-origin will be ignored. - Autoscroll bool - - // If Frame is true, Title allows to configure a title for the view. - Title string - - // If Mask is true, the View will display the mask instead of the real - // content - Mask rune - // contains filtered or unexported fields -} -``` - - -### func \(\*View\) Buffer - -```go -func (v *View) Buffer() string -``` - -Buffer returns a string with the contents of the view's internal buffer. - - -### func \(\*View\) BufferLines - -```go -func (v *View) BufferLines() []string -``` - -BufferLines returns the lines in the view's internal buffer. - - -### func \(\*View\) Clear - -```go -func (v *View) Clear() -``` - -Clear empties the view's internal buffer. - - -### func \(\*View\) Cursor - -```go -func (v *View) Cursor() (x, y int) -``` - -Cursor returns the cursor position of the view. - - -### func \(\*View\) EditDelete - -```go -func (v *View) EditDelete(back bool) -``` - -EditDelete deletes a rune at the cursor position. back determines the direction. - - -### func \(\*View\) EditNewLine - -```go -func (v *View) EditNewLine() -``` - -EditNewLine inserts a new line under the cursor. - - -### func \(\*View\) EditWrite - -```go -func (v *View) EditWrite(ch rune) -``` - -EditWrite writes a rune at the cursor position. - - -### func \(\*View\) Line - -```go -func (v *View) Line(y int) (string, error) -``` - -Line returns a string with the line of the view's internal buffer at the position corresponding to the point \(x, y\). - - -### func \(\*View\) MoveCursor - -```go -func (v *View) MoveCursor(dx, dy int, writeMode bool) -``` - -MoveCursor moves the cursor taking into account the width of the line/view, displacing the origin if necessary. - - -### func \(\*View\) Name - -```go -func (v *View) Name() string -``` - -Name returns the name of the view. - - -### func \(\*View\) Origin - -```go -func (v *View) Origin() (x, y int) -``` - -Origin returns the origin position of the view. - - -### func \(\*View\) Read - -```go -func (v *View) Read(p []byte) (n int, err error) -``` - -Read reads data into p. It returns the number of bytes read into p. At EOF, err will be io.EOF. Calling Read\(\) after Rewind\(\) makes the cache to be refreshed with the contents of the view. - - -### func \(\*View\) Rewind - -```go -func (v *View) Rewind() -``` - -Rewind sets the offset for the next Read to 0, which also refresh the read cache. - - -### func \(\*View\) SetCursor - -```go -func (v *View) SetCursor(x, y int) error -``` - -SetCursor sets the cursor position of the view at the given point, relative to the view. It checks if the position is valid. - - -### func \(\*View\) SetOrigin - -```go -func (v *View) SetOrigin(x, y int) error -``` - -SetOrigin sets the origin position of the view's internal buffer, so the buffer starts to be printed from this point, which means that it is linked with the origin point of view. It can be used to implement Horizontal and Vertical scrolling with just incrementing or decrementing ox and oy. - - -### func \(\*View\) Size - -```go -func (v *View) Size() (x, y int) -``` - -Size returns the number of visible columns and rows in the View. - - -### func \(\*View\) ViewBuffer - -```go -func (v *View) ViewBuffer() string -``` - -ViewBuffer returns a string with the contents of the view's buffer that is shown to the user. - - -### func \(\*View\) ViewBufferLines - -```go -func (v *View) ViewBufferLines() []string -``` - -ViewBufferLines returns the lines in the view's internal buffer that is shown to the user. - - -### func \(\*View\) Word - -```go -func (v *View) Word(x, y int) (string, error) -``` - -Word returns a string with the word of the view's internal buffer at the position corresponding to the point \(x, y\). - - -### func \(\*View\) Write - -```go -func (v *View) Write(p []byte) (n int, err error) -``` - -Write appends a byte slice into the view's internal buffer. Because View implements the io.Writer interface, it can be passed as parameter of functions like fmt.Fprintf, fmt.Fprintln, io.Copy, etc. Clear must be called to clear the view's buffer. - -Generated by [gomarkdoc]() diff --git a/docs/lib/lipgloss.md b/docs/lib/lipgloss.md new file mode 100644 index 0000000..6fbdd8c --- /dev/null +++ b/docs/lib/lipgloss.md @@ -0,0 +1,2632 @@ + + +# lipgloss + +```go +import "github.com/charmbracelet/lipgloss" +``` + +## Index + +- [Constants](<#constants>) +- [func ColorProfile\(\) termenv.Profile](<#ColorProfile>) +- [func HasDarkBackground\(\) bool](<#HasDarkBackground>) +- [func Height\(str string\) int](<#Height>) +- [func JoinHorizontal\(pos Position, strs ...string\) string](<#JoinHorizontal>) +- [func JoinVertical\(pos Position, strs ...string\) string](<#JoinVertical>) +- [func Place\(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption\) string](<#Place>) +- [func PlaceHorizontal\(width int, pos Position, str string, opts ...WhitespaceOption\) string](<#PlaceHorizontal>) +- [func PlaceVertical\(height int, pos Position, str string, opts ...WhitespaceOption\) string](<#PlaceVertical>) +- [func SetColorProfile\(p termenv.Profile\)](<#SetColorProfile>) +- [func SetDefaultRenderer\(r \*Renderer\)](<#SetDefaultRenderer>) +- [func SetHasDarkBackground\(b bool\)](<#SetHasDarkBackground>) +- [func Size\(str string\) \(width, height int\)](<#Size>) +- [func StyleRanges\(s string, ranges ...Range\) string](<#StyleRanges>) +- [func StyleRunes\(str string, indices \[\]int, matched, unmatched Style\) string](<#StyleRunes>) +- [func Width\(str string\) \(width int\)](<#Width>) +- [type ANSIColor](<#ANSIColor>) + - [func \(ac ANSIColor\) RGBA\(\) \(r, g, b, a uint32\)](<#ANSIColor.RGBA>) +- [type AdaptiveColor](<#AdaptiveColor>) + - [func \(ac AdaptiveColor\) RGBA\(\) \(r, g, b, a uint32\)](<#AdaptiveColor.RGBA>) +- [type Border](<#Border>) + - [func ASCIIBorder\(\) Border](<#ASCIIBorder>) + - [func BlockBorder\(\) Border](<#BlockBorder>) + - [func DoubleBorder\(\) Border](<#DoubleBorder>) + - [func HiddenBorder\(\) Border](<#HiddenBorder>) + - [func InnerHalfBlockBorder\(\) Border](<#InnerHalfBlockBorder>) + - [func MarkdownBorder\(\) Border](<#MarkdownBorder>) + - [func NormalBorder\(\) Border](<#NormalBorder>) + - [func OuterHalfBlockBorder\(\) Border](<#OuterHalfBlockBorder>) + - [func RoundedBorder\(\) Border](<#RoundedBorder>) + - [func ThickBorder\(\) Border](<#ThickBorder>) + - [func \(b Border\) GetBottomSize\(\) int](<#Border.GetBottomSize>) + - [func \(b Border\) GetLeftSize\(\) int](<#Border.GetLeftSize>) + - [func \(b Border\) GetRightSize\(\) int](<#Border.GetRightSize>) + - [func \(b Border\) GetTopSize\(\) int](<#Border.GetTopSize>) +- [type Color](<#Color>) + - [func \(c Color\) RGBA\(\) \(r, g, b, a uint32\)](<#Color.RGBA>) +- [type CompleteAdaptiveColor](<#CompleteAdaptiveColor>) + - [func \(cac CompleteAdaptiveColor\) RGBA\(\) \(r, g, b, a uint32\)](<#CompleteAdaptiveColor.RGBA>) +- [type CompleteColor](<#CompleteColor>) + - [func \(c CompleteColor\) RGBA\(\) \(r, g, b, a uint32\)](<#CompleteColor.RGBA>) +- [type NoColor](<#NoColor>) + - [func \(n NoColor\) RGBA\(\) \(r, g, b, a uint32\)](<#NoColor.RGBA>) +- [type Position](<#Position>) +- [type Range](<#Range>) + - [func NewRange\(start, end int, style Style\) Range](<#NewRange>) +- [type Renderer](<#Renderer>) + - [func DefaultRenderer\(\) \*Renderer](<#DefaultRenderer>) + - [func NewRenderer\(w io.Writer, opts ...termenv.OutputOption\) \*Renderer](<#NewRenderer>) + - [func \(r \*Renderer\) ColorProfile\(\) termenv.Profile](<#Renderer.ColorProfile>) + - [func \(r \*Renderer\) HasDarkBackground\(\) bool](<#Renderer.HasDarkBackground>) + - [func \(r \*Renderer\) NewStyle\(\) Style](<#Renderer.NewStyle>) + - [func \(r \*Renderer\) Output\(\) \*termenv.Output](<#Renderer.Output>) + - [func \(r \*Renderer\) Place\(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption\) string](<#Renderer.Place>) + - [func \(r \*Renderer\) PlaceHorizontal\(width int, pos Position, str string, opts ...WhitespaceOption\) string](<#Renderer.PlaceHorizontal>) + - [func \(r \*Renderer\) PlaceVertical\(height int, pos Position, str string, opts ...WhitespaceOption\) string](<#Renderer.PlaceVertical>) + - [func \(r \*Renderer\) SetColorProfile\(p termenv.Profile\)](<#Renderer.SetColorProfile>) + - [func \(r \*Renderer\) SetHasDarkBackground\(b bool\)](<#Renderer.SetHasDarkBackground>) + - [func \(r \*Renderer\) SetOutput\(o \*termenv.Output\)](<#Renderer.SetOutput>) +- [type Style](<#Style>) + - [func NewStyle\(\) Style](<#NewStyle>) + - [func \(s Style\) Align\(p ...Position\) Style](<#Style.Align>) + - [func \(s Style\) AlignHorizontal\(p Position\) Style](<#Style.AlignHorizontal>) + - [func \(s Style\) AlignVertical\(p Position\) Style](<#Style.AlignVertical>) + - [func \(s Style\) Background\(c TerminalColor\) Style](<#Style.Background>) + - [func \(s Style\) Blink\(v bool\) Style](<#Style.Blink>) + - [func \(s Style\) Bold\(v bool\) Style](<#Style.Bold>) + - [func \(s Style\) Border\(b Border, sides ...bool\) Style](<#Style.Border>) + - [func \(s Style\) BorderBackground\(c ...TerminalColor\) Style](<#Style.BorderBackground>) + - [func \(s Style\) BorderBottom\(v bool\) Style](<#Style.BorderBottom>) + - [func \(s Style\) BorderBottomBackground\(c TerminalColor\) Style](<#Style.BorderBottomBackground>) + - [func \(s Style\) BorderBottomForeground\(c TerminalColor\) Style](<#Style.BorderBottomForeground>) + - [func \(s Style\) BorderForeground\(c ...TerminalColor\) Style](<#Style.BorderForeground>) + - [func \(s Style\) BorderLeft\(v bool\) Style](<#Style.BorderLeft>) + - [func \(s Style\) BorderLeftBackground\(c TerminalColor\) Style](<#Style.BorderLeftBackground>) + - [func \(s Style\) BorderLeftForeground\(c TerminalColor\) Style](<#Style.BorderLeftForeground>) + - [func \(s Style\) BorderRight\(v bool\) Style](<#Style.BorderRight>) + - [func \(s Style\) BorderRightBackground\(c TerminalColor\) Style](<#Style.BorderRightBackground>) + - [func \(s Style\) BorderRightForeground\(c TerminalColor\) Style](<#Style.BorderRightForeground>) + - [func \(s Style\) BorderStyle\(b Border\) Style](<#Style.BorderStyle>) + - [func \(s Style\) BorderTop\(v bool\) Style](<#Style.BorderTop>) + - [func \(s Style\) BorderTopBackground\(c TerminalColor\) Style](<#Style.BorderTopBackground>) + - [func \(s Style\) BorderTopForeground\(c TerminalColor\) Style](<#Style.BorderTopForeground>) + - [func \(s Style\) ColorWhitespace\(v bool\) Style](<#Style.ColorWhitespace>) + - [func \(s Style\) Copy\(\) Style](<#Style.Copy>) + - [func \(s Style\) Faint\(v bool\) Style](<#Style.Faint>) + - [func \(s Style\) Foreground\(c TerminalColor\) Style](<#Style.Foreground>) + - [func \(s Style\) GetAlign\(\) Position](<#Style.GetAlign>) + - [func \(s Style\) GetAlignHorizontal\(\) Position](<#Style.GetAlignHorizontal>) + - [func \(s Style\) GetAlignVertical\(\) Position](<#Style.GetAlignVertical>) + - [func \(s Style\) GetBackground\(\) TerminalColor](<#Style.GetBackground>) + - [func \(s Style\) GetBlink\(\) bool](<#Style.GetBlink>) + - [func \(s Style\) GetBold\(\) bool](<#Style.GetBold>) + - [func \(s Style\) GetBorder\(\) \(b Border, top, right, bottom, left bool\)](<#Style.GetBorder>) + - [func \(s Style\) GetBorderBottom\(\) bool](<#Style.GetBorderBottom>) + - [func \(s Style\) GetBorderBottomBackground\(\) TerminalColor](<#Style.GetBorderBottomBackground>) + - [func \(s Style\) GetBorderBottomForeground\(\) TerminalColor](<#Style.GetBorderBottomForeground>) + - [func \(s Style\) GetBorderBottomSize\(\) int](<#Style.GetBorderBottomSize>) + - [func \(s Style\) GetBorderLeft\(\) bool](<#Style.GetBorderLeft>) + - [func \(s Style\) GetBorderLeftBackground\(\) TerminalColor](<#Style.GetBorderLeftBackground>) + - [func \(s Style\) GetBorderLeftForeground\(\) TerminalColor](<#Style.GetBorderLeftForeground>) + - [func \(s Style\) GetBorderLeftSize\(\) int](<#Style.GetBorderLeftSize>) + - [func \(s Style\) GetBorderRight\(\) bool](<#Style.GetBorderRight>) + - [func \(s Style\) GetBorderRightBackground\(\) TerminalColor](<#Style.GetBorderRightBackground>) + - [func \(s Style\) GetBorderRightForeground\(\) TerminalColor](<#Style.GetBorderRightForeground>) + - [func \(s Style\) GetBorderRightSize\(\) int](<#Style.GetBorderRightSize>) + - [func \(s Style\) GetBorderStyle\(\) Border](<#Style.GetBorderStyle>) + - [func \(s Style\) GetBorderTop\(\) bool](<#Style.GetBorderTop>) + - [func \(s Style\) GetBorderTopBackground\(\) TerminalColor](<#Style.GetBorderTopBackground>) + - [func \(s Style\) GetBorderTopForeground\(\) TerminalColor](<#Style.GetBorderTopForeground>) + - [func \(s Style\) GetBorderTopSize\(\) int](<#Style.GetBorderTopSize>) + - [func \(s Style\) GetBorderTopWidth\(\) int](<#Style.GetBorderTopWidth>) + - [func \(s Style\) GetColorWhitespace\(\) bool](<#Style.GetColorWhitespace>) + - [func \(s Style\) GetFaint\(\) bool](<#Style.GetFaint>) + - [func \(s Style\) GetForeground\(\) TerminalColor](<#Style.GetForeground>) + - [func \(s Style\) GetFrameSize\(\) \(x, y int\)](<#Style.GetFrameSize>) + - [func \(s Style\) GetHeight\(\) int](<#Style.GetHeight>) + - [func \(s Style\) GetHorizontalBorderSize\(\) int](<#Style.GetHorizontalBorderSize>) + - [func \(s Style\) GetHorizontalFrameSize\(\) int](<#Style.GetHorizontalFrameSize>) + - [func \(s Style\) GetHorizontalMargins\(\) int](<#Style.GetHorizontalMargins>) + - [func \(s Style\) GetHorizontalPadding\(\) int](<#Style.GetHorizontalPadding>) + - [func \(s Style\) GetInline\(\) bool](<#Style.GetInline>) + - [func \(s Style\) GetItalic\(\) bool](<#Style.GetItalic>) + - [func \(s Style\) GetMargin\(\) \(top, right, bottom, left int\)](<#Style.GetMargin>) + - [func \(s Style\) GetMarginBottom\(\) int](<#Style.GetMarginBottom>) + - [func \(s Style\) GetMarginLeft\(\) int](<#Style.GetMarginLeft>) + - [func \(s Style\) GetMarginRight\(\) int](<#Style.GetMarginRight>) + - [func \(s Style\) GetMarginTop\(\) int](<#Style.GetMarginTop>) + - [func \(s Style\) GetMaxHeight\(\) int](<#Style.GetMaxHeight>) + - [func \(s Style\) GetMaxWidth\(\) int](<#Style.GetMaxWidth>) + - [func \(s Style\) GetPadding\(\) \(top, right, bottom, left int\)](<#Style.GetPadding>) + - [func \(s Style\) GetPaddingBottom\(\) int](<#Style.GetPaddingBottom>) + - [func \(s Style\) GetPaddingLeft\(\) int](<#Style.GetPaddingLeft>) + - [func \(s Style\) GetPaddingRight\(\) int](<#Style.GetPaddingRight>) + - [func \(s Style\) GetPaddingTop\(\) int](<#Style.GetPaddingTop>) + - [func \(s Style\) GetReverse\(\) bool](<#Style.GetReverse>) + - [func \(s Style\) GetStrikethrough\(\) bool](<#Style.GetStrikethrough>) + - [func \(s Style\) GetStrikethroughSpaces\(\) bool](<#Style.GetStrikethroughSpaces>) + - [func \(s Style\) GetTabWidth\(\) int](<#Style.GetTabWidth>) + - [func \(s Style\) GetTransform\(\) func\(string\) string](<#Style.GetTransform>) + - [func \(s Style\) GetUnderline\(\) bool](<#Style.GetUnderline>) + - [func \(s Style\) GetUnderlineSpaces\(\) bool](<#Style.GetUnderlineSpaces>) + - [func \(s Style\) GetVerticalBorderSize\(\) int](<#Style.GetVerticalBorderSize>) + - [func \(s Style\) GetVerticalFrameSize\(\) int](<#Style.GetVerticalFrameSize>) + - [func \(s Style\) GetVerticalMargins\(\) int](<#Style.GetVerticalMargins>) + - [func \(s Style\) GetVerticalPadding\(\) int](<#Style.GetVerticalPadding>) + - [func \(s Style\) GetWidth\(\) int](<#Style.GetWidth>) + - [func \(s Style\) Height\(i int\) Style](<#Style.Height>) + - [func \(s Style\) Inherit\(i Style\) Style](<#Style.Inherit>) + - [func \(s Style\) Inline\(v bool\) Style](<#Style.Inline>) + - [func \(s Style\) Italic\(v bool\) Style](<#Style.Italic>) + - [func \(s Style\) Margin\(i ...int\) Style](<#Style.Margin>) + - [func \(s Style\) MarginBackground\(c TerminalColor\) Style](<#Style.MarginBackground>) + - [func \(s Style\) MarginBottom\(i int\) Style](<#Style.MarginBottom>) + - [func \(s Style\) MarginLeft\(i int\) Style](<#Style.MarginLeft>) + - [func \(s Style\) MarginRight\(i int\) Style](<#Style.MarginRight>) + - [func \(s Style\) MarginTop\(i int\) Style](<#Style.MarginTop>) + - [func \(s Style\) MaxHeight\(n int\) Style](<#Style.MaxHeight>) + - [func \(s Style\) MaxWidth\(n int\) Style](<#Style.MaxWidth>) + - [func \(s Style\) Padding\(i ...int\) Style](<#Style.Padding>) + - [func \(s Style\) PaddingBottom\(i int\) Style](<#Style.PaddingBottom>) + - [func \(s Style\) PaddingLeft\(i int\) Style](<#Style.PaddingLeft>) + - [func \(s Style\) PaddingRight\(i int\) Style](<#Style.PaddingRight>) + - [func \(s Style\) PaddingTop\(i int\) Style](<#Style.PaddingTop>) + - [func \(s Style\) Render\(strs ...string\) string](<#Style.Render>) + - [func \(s Style\) Renderer\(r \*Renderer\) Style](<#Style.Renderer>) + - [func \(s Style\) Reverse\(v bool\) Style](<#Style.Reverse>) + - [func \(s Style\) SetString\(strs ...string\) Style](<#Style.SetString>) + - [func \(s Style\) Strikethrough\(v bool\) Style](<#Style.Strikethrough>) + - [func \(s Style\) StrikethroughSpaces\(v bool\) Style](<#Style.StrikethroughSpaces>) + - [func \(s Style\) String\(\) string](<#Style.String>) + - [func \(s Style\) TabWidth\(n int\) Style](<#Style.TabWidth>) + - [func \(s Style\) Transform\(fn func\(string\) string\) Style](<#Style.Transform>) + - [func \(s Style\) Underline\(v bool\) Style](<#Style.Underline>) + - [func \(s Style\) UnderlineSpaces\(v bool\) Style](<#Style.UnderlineSpaces>) + - [func \(s Style\) UnsetAlign\(\) Style](<#Style.UnsetAlign>) + - [func \(s Style\) UnsetAlignHorizontal\(\) Style](<#Style.UnsetAlignHorizontal>) + - [func \(s Style\) UnsetAlignVertical\(\) Style](<#Style.UnsetAlignVertical>) + - [func \(s Style\) UnsetBackground\(\) Style](<#Style.UnsetBackground>) + - [func \(s Style\) UnsetBlink\(\) Style](<#Style.UnsetBlink>) + - [func \(s Style\) UnsetBold\(\) Style](<#Style.UnsetBold>) + - [func \(s Style\) UnsetBorderBackground\(\) Style](<#Style.UnsetBorderBackground>) + - [func \(s Style\) UnsetBorderBottom\(\) Style](<#Style.UnsetBorderBottom>) + - [func \(s Style\) UnsetBorderBottomBackground\(\) Style](<#Style.UnsetBorderBottomBackground>) + - [func \(s Style\) UnsetBorderBottomForeground\(\) Style](<#Style.UnsetBorderBottomForeground>) + - [func \(s Style\) UnsetBorderForeground\(\) Style](<#Style.UnsetBorderForeground>) + - [func \(s Style\) UnsetBorderLeft\(\) Style](<#Style.UnsetBorderLeft>) + - [func \(s Style\) UnsetBorderLeftBackground\(\) Style](<#Style.UnsetBorderLeftBackground>) + - [func \(s Style\) UnsetBorderLeftForeground\(\) Style](<#Style.UnsetBorderLeftForeground>) + - [func \(s Style\) UnsetBorderRight\(\) Style](<#Style.UnsetBorderRight>) + - [func \(s Style\) UnsetBorderRightBackground\(\) Style](<#Style.UnsetBorderRightBackground>) + - [func \(s Style\) UnsetBorderRightForeground\(\) Style](<#Style.UnsetBorderRightForeground>) + - [func \(s Style\) UnsetBorderStyle\(\) Style](<#Style.UnsetBorderStyle>) + - [func \(s Style\) UnsetBorderTop\(\) Style](<#Style.UnsetBorderTop>) + - [func \(s Style\) UnsetBorderTopBackground\(\) Style](<#Style.UnsetBorderTopBackground>) + - [func \(s Style\) UnsetBorderTopBackgroundColor\(\) Style](<#Style.UnsetBorderTopBackgroundColor>) + - [func \(s Style\) UnsetBorderTopForeground\(\) Style](<#Style.UnsetBorderTopForeground>) + - [func \(s Style\) UnsetColorWhitespace\(\) Style](<#Style.UnsetColorWhitespace>) + - [func \(s Style\) UnsetFaint\(\) Style](<#Style.UnsetFaint>) + - [func \(s Style\) UnsetForeground\(\) Style](<#Style.UnsetForeground>) + - [func \(s Style\) UnsetHeight\(\) Style](<#Style.UnsetHeight>) + - [func \(s Style\) UnsetInline\(\) Style](<#Style.UnsetInline>) + - [func \(s Style\) UnsetItalic\(\) Style](<#Style.UnsetItalic>) + - [func \(s Style\) UnsetMarginBackground\(\) Style](<#Style.UnsetMarginBackground>) + - [func \(s Style\) UnsetMarginBottom\(\) Style](<#Style.UnsetMarginBottom>) + - [func \(s Style\) UnsetMarginLeft\(\) Style](<#Style.UnsetMarginLeft>) + - [func \(s Style\) UnsetMarginRight\(\) Style](<#Style.UnsetMarginRight>) + - [func \(s Style\) UnsetMarginTop\(\) Style](<#Style.UnsetMarginTop>) + - [func \(s Style\) UnsetMargins\(\) Style](<#Style.UnsetMargins>) + - [func \(s Style\) UnsetMaxHeight\(\) Style](<#Style.UnsetMaxHeight>) + - [func \(s Style\) UnsetMaxWidth\(\) Style](<#Style.UnsetMaxWidth>) + - [func \(s Style\) UnsetPadding\(\) Style](<#Style.UnsetPadding>) + - [func \(s Style\) UnsetPaddingBottom\(\) Style](<#Style.UnsetPaddingBottom>) + - [func \(s Style\) UnsetPaddingLeft\(\) Style](<#Style.UnsetPaddingLeft>) + - [func \(s Style\) UnsetPaddingRight\(\) Style](<#Style.UnsetPaddingRight>) + - [func \(s Style\) UnsetPaddingTop\(\) Style](<#Style.UnsetPaddingTop>) + - [func \(s Style\) UnsetReverse\(\) Style](<#Style.UnsetReverse>) + - [func \(s Style\) UnsetStrikethrough\(\) Style](<#Style.UnsetStrikethrough>) + - [func \(s Style\) UnsetStrikethroughSpaces\(\) Style](<#Style.UnsetStrikethroughSpaces>) + - [func \(s Style\) UnsetString\(\) Style](<#Style.UnsetString>) + - [func \(s Style\) UnsetTabWidth\(\) Style](<#Style.UnsetTabWidth>) + - [func \(s Style\) UnsetTransform\(\) Style](<#Style.UnsetTransform>) + - [func \(s Style\) UnsetUnderline\(\) Style](<#Style.UnsetUnderline>) + - [func \(s Style\) UnsetUnderlineSpaces\(\) Style](<#Style.UnsetUnderlineSpaces>) + - [func \(s Style\) UnsetWidth\(\) Style](<#Style.UnsetWidth>) + - [func \(s Style\) Value\(\) string](<#Style.Value>) + - [func \(s Style\) Width\(i int\) Style](<#Style.Width>) +- [type TerminalColor](<#TerminalColor>) +- [type WhitespaceOption](<#WhitespaceOption>) + - [func WithWhitespaceBackground\(c TerminalColor\) WhitespaceOption](<#WithWhitespaceBackground>) + - [func WithWhitespaceChars\(s string\) WhitespaceOption](<#WithWhitespaceChars>) + - [func WithWhitespaceForeground\(c TerminalColor\) WhitespaceOption](<#WithWhitespaceForeground>) + + +## Constants + +NoTabConversion can be passed to [Style.TabWidth](<#Style.TabWidth>) to disable the replacement of tabs with spaces at render time. + +```go +const NoTabConversion = -1 +``` + + +## func ColorProfile + +```go +func ColorProfile() termenv.Profile +``` + +ColorProfile returns the detected termenv color profile. + + +## func HasDarkBackground + +```go +func HasDarkBackground() bool +``` + +HasDarkBackground returns whether or not the terminal has a dark background. + + +## func Height + +```go +func Height(str string) int +``` + +Height returns height of a string in cells. This is done simply by counting \\n characters. If your strings use \\r\\n for newlines you should convert them to \\n first, or simply write a separate function for measuring height. + + +## func JoinHorizontal + +```go +func JoinHorizontal(pos Position, strs ...string) string +``` + +JoinHorizontal is a utility function for horizontally joining two potentially multi\-lined strings along a vertical axis. The first argument is the position, with 0 being all the way at the top and 1 being all the way at the bottom. + +If you just want to align to the top, center or bottom you may as well just use the helper constants Top, Center, and Bottom. + +Example: + +``` +blockB := "...\n...\n..." +blockA := "...\n...\n...\n...\n..." + +// Join 20% from the top +str := lipgloss.JoinHorizontal(0.2, blockA, blockB) + +// Join on the top edge +str := lipgloss.JoinHorizontal(lipgloss.Top, blockA, blockB) +``` + + +## func JoinVertical + +```go +func JoinVertical(pos Position, strs ...string) string +``` + +JoinVertical is a utility function for vertically joining two potentially multi\-lined strings along a horizontal axis. The first argument is the position, with 0 being all the way to the left and 1 being all the way to the right. + +If you just want to align to the left, right or center you may as well just use the helper constants Left, Center, and Right. + +Example: + +``` +blockB := "...\n...\n..." +blockA := "...\n...\n...\n...\n..." + +// Join 20% from the top +str := lipgloss.JoinVertical(0.2, blockA, blockB) + +// Join on the right edge +str := lipgloss.JoinVertical(lipgloss.Right, blockA, blockB) +``` + + +## func Place + +```go +func Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string +``` + +Place places a string or text block vertically in an unstyled box of a given width or height. + + +## func PlaceHorizontal + +```go +func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string +``` + +PlaceHorizontal places a string or text block horizontally in an unstyled block of a given width. If the given width is shorter than the max width of the string \(measured by its longest line\) this will be a noop. + + +## func PlaceVertical + +```go +func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string +``` + +PlaceVertical places a string or text block vertically in an unstyled block of a given height. If the given height is shorter than the height of the string \(measured by its newlines\) then this will be a noop. + + +## func SetColorProfile + +```go +func SetColorProfile(p termenv.Profile) +``` + +SetColorProfile sets the color profile on the default renderer. This function exists mostly for testing purposes so that you can assure you're testing against a specific profile. + +Outside of testing you likely won't want to use this function as the color profile will detect and cache the terminal's color capabilities and choose the best available profile. + +Available color profiles are: + +``` +termenv.Ascii // no color, 1-bit +termenv.ANSI //16 colors, 4-bit +termenv.ANSI256 // 256 colors, 8-bit +termenv.TrueColor // 16,777,216 colors, 24-bit +``` + +This function is thread\-safe. + + +## func SetDefaultRenderer + +```go +func SetDefaultRenderer(r *Renderer) +``` + +SetDefaultRenderer sets the default global renderer. + + +## func SetHasDarkBackground + +```go +func SetHasDarkBackground(b bool) +``` + +SetHasDarkBackground sets the background color detection value for the default renderer. This function exists mostly for testing purposes so that you can assure you're testing against a specific background color setting. + +Outside of testing you likely won't want to use this function as the backgrounds value will be automatically detected and cached against the terminal's current background color setting. + +This function is thread\-safe. + + +## func Size + +```go +func Size(str string) (width, height int) +``` + +Size returns the width and height of the string in cells. ANSI sequences are ignored and characters wider than one cell \(such as Chinese characters and emojis\) are appropriately measured. + + +## func StyleRanges + +```go +func StyleRanges(s string, ranges ...Range) string +``` + +StyleRanges allows to, given a string, style ranges of it differently. The function will take into account existing styles. Ranges should not overlap. + + +## func StyleRunes + +```go +func StyleRunes(str string, indices []int, matched, unmatched Style) string +``` + +StyleRunes apply a given style to runes at the given indices in the string. Note that you must provide styling options for both matched and unmatched runes. Indices out of bounds will be ignored. + + +## func Width + +```go +func Width(str string) (width int) +``` + +Width returns the cell width of characters in the string. ANSI sequences are ignored and characters wider than one cell \(such as Chinese characters and emojis\) are appropriately measured. + +You should use this instead of len\(string\) len\(\[\]rune\(string\) as neither will give you accurate results. + + +## type ANSIColor + +ANSIColor is a color specified by an ANSI color value. It's merely syntactic sugar for the more general Color function. Invalid colors will render as black. + +Example usage: + +``` +// These two statements are equivalent. +colorA := lipgloss.ANSIColor(21) +colorB := lipgloss.Color("21") +``` + +```go +type ANSIColor uint +``` + + +### func \(ANSIColor\) RGBA + +```go +func (ac ANSIColor) RGBA() (r, g, b, a uint32) +``` + +RGBA returns the RGBA value of this color. This satisfies the Go Color interface. Note that on error we return black with 100% opacity, or: + +Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. + +Deprecated. + + +## type AdaptiveColor + +AdaptiveColor provides color options for light and dark backgrounds. The appropriate color will be returned at runtime based on the darkness of the terminal background color. + +Example usage: + +``` +color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"} +``` + +```go +type AdaptiveColor struct { + Light string + Dark string +} +``` + + +### func \(AdaptiveColor\) RGBA + +```go +func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) +``` + +RGBA returns the RGBA value of this color. This satisfies the Go Color interface. Note that on error we return black with 100% opacity, or: + +Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. + +Deprecated. + + +## type Border + +Border contains a series of values which comprise the various parts of a border. + +```go +type Border struct { + Top string + Bottom string + Left string + Right string + TopLeft string + TopRight string + BottomLeft string + BottomRight string + MiddleLeft string + MiddleRight string + Middle string + MiddleTop string + MiddleBottom string +} +``` + + +### func ASCIIBorder + +```go +func ASCIIBorder() Border +``` + +ASCIIBorder returns a table border with ASCII characters. + + +### func BlockBorder + +```go +func BlockBorder() Border +``` + +BlockBorder returns a border that takes the whole block. + + +### func DoubleBorder + +```go +func DoubleBorder() Border +``` + +DoubleBorder returns a border comprised of two thin strokes. + + +### func HiddenBorder + +```go +func HiddenBorder() Border +``` + +HiddenBorder returns a border that renders as a series of single\-cell spaces. It's useful for cases when you want to remove a standard border but maintain layout positioning. This said, you can still apply a background color to a hidden border. + + +### func InnerHalfBlockBorder + +```go +func InnerHalfBlockBorder() Border +``` + +InnerHalfBlockBorder returns a half\-block border that sits inside the frame. + + +### func MarkdownBorder + +```go +func MarkdownBorder() Border +``` + +MarkdownBorder return a table border in markdown style. + +Make sure to disable top and bottom border for the best result. This will ensure that the output is valid markdown. + +``` +table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false) +``` + + +### func NormalBorder + +```go +func NormalBorder() Border +``` + +NormalBorder returns a standard\-type border with a normal weight and 90 degree corners. + + +### func OuterHalfBlockBorder + +```go +func OuterHalfBlockBorder() Border +``` + +OuterHalfBlockBorder returns a half\-block border that sits outside the frame. + + +### func RoundedBorder + +```go +func RoundedBorder() Border +``` + +RoundedBorder returns a border with rounded corners. + + +### func ThickBorder + +```go +func ThickBorder() Border +``` + +ThickBorder returns a border that's thicker than the one returned by NormalBorder. + + +### func \(Border\) GetBottomSize + +```go +func (b Border) GetBottomSize() int +``` + +GetBottomSize returns the width of the bottom border. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the bottom edge, 0 is returned. + + +### func \(Border\) GetLeftSize + +```go +func (b Border) GetLeftSize() int +``` + +GetLeftSize returns the width of the left border. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the left edge, 0 is returned. + + +### func \(Border\) GetRightSize + +```go +func (b Border) GetRightSize() int +``` + +GetRightSize returns the width of the right border. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the right edge, 0 is returned. + + +### func \(Border\) GetTopSize + +```go +func (b Border) GetTopSize() int +``` + +GetTopSize returns the width of the top border. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the top edge, 0 is returned. + + +## type Color + +Color specifies a color by hex or ANSI value. For example: + +``` +ansiColor := lipgloss.Color("21") +hexColor := lipgloss.Color("#0000ff") +``` + +```go +type Color string +``` + + +### func \(Color\) RGBA + +```go +func (c Color) RGBA() (r, g, b, a uint32) +``` + +RGBA returns the RGBA value of this color. This satisfies the Go Color interface. Note that on error we return black with 100% opacity, or: + +Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. + +Deprecated. + + +## type CompleteAdaptiveColor + +CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color profiles, with separate options for light and dark backgrounds. Automatic color degradation will not be performed. + +```go +type CompleteAdaptiveColor struct { + Light CompleteColor + Dark CompleteColor +} +``` + + +### func \(CompleteAdaptiveColor\) RGBA + +```go +func (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) +``` + +RGBA returns the RGBA value of this color. This satisfies the Go Color interface. Note that on error we return black with 100% opacity, or: + +Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. + +Deprecated. + + +## type CompleteColor + +CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color profiles. Automatic color degradation will not be performed. + +```go +type CompleteColor struct { + TrueColor string + ANSI256 string + ANSI string +} +``` + + +### func \(CompleteColor\) RGBA + +```go +func (c CompleteColor) RGBA() (r, g, b, a uint32) +``` + +RGBA returns the RGBA value of this color. This satisfies the Go Color interface. Note that on error we return black with 100% opacity, or: + +Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color + +Deprecated. + + +## type NoColor + +NoColor is used to specify the absence of color styling. When this is active foreground colors will be rendered with the terminal's default text color, and background colors will not be drawn at all. + +Example usage: + +``` +var style = someStyle.Background(lipgloss.NoColor{}) +``` + +```go +type NoColor struct{} +``` + + +### func \(NoColor\) RGBA + +```go +func (n NoColor) RGBA() (r, g, b, a uint32) +``` + +RGBA returns the RGBA value of this color. Because we have to return something, despite this color being the absence of color, we're returning black with 100% opacity. + +Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. + +Deprecated. + + +## type Position + +Position represents a position along a horizontal or vertical axis. It's in situations where an axis is involved, like alignment, joining, placement and so on. + +A value of 0 represents the start \(the left or top\) and 1 represents the end \(the right or bottom\). 0.5 represents the center. + +There are constants Top, Bottom, Center, Left and Right in this package that can be used to aid readability. + +```go +type Position float64 +``` + +Position aliases. + +```go +const ( + Top Position = 0.0 + Bottom Position = 1.0 + Center Position = 0.5 + Left Position = 0.0 + Right Position = 1.0 +) +``` + + +## type Range + +Range to be used with [StyleRanges](<#StyleRanges>). + +```go +type Range struct { + Start, End int + Style Style +} +``` + + +### func NewRange + +```go +func NewRange(start, end int, style Style) Range +``` + +NewRange returns a range that can be used with [StyleRanges](<#StyleRanges>). + + +## type Renderer + +Renderer is a lipgloss terminal renderer. + +```go +type Renderer struct { + // contains filtered or unexported fields +} +``` + + +### func DefaultRenderer + +```go +func DefaultRenderer() *Renderer +``` + +DefaultRenderer returns the default renderer. + + +### func NewRenderer + +```go +func NewRenderer(w io.Writer, opts ...termenv.OutputOption) *Renderer +``` + +NewRenderer creates a new Renderer. + +w will be used to determine the terminal's color capabilities. + + +### func \(\*Renderer\) ColorProfile + +```go +func (r *Renderer) ColorProfile() termenv.Profile +``` + +ColorProfile returns the detected termenv color profile. + + +### func \(\*Renderer\) HasDarkBackground + +```go +func (r *Renderer) HasDarkBackground() bool +``` + +HasDarkBackground returns whether or not the renderer will render to a dark background. A dark background can either be auto\-detected, or set explicitly on the renderer. + + +### func \(\*Renderer\) NewStyle + +```go +func (r *Renderer) NewStyle() Style +``` + +NewStyle returns a new, empty Style. While it's syntactic sugar for the Style\{\} primitive, it's recommended to use this function for creating styles in case the underlying implementation changes. It takes an optional string value to be set as the underlying string value for this style. + + +### func \(\*Renderer\) Output + +```go +func (r *Renderer) Output() *termenv.Output +``` + +Output returns the termenv output. + + +### func \(\*Renderer\) Place + +```go +func (r *Renderer) Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string +``` + +Place places a string or text block vertically in an unstyled box of a given width or height. + + +### func \(\*Renderer\) PlaceHorizontal + +```go +func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string +``` + +PlaceHorizontal places a string or text block horizontally in an unstyled block of a given width. If the given width is shorter than the max width of the string \(measured by its longest line\) this will be a noöp. + + +### func \(\*Renderer\) PlaceVertical + +```go +func (r *Renderer) PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string +``` + +PlaceVertical places a string or text block vertically in an unstyled block of a given height. If the given height is shorter than the height of the string \(measured by its newlines\) then this will be a noöp. + + +### func \(\*Renderer\) SetColorProfile + +```go +func (r *Renderer) SetColorProfile(p termenv.Profile) +``` + +SetColorProfile sets the color profile on the renderer. This function exists mostly for testing purposes so that you can assure you're testing against a specific profile. + +Outside of testing you likely won't want to use this function as the color profile will detect and cache the terminal's color capabilities and choose the best available profile. + +Available color profiles are: + +``` +termenv.Ascii // no color, 1-bit +termenv.ANSI //16 colors, 4-bit +termenv.ANSI256 // 256 colors, 8-bit +termenv.TrueColor // 16,777,216 colors, 24-bit +``` + +This function is thread\-safe. + + +### func \(\*Renderer\) SetHasDarkBackground + +```go +func (r *Renderer) SetHasDarkBackground(b bool) +``` + +SetHasDarkBackground sets the background color detection value on the renderer. This function exists mostly for testing purposes so that you can assure you're testing against a specific background color setting. + +Outside of testing you likely won't want to use this function as the backgrounds value will be automatically detected and cached against the terminal's current background color setting. + +This function is thread\-safe. + + +### func \(\*Renderer\) SetOutput + +```go +func (r *Renderer) SetOutput(o *termenv.Output) +``` + +SetOutput sets the termenv output. + + +## type Style + +Style contains a set of rules that comprise a style as a whole. + +```go +type Style struct { + // contains filtered or unexported fields +} +``` + + +### func NewStyle + +```go +func NewStyle() Style +``` + +NewStyle returns a new, empty Style. While it's syntactic sugar for the Style\{\} primitive, it's recommended to use this function for creating styles in case the underlying implementation changes. It takes an optional string value to be set as the underlying string value for this style. + + +### func \(Style\) Align + +```go +func (s Style) Align(p ...Position) Style +``` + +Align is a shorthand method for setting horizontal and vertical alignment. + +With one argument, the position value is applied to the horizontal alignment. + +With two arguments, the value is applied to the horizontal and vertical alignments, in that order. + + +### func \(Style\) AlignHorizontal + +```go +func (s Style) AlignHorizontal(p Position) Style +``` + +AlignHorizontal sets a horizontal text alignment rule. + + +### func \(Style\) AlignVertical + +```go +func (s Style) AlignVertical(p Position) Style +``` + +AlignVertical sets a vertical text alignment rule. + + +### func \(Style\) Background + +```go +func (s Style) Background(c TerminalColor) Style +``` + +Background sets a background color. + + +### func \(Style\) Blink + +```go +func (s Style) Blink(v bool) Style +``` + +Blink sets a rule for blinking foreground text. + + +### func \(Style\) Bold + +```go +func (s Style) Bold(v bool) Style +``` + +Bold sets a bold formatting rule. + + +### func \(Style\) Border + +```go +func (s Style) Border(b Border, sides ...bool) Style +``` + +Border is shorthand for setting the border style and which sides should have a border at once. The variadic argument sides works as follows: + +With one value, the value is applied to all sides. + +With two values, the values are applied to the vertical and horizontal sides, in that order. + +With three values, the values are applied to the top side, the horizontal sides, and the bottom side, in that order. + +With four values, the values are applied clockwise starting from the top side, followed by the right side, then the bottom, and finally the left. + +With more than four arguments the border will be applied to all sides. + +Examples: + +``` +// Applies borders to the top and bottom only +lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false) + +// Applies rounded borders to the right and bottom only +lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), false, true, true, false) +``` + + +### func \(Style\) BorderBackground + +```go +func (s Style) BorderBackground(c ...TerminalColor) Style +``` + +BorderBackground is a shorthand function for setting all of the background colors of the borders at once. The arguments work as follows: + +With one argument, the argument is applied to all sides. + +With two arguments, the arguments are applied to the vertical and horizontal sides, in that order. + +With three arguments, the arguments are applied to the top side, the horizontal sides, and the bottom side, in that order. + +With four arguments, the arguments are applied clockwise starting from the top side, followed by the right side, then the bottom, and finally the left. + +With more than four arguments nothing will be set. + + +### func \(Style\) BorderBottom + +```go +func (s Style) BorderBottom(v bool) Style +``` + +BorderBottom determines whether or not to draw a bottom border. + + +### func \(Style\) BorderBottomBackground + +```go +func (s Style) BorderBottomBackground(c TerminalColor) Style +``` + +BorderBottomBackground sets the background color of the bottom of the border. + + +### func \(Style\) BorderBottomForeground + +```go +func (s Style) BorderBottomForeground(c TerminalColor) Style +``` + +BorderBottomForeground sets the foreground color for the bottom of the border. + + +### func \(Style\) BorderForeground + +```go +func (s Style) BorderForeground(c ...TerminalColor) Style +``` + +BorderForeground is a shorthand function for setting all of the foreground colors of the borders at once. The arguments work as follows: + +With one argument, the argument is applied to all sides. + +With two arguments, the arguments are applied to the vertical and horizontal sides, in that order. + +With three arguments, the arguments are applied to the top side, the horizontal sides, and the bottom side, in that order. + +With four arguments, the arguments are applied clockwise starting from the top side, followed by the right side, then the bottom, and finally the left. + +With more than four arguments nothing will be set. + + +### func \(Style\) BorderLeft + +```go +func (s Style) BorderLeft(v bool) Style +``` + +BorderLeft determines whether or not to draw a left border. + + +### func \(Style\) BorderLeftBackground + +```go +func (s Style) BorderLeftBackground(c TerminalColor) Style +``` + +BorderLeftBackground set the background color of the left side of the border. + + +### func \(Style\) BorderLeftForeground + +```go +func (s Style) BorderLeftForeground(c TerminalColor) Style +``` + +BorderLeftForeground sets the foreground color for the left side of the border. + + +### func \(Style\) BorderRight + +```go +func (s Style) BorderRight(v bool) Style +``` + +BorderRight determines whether or not to draw a right border. + + +### func \(Style\) BorderRightBackground + +```go +func (s Style) BorderRightBackground(c TerminalColor) Style +``` + +BorderRightBackground sets the background color of right side the border. + + +### func \(Style\) BorderRightForeground + +```go +func (s Style) BorderRightForeground(c TerminalColor) Style +``` + +BorderRightForeground sets the foreground color for the right side of the border. + + +### func \(Style\) BorderStyle + +```go +func (s Style) BorderStyle(b Border) Style +``` + +BorderStyle defines the Border on a style. A Border contains a series of definitions for the sides and corners of a border. + +Note that if border visibility has not been set for any sides when setting the border style, the border will be enabled for all sides during rendering. + +You can define border characters as you'd like, though several default styles are included: NormalBorder\(\), RoundedBorder\(\), BlockBorder\(\), OuterHalfBlockBorder\(\), InnerHalfBlockBorder\(\), ThickBorder\(\), and DoubleBorder\(\). + +Example: + +``` +lipgloss.NewStyle().BorderStyle(lipgloss.ThickBorder()) +``` + + +### func \(Style\) BorderTop + +```go +func (s Style) BorderTop(v bool) Style +``` + +BorderTop determines whether or not to draw a top border. + + +### func \(Style\) BorderTopBackground + +```go +func (s Style) BorderTopBackground(c TerminalColor) Style +``` + +BorderTopBackground sets the background color of the top of the border. + + +### func \(Style\) BorderTopForeground + +```go +func (s Style) BorderTopForeground(c TerminalColor) Style +``` + +BorderTopForeground set the foreground color for the top of the border. + + +### func \(Style\) ColorWhitespace + +```go +func (s Style) ColorWhitespace(v bool) Style +``` + +ColorWhitespace determines whether or not the background color should be applied to the padding. This is true by default as it's more than likely the desired and expected behavior, but it can be disabled for certain graphic effects. + +Deprecated: Just use margins and padding. + + +### func \(Style\) Copy + +```go +func (s Style) Copy() Style +``` + +Copy returns a copy of this style, including any underlying string values. + +Deprecated: to copy just use assignment \(i.e. a := b\). All methods also return a new style. + + +### func \(Style\) Faint + +```go +func (s Style) Faint(v bool) Style +``` + +Faint sets a rule for rendering the foreground color in a dimmer shade. + + +### func \(Style\) Foreground + +```go +func (s Style) Foreground(c TerminalColor) Style +``` + +Foreground sets a foreground color. + +``` +// Sets the foreground to blue +s := lipgloss.NewStyle().Foreground(lipgloss.Color("#0000ff")) + +// Removes the foreground color +s.Foreground(lipgloss.NoColor) +``` + + +### func \(Style\) GetAlign + +```go +func (s Style) GetAlign() Position +``` + +GetAlign returns the style's implicit horizontal alignment setting. If no alignment is set Position.Left is returned. + + +### func \(Style\) GetAlignHorizontal + +```go +func (s Style) GetAlignHorizontal() Position +``` + +GetAlignHorizontal returns the style's implicit horizontal alignment setting. If no alignment is set Position.Left is returned. + + +### func \(Style\) GetAlignVertical + +```go +func (s Style) GetAlignVertical() Position +``` + +GetAlignVertical returns the style's implicit vertical alignment setting. If no alignment is set Position.Top is returned. + + +### func \(Style\) GetBackground + +```go +func (s Style) GetBackground() TerminalColor +``` + +GetBackground returns the style's background color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetBlink + +```go +func (s Style) GetBlink() bool +``` + +GetBlink returns the style's blink value. If no value is set false is returned. + + +### func \(Style\) GetBold + +```go +func (s Style) GetBold() bool +``` + +GetBold returns the style's bold value. If no value is set false is returned. + + +### func \(Style\) GetBorder + +```go +func (s Style) GetBorder() (b Border, top, right, bottom, left bool) +``` + +GetBorder returns the style's border style \(type Border\) and value for the top, right, bottom, and left in that order. If no value is set for the border style, Border\{\} is returned. For all other unset values false is returned. + + +### func \(Style\) GetBorderBottom + +```go +func (s Style) GetBorderBottom() bool +``` + +GetBorderBottom returns the style's bottom border setting. If no value is set false is returned. + + +### func \(Style\) GetBorderBottomBackground + +```go +func (s Style) GetBorderBottomBackground() TerminalColor +``` + +GetBorderBottomBackground returns the style's border bottom background color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetBorderBottomForeground + +```go +func (s Style) GetBorderBottomForeground() TerminalColor +``` + +GetBorderBottomForeground returns the style's border bottom foreground color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetBorderBottomSize + +```go +func (s Style) GetBorderBottomSize() int +``` + +GetBorderBottomSize returns the width of the bottom border. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the left edge, 0 is returned. + + +### func \(Style\) GetBorderLeft + +```go +func (s Style) GetBorderLeft() bool +``` + +GetBorderLeft returns the style's left border setting. If no value is set false is returned. + + +### func \(Style\) GetBorderLeftBackground + +```go +func (s Style) GetBorderLeftBackground() TerminalColor +``` + +GetBorderLeftBackground returns the style's border left background color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetBorderLeftForeground + +```go +func (s Style) GetBorderLeftForeground() TerminalColor +``` + +GetBorderLeftForeground returns the style's border left foreground color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetBorderLeftSize + +```go +func (s Style) GetBorderLeftSize() int +``` + +GetBorderLeftSize returns the width of the left border. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the left edge, 0 is returned. + + +### func \(Style\) GetBorderRight + +```go +func (s Style) GetBorderRight() bool +``` + +GetBorderRight returns the style's right border setting. If no value is set false is returned. + + +### func \(Style\) GetBorderRightBackground + +```go +func (s Style) GetBorderRightBackground() TerminalColor +``` + +GetBorderRightBackground returns the style's border right background color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetBorderRightForeground + +```go +func (s Style) GetBorderRightForeground() TerminalColor +``` + +GetBorderRightForeground returns the style's border right foreground color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetBorderRightSize + +```go +func (s Style) GetBorderRightSize() int +``` + +GetBorderRightSize returns the width of the right border. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the right edge, 0 is returned. + + +### func \(Style\) GetBorderStyle + +```go +func (s Style) GetBorderStyle() Border +``` + +GetBorderStyle returns the style's border style \(type Border\). If no value is set Border\{\} is returned. + + +### func \(Style\) GetBorderTop + +```go +func (s Style) GetBorderTop() bool +``` + +GetBorderTop returns the style's top border setting. If no value is set false is returned. + + +### func \(Style\) GetBorderTopBackground + +```go +func (s Style) GetBorderTopBackground() TerminalColor +``` + +GetBorderTopBackground returns the style's border top background color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetBorderTopForeground + +```go +func (s Style) GetBorderTopForeground() TerminalColor +``` + +GetBorderTopForeground returns the style's border top foreground color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetBorderTopSize + +```go +func (s Style) GetBorderTopSize() int +``` + +GetBorderTopSize returns the width of the top border. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the top edge, 0 is returned. + + +### func \(Style\) GetBorderTopWidth + +```go +func (s Style) GetBorderTopWidth() int +``` + +GetBorderTopWidth returns the width of the top border. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the top edge, 0 is returned. + +Deprecated: This function simply calls Style.GetBorderTopSize. + + +### func \(Style\) GetColorWhitespace + +```go +func (s Style) GetColorWhitespace() bool +``` + +GetColorWhitespace returns the style's whitespace coloring setting. If no value is set false is returned. + + +### func \(Style\) GetFaint + +```go +func (s Style) GetFaint() bool +``` + +GetFaint returns the style's faint value. If no value is set false is returned. + + +### func \(Style\) GetForeground + +```go +func (s Style) GetForeground() TerminalColor +``` + +GetForeground returns the style's foreground color. If no value is set NoColor\{\} is returned. + + +### func \(Style\) GetFrameSize + +```go +func (s Style) GetFrameSize() (x, y int) +``` + +GetFrameSize returns the sum of the margins, padding and border width for both the horizontal and vertical margins. + + +### func \(Style\) GetHeight + +```go +func (s Style) GetHeight() int +``` + +GetHeight returns the style's height setting. If no height is set 0 is returned. + + +### func \(Style\) GetHorizontalBorderSize + +```go +func (s Style) GetHorizontalBorderSize() int +``` + +GetHorizontalBorderSize returns the width of the horizontal borders. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the horizontal edges, 0 is returned. + + +### func \(Style\) GetHorizontalFrameSize + +```go +func (s Style) GetHorizontalFrameSize() int +``` + +GetHorizontalFrameSize returns the sum of the style's horizontal margins, padding and border widths. + +Provisional: this method may be renamed. + + +### func \(Style\) GetHorizontalMargins + +```go +func (s Style) GetHorizontalMargins() int +``` + +GetHorizontalMargins returns the style's left and right margins. Unset values are measured as 0. + + +### func \(Style\) GetHorizontalPadding + +```go +func (s Style) GetHorizontalPadding() int +``` + +GetHorizontalPadding returns the style's left and right padding. Unset values are measured as 0. + + +### func \(Style\) GetInline + +```go +func (s Style) GetInline() bool +``` + +GetInline returns the style's inline setting. If no value is set false is returned. + + +### func \(Style\) GetItalic + +```go +func (s Style) GetItalic() bool +``` + +GetItalic returns the style's italic value. If no value is set false is returned. + + +### func \(Style\) GetMargin + +```go +func (s Style) GetMargin() (top, right, bottom, left int) +``` + +GetMargin returns the style's top, right, bottom, and left margins, in that order. 0 is returned for unset values. + + +### func \(Style\) GetMarginBottom + +```go +func (s Style) GetMarginBottom() int +``` + +GetMarginBottom returns the style's bottom margin. If no value is set 0 is returned. + + +### func \(Style\) GetMarginLeft + +```go +func (s Style) GetMarginLeft() int +``` + +GetMarginLeft returns the style's left margin. If no value is set 0 is returned. + + +### func \(Style\) GetMarginRight + +```go +func (s Style) GetMarginRight() int +``` + +GetMarginRight returns the style's right margin. If no value is set 0 is returned. + + +### func \(Style\) GetMarginTop + +```go +func (s Style) GetMarginTop() int +``` + +GetMarginTop returns the style's top margin. If no value is set 0 is returned. + + +### func \(Style\) GetMaxHeight + +```go +func (s Style) GetMaxHeight() int +``` + +GetMaxHeight returns the style's max height setting. If no value is set 0 is returned. + + +### func \(Style\) GetMaxWidth + +```go +func (s Style) GetMaxWidth() int +``` + +GetMaxWidth returns the style's max width setting. If no value is set 0 is returned. + + +### func \(Style\) GetPadding + +```go +func (s Style) GetPadding() (top, right, bottom, left int) +``` + +GetPadding returns the style's top, right, bottom, and left padding values, in that order. 0 is returned for unset values. + + +### func \(Style\) GetPaddingBottom + +```go +func (s Style) GetPaddingBottom() int +``` + +GetPaddingBottom returns the style's bottom padding. If no value is set 0 is returned. + + +### func \(Style\) GetPaddingLeft + +```go +func (s Style) GetPaddingLeft() int +``` + +GetPaddingLeft returns the style's left padding. If no value is set 0 is returned. + + +### func \(Style\) GetPaddingRight + +```go +func (s Style) GetPaddingRight() int +``` + +GetPaddingRight returns the style's right padding. If no value is set 0 is returned. + + +### func \(Style\) GetPaddingTop + +```go +func (s Style) GetPaddingTop() int +``` + +GetPaddingTop returns the style's top padding. If no value is set 0 is returned. + + +### func \(Style\) GetReverse + +```go +func (s Style) GetReverse() bool +``` + +GetReverse returns the style's reverse value. If no value is set false is returned. + + +### func \(Style\) GetStrikethrough + +```go +func (s Style) GetStrikethrough() bool +``` + +GetStrikethrough returns the style's strikethrough value. If no value is set false is returned. + + +### func \(Style\) GetStrikethroughSpaces + +```go +func (s Style) GetStrikethroughSpaces() bool +``` + +GetStrikethroughSpaces returns whether or not the style is set to strikethrough spaces. If not value is set false is returned. + + +### func \(Style\) GetTabWidth + +```go +func (s Style) GetTabWidth() int +``` + +GetTabWidth returns the style's tab width setting. If no value is set 4 is returned which is the implicit default. + + +### func \(Style\) GetTransform + +```go +func (s Style) GetTransform() func(string) string +``` + +GetTransform returns the transform set on the style. If no transform is set nil is returned. + + +### func \(Style\) GetUnderline + +```go +func (s Style) GetUnderline() bool +``` + +GetUnderline returns the style's underline value. If no value is set false is returned. + + +### func \(Style\) GetUnderlineSpaces + +```go +func (s Style) GetUnderlineSpaces() bool +``` + +GetUnderlineSpaces returns whether or not the style is set to underline spaces. If not value is set false is returned. + + +### func \(Style\) GetVerticalBorderSize + +```go +func (s Style) GetVerticalBorderSize() int +``` + +GetVerticalBorderSize returns the width of the vertical borders. If borders contain runes of varying widths, the widest rune is returned. If no border exists on the vertical edges, 0 is returned. + + +### func \(Style\) GetVerticalFrameSize + +```go +func (s Style) GetVerticalFrameSize() int +``` + +GetVerticalFrameSize returns the sum of the style's vertical margins, padding and border widths. + +Provisional: this method may be renamed. + + +### func \(Style\) GetVerticalMargins + +```go +func (s Style) GetVerticalMargins() int +``` + +GetVerticalMargins returns the style's top and bottom margins. Unset values are measured as 0. + + +### func \(Style\) GetVerticalPadding + +```go +func (s Style) GetVerticalPadding() int +``` + +GetVerticalPadding returns the style's top and bottom padding. Unset values are measured as 0. + + +### func \(Style\) GetWidth + +```go +func (s Style) GetWidth() int +``` + +GetWidth returns the style's width setting. If no width is set 0 is returned. + + +### func \(Style\) Height + +```go +func (s Style) Height(i int) Style +``` + +Height sets the height of the block before applying margins. If the height of the text block is less than this value after applying padding \(or not\), the block will be set to this height. + + +### func \(Style\) Inherit + +```go +func (s Style) Inherit(i Style) Style +``` + +Inherit overlays the style in the argument onto this style by copying each explicitly set value from the argument style onto this style if it is not already explicitly set. Existing set values are kept intact and not overwritten. + +Margins, padding, and underlying string values are not inherited. + + +### func \(Style\) Inline + +```go +func (s Style) Inline(v bool) Style +``` + +Inline makes rendering output one line and disables the rendering of margins, padding and borders. This is useful when you need a style to apply only to font rendering and don't want it to change any physical dimensions. It works well with Style.MaxWidth. + +Because this in intended to be used at the time of render, this method will not mutate the style and instead return a copy. + +Example: + +``` +var userInput string = "..." +var userStyle = text.Style{ /* ... */ } +fmt.Println(userStyle.Inline(true).Render(userInput)) +``` + + +### func \(Style\) Italic + +```go +func (s Style) Italic(v bool) Style +``` + +Italic sets an italic formatting rule. In some terminal emulators this will render with "reverse" coloring if not italic font variant is available. + + +### func \(Style\) Margin + +```go +func (s Style) Margin(i ...int) Style +``` + +Margin is a shorthand method for setting margins on all sides at once. + +With one argument, the value is applied to all sides. + +With two arguments, the value is applied to the vertical and horizontal sides, in that order. + +With three arguments, the value is applied to the top side, the horizontal sides, and the bottom side, in that order. + +With four arguments, the value is applied clockwise starting from the top side, followed by the right side, then the bottom, and finally the left. + +With more than four arguments no margin will be added. + + +### func \(Style\) MarginBackground + +```go +func (s Style) MarginBackground(c TerminalColor) Style +``` + +MarginBackground sets the background color of the margin. Note that this is also set when inheriting from a style with a background color. In that case the background color on that style will set the margin color on this style. + + +### func \(Style\) MarginBottom + +```go +func (s Style) MarginBottom(i int) Style +``` + +MarginBottom sets the value of the bottom margin. + + +### func \(Style\) MarginLeft + +```go +func (s Style) MarginLeft(i int) Style +``` + +MarginLeft sets the value of the left margin. + + +### func \(Style\) MarginRight + +```go +func (s Style) MarginRight(i int) Style +``` + +MarginRight sets the value of the right margin. + + +### func \(Style\) MarginTop + +```go +func (s Style) MarginTop(i int) Style +``` + +MarginTop sets the value of the top margin. + + +### func \(Style\) MaxHeight + +```go +func (s Style) MaxHeight(n int) Style +``` + +MaxHeight applies a max height to a given style. This is useful in enforcing a certain height at render time, particularly with arbitrary strings and styles. + +Because this in intended to be used at the time of render, this method will not mutate the style and instead returns a copy. + + +### func \(Style\) MaxWidth + +```go +func (s Style) MaxWidth(n int) Style +``` + +MaxWidth applies a max width to a given style. This is useful in enforcing a certain width at render time, particularly with arbitrary strings and styles. + +Because this in intended to be used at the time of render, this method will not mutate the style and instead return a copy. + +Example: + +``` +var userInput string = "..." +var userStyle = text.Style{ /* ... */ } +fmt.Println(userStyle.MaxWidth(16).Render(userInput)) +``` + + +### func \(Style\) Padding + +```go +func (s Style) Padding(i ...int) Style +``` + +Padding is a shorthand method for setting padding on all sides at once. + +With one argument, the value is applied to all sides. + +With two arguments, the value is applied to the vertical and horizontal sides, in that order. + +With three arguments, the value is applied to the top side, the horizontal sides, and the bottom side, in that order. + +With four arguments, the value is applied clockwise starting from the top side, followed by the right side, then the bottom, and finally the left. + +With more than four arguments no padding will be added. + + +### func \(Style\) PaddingBottom + +```go +func (s Style) PaddingBottom(i int) Style +``` + +PaddingBottom adds padding to the bottom of the block. + + +### func \(Style\) PaddingLeft + +```go +func (s Style) PaddingLeft(i int) Style +``` + +PaddingLeft adds padding on the left. + + +### func \(Style\) PaddingRight + +```go +func (s Style) PaddingRight(i int) Style +``` + +PaddingRight adds padding on the right. + + +### func \(Style\) PaddingTop + +```go +func (s Style) PaddingTop(i int) Style +``` + +PaddingTop adds padding to the top of the block. + + +### func \(Style\) Render + +```go +func (s Style) Render(strs ...string) string +``` + +Render applies the defined style formatting to a given string. + + +### func \(Style\) Renderer + +```go +func (s Style) Renderer(r *Renderer) Style +``` + +Renderer sets the renderer for the style. This is useful for changing the renderer for a style that is being used in a different context. + + +### func \(Style\) Reverse + +```go +func (s Style) Reverse(v bool) Style +``` + +Reverse sets a rule for inverting foreground and background colors. + + +### func \(Style\) SetString + +```go +func (s Style) SetString(strs ...string) Style +``` + +SetString sets the underlying string value for this style. To render once the underlying string is set, use the Style.String. This method is a convenience for cases when having a stringer implementation is handy, such as when using fmt.Sprintf. You can also simply define a style and render out strings directly with Style.Render. + + +### func \(Style\) Strikethrough + +```go +func (s Style) Strikethrough(v bool) Style +``` + +Strikethrough sets a strikethrough rule. By default, strikes will not be drawn on whitespace like margins and padding. To change this behavior set StrikethroughSpaces. + + +### func \(Style\) StrikethroughSpaces + +```go +func (s Style) StrikethroughSpaces(v bool) Style +``` + +StrikethroughSpaces determines whether to apply strikethroughs to spaces between words. By default, this is true. Spaces can also be struck without underlining the text itself. + + +### func \(Style\) String + +```go +func (s Style) String() string +``` + +String implements stringer for a Style, returning the rendered result based on the rules in this style. An underlying string value must be set with Style.SetString prior to using this method. + + +### func \(Style\) TabWidth + +```go +func (s Style) TabWidth(n int) Style +``` + +TabWidth sets the number of spaces that a tab \(/t\) should be rendered as. When set to 0, tabs will be removed. To disable the replacement of tabs with spaces entirely, set this to [NoTabConversion](<#NoTabConversion>). + +By default, tabs will be replaced with 4 spaces. + + +### func \(Style\) Transform + +```go +func (s Style) Transform(fn func(string) string) Style +``` + +Transform applies a given function to a string at render time, allowing for the string being rendered to be manipuated. + +Example: + +``` +s := NewStyle().Transform(strings.ToUpper) +fmt.Println(s.Render("raow!") // "RAOW!" +``` + + +### func \(Style\) Underline + +```go +func (s Style) Underline(v bool) Style +``` + +Underline sets an underline rule. By default, underlines will not be drawn on whitespace like margins and padding. To change this behavior set UnderlineSpaces. + + +### func \(Style\) UnderlineSpaces + +```go +func (s Style) UnderlineSpaces(v bool) Style +``` + +UnderlineSpaces determines whether to underline spaces between words. By default, this is true. Spaces can also be underlined without underlining the text itself. + + +### func \(Style\) UnsetAlign + +```go +func (s Style) UnsetAlign() Style +``` + +UnsetAlign removes the horizontal and vertical text alignment style rule, if set. + + +### func \(Style\) UnsetAlignHorizontal + +```go +func (s Style) UnsetAlignHorizontal() Style +``` + +UnsetAlignHorizontal removes the horizontal text alignment style rule, if set. + + +### func \(Style\) UnsetAlignVertical + +```go +func (s Style) UnsetAlignVertical() Style +``` + +UnsetAlignVertical removes the vertical text alignment style rule, if set. + + +### func \(Style\) UnsetBackground + +```go +func (s Style) UnsetBackground() Style +``` + +UnsetBackground removes the background style rule, if set. + + +### func \(Style\) UnsetBlink + +```go +func (s Style) UnsetBlink() Style +``` + +UnsetBlink removes the blink style rule, if set. + + +### func \(Style\) UnsetBold + +```go +func (s Style) UnsetBold() Style +``` + +UnsetBold removes the bold style rule, if set. + + +### func \(Style\) UnsetBorderBackground + +```go +func (s Style) UnsetBorderBackground() Style +``` + +UnsetBorderBackground removes all border background color styles, if set. + + +### func \(Style\) UnsetBorderBottom + +```go +func (s Style) UnsetBorderBottom() Style +``` + +UnsetBorderBottom removes the border bottom style rule, if set. + + +### func \(Style\) UnsetBorderBottomBackground + +```go +func (s Style) UnsetBorderBottomBackground() Style +``` + +UnsetBorderBottomBackground removes the bottom border background color rule, if set. + + +### func \(Style\) UnsetBorderBottomForeground + +```go +func (s Style) UnsetBorderBottomForeground() Style +``` + +UnsetBorderBottomForeground removes the bottom border foreground color rule, if set. + + +### func \(Style\) UnsetBorderForeground + +```go +func (s Style) UnsetBorderForeground() Style +``` + +UnsetBorderForeground removes all border foreground color styles, if set. + + +### func \(Style\) UnsetBorderLeft + +```go +func (s Style) UnsetBorderLeft() Style +``` + +UnsetBorderLeft removes the border left style rule, if set. + + +### func \(Style\) UnsetBorderLeftBackground + +```go +func (s Style) UnsetBorderLeftBackground() Style +``` + +UnsetBorderLeftBackground removes the left border color rule, if set. + + +### func \(Style\) UnsetBorderLeftForeground + +```go +func (s Style) UnsetBorderLeftForeground() Style +``` + +UnsetBorderLeftForeground removes the left border foreground color rule, if set. + + +### func \(Style\) UnsetBorderRight + +```go +func (s Style) UnsetBorderRight() Style +``` + +UnsetBorderRight removes the border right style rule, if set. + + +### func \(Style\) UnsetBorderRightBackground + +```go +func (s Style) UnsetBorderRightBackground() Style +``` + +UnsetBorderRightBackground removes the right border background color rule, if set. + + +### func \(Style\) UnsetBorderRightForeground + +```go +func (s Style) UnsetBorderRightForeground() Style +``` + +UnsetBorderRightForeground removes the right border foreground color rule, if set. + + +### func \(Style\) UnsetBorderStyle + +```go +func (s Style) UnsetBorderStyle() Style +``` + +UnsetBorderStyle removes the border style rule, if set. + + +### func \(Style\) UnsetBorderTop + +```go +func (s Style) UnsetBorderTop() Style +``` + +UnsetBorderTop removes the border top style rule, if set. + + +### func \(Style\) UnsetBorderTopBackground + +```go +func (s Style) UnsetBorderTopBackground() Style +``` + +UnsetBorderTopBackground removes the top border background color rule, if set. + + +### func \(Style\) UnsetBorderTopBackgroundColor + +```go +func (s Style) UnsetBorderTopBackgroundColor() Style +``` + +UnsetBorderTopBackgroundColor removes the top border background color rule, if set. + +Deprecated: This function simply calls Style.UnsetBorderTopBackground. + + +### func \(Style\) UnsetBorderTopForeground + +```go +func (s Style) UnsetBorderTopForeground() Style +``` + +UnsetBorderTopForeground removes the top border foreground color rule, if set. + + +### func \(Style\) UnsetColorWhitespace + +```go +func (s Style) UnsetColorWhitespace() Style +``` + +UnsetColorWhitespace removes the rule for coloring padding, if set. + + +### func \(Style\) UnsetFaint + +```go +func (s Style) UnsetFaint() Style +``` + +UnsetFaint removes the faint style rule, if set. + + +### func \(Style\) UnsetForeground + +```go +func (s Style) UnsetForeground() Style +``` + +UnsetForeground removes the foreground style rule, if set. + + +### func \(Style\) UnsetHeight + +```go +func (s Style) UnsetHeight() Style +``` + +UnsetHeight removes the height style rule, if set. + + +### func \(Style\) UnsetInline + +```go +func (s Style) UnsetInline() Style +``` + +UnsetInline removes the inline style rule, if set. + + +### func \(Style\) UnsetItalic + +```go +func (s Style) UnsetItalic() Style +``` + +UnsetItalic removes the italic style rule, if set. + + +### func \(Style\) UnsetMarginBackground + +```go +func (s Style) UnsetMarginBackground() Style +``` + +UnsetMarginBackground removes the margin's background color. Note that the margin's background color can be set from the background color of another style during inheritance. + + +### func \(Style\) UnsetMarginBottom + +```go +func (s Style) UnsetMarginBottom() Style +``` + +UnsetMarginBottom removes the bottom margin style rule, if set. + + +### func \(Style\) UnsetMarginLeft + +```go +func (s Style) UnsetMarginLeft() Style +``` + +UnsetMarginLeft removes the left margin style rule, if set. + + +### func \(Style\) UnsetMarginRight + +```go +func (s Style) UnsetMarginRight() Style +``` + +UnsetMarginRight removes the right margin style rule, if set. + + +### func \(Style\) UnsetMarginTop + +```go +func (s Style) UnsetMarginTop() Style +``` + +UnsetMarginTop removes the top margin style rule, if set. + + +### func \(Style\) UnsetMargins + +```go +func (s Style) UnsetMargins() Style +``` + +UnsetMargins removes all margin style rules. + + +### func \(Style\) UnsetMaxHeight + +```go +func (s Style) UnsetMaxHeight() Style +``` + +UnsetMaxHeight removes the max height style rule, if set. + + +### func \(Style\) UnsetMaxWidth + +```go +func (s Style) UnsetMaxWidth() Style +``` + +UnsetMaxWidth removes the max width style rule, if set. + + +### func \(Style\) UnsetPadding + +```go +func (s Style) UnsetPadding() Style +``` + +UnsetPadding removes all padding style rules. + + +### func \(Style\) UnsetPaddingBottom + +```go +func (s Style) UnsetPaddingBottom() Style +``` + +UnsetPaddingBottom removes the bottom padding style rule, if set. + + +### func \(Style\) UnsetPaddingLeft + +```go +func (s Style) UnsetPaddingLeft() Style +``` + +UnsetPaddingLeft removes the left padding style rule, if set. + + +### func \(Style\) UnsetPaddingRight + +```go +func (s Style) UnsetPaddingRight() Style +``` + +UnsetPaddingRight removes the right padding style rule, if set. + + +### func \(Style\) UnsetPaddingTop + +```go +func (s Style) UnsetPaddingTop() Style +``` + +UnsetPaddingTop removes the top padding style rule, if set. + + +### func \(Style\) UnsetReverse + +```go +func (s Style) UnsetReverse() Style +``` + +UnsetReverse removes the reverse style rule, if set. + + +### func \(Style\) UnsetStrikethrough + +```go +func (s Style) UnsetStrikethrough() Style +``` + +UnsetStrikethrough removes the strikethrough style rule, if set. + + +### func \(Style\) UnsetStrikethroughSpaces + +```go +func (s Style) UnsetStrikethroughSpaces() Style +``` + +UnsetStrikethroughSpaces removes the value set by StrikethroughSpaces. + + +### func \(Style\) UnsetString + +```go +func (s Style) UnsetString() Style +``` + +UnsetString sets the underlying string value to the empty string. + + +### func \(Style\) UnsetTabWidth + +```go +func (s Style) UnsetTabWidth() Style +``` + +UnsetTabWidth removes the tab width style rule, if set. + + +### func \(Style\) UnsetTransform + +```go +func (s Style) UnsetTransform() Style +``` + +UnsetTransform removes the value set by Transform. + + +### func \(Style\) UnsetUnderline + +```go +func (s Style) UnsetUnderline() Style +``` + +UnsetUnderline removes the underline style rule, if set. + + +### func \(Style\) UnsetUnderlineSpaces + +```go +func (s Style) UnsetUnderlineSpaces() Style +``` + +UnsetUnderlineSpaces removes the value set by UnderlineSpaces. + + +### func \(Style\) UnsetWidth + +```go +func (s Style) UnsetWidth() Style +``` + +UnsetWidth removes the width style rule, if set. + + +### func \(Style\) Value + +```go +func (s Style) Value() string +``` + +Value returns the raw, unformatted, underlying string value for this style. + + +### func \(Style\) Width + +```go +func (s Style) Width(i int) Style +``` + +Width sets the width of the block before applying margins. The width, if set, also determines where text will wrap. + + +## type TerminalColor + +TerminalColor is a color intended to be rendered in the terminal. + +```go +type TerminalColor interface { + RGBA() (r, g, b, a uint32) + // contains filtered or unexported methods +} +``` + + +## type WhitespaceOption + +WhitespaceOption sets a styling rule for rendering whitespace. + +```go +type WhitespaceOption func(*whitespace) +``` + + +### func WithWhitespaceBackground + +```go +func WithWhitespaceBackground(c TerminalColor) WhitespaceOption +``` + +WithWhitespaceBackground sets the background color of the whitespace. + + +### func WithWhitespaceChars + +```go +func WithWhitespaceChars(s string) WhitespaceOption +``` + +WithWhitespaceChars sets the characters to be rendered in the whitespace. + + +### func WithWhitespaceForeground + +```go +func WithWhitespaceForeground(c TerminalColor) WhitespaceOption +``` + +WithWhitespaceForeground sets the color of the characters in the whitespace. + +Generated by [gomarkdoc]() diff --git a/docs/lib/textinput.md b/docs/lib/textinput.md new file mode 100644 index 0000000..be2b645 --- /dev/null +++ b/docs/lib/textinput.md @@ -0,0 +1,466 @@ + + +# textinput + +```go +import "github.com/charmbracelet/bubbles/textinput" +``` + +Package textinput provides a text input component for Bubble Tea applications. + +## Index + +- [Constants](<#constants>) +- [Variables](<#variables>) +- [func Blink\(\) tea.Msg](<#Blink>) +- [func Paste\(\) tea.Msg](<#Paste>) +- [type CursorMode](<#CursorMode>) + - [func \(c CursorMode\) String\(\) string](<#CursorMode.String>) +- [type EchoMode](<#EchoMode>) +- [type KeyMap](<#KeyMap>) +- [type Model](<#Model>) + - [func New\(\) Model](<#New>) + - [func \(m \*Model\) AvailableSuggestions\(\) \[\]string](<#Model.AvailableSuggestions>) + - [func \(m \*Model\) Blur\(\)](<#Model.Blur>) + - [func \(m \*Model\) CurrentSuggestion\(\) string](<#Model.CurrentSuggestion>) + - [func \(m \*Model\) CurrentSuggestionIndex\(\) int](<#Model.CurrentSuggestionIndex>) + - [func \(m \*Model\) CursorEnd\(\)](<#Model.CursorEnd>) + - [func \(m Model\) CursorMode\(\) CursorMode](<#Model.CursorMode>) + - [func \(m \*Model\) CursorStart\(\)](<#Model.CursorStart>) + - [func \(m \*Model\) Focus\(\) tea.Cmd](<#Model.Focus>) + - [func \(m Model\) Focused\(\) bool](<#Model.Focused>) + - [func \(m \*Model\) MatchedSuggestions\(\) \[\]string](<#Model.MatchedSuggestions>) + - [func \(m Model\) Position\(\) int](<#Model.Position>) + - [func \(m \*Model\) Reset\(\)](<#Model.Reset>) + - [func \(m \*Model\) SetCursor\(pos int\)](<#Model.SetCursor>) + - [func \(m \*Model\) SetCursorMode\(mode CursorMode\) tea.Cmd](<#Model.SetCursorMode>) + - [func \(m \*Model\) SetSuggestions\(suggestions \[\]string\)](<#Model.SetSuggestions>) + - [func \(m \*Model\) SetValue\(s string\)](<#Model.SetValue>) + - [func \(m Model\) Update\(msg tea.Msg\) \(Model, tea.Cmd\)](<#Model.Update>) + - [func \(m Model\) Value\(\) string](<#Model.Value>) + - [func \(m Model\) View\(\) string](<#Model.View>) +- [type ValidateFunc](<#ValidateFunc>) + + +## Constants + + + +```go +const ( + // Deprecated: use [cursor.CursorBlink]. + CursorBlink = CursorMode(cursor.CursorBlink) + // Deprecated: use [cursor.CursorStatic]. + CursorStatic = CursorMode(cursor.CursorStatic) + // Deprecated: use [cursor.CursorHide]. + CursorHide = CursorMode(cursor.CursorHide) +) +``` + +## Variables + +DefaultKeyMap is the default set of key bindings for navigating and acting upon the textinput. + +```go +var DefaultKeyMap = KeyMap{ + CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f")), + CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b")), + WordForward: key.NewBinding(key.WithKeys("alt+right", "ctrl+right", "alt+f")), + WordBackward: key.NewBinding(key.WithKeys("alt+left", "ctrl+left", "alt+b")), + DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w")), + DeleteWordForward: key.NewBinding(key.WithKeys("alt+delete", "alt+d")), + DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k")), + DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u")), + DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h")), + DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d")), + LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")), + LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")), + Paste: key.NewBinding(key.WithKeys("ctrl+v")), + AcceptSuggestion: key.NewBinding(key.WithKeys("tab")), + NextSuggestion: key.NewBinding(key.WithKeys("down", "ctrl+n")), + PrevSuggestion: key.NewBinding(key.WithKeys("up", "ctrl+p")), +} +``` + +NewModel creates a new model with default settings. + +Deprecated: Use [New](<#New>) instead. + +```go +var NewModel = New +``` + + +## func Blink + +```go +func Blink() tea.Msg +``` + +Blink is a command used to initialize cursor blinking. + + +## func Paste + +```go +func Paste() tea.Msg +``` + +Paste is a command for pasting from the clipboard into the text input. + + +## type CursorMode + +Deprecated: use \[cursor.Mode\]. + +```go +type CursorMode int +``` + + +### func \(CursorMode\) String + +```go +func (c CursorMode) String() string +``` + + + + +## type EchoMode + +EchoMode sets the input behavior of the text input field. + +```go +type EchoMode int +``` + + + +```go +const ( + // EchoNormal displays text as is. This is the default behavior. + EchoNormal EchoMode = iota + + // EchoPassword displays the EchoCharacter mask instead of actual + // characters. This is commonly used for password fields. + EchoPassword + + // EchoNone displays nothing as characters are entered. This is commonly + // seen for password fields on the command line. + EchoNone +) +``` + + +## type KeyMap + +KeyMap is the key bindings for different actions within the textinput. + +```go +type KeyMap struct { + CharacterForward key.Binding + CharacterBackward key.Binding + WordForward key.Binding + WordBackward key.Binding + DeleteWordBackward key.Binding + DeleteWordForward key.Binding + DeleteAfterCursor key.Binding + DeleteBeforeCursor key.Binding + DeleteCharacterBackward key.Binding + DeleteCharacterForward key.Binding + LineStart key.Binding + LineEnd key.Binding + Paste key.Binding + AcceptSuggestion key.Binding + NextSuggestion key.Binding + PrevSuggestion key.Binding +} +``` + + +## type Model + +Model is the Bubble Tea model for this text input element. + +```go +type Model struct { + Err error + + // General settings. + Prompt string + Placeholder string + EchoMode EchoMode + EchoCharacter rune + Cursor cursor.Model + + // Deprecated: use [cursor.BlinkSpeed] instead. + BlinkSpeed time.Duration + + // Styles. These will be applied as inline styles. + // + // For an introduction to styling with Lip Gloss see: + // https://github.com/charmbracelet/lipgloss + PromptStyle lipgloss.Style + TextStyle lipgloss.Style + PlaceholderStyle lipgloss.Style + CompletionStyle lipgloss.Style + + // Deprecated: use Cursor.Style instead. + CursorStyle lipgloss.Style + + // CharLimit is the maximum amount of characters this input element will + // accept. If 0 or less, there's no limit. + CharLimit int + + // Width is the maximum number of characters that can be displayed at once. + // It essentially treats the text field like a horizontally scrolling + // viewport. If 0 or less this setting is ignored. + Width int + + // KeyMap encodes the keybindings recognized by the widget. + KeyMap KeyMap + + // Validate is a function that checks whether or not the text within the + // input is valid. If it is not valid, the `Err` field will be set to the + // error returned by the function. If the function is not defined, all + // input is considered valid. + Validate ValidateFunc + + // Should the input suggest to complete + ShowSuggestions bool + // contains filtered or unexported fields +} +``` + + +### func New + +```go +func New() Model +``` + +New creates a new model with default settings. + + +### func \(\*Model\) AvailableSuggestions + +```go +func (m *Model) AvailableSuggestions() []string +``` + +AvailableSuggestions returns the list of available suggestions. + + +### func \(\*Model\) Blur + +```go +func (m *Model) Blur() +``` + +Blur removes the focus state on the model. When the model is blurred it can not receive keyboard input and the cursor will be hidden. + + +### func \(\*Model\) CurrentSuggestion + +```go +func (m *Model) CurrentSuggestion() string +``` + +CurrentSuggestion returns the currently selected suggestion. + + +### func \(\*Model\) CurrentSuggestionIndex + +```go +func (m *Model) CurrentSuggestionIndex() int +``` + +CurrentSuggestionIndex returns the currently selected suggestion index. + + +### func \(\*Model\) CursorEnd + +```go +func (m *Model) CursorEnd() +``` + +CursorEnd moves the cursor to the end of the input field. + + +### func \(Model\) CursorMode + +```go +func (m Model) CursorMode() CursorMode +``` + +Deprecated: use \[cursor.Mode\]. + + +### func \(\*Model\) CursorStart + +```go +func (m *Model) CursorStart() +``` + +CursorStart moves the cursor to the start of the input field. + + +### func \(\*Model\) Focus + +```go +func (m *Model) Focus() tea.Cmd +``` + +Focus sets the focus state on the model. When the model is in focus it can receive keyboard input and the cursor will be shown. + + +### func \(Model\) Focused + +```go +func (m Model) Focused() bool +``` + +Focused returns the focus state on the model. + + +### func \(\*Model\) MatchedSuggestions + +```go +func (m *Model) MatchedSuggestions() []string +``` + +MatchedSuggestions returns the list of matched suggestions. + + +### func \(Model\) Position + +```go +func (m Model) Position() int +``` + +Position returns the cursor position. + + +### func \(\*Model\) Reset + +```go +func (m *Model) Reset() +``` + +Reset sets the input to its default state with no input. + + +### func \(\*Model\) SetCursor + +```go +func (m *Model) SetCursor(pos int) +``` + +SetCursor moves the cursor to the given position. If the position is out of bounds the cursor will be moved to the start or end accordingly. + + +### func \(\*Model\) SetCursorMode + +```go +func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd +``` + +Deprecated: use cursor.SetMode\(\). + + +### func \(\*Model\) SetSuggestions + +```go +func (m *Model) SetSuggestions(suggestions []string) +``` + +SetSuggestions sets the suggestions for the input. + + +### func \(\*Model\) SetValue + +```go +func (m *Model) SetValue(s string) +``` + +SetValue sets the value of the text input. + + +### func \(Model\) Update + +```go +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) +``` + +Update is the Bubble Tea update loop. + + +### func \(Model\) Value + +```go +func (m Model) Value() string +``` + +Value returns the value of the text input. + + +### func \(Model\) View + +```go +func (m Model) View() string +``` + +View renders the textinput in its current state. + + +## type ValidateFunc + +ValidateFunc is a function that returns an error if the input is invalid. + +```go +type ValidateFunc func(string) error +``` + +
Example +

+ + + +```go +creditCardNumber := New() +creditCardNumber.Placeholder = "4505 **** **** 1234" +creditCardNumber.Focus() +creditCardNumber.CharLimit = 20 +creditCardNumber.Width = 30 +creditCardNumber.Prompt = "" +// This anonymous function is a valid function for ValidateFunc. +creditCardNumber.Validate = func(s string) error { + // Credit Card Number should a string less than 20 digits + // It should include 16 integers and 3 spaces + if len(s) > 16+3 { + return fmt.Errorf("CCN is too long") + } + + if len(s) == 0 || len(s)%5 != 0 && (s[len(s)-1] < '0' || s[len(s)-1] > '9') { + return fmt.Errorf("CCN is invalid") + } + + // The last digit should be a number unless it is a multiple of 4 in which + // case it should be a space + if len(s)%5 == 0 && s[len(s)-1] != ' ' { + return fmt.Errorf("CCN must separate groups with spaces") + } + + // The remaining digits should be integers + c := strings.ReplaceAll(s, " ", "") + _, err := strconv.ParseInt(c, 10, 64) + + return err +} +``` + +

+
+ +Generated by [gomarkdoc]() diff --git a/flake.nix b/flake.nix index db5ea66..54a8d3f 100644 --- a/flake.nix +++ b/flake.nix @@ -52,7 +52,7 @@ pname = "graftx"; version = "0.1.0"; src = ./.; - vendorHash = "sha256-herMHZOw1VTTrIGOhmDnygi2LfRcqDpXEZEX6KsYoWI="; + vendorHash = "sha256-TUbaUoqDZoQTkcOMtoE/FlAiqkWN+x49JeGkDguh2UU="; # サンドボックス環境ではホームディレクトリが存在しないためテストをスキップ doCheck = false; }; diff --git a/go.mod b/go.mod index b9a510a..7a99acf 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,31 @@ module github.com/myuron/graftx go 1.26.1 require ( - github.com/jroimartin/gocui v0.5.0 - github.com/mattn/go-runewidth v0.0.16 + github.com/charmbracelet/bubbles v1.0.0 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/mattn/go-runewidth v0.0.19 + github.com/muesli/termenv v0.16.0 ) require ( - github.com/nsf/termbox-go v1.1.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.9.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 4cfbd15..e92ed90 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,52 @@ -github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4= -github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= -github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= +github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/internal/ui/keybinding.go b/internal/ui/keybinding.go index eb016f8..312dcea 100644 --- a/internal/ui/keybinding.go +++ b/internal/ui/keybinding.go @@ -6,114 +6,10 @@ import ( "path/filepath" "strings" - "github.com/jroimartin/gocui" + tea "github.com/charmbracelet/bubbletea" "github.com/myuron/graftx/internal/pane" ) -// paneViews はペインビュー名の一覧。 -var paneViews = []string{viewLeft, viewRight} - -// bindPaneKey はペインビュー共通のキーバインドを登録するヘルパー。 -func (a *App) bindPaneKey(g *gocui.Gui, key interface{}, mod gocui.Modifier, handler func(*gocui.Gui, *gocui.View) error) error { - for _, view := range paneViews { - if err := g.SetKeybinding(view, key, mod, handler); err != nil { - return err - } - } - return nil -} - -// setKeybindings は全キーバインドを登録する。 -// ペイン操作のキーバインドは左右ペインビューにのみ登録し、入力ビューと競合しないようにする。 -func (a *App) setKeybindings(g *gocui.Gui) error { - // ペインビュー共通のキーバインド - if err := a.bindPaneKey(g, 'j', gocui.ModNone, a.cursorDown); err != nil { - return err - } - if err := a.bindPaneKey(g, 'k', gocui.ModNone, a.cursorUp); err != nil { - return err - } - if err := a.bindPaneKey(g, 'l', gocui.ModNone, a.enterDir); err != nil { - return err - } - if err := a.bindPaneKey(g, 'h', gocui.ModNone, a.parentDir); err != nil { - return err - } - if err := a.bindPaneKey(g, gocui.KeyTab, gocui.ModNone, a.toggleFocus); err != nil { - return err - } - if err := a.bindPaneKey(g, 's', gocui.ModNone, a.selectRepo); err != nil { - return err - } - if err := a.bindPaneKey(g, 'q', gocui.ModNone, a.quit); err != nil { - return err - } - if err := a.bindPaneKey(g, 'g', gocui.ModNone, a.handleG); err != nil { - return err - } - if err := a.bindPaneKey(g, 'G', gocui.ModNone, a.moveToBottom); err != nil { - return err - } - if err := a.bindPaneKey(g, gocui.KeySpace, gocui.ModNone, a.toggleSelect); err != nil { - return err - } - if err := a.bindPaneKey(g, gocui.KeyCtrlA, gocui.ModNone, a.selectAll); err != nil { - return err - } - if err := a.bindPaneKey(g, gocui.KeyCtrlR, gocui.ModNone, a.invertSelection); err != nil { - return err - } - if err := a.bindPaneKey(g, gocui.KeyEsc, gocui.ModNone, a.handleEsc); err != nil { - return err - } - if err := a.bindPaneKey(g, gocui.KeyCtrlC, gocui.ModNone, a.handleEsc); err != nil { - return err - } - if err := a.bindPaneKey(g, 'y', gocui.ModNone, a.yank); err != nil { - return err - } - if err := a.bindPaneKey(g, 'p', gocui.ModNone, a.paste); err != nil { - return err - } - if err := a.bindPaneKey(g, 'P', gocui.ModNone, a.pasteOverwrite); err != nil { - return err - } - if err := a.bindPaneKey(g, 'd', gocui.ModNone, a.trashFile); err != nil { - return err - } - if err := a.bindPaneKey(g, 'D', gocui.ModNone, a.deleteFile); err != nil { - return err - } - if err := a.bindPaneKey(g, 'a', gocui.ModNone, a.createFile); err != nil { - return err - } - if err := a.bindPaneKey(g, 'r', gocui.ModNone, a.renameFile); err != nil { - return err - } - if err := a.bindPaneKey(g, '/', gocui.ModNone, a.searchForward); err != nil { - return err - } - if err := a.bindPaneKey(g, 'n', gocui.ModNone, a.searchNext); err != nil { - return err - } - if err := a.bindPaneKey(g, 'N', gocui.ModNone, a.searchPrev); err != nil { - return err - } - if err := a.bindPaneKey(g, 'f', gocui.ModNone, a.filterMode); err != nil { - return err - } - if err := a.bindPaneKey(g, '.', gocui.ModNone, a.toggleHidden); err != nil { - return err - } - - // 確認モード用のy/nキーバインド(ステータスバーに表示される確認ダイアログ用) - // 通常モードではyはyank、nはsearchNextとして動作するが、 - // 確認モード中はpaneにフォーカスがあるのでpane keybindingsのハンドラが呼ばれる。 - // trashFile/deleteFileの確認はpaneビューでy/nとして既に処理される。 - - return nil -} - // resetGPending はgPendingをリセットする。 func (a *App) resetGPending() { a.gPending = false @@ -128,182 +24,175 @@ func (a *App) focusedPane() *pane.Pane { return a.DestPane } +// enterTextInput はテキスト入力モードに入り、入力欄を初期化してフォーカスする。 +func (a *App) enterTextInput(mode InputMode, initial string) tea.Cmd { + a.inputMode = mode + a.input.SetValue(initial) + a.input.CursorEnd() + a.Status = "" + return a.input.Focus() +} + // cursorDown はカーソルを1つ下に移動する。 -func (a *App) cursorDown(g *gocui.Gui, v *gocui.View) error { +func (a *App) cursorDown() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } p.MoveDown() - return nil } // cursorUp はカーソルを1つ上に移動する。 -func (a *App) cursorUp(g *gocui.Gui, v *gocui.View) error { +func (a *App) cursorUp() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } p.MoveUp() - return nil } // enterDir はカーソル行のディレクトリに入る。 -func (a *App) enterDir(g *gocui.Gui, v *gocui.View) error { +func (a *App) enterDir() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } if err := p.EnterDir(); err != nil { a.Status = err.Error() } - return nil } // parentDir は親ディレクトリに戻る。 -func (a *App) parentDir(g *gocui.Gui, v *gocui.View) error { +func (a *App) parentDir() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } if err := p.ParentDir(); err != nil { a.Status = err.Error() } - return nil } // toggleFocus はペイン間のフォーカスを切り替える。 -func (a *App) toggleFocus(g *gocui.Gui, v *gocui.View) error { +func (a *App) toggleFocus() { a.resetGPending() a.FocusLeft = !a.FocusLeft a.Status = "" - return nil } // selectRepo はリポジトリ選択ポップアップを開く。 -func (a *App) selectRepo(g *gocui.Gui, v *gocui.View) error { +func (a *App) selectRepo() tea.Cmd { a.resetGPending() - return a.openRepoSelector(g, v) + return a.openRepoSelector() } // quit はアプリケーションを終了する。 -func (a *App) quit(g *gocui.Gui, v *gocui.View) error { +func (a *App) quit() tea.Cmd { a.resetGPending() a.ExitReason = ExitReasonQuit - return gocui.ErrQuit + return tea.Quit } // handleG は'g'キーを処理する。ggで先頭ジャンプ。 -func (a *App) handleG(g *gocui.Gui, v *gocui.View) error { +func (a *App) handleG() { if a.gPending { // gg: 先頭にジャンプ a.gPending = false p := a.focusedPane() if p == nil { - return nil + return } p.MoveToTop() a.Status = "" - return nil + return } a.gPending = true - return nil } // moveToBottom はカーソルを末尾に移動する。 -func (a *App) moveToBottom(g *gocui.Gui, v *gocui.View) error { +func (a *App) moveToBottom() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } p.MoveToBottom() - return nil } -// toggleSelect は選択をトグルしてカーソルを下に移動する。 -func (a *App) toggleSelect(g *gocui.Gui, v *gocui.View) error { +// toggleSelect はカーソル行の選択をトグルする。 +func (a *App) toggleSelect() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } p.ToggleSelect() - p.MoveDown() - return nil } // selectAll は全エントリを選択する。 -func (a *App) selectAll(g *gocui.Gui, v *gocui.View) error { +func (a *App) selectAll() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } p.SelectAll() a.Status = fmt.Sprintf("%d selected", len(p.Selected)) - return nil } // invertSelection は選択を反転する。 -func (a *App) invertSelection(g *gocui.Gui, v *gocui.View) error { +func (a *App) invertSelection() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } p.InvertSelection() a.Status = fmt.Sprintf("%d selected", len(p.Selected)) - return nil } -// handleEsc はEscキーを処理する。選択解除。 -func (a *App) handleEsc(g *gocui.Gui, v *gocui.View) error { +// handleEsc はEscキーを処理する。フィルタ解除または選択解除。 +func (a *App) handleEsc() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } // フィルタがかかっている場合はフィルタ解除 if p.FilterQuery != "" { if err := p.ClearFilter(); err != nil { a.Status = fmt.Sprintf("refresh error: %v", err) - return nil + return } a.Status = "filter cleared" - return nil + return } // 選択がある場合は選択解除 if len(p.Selected) > 0 { p.ClearSelection() a.Status = "selection cleared" - return nil + return } a.Status = "" - return nil } // yank はフォーカス中のペインからヤンクする。 -func (a *App) yank(g *gocui.Gui, v *gocui.View) error { +func (a *App) yank() { a.resetGPending() - // 確認モード中の'y'入力を処理 - if a.inputMode == InputModeConfirmDelete || a.inputMode == InputModeConfirmForceDelete { - return a.executeConfirm(g) - } p := a.focusedPane() if p == nil { - return nil + return } // 同じペインで再度yを押した場合はヤンク取り消し if a.YankBuf != nil && len(a.YankBuf.Entries) > 0 && a.YankBuf.SrcDir == p.Dir { a.YankBuf = nil p.ClearSelection() a.Status = "yank cancelled" - return nil + return } // 選択なしの場合はカーソル行を選択してからヤンク(*マーク表示のため) if len(p.Selected) == 0 && len(p.Entries) > 0 { @@ -311,22 +200,21 @@ func (a *App) yank(g *gocui.Gui, v *gocui.View) error { } a.YankBuf = p.Yank() a.Status = fmt.Sprintf("%d item(s) yanked", len(a.YankBuf.Entries)) - return nil } // paste はヤンクバッファの内容をフォーカス中のペインにコピーする。 // 同名ファイルが存在する場合はスキップしてステータスに表示する。 -func (a *App) paste(g *gocui.Gui, v *gocui.View) error { +func (a *App) paste() { a.resetGPending() if a.YankBuf == nil || len(a.YankBuf.Entries) == 0 { a.Status = "yank buffer is empty" - return nil + return } dst := a.focusedPane() if dst == nil { a.Status = "no target pane for paste" - return nil + return } copied := 0 @@ -343,14 +231,14 @@ func (a *App) paste(g *gocui.Gui, v *gocui.View) error { if err := a.FS.Copy(src, dstPath); err != nil { a.Status = fmt.Sprintf("copy error: %v", err) - return nil + return } copied++ } if err := dst.Refresh(); err != nil { a.Status = fmt.Sprintf("refresh error: %v", err) - return nil + return } // コピー元ペインの選択状態とヤンクバッファをクリア @@ -362,21 +250,20 @@ func (a *App) paste(g *gocui.Gui, v *gocui.View) error { } else { a.Status = fmt.Sprintf("%d item(s) copied", copied) } - return nil } // pasteOverwrite はヤンクバッファの内容をフォーカス中のペインに上書きコピーする。 -func (a *App) pasteOverwrite(g *gocui.Gui, v *gocui.View) error { +func (a *App) pasteOverwrite() { a.resetGPending() if a.YankBuf == nil || len(a.YankBuf.Entries) == 0 { a.Status = "yank buffer is empty" - return nil + return } dst := a.focusedPane() if dst == nil { a.Status = "no target pane for paste" - return nil + return } for _, src := range a.YankBuf.Entries { @@ -384,13 +271,13 @@ func (a *App) pasteOverwrite(g *gocui.Gui, v *gocui.View) error { dstPath := filepath.Join(dst.Dir, name) if err := a.FS.Copy(src, dstPath); err != nil { a.Status = fmt.Sprintf("copy error: %v", err) - return nil + return } } if err := dst.Refresh(); err != nil { a.Status = fmt.Sprintf("refresh error: %v", err) - return nil + return } // コピー元ペインの選択状態とヤンクバッファをクリア @@ -399,45 +286,42 @@ func (a *App) pasteOverwrite(g *gocui.Gui, v *gocui.View) error { a.YankBuf = nil a.Status = fmt.Sprintf("%d item(s) overwritten", count) - return nil } // trashFile はカーソル行または選択中のエントリをゴミ箱に移動する確認モードに入る。 -func (a *App) trashFile(g *gocui.Gui, v *gocui.View) error { +func (a *App) trashFile() { a.resetGPending() p := a.focusedPane() if p == nil || len(p.Entries) == 0 { - return nil + return } a.pendingTargets = a.getTargetPaths(p) a.inputMode = InputModeConfirmDelete a.Status = fmt.Sprintf("move to trash? (%d item(s)) [y/n]", len(a.pendingTargets)) - return nil } // deleteFile はカーソル行または選択中のエントリを完全削除する確認モードに入る。 -func (a *App) deleteFile(g *gocui.Gui, v *gocui.View) error { +func (a *App) deleteFile() { a.resetGPending() p := a.focusedPane() if p == nil || len(p.Entries) == 0 { - return nil + return } a.pendingTargets = a.getTargetPaths(p) a.inputMode = InputModeConfirmForceDelete a.Status = fmt.Sprintf("permanently delete? (%d item(s)) [y/n]", len(a.pendingTargets)) - return nil } // executeConfirm は確認モードの実行を行う(y押下時)。 -func (a *App) executeConfirm(g *gocui.Gui) error { +func (a *App) executeConfirm() { p := a.focusedPane() if p == nil { a.inputMode = InputModeNone a.pendingTargets = nil a.Status = "" - return nil + return } targets := a.pendingTargets @@ -463,7 +347,7 @@ func (a *App) executeConfirm(g *gocui.Gui) error { if err := p.Refresh(); err != nil { a.Status = fmt.Sprintf("refresh error: %v", err) - return nil + return } if lastErr != nil { @@ -473,221 +357,151 @@ func (a *App) executeConfirm(g *gocui.Gui) error { } else { a.Status = fmt.Sprintf("%d item(s) permanently deleted", count) } - return nil } // createFile は新規ファイル/ディレクトリ作成モードに入る。 -func (a *App) createFile(g *gocui.Gui, v *gocui.View) error { +func (a *App) createFile() tea.Cmd { a.resetGPending() p := a.focusedPane() if p == nil { return nil } - a.inputMode = InputModeCreate - a.inputBuf = "" - a.Status = "" - // 入力ビューのキーバインドを登録 - return a.setupInputKeybindings(g) + return a.enterTextInput(InputModeCreate, "") } // renameFile はリネームモードに入る。 -func (a *App) renameFile(g *gocui.Gui, v *gocui.View) error { +func (a *App) renameFile() tea.Cmd { a.resetGPending() p := a.focusedPane() if p == nil || len(p.Entries) == 0 { return nil } - a.inputMode = InputModeRename - a.inputBuf = p.Entries[p.Cursor].Name - a.Status = "" - return a.setupInputKeybindings(g) + return a.enterTextInput(InputModeRename, p.Entries[p.Cursor].Name) } // searchForward は前方検索モードに入る。 -func (a *App) searchForward(g *gocui.Gui, v *gocui.View) error { +func (a *App) searchForward() tea.Cmd { a.resetGPending() - a.inputMode = InputModeSearch - a.inputBuf = "" a.searchFwd = true - a.Status = "" - return a.setupInputKeybindings(g) + return a.enterTextInput(InputModeSearch, "") } // searchNext は次の検索結果に移動する。 -func (a *App) searchNext(g *gocui.Gui, v *gocui.View) error { +func (a *App) searchNext() { a.resetGPending() - // 確認モード中の'n'入力を処理(キャンセル) - if a.inputMode == InputModeConfirmDelete || a.inputMode == InputModeConfirmForceDelete { - a.inputMode = InputModeNone - a.pendingTargets = nil - a.Status = "cancelled" - return nil - } if a.searchQuery == "" { - return nil + return } p := a.focusedPane() if p == nil { - return nil + return } - a.doSearch(p, a.searchFwd) - return nil } // searchPrev は前の検索結果に移動する。 -func (a *App) searchPrev(g *gocui.Gui, v *gocui.View) error { +func (a *App) searchPrev() { a.resetGPending() if a.searchQuery == "" { - return nil + return } p := a.focusedPane() if p == nil { - return nil + return } - a.doSearch(p, !a.searchFwd) - return nil } // filterMode はフィルタモードに入る。 -func (a *App) filterMode(g *gocui.Gui, v *gocui.View) error { +func (a *App) filterMode() tea.Cmd { a.resetGPending() - a.inputMode = InputModeFilter - a.inputBuf = "" - a.Status = "" - return a.setupInputKeybindings(g) + return a.enterTextInput(InputModeFilter, "") } // toggleHidden は隠しファイルの表示/非表示を切り替える。 -func (a *App) toggleHidden(g *gocui.Gui, v *gocui.View) error { +func (a *App) toggleHidden() { a.resetGPending() p := a.focusedPane() if p == nil { - return nil + return } p.ShowHidden = !p.ShowHidden if err := p.Refresh(); err != nil { a.Status = fmt.Sprintf("refresh error: %v", err) - return nil + return } if p.ShowHidden { a.Status = "show hidden files" } else { a.Status = "hide hidden files" } - return nil -} - -// setupInputKeybindings は入力ビュー用のキーバインドを登録する。 -func (a *App) setupInputKeybindings(g *gocui.Gui) error { - if a.inputKeybindingsInited { - return nil - } - a.inputKeybindingsInited = true - // Enter: 入力確定 - if err := g.SetKeybinding(viewInput, gocui.KeyEnter, gocui.ModNone, a.inputSubmit); err != nil { - return err - } - // Esc: 入力キャンセル - if err := g.SetKeybinding(viewInput, gocui.KeyEsc, gocui.ModNone, a.inputCancel); err != nil { - return err - } - // Ctrl+C: 入力キャンセル(Escの代替) - if err := g.SetKeybinding(viewInput, gocui.KeyCtrlC, gocui.ModNone, a.inputCancel); err != nil { - return err - } - return nil } // inputSubmit は入力モードの確定処理を行う。 -func (a *App) inputSubmit(g *gocui.Gui, v *gocui.View) error { - input := a.inputBuf +func (a *App) inputSubmit() { + input := a.input.Value() mode := a.inputMode a.inputMode = InputModeNone - a.inputBuf = "" - g.DeleteKeybindings(viewInput) - a.inputKeybindingsInited = false + a.input.Blur() + a.input.SetValue("") switch mode { case InputModeSearch, InputModeSearchBackward: a.searchQuery = input if input == "" { a.Status = "" - return nil + return } p := a.focusedPane() if p == nil { - return nil + return } a.doSearch(p, mode == InputModeSearch) case InputModeFilter: p := a.focusedPane() if p == nil { - return nil + return } if input == "" { if err := p.ClearFilter(); err != nil { a.Status = fmt.Sprintf("refresh error: %v", err) - return nil + return } a.Status = "filter cleared" } else { p.FilterQuery = input if err := p.Refresh(); err != nil { a.Status = fmt.Sprintf("refresh error: %v", err) - return nil + return } a.Status = fmt.Sprintf("filter: %s (%d item(s))", input, len(p.Entries)) } case InputModeCreate: - return a.executeCreate(input) + a.executeCreate(input) case InputModeRename: - return a.executeRename(input) + a.executeRename(input) } - - return nil } // inputCancel は入力モードをキャンセルする。 -func (a *App) inputCancel(g *gocui.Gui, v *gocui.View) error { +func (a *App) inputCancel() { a.inputMode = InputModeNone - a.inputBuf = "" + a.input.Blur() + a.input.SetValue("") a.Status = "" - g.DeleteKeybindings(viewInput) - a.inputKeybindingsInited = false - return nil -} - -// inputEditor は入力ビューのカスタムエディタ。 -// ユーザーの入力をinputBufに反映し、Layoutサイクルで消えないようにする。 -func (a *App) inputEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { - switch { - case key == gocui.KeyBackspace || key == gocui.KeyBackspace2: - if len(a.inputBuf) > 0 { - runes := []rune(a.inputBuf) - a.inputBuf = string(runes[:len(runes)-1]) - } - case ch != 0 && key == 0: - a.inputBuf += string(ch) - case key == gocui.KeySpace: - a.inputBuf += " " - default: - return - } } // executeCreate は新規ファイル/ディレクトリの作成を実行する。 -func (a *App) executeCreate(name string) error { +func (a *App) executeCreate(name string) { if name == "" { a.Status = "name is empty" - return nil + return } p := a.focusedPane() if p == nil { - return nil + return } isDir := strings.HasSuffix(name, "/") @@ -699,18 +513,18 @@ func (a *App) executeCreate(name string) error { base := filepath.Base(name) if base != name || base == "." || base == ".." { a.Status = "invalid filename" - return nil + return } path := filepath.Join(p.Dir, name) if err := a.FS.Create(path, isDir); err != nil { a.Status = fmt.Sprintf("create error: %v", err) - return nil + return } if err := p.Refresh(); err != nil { a.Status = fmt.Sprintf("refresh error: %v", err) - return nil + return } // 作成したエントリにカーソルを移動 @@ -726,44 +540,43 @@ func (a *App) executeCreate(name string) error { } else { a.Status = fmt.Sprintf("file created: %s", name) } - return nil } // executeRename はリネームを実行する。 -func (a *App) executeRename(newName string) error { +func (a *App) executeRename(newName string) { if newName == "" { a.Status = "name is empty" - return nil + return } p := a.focusedPane() if p == nil || len(p.Entries) == 0 { - return nil + return } oldName := p.Entries[p.Cursor].Name if oldName == newName { a.Status = "name not changed" - return nil + return } // パストラバーサル防止: ベース名のみ許可 base := filepath.Base(newName) if base != newName || base == "." || base == ".." { a.Status = "invalid filename" - return nil + return } oldPath := filepath.Join(p.Dir, oldName) newPath := filepath.Join(p.Dir, newName) if err := a.FS.Rename(oldPath, newPath); err != nil { a.Status = fmt.Sprintf("rename error: %v", err) - return nil + return } if err := p.Refresh(); err != nil { a.Status = fmt.Sprintf("refresh error: %v", err) - return nil + return } // リネームしたエントリにカーソルを移動 @@ -775,7 +588,6 @@ func (a *App) executeRename(newName string) error { } a.Status = fmt.Sprintf("renamed: %s -> %s", oldName, newName) - return nil } // getTargetPaths は選択中またはカーソル行のエントリのフルパス一覧を返す。 diff --git a/internal/ui/keybinding_test.go b/internal/ui/keybinding_test.go index 99da818..a375b78 100644 --- a/internal/ui/keybinding_test.go +++ b/internal/ui/keybinding_test.go @@ -23,9 +23,7 @@ func TestYank_トグルで取り消し(t *testing.T) { } // 1回目のy: ヤンクされる - if err := app.yank(nil, nil); err != nil { - t.Fatal(err) - } + app.yank() if app.YankBuf == nil || len(app.YankBuf.Entries) == 0 { t.Fatal("1回目のyでYankBufが空") } @@ -34,9 +32,7 @@ func TestYank_トグルで取り消し(t *testing.T) { } // 2回目のy(同じペイン): ヤンク取り消し - if err := app.yank(nil, nil); err != nil { - t.Fatal(err) - } + app.yank() if app.YankBuf != nil && len(app.YankBuf.Entries) > 0 { t.Error("2回目のyでYankBufがクリアされていない") } @@ -69,18 +65,14 @@ func TestYank_別ペインからは取り消しではなく新規ヤンク(t *te } // 左ペインでヤンク - if err := app.yank(nil, nil); err != nil { - t.Fatal(err) - } + app.yank() if app.YankBuf == nil || len(app.YankBuf.Entries) != 1 { t.Fatal("左ペインでのヤンクが失敗") } // 右ペインに切り替えてy → 取り消しではなく新規ヤンク app.FocusLeft = false - if err := app.yank(nil, nil); err != nil { - t.Fatal(err) - } + app.yank() if app.YankBuf == nil || len(app.YankBuf.Entries) == 0 { t.Fatal("別ペインからのyでYankBufが空") } diff --git a/internal/ui/layout.go b/internal/ui/layout.go index 85f7ff8..44c8250 100644 --- a/internal/ui/layout.go +++ b/internal/ui/layout.go @@ -1,18 +1,14 @@ -// Package ui はgocuiを使ったTUIの描画とイベント処理を行うパッケージ。 +// Package ui はBubble Teaを使ったTUIの描画とイベント処理を行うパッケージ。 package ui import ( - "fmt" - "strings" - - "github.com/jroimartin/gocui" - "github.com/mattn/go-runewidth" + "github.com/charmbracelet/bubbles/textinput" "github.com/myuron/graftx/internal/fs" "github.com/myuron/graftx/internal/pane" "github.com/myuron/graftx/internal/selector" ) -// ExitReason はMainLoop終了後の理由を表す型。 +// ExitReason はプログラム終了後の理由を表す型。 type ExitReason int const ( @@ -22,16 +18,6 @@ const ( ExitReasonQuit ) -// View名の定数。 -const ( - viewLeft = "left" - viewRight = "right" - viewStatus = "status" - viewInput = "input" - viewSelectorFilter = "selector_filter" - viewSelectorList = "selector_list" -) - // InputMode はステータスバーの入力モードを表す型。 type InputMode int @@ -59,28 +45,30 @@ const ( ) // App はTUIアプリケーション全体の状態を管理する構造体。 +// Bubble Teaのtea.Modelインターフェースを実装する。 type App struct { - SourcePane *pane.Pane // 左ペイン(コピー元)、未選択時はnil - DestPane *pane.Pane // 右ペイン(コピー先) - Selector selector.CommandRunner // リポジトリ選択 - FS fs.FileSystem // ファイルシステム - FocusLeft bool // trueなら左ペインにフォーカス - Status string // ステータスバーに表示するメッセージ - ExitReason ExitReason // MainLoop終了理由 - YankBuf *pane.YankBuffer // ヤンクバッファ - gPending bool // 'g'キーが押された状態 - inputMode InputMode // 入力モード - inputBuf string // 入力バッファ - searchQuery string // 現在の検索クエリ - searchFwd bool // 検索方向(trueなら前方) - gui *gocui.Gui // gocuiインスタンスへの参照 - pendingTargets []string // 削除確認時のターゲットパスのスナップショット - inputKeybindingsInited bool // 入力ビューのキーバインド登録済みフラグ - repoList []string // リポジトリ一覧(全件) - filteredRepoList []string // フィルタ済みリポジトリ一覧 - repoSelectorCursor int // リポジトリ選択カーソル位置 - repoFilterQuery string // リポジトリフィルタクエリ - selectorKeybindingsInited bool // セレクタキーバインド登録済みフラグ + SourcePane *pane.Pane // 左ペイン(コピー元)、未選択時はnil + DestPane *pane.Pane // 右ペイン(コピー先) + Selector selector.CommandRunner // リポジトリ選択 + FS fs.FileSystem // ファイルシステム + FocusLeft bool // trueなら左ペインにフォーカス + Status string // ステータスバーに表示するメッセージ + ExitReason ExitReason // プログラム終了理由 + YankBuf *pane.YankBuffer // ヤンクバッファ + gPending bool // 'g'キーが押された状態 + inputMode InputMode // 入力モード + searchQuery string // 現在の検索クエリ + searchFwd bool // 検索方向(trueなら前方) + width int // 端末の幅 + height int // 端末の高さ + input textinput.Model // 入力欄(検索/フィルタ/作成/リネーム) + pendingTargets []string // 削除確認時のターゲットパスのスナップショット + + repoList []string // リポジトリ一覧(全件) + filteredRepoList []string // フィルタ済みリポジトリ一覧 + repoSelectorCursor int // リポジトリ選択カーソル位置 + repoFilterQuery string // リポジトリフィルタクエリ + filterInput textinput.Model // セレクタフィルタ入力 } // NewApp は新しいAppを作成する。 @@ -91,12 +79,20 @@ func NewApp(destDir string, fsys fs.FileSystem, sel selector.CommandRunner) (*Ap return nil, err } + // 入力欄の初期化(プロンプトはステータスバー側で描画するため空にする) + input := textinput.New() + input.Prompt = "" + filterInput := textinput.New() + filterInput.Prompt = "" + return &App{ - DestPane: destPane, - Selector: sel, - FS: fsys, - FocusLeft: false, - ExitReason: ExitReasonNone, + DestPane: destPane, + Selector: sel, + FS: fsys, + FocusLeft: false, + ExitReason: ExitReasonNone, + input: input, + filterInput: filterInput, }, nil } @@ -113,237 +109,6 @@ func (a *App) SetSourceDir(dir string) error { return a.SourcePane.ChangeDir(dir) } -// SetGui はgocuiのGuiインスタンスを設定し、Manager登録・GUI設定・キーバインド登録を行う。 -func (a *App) SetGui(g *gocui.Gui) error { - a.gui = g - g.SetManager(a) - g.Highlight = true - g.SelFgColor = gocui.ColorGreen - g.SelBgColor = gocui.ColorDefault - g.Cursor = false - return a.setKeybindings(g) -} - -// Layout はgocui.Managerインターフェースの実装。 -// 左右ペインとステータスバーのレイアウトを構築する。 -func (a *App) Layout(g *gocui.Gui) error { - maxX, maxY := g.Size() - - // 左ペイン - if v, err := g.SetView(viewLeft, 0, 0, maxX/2-1, maxY-3); err != nil { - if err != gocui.ErrUnknownView { - return err - } - // 初回作成時の設定 - v.Highlight = true - v.SelBgColor = gocui.ColorCyan - v.SelFgColor = gocui.ColorBlack - } - - // 右ペイン - if v, err := g.SetView(viewRight, maxX/2, 0, maxX-1, maxY-3); err != nil { - if err != gocui.ErrUnknownView { - return err - } - // 初回作成時の設定 - v.Highlight = true - v.SelBgColor = gocui.ColorCyan - v.SelFgColor = gocui.ColorBlack - } - - // ステータスバー - if v, err := g.SetView(viewStatus, 0, maxY-2, maxX-1, maxY); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Frame = false - } - - // リポジトリ選択ポップアップモード - if a.inputMode == InputModeSelectRepo { - // ポップアップモード中はペインビューのcurrentViewを設定しない - if err := a.renderSelectorPopup(g); err != nil { - return err - } - } else { - // ポップアップビューが残っていれば削除 - _ = g.DeleteView(viewSelectorFilter) - _ = g.DeleteView(viewSelectorList) - - // 入力モード時は入力ビューを表示 - if a.isTextInputMode() { - if v, err := g.SetView(viewInput, 0, maxY-2, maxX-1, maxY); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Frame = false - v.Editable = true - v.Editor = gocui.EditorFunc(a.inputEditor) - } - if _, err := g.SetCurrentView(viewInput); err != nil { - return err - } - } else { - // 入力ビューが存在したら削除 - _ = g.DeleteView(viewInput) - - // フォーカス中のビューをcurrentViewに設定 - if a.FocusLeft { - if _, err := g.SetCurrentView(viewLeft); err != nil { - return err - } - } else { - if _, err := g.SetCurrentView(viewRight); err != nil { - return err - } - } - } - } - - // 左ペインの描画 - if err := a.renderLeftPane(g); err != nil { - return err - } - - // 右ペインの描画 - if err := a.renderRightPane(g); err != nil { - return err - } - - // ステータスバーの描画 - if err := a.renderStatus(g); err != nil { - return err - } - - return nil -} - -// renderLeftPane は左ペインの内容を描画する。 -func (a *App) renderLeftPane(g *gocui.Gui) error { - v, err := g.View(viewLeft) - if err != nil { - return err - } - v.Clear() - // カレントペインのみハイライトを有効にする - v.Highlight = a.FocusLeft - - if a.SourcePane == nil { - v.Title = " Source " - w, h := v.Size() - msg := "[s]select repository" - msgWidth := runewidth.StringWidth(msg) - padX := 0 - if w > msgWidth { - padX = (w - msgWidth) / 2 - } - padY := 0 - if h > 1 { - padY = h / 2 - } - for i := 0; i < padY; i++ { - _, _ = fmt.Fprintln(v, "") - } - _, _ = fmt.Fprintf(v, "%s%s\n", strings.Repeat(" ", padX), msg) - return nil - } - - v.Title = fmt.Sprintf(" %s ", a.SourcePane.Dir) - a.renderEntries(v, a.SourcePane) - return a.syncScroll(v, a.SourcePane) -} - -// renderRightPane は右ペインの内容を描画する。 -func (a *App) renderRightPane(g *gocui.Gui) error { - v, err := g.View(viewRight) - if err != nil { - return err - } - v.Clear() - // カレントペインのみハイライトを有効にする - v.Highlight = !a.FocusLeft - - v.Title = fmt.Sprintf(" %s ", a.DestPane.Dir) - a.renderEntries(v, a.DestPane) - return a.syncScroll(v, a.DestPane) -} - -// highlightMatch は検索クエリにマッチする部分をANSIカラーでハイライトする。 -// 大文字小文字を無視してマッチし、最初のマッチ部分を黄色背景+黒文字で装飾する。 -// クエリが空またはマッチなしの場合はそのまま返す。 -func highlightMatch(name, query string) string { - if query == "" { - return name - } - lower := strings.ToLower(name) - q := strings.ToLower(query) - idx := strings.Index(lower, q) - if idx < 0 { - return name - } - // 元の文字列の大文字小文字を保持してハイライト - return name[:idx] + "\x1b[30;43m" + name[idx:idx+len(query)] + "\x1b[0m" + name[idx+len(query):] -} - -// renderEntries はペインのエントリ一覧をViewに描画する。 -// 選択済みエントリには "* " プレフィックス、ディレクトリには "/" サフィックスを付与する。 -// 各行はビュー幅いっぱいまでスペースで埋めてハイライトが横幅全体に表示されるようにする。 -func (a *App) renderEntries(v *gocui.View, p *pane.Pane) { - viewWidth, _ := v.Size() - - for i, entry := range p.Entries { - prefix := " " - if p.Selected[i] { - prefix = "* " - } - - suffix := "" - if entry.IsDir { - suffix = "/" - } - - icon := iconForEntry(entry) + " " - - // ANSIコードを含まない元テキストで幅を計算 - text := prefix + icon + entry.Name + suffix - textWidth := runewidth.StringWidth(text) - pad := 0 - if viewWidth > textWidth { - pad = viewWidth - textWidth - } - - // 検索クエリがある場合はマッチ部分をハイライト - displayName := entry.Name - if a.searchQuery != "" { - displayName = highlightMatch(entry.Name, a.searchQuery) - } - displayText := prefix + icon + displayName + suffix - - _, _ = fmt.Fprintf(v, "%s%s\n", displayText, strings.Repeat(" ", pad)) - } -} - -// syncScroll はViewのカーソル位置とスクロール位置をPaneのカーソルに同期させる。 -func (a *App) syncScroll(v *gocui.View, p *pane.Pane) error { - _, viewHeight := v.Size() - if viewHeight <= 0 { - return nil - } - - // スクロールオフセットを計算 - oy := 0 - cy := p.Cursor - if p.Cursor >= viewHeight { - oy = p.Cursor - viewHeight + 1 - cy = viewHeight - 1 - } - - if err := v.SetOrigin(0, oy); err != nil { - return err - } - return v.SetCursor(0, cy) -} - // inputPrefix は入力モードごとのプレフィックス文字列を返す。 func (a *App) inputPrefix() string { switch a.inputMode { @@ -369,35 +134,3 @@ func (a *App) isTextInputMode() bool { } return false } - -// renderStatus はステータスバーを描画する。 -func (a *App) renderStatus(g *gocui.Gui) error { - // テキスト入力モード時は入力ビューに描画 - if a.isTextInputMode() { - v, err := g.View(viewInput) - if err != nil { - return nil // 入力ビューがまだ作られていない場合はスキップ - } - v.Clear() - _, _ = fmt.Fprint(v, a.inputPrefix()+a.inputBuf) - return nil - } - - v, err := g.View(viewStatus) - if err != nil { - return err - } - v.Clear() - - if a.Status != "" { - _, _ = fmt.Fprint(v, a.Status) - } else { - focus := "right" - if a.FocusLeft { - focus = "left" - } - _, _ = fmt.Fprintf(v, " [%s] [q]Quit [Tab]Switch [h/j/k/l]Move", focus) - } - - return nil -} diff --git a/internal/ui/layout_test.go b/internal/ui/layout_test.go deleted file mode 100644 index cf3bc02..0000000 --- a/internal/ui/layout_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package ui - -import ( - "testing" - - "github.com/jroimartin/gocui" - "github.com/myuron/graftx/internal/pane" -) - -// newTestGui はテスト用のgocui.Guiインスタンスを返す。 -// gocui.NewGuiが失敗する環境ではテストをスキップする。 -func newTestGui(t *testing.T) *gocui.Gui { - t.Helper() - g, err := gocui.NewGui(gocui.OutputNormal) - if err != nil { - t.Skip("gocui.NewGui failed (no terminal):", err) - } - return g -} - -func TestHighlightMatch_マッチ部分を黄色背景でハイライト(t *testing.T) { - tests := []struct { - name string - input string - query string - want string - }{ - { - name: "完全一致", - input: "foo", - query: "foo", - want: "\x1b[30;43mfoo\x1b[0m", - }, - { - name: "部分一致", - input: "foobar", - query: "bar", - want: "foo\x1b[30;43mbar\x1b[0m", - }, - { - name: "大文字小文字を無視", - input: "FooBar", - query: "foo", - want: "\x1b[30;43mFoo\x1b[0m" + "Bar", - }, - { - name: "マッチなし", - input: "hello", - query: "xyz", - want: "hello", - }, - { - name: "空クエリ", - input: "hello", - query: "", - want: "hello", - }, - { - name: "複数マッチは最初のみ", - input: "abcabc", - query: "abc", - want: "\x1b[30;43mabc\x1b[0m" + "abc", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := highlightMatch(tt.input, tt.query) - if got != tt.want { - t.Errorf("highlightMatch(%q, %q) = %q, want %q", tt.input, tt.query, got, tt.want) - } - }) - } -} - -func TestHighlight_フォーカスペインのみハイライト_右フォーカス(t *testing.T) { - g := newTestGui(t) - defer g.Close() - - app := &App{ - DestPane: &pane.Pane{}, - FocusLeft: false, - } - app.gui = g - g.SetManager(app) - - // Layoutを1回実行してビューを作成 - if err := app.Layout(g); err != nil { - t.Fatal(err) - } - - leftView, err := g.View(viewLeft) - if err != nil { - t.Fatal(err) - } - rightView, err := g.View(viewRight) - if err != nil { - t.Fatal(err) - } - - // 右フォーカス時: 右ペインのみHighlightがtrue - if leftView.Highlight { - t.Error("左ペインのHighlightがtrue、右フォーカス時はfalseであるべき") - } - if !rightView.Highlight { - t.Error("右ペインのHighlightがfalse、右フォーカス時はtrueであるべき") - } -} - -func TestHighlight_フォーカスペインのみハイライト_左フォーカス(t *testing.T) { - g := newTestGui(t) - defer g.Close() - - app := &App{ - DestPane: &pane.Pane{}, - SourcePane: &pane.Pane{}, - FocusLeft: true, - } - app.gui = g - g.SetManager(app) - - if err := app.Layout(g); err != nil { - t.Fatal(err) - } - - leftView, err := g.View(viewLeft) - if err != nil { - t.Fatal(err) - } - rightView, err := g.View(viewRight) - if err != nil { - t.Fatal(err) - } - - // 左フォーカス時: 左ペインのみHighlightがtrue - if !leftView.Highlight { - t.Error("左ペインのHighlightがfalse、左フォーカス時はtrueであるべき") - } - if rightView.Highlight { - t.Error("右ペインのHighlightがtrue、左フォーカス時はfalseであるべき") - } -} - -func TestHighlight_フォーカス切替で動的に変化(t *testing.T) { - g := newTestGui(t) - defer g.Close() - - app := &App{ - DestPane: &pane.Pane{}, - SourcePane: &pane.Pane{}, - FocusLeft: false, - } - app.gui = g - g.SetManager(app) - - // 最初は右フォーカス - if err := app.Layout(g); err != nil { - t.Fatal(err) - } - - leftView, _ := g.View(viewLeft) - rightView, _ := g.View(viewRight) - - if leftView.Highlight { - t.Error("初期状態: 左ペインのHighlightがtrue") - } - if !rightView.Highlight { - t.Error("初期状態: 右ペインのHighlightがfalse") - } - - // フォーカスを左に切り替え - app.FocusLeft = true - if err := app.Layout(g); err != nil { - t.Fatal(err) - } - - if !leftView.Highlight { - t.Error("左フォーカス後: 左ペインのHighlightがfalse") - } - if rightView.Highlight { - t.Error("左フォーカス後: 右ペインのHighlightがtrue") - } -} diff --git a/internal/ui/model.go b/internal/ui/model.go new file mode 100644 index 0000000..7410812 --- /dev/null +++ b/internal/ui/model.go @@ -0,0 +1,129 @@ +package ui + +import ( + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +// Init はtea.Modelインターフェースの実装。初期コマンドを返す。 +func (a *App) Init() tea.Cmd { + return textinput.Blink +} + +// Update はtea.Modelインターフェースの実装。 +// メッセージを受け取り状態を更新する。 +func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + a.width = msg.Width + a.height = msg.Height + return a, nil + case tea.KeyMsg: + return a.handleKey(msg) + } + return a, nil +} + +// handleKey はキー入力を現在のモードに応じてルーティングする。 +func (a *App) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch { + case a.inputMode == InputModeSelectRepo: + return a.handleSelectorKey(msg) + case a.isTextInputMode(): + return a.handleInputKey(msg) + case a.inputMode == InputModeConfirmDelete || a.inputMode == InputModeConfirmForceDelete: + return a.handleConfirmKey(msg) + default: + return a.handleNormalKey(msg) + } +} + +// handleNormalKey は通常モードのVimバインドを処理する。 +func (a *App) handleNormalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "j": + a.cursorDown() + case "k": + a.cursorUp() + case "l": + a.enterDir() + case "h": + a.parentDir() + case "tab": + a.toggleFocus() + case "s": + return a, a.selectRepo() + case "q": + return a, a.quit() + case "g": + a.handleG() + case "G": + a.moveToBottom() + case " ": + a.toggleSelect() + case "ctrl+a": + a.selectAll() + case "ctrl+r": + a.invertSelection() + case "esc", "ctrl+c": + a.handleEsc() + case "y": + a.yank() + case "p": + a.paste() + case "P": + a.pasteOverwrite() + case "d": + a.trashFile() + case "D": + a.deleteFile() + case "a": + return a, a.createFile() + case "r": + return a, a.renameFile() + case "/": + return a, a.searchForward() + case "n": + a.searchNext() + case "N": + a.searchPrev() + case "f": + return a, a.filterMode() + case ".": + a.toggleHidden() + } + return a, nil +} + +// handleInputKey はテキスト入力モードのキー入力を処理する。 +func (a *App) handleInputKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": + a.inputSubmit() + return a, nil + case "esc", "ctrl+c": + a.inputCancel() + return a, nil + } + var cmd tea.Cmd + a.input, cmd = a.input.Update(msg) + return a, cmd +} + +// handleConfirmKey は確認モード(y/n)のキー入力を処理する。 +func (a *App) handleConfirmKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "y": + a.executeConfirm() + case "n", "esc", "ctrl+c": + a.inputMode = InputModeNone + a.pendingTargets = nil + a.Status = "cancelled" + } + return a, nil +} + +// View はtea.Modelインターフェースの実装。画面全体を文字列として返す。 +func (a *App) View() string { + return a.render() +} diff --git a/internal/ui/model_test.go b/internal/ui/model_test.go new file mode 100644 index 0000000..0bd688c --- /dev/null +++ b/internal/ui/model_test.go @@ -0,0 +1,210 @@ +package ui + +import ( + "strings" + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/myuron/graftx/internal/fs" + "github.com/myuron/graftx/internal/pane" +) + +// keyRunes はルーン入力のキーメッセージを生成する。 +func keyRunes(s string) tea.KeyMsg { + return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)} +} + +// stubFS は削除系テスト用の最小のFileSystem実装。 +type stubFS struct { + entries []fs.Entry + trashed []string + removed []string +} + +func (s *stubFS) ReadDir(string) ([]fs.Entry, error) { return s.entries, nil } +func (s *stubFS) Copy(string, string) error { return nil } +func (s *stubFS) Remove(p string) error { s.removed = append(s.removed, p); return nil } +func (s *stubFS) Trash(p string) error { s.trashed = append(s.trashed, p); return nil } +func (s *stubFS) Rename(string, string) error { return nil } +func (s *stubFS) Create(string, bool) error { return nil } + +func newDestApp() *App { + app, _ := NewApp("/dst", &stubFS{}, nil) + app.DestPane = &pane.Pane{ + Dir: "/dst", + Entries: []fs.Entry{{Name: "apple"}, {Name: "banana"}, {Name: "cherry"}}, + Cursor: 0, + Selected: map[int]bool{}, + } + app.width = 80 + app.height = 24 + return app +} + +func TestUpdate_WindowSizeで幅高さを保存(t *testing.T) { + app, _ := NewApp("/dst", &stubFS{}, nil) + m, _ := app.Update(tea.WindowSizeMsg{Width: 100, Height: 40}) + got := m.(*App) + if got.width != 100 || got.height != 40 { + t.Errorf("width/height = %d/%d, want 100/40", got.width, got.height) + } +} + +func TestUpdate_jでカーソル下移動(t *testing.T) { + app := newDestApp() + app.Update(keyRunes("j")) + if app.DestPane.Cursor != 1 { + t.Errorf("Cursor = %d, want 1", app.DestPane.Cursor) + } +} + +func TestUpdate_スペースでマークのみトグルしカーソルは移動しない(t *testing.T) { + app := newDestApp() + if app.DestPane.Cursor != 0 { + t.Fatalf("初期Cursor = %d, want 0", app.DestPane.Cursor) + } + + // スペースでカーソル行をトグル選択 + app.Update(tea.KeyMsg{Type: tea.KeySpace}) + + if !app.DestPane.Selected[0] { + t.Error("スペースでカーソル行が選択されるべき") + } + if app.DestPane.Cursor != 0 { + t.Errorf("スペース後のCursor = %d, want 0(移動しないべき)", app.DestPane.Cursor) + } + + // 再度スペースで選択解除(カーソルは依然移動しない) + app.Update(tea.KeyMsg{Type: tea.KeySpace}) + if app.DestPane.Selected[0] { + t.Error("再度のスペースで選択解除されるべき") + } + if app.DestPane.Cursor != 0 { + t.Errorf("2回目スペース後のCursor = %d, want 0", app.DestPane.Cursor) + } +} + +func TestUpdate_Tabでフォーカス切替(t *testing.T) { + app := newDestApp() + if app.FocusLeft { + t.Fatal("初期状態は右フォーカスのはず") + } + app.Update(tea.KeyMsg{Type: tea.KeyTab}) + if !app.FocusLeft { + t.Error("Tab後は左フォーカスになるべき") + } +} + +func TestUpdate_qで終了コマンド(t *testing.T) { + app := newDestApp() + _, cmd := app.Update(keyRunes("q")) + if app.ExitReason != ExitReasonQuit { + t.Errorf("ExitReason = %d, want %d", app.ExitReason, ExitReasonQuit) + } + if cmd == nil { + t.Fatal("qで終了コマンドが返るべき") + } + if _, ok := cmd().(tea.QuitMsg); !ok { + t.Error("返されたコマンドはtea.Quitであるべき") + } +} + +func TestUpdate_検索モード遷移と確定(t *testing.T) { + app := newDestApp() + + // '/' で検索モードに入る + app.Update(keyRunes("/")) + if app.inputMode != InputModeSearch { + t.Fatalf("inputMode = %d, want InputModeSearch", app.inputMode) + } + + // "ban" と入力 + for _, ch := range []string{"b", "a", "n"} { + app.Update(keyRunes(ch)) + } + + // Enterで確定 → banana(idx1)にジャンプ + app.Update(tea.KeyMsg{Type: tea.KeyEnter}) + if app.inputMode != InputModeNone { + t.Errorf("確定後のinputMode = %d, want InputModeNone", app.inputMode) + } + if app.searchQuery != "ban" { + t.Errorf("searchQuery = %q, want \"ban\"", app.searchQuery) + } + if app.DestPane.Cursor != 1 { + t.Errorf("Cursor = %d, want 1 (banana)", app.DestPane.Cursor) + } +} + +func TestUpdate_削除確認_yで実行(t *testing.T) { + stub := &stubFS{entries: []fs.Entry{}} + app, _ := NewApp("/dst", stub, nil) + app.DestPane = &pane.Pane{ + Dir: "/dst", + Entries: []fs.Entry{{Name: "apple"}}, + Cursor: 0, + Selected: map[int]bool{}, + FS: stub, + } + app.width, app.height = 80, 24 + + // 'd' でゴミ箱確認モード + app.Update(keyRunes("d")) + if app.inputMode != InputModeConfirmDelete { + t.Fatalf("inputMode = %d, want InputModeConfirmDelete", app.inputMode) + } + + // 'y' で実行 + app.Update(keyRunes("y")) + if app.inputMode != InputModeNone { + t.Errorf("実行後のinputMode = %d, want InputModeNone", app.inputMode) + } + if len(stub.trashed) != 1 || stub.trashed[0] != "/dst/apple" { + t.Errorf("trashed = %v, want [/dst/apple]", stub.trashed) + } +} + +func TestUpdate_削除確認_nでキャンセル(t *testing.T) { + app := newDestApp() + app.Update(keyRunes("d")) + app.Update(keyRunes("n")) + if app.inputMode != InputModeNone { + t.Errorf("キャンセル後のinputMode = %d, want InputModeNone", app.inputMode) + } + if app.Status != "cancelled" { + t.Errorf("Status = %q, want \"cancelled\"", app.Status) + } +} + +func TestView_サイズ未設定なら空文字(t *testing.T) { + app, _ := NewApp("/dst", &stubFS{}, nil) + if app.View() != "" { + t.Error("width/height未設定ではViewは空文字を返すべき") + } +} + +func TestView_基本描画でエントリとステータスを含む(t *testing.T) { + app := newDestApp() + out := app.View() + if out == "" { + t.Fatal("サイズ設定済みではViewは空でないべき") + } + // エントリ名・ディレクトリ名・デフォルトステータスが描画されている + for _, want := range []string{"apple", "banana", "/dst", "[q]Quit"} { + if !strings.Contains(out, want) { + t.Errorf("View出力に %q が含まれていない", want) + } + } +} + +func TestView_セレクタモーダル描画(t *testing.T) { + app := newDestApp() + app.inputMode = InputModeSelectRepo + app.filteredRepoList = []string{"/repo/alpha", "/repo/beta"} + out := app.View() + for _, want := range []string{"Filter", "/repo/alpha", "/repo/beta"} { + if !strings.Contains(out, want) { + t.Errorf("セレクタモーダル出力に %q が含まれていない", want) + } + } +} diff --git a/internal/ui/render.go b/internal/ui/render.go new file mode 100644 index 0000000..bcc5bb4 --- /dev/null +++ b/internal/ui/render.go @@ -0,0 +1,269 @@ +package ui + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/mattn/go-runewidth" + "github.com/myuron/graftx/internal/pane" +) + +// 配色定数。 +var ( + colorCursorBg = lipgloss.Color("6") // シアン(カーソル行背景、旧gocui.ColorCyan) + colorCursorFg = lipgloss.Color("0") // 黒(カーソル行前景、旧gocui.ColorBlack) + colorBorderActive = lipgloss.Color("2") // 緑(アクティブペイン枠) + colorBorderIdle = lipgloss.Color("240") // グレー(非アクティブペイン枠) +) + +// highlightMatch は検索クエリにマッチする部分をANSIカラーでハイライトする。 +// 大文字小文字を無視してマッチし、最初のマッチ部分を黄色背景+黒文字で装飾する。 +// クエリが空またはマッチなしの場合はそのまま返す。 +func highlightMatch(name, query string) string { + if query == "" { + return name + } + lower := strings.ToLower(name) + q := strings.ToLower(query) + idx := strings.Index(lower, q) + if idx < 0 { + return name + } + // 元の文字列の大文字小文字を保持してハイライト + return name[:idx] + "\x1b[30;43m" + name[idx:idx+len(query)] + "\x1b[0m" + name[idx+len(query):] +} + +// render は画面全体を文字列として描画する。 +func (a *App) render() string { + if a.width <= 0 || a.height <= 0 { + return "" + } + + // リポジトリ選択ポップアップモードは中央モーダルで全画面を占有する + if a.inputMode == InputModeSelectRepo { + return lipgloss.Place(a.width, a.height, lipgloss.Center, lipgloss.Center, a.renderSelectorModal()) + } + + statusHeight := 1 + paneAreaHeight := a.height - statusHeight + if paneAreaHeight < 3 { + paneAreaHeight = 3 + } + + leftWidth := a.width / 2 + rightWidth := a.width - leftWidth + + left := a.renderPane(a.SourcePane, " Source ", a.FocusLeft, leftWidth, paneAreaHeight) + right := a.renderPane(a.DestPane, " Dest ", !a.FocusLeft, rightWidth, paneAreaHeight) + + panes := lipgloss.JoinHorizontal(lipgloss.Top, left, right) + return panes + "\n" + a.renderStatus() +} + +// renderPane は1つのペインを枠付きで描画する。 +// pがnilの場合(左ペイン未選択時)はプレースホルダを表示する。 +func (a *App) renderPane(p *pane.Pane, defaultTitle string, focused bool, outerW, outerH int) string { + innerW := outerW - 2 + innerH := outerH - 2 + if innerW < 1 { + innerW = 1 + } + if innerH < 1 { + innerH = 1 + } + + borderColor := colorBorderIdle + if focused { + borderColor = colorBorderActive + } + boxStyle := lipgloss.NewStyle(). + Border(lipgloss.NormalBorder()). + BorderForeground(borderColor). + Width(innerW). + Height(innerH) + + var title string + var body string + if p == nil { + title = defaultTitle + body = a.renderPlaceholder(innerW, innerH-1) + } else { + title = fmt.Sprintf(" %s ", p.Dir) + body = a.renderEntries(p, focused, innerW, innerH-1) + } + + titleLine := lipgloss.NewStyle().Bold(true).MaxWidth(innerW).Render(title) + content := titleLine + "\n" + body + return boxStyle.Render(content) +} + +// renderPlaceholder は左ペイン未選択時の中央メッセージを描画する。 +func (a *App) renderPlaceholder(w, h int) string { + msg := "[s]select repository" + return lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, msg) +} + +// renderEntries はペインのエントリ一覧を可視範囲ぶん描画する。 +// 選択済みエントリには "* " プレフィックス、ディレクトリには "/" サフィックスを付与する。 +// focusedがtrueの場合はカーソル行をハイライトする。 +func (a *App) renderEntries(p *pane.Pane, focused bool, width, visible int) string { + if visible < 1 { + visible = 1 + } + + // スクロールオフセットの計算(旧syncScrollと同じロジック) + oy := 0 + if p.Cursor >= visible { + oy = p.Cursor - visible + 1 + } + end := oy + visible + if end > len(p.Entries) { + end = len(p.Entries) + } + + cursorStyle := lipgloss.NewStyle().Background(colorCursorBg).Foreground(colorCursorFg).Width(width) + normalStyle := lipgloss.NewStyle().Width(width) + + lines := make([]string, 0, visible) + for i := oy; i < end; i++ { + entry := p.Entries[i] + + prefix := " " + if p.Selected[i] { + prefix = "* " + } + suffix := "" + if entry.IsDir { + suffix = "/" + } + icon := iconForEntry(entry) + " " + + // 検索クエリがある場合はマッチ部分をハイライト + displayName := entry.Name + if a.searchQuery != "" { + displayName = highlightMatch(entry.Name, a.searchQuery) + } + text := prefix + icon + displayName + suffix + + if focused && i == p.Cursor { + lines = append(lines, cursorStyle.Render(text)) + } else { + lines = append(lines, normalStyle.Render(text)) + } + } + + // 残りを空行で埋める + for len(lines) < visible { + lines = append(lines, normalStyle.Render("")) + } + + return strings.Join(lines, "\n") +} + +// renderStatus はステータスバーを描画する。 +func (a *App) renderStatus() string { + if a.isTextInputMode() { + return a.inputPrefix() + a.input.View() + } + + if a.Status != "" { + return a.Status + } + + focus := "right" + if a.FocusLeft { + focus = "left" + } + return fmt.Sprintf(" [%s] [q]Quit [Tab]Switch [h/j/k/l]Move", focus) +} + +// renderSelectorModal はリポジトリ選択ポップアップのモーダルを描画する。 +func (a *App) renderSelectorModal() string { + // モーダルサイズ計算(60%幅 x 70%高さ、最小40x10) + w := a.width * 60 / 100 + if w < 40 { + w = 40 + } + if w > a.width-2 { + w = a.width - 2 + } + h := a.height * 70 / 100 + if h < 10 { + h = 10 + } + if h > a.height-2 { + h = a.height - 2 + } + + innerW := w - 2 + innerH := h - 2 + if innerW < 1 { + innerW = 1 + } + if innerH < 1 { + innerH = 1 + } + + // フィルタ行(タイトル + 入力内容) + filterLine := lipgloss.NewStyle().Bold(true).MaxWidth(innerW).Render(" Filter ") + inputLine := lipgloss.NewStyle().MaxWidth(innerW).Render(a.filterInput.View()) + + // リスト表示領域(フィルタ2行ぶんを差し引く) + listVisible := innerH - 2 + if listVisible < 1 { + listVisible = 1 + } + listBody := a.renderSelectorList(innerW, listVisible) + + content := filterLine + "\n" + inputLine + "\n" + listBody + boxStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(colorBorderActive). + Width(innerW). + Height(innerH) + return boxStyle.Render(content) +} + +// renderSelectorList はリポジトリ一覧を可視範囲ぶん描画する。 +func (a *App) renderSelectorList(width, visible int) string { + if visible < 1 { + visible = 1 + } + + // スクロールオフセットの計算 + oy := 0 + if a.repoSelectorCursor >= visible { + oy = a.repoSelectorCursor - visible + 1 + } + end := oy + visible + if end > len(a.filteredRepoList) { + end = len(a.filteredRepoList) + } + + cursorStyle := lipgloss.NewStyle().Background(colorCursorBg).Foreground(colorCursorFg).Width(width) + normalStyle := lipgloss.NewStyle().Width(width) + + lines := make([]string, 0, visible) + for i := oy; i < end; i++ { + text := truncateToWidth(a.filteredRepoList[i], width) + if i == a.repoSelectorCursor { + lines = append(lines, cursorStyle.Render(text)) + } else { + lines = append(lines, normalStyle.Render(text)) + } + } + for len(lines) < visible { + lines = append(lines, normalStyle.Render("")) + } + + return strings.Join(lines, "\n") +} + +// truncateToWidth は表示幅がwを超える場合に末尾を切り詰める。 +func truncateToWidth(s string, w int) string { + if runewidth.StringWidth(s) <= w { + return s + } + return runewidth.Truncate(s, w, "") +} diff --git a/internal/ui/render_test.go b/internal/ui/render_test.go new file mode 100644 index 0000000..dd557a0 --- /dev/null +++ b/internal/ui/render_test.go @@ -0,0 +1,113 @@ +package ui + +import ( + "strings" + "testing" + + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" + "github.com/myuron/graftx/internal/fs" + "github.com/myuron/graftx/internal/pane" +) + +// テスト中は色出力を決定的にするためANSIプロファイルを強制する。 +func init() { + lipgloss.SetColorProfile(termenv.ANSI) +} + +func TestHighlightMatch_マッチ部分を黄色背景でハイライト(t *testing.T) { + tests := []struct { + name string + input string + query string + want string + }{ + { + name: "完全一致", + input: "foo", + query: "foo", + want: "\x1b[30;43mfoo\x1b[0m", + }, + { + name: "部分一致", + input: "foobar", + query: "bar", + want: "foo\x1b[30;43mbar\x1b[0m", + }, + { + name: "大文字小文字を無視", + input: "FooBar", + query: "foo", + want: "\x1b[30;43mFoo\x1b[0m" + "Bar", + }, + { + name: "マッチなし", + input: "hello", + query: "xyz", + want: "hello", + }, + { + name: "空クエリ", + input: "hello", + query: "", + want: "hello", + }, + { + name: "複数マッチは最初のみ", + input: "abcabc", + query: "abc", + want: "\x1b[30;43mabc\x1b[0m" + "abc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := highlightMatch(tt.input, tt.query) + if got != tt.want { + t.Errorf("highlightMatch(%q, %q) = %q, want %q", tt.input, tt.query, got, tt.want) + } + }) + } +} + +// renderEntries はフォーカス中のペインでのみカーソル行をハイライトする。 +func TestRenderEntries_フォーカス時のみカーソル行をハイライト(t *testing.T) { + p := &pane.Pane{ + Entries: []fs.Entry{{Name: "a.txt"}, {Name: "b.txt"}}, + Cursor: 0, + Selected: map[int]bool{}, + } + + focused := (&App{}).renderEntries(p, true, 20, 2) + if !strings.Contains(focused, "\x1b[") { + t.Errorf("フォーカス時はカーソル行に色エスケープが含まれるべき: %q", focused) + } + + idle := (&App{}).renderEntries(p, false, 20, 2) + if strings.Contains(idle, "\x1b[") { + t.Errorf("非フォーカス時は色エスケープを含まないべき: %q", idle) + } +} + +// renderEntries はスクロールオフセットを正しく計算し可視範囲を返す。 +func TestRenderEntries_スクロールで可視範囲を切り替え(t *testing.T) { + entries := []fs.Entry{ + {Name: "e0"}, {Name: "e1"}, {Name: "e2"}, {Name: "e3"}, {Name: "e4"}, + } + p := &pane.Pane{ + Entries: entries, + Cursor: 4, + Selected: map[int]bool{}, + } + + // 可視3行・カーソル4 → オフセット2、e2/e3/e4が見える + out := (&App{}).renderEntries(p, true, 20, 3) + if strings.Contains(out, "e0") || strings.Contains(out, "e1") { + t.Errorf("スクロール後にe0/e1が表示されている: %q", out) + } + for _, name := range []string{"e2", "e3", "e4"} { + if !strings.Contains(out, name) { + t.Errorf("%s が可視範囲に表示されていない: %q", name, out) + } + } +} diff --git a/internal/ui/selector.go b/internal/ui/selector.go index 1fc6865..29507cb 100644 --- a/internal/ui/selector.go +++ b/internal/ui/selector.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/jroimartin/gocui" + tea "github.com/charmbracelet/bubbletea" ) // filterRepoList はrepoFilterQueryでリポジトリ一覧をフィルタする。 @@ -43,8 +43,32 @@ func (a *App) selectorMoveCursorUp() { } } +// handleSelectorKey はリポジトリ選択ポップアップモードのキー入力を処理する。 +func (a *App) handleSelectorKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": + a.selectorConfirm() + return a, nil + case "esc", "ctrl+c": + a.selectorCancel() + return a, nil + case "ctrl+j", "ctrl+n", "down": + a.selectorMoveCursorDown() + return a, nil + case "ctrl+k", "ctrl+p", "up": + a.selectorMoveCursorUp() + return a, nil + } + // それ以外はフィルタ入力欄に委譲し、フィルタを更新する + var cmd tea.Cmd + a.filterInput, cmd = a.filterInput.Update(msg) + a.repoFilterQuery = a.filterInput.Value() + a.filterRepoList() + return a, cmd +} + // openRepoSelector はリポジトリ選択ポップアップを開く。 -func (a *App) openRepoSelector(g *gocui.Gui, v *gocui.View) error { +func (a *App) openRepoSelector() tea.Cmd { repos, err := a.Selector.ListRepositories() if err != nil { a.Status = fmt.Sprintf("failed to list repositories: %v", err) @@ -56,218 +80,41 @@ func (a *App) openRepoSelector(g *gocui.Gui, v *gocui.View) error { a.repoSelectorCursor = 0 a.filterRepoList() a.inputMode = InputModeSelectRepo - - return a.setupSelectorKeybindings(g) + a.filterInput.SetValue("") + return a.filterInput.Focus() } // closeRepoSelector はリポジトリ選択ポップアップを閉じる。 -func (a *App) closeRepoSelector(g *gocui.Gui) { +func (a *App) closeRepoSelector() { a.inputMode = InputModeNone a.repoList = nil a.filteredRepoList = nil a.repoSelectorCursor = 0 a.repoFilterQuery = "" - - _ = g.DeleteView(viewSelectorFilter) - _ = g.DeleteView(viewSelectorList) - g.DeleteKeybindings(viewSelectorFilter) - g.DeleteKeybindings(viewSelectorList) - a.selectorKeybindingsInited = false + a.filterInput.Blur() + a.filterInput.SetValue("") } // selectorConfirm は選択確定処理。 -func (a *App) selectorConfirm(g *gocui.Gui, v *gocui.View) error { +func (a *App) selectorConfirm() { if len(a.filteredRepoList) == 0 { - return nil + return } selected := a.filteredRepoList[a.repoSelectorCursor] - a.closeRepoSelector(g) + a.closeRepoSelector() if err := a.SetSourceDir(selected); err != nil { a.Status = fmt.Sprintf("failed to set source directory: %v", err) - return nil + return } a.FocusLeft = true a.Status = "" - return nil } // selectorCancel はキャンセル処理。 -func (a *App) selectorCancel(g *gocui.Gui, v *gocui.View) error { - a.closeRepoSelector(g) +func (a *App) selectorCancel() { + a.closeRepoSelector() a.Status = "" - return nil -} - -// selectorCursorDown はリスト内カーソル下移動ハンドラ。 -func (a *App) selectorCursorDown(g *gocui.Gui, v *gocui.View) error { - a.selectorMoveCursorDown() - return nil -} - -// selectorCursorUp はリスト内カーソル上移動ハンドラ。 -func (a *App) selectorCursorUp(g *gocui.Gui, v *gocui.View) error { - a.selectorMoveCursorUp() - return nil -} - -// selectorFilterEditor はセレクタフィルタ入力のカスタムエディタ。 -func (a *App) selectorFilterEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { - switch { - case key == gocui.KeyBackspace || key == gocui.KeyBackspace2: - if len(a.repoFilterQuery) > 0 { - // UTF-8対応: runeスライスで処理 - runes := []rune(a.repoFilterQuery) - a.repoFilterQuery = string(runes[:len(runes)-1]) - } - case ch != 0 && key == 0: - a.repoFilterQuery += string(ch) - case key == gocui.KeySpace: - a.repoFilterQuery += " " - default: - return - } - a.filterRepoList() -} - -// setupSelectorKeybindings はセレクタ用キーバインドを登録する。 -func (a *App) setupSelectorKeybindings(g *gocui.Gui) error { - if a.selectorKeybindingsInited { - return nil - } - a.selectorKeybindingsInited = true - - // フィルタビュー用キーバインド - if err := g.SetKeybinding(viewSelectorFilter, gocui.KeyEnter, gocui.ModNone, a.selectorConfirm); err != nil { - return err - } - if err := g.SetKeybinding(viewSelectorFilter, gocui.KeyEsc, gocui.ModNone, a.selectorCancel); err != nil { - return err - } - if err := g.SetKeybinding(viewSelectorFilter, gocui.KeyCtrlC, gocui.ModNone, a.selectorCancel); err != nil { - return err - } - if err := g.SetKeybinding(viewSelectorFilter, gocui.KeyCtrlJ, gocui.ModNone, a.selectorCursorDown); err != nil { - return err - } - if err := g.SetKeybinding(viewSelectorFilter, gocui.KeyCtrlK, gocui.ModNone, a.selectorCursorUp); err != nil { - return err - } - if err := g.SetKeybinding(viewSelectorFilter, gocui.KeyCtrlN, gocui.ModNone, a.selectorCursorDown); err != nil { - return err - } - if err := g.SetKeybinding(viewSelectorFilter, gocui.KeyCtrlP, gocui.ModNone, a.selectorCursorUp); err != nil { - return err - } - - return nil -} - -// renderSelectorPopup はリポジトリ選択ポップアップを描画する。 -func (a *App) renderSelectorPopup(g *gocui.Gui) error { - maxX, maxY := g.Size() - - // ポップアップサイズ計算(60%幅 x 70%高さ、最小40x10) - w := maxX * 60 / 100 - if w < 40 { - w = 40 - } - if w > maxX-2 { - w = maxX - 2 - } - h := maxY * 70 / 100 - if h < 10 { - h = 10 - } - if h > maxY-2 { - h = maxY - 2 - } - - // 中央配置 - x0 := (maxX - w) / 2 - y0 := (maxY - h) / 2 - x1 := x0 + w - y1 := y0 + h - - // フィルタ入力ビュー(1行) - filterY1 := y0 + 2 - if v, err := g.SetView(viewSelectorFilter, x0, y0, x1, filterY1); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = " Filter " - v.Editable = true - v.Editor = gocui.EditorFunc(a.selectorFilterEditor) - } - - // リストビュー - if v, err := g.SetView(viewSelectorList, x0, filterY1, x1, y1); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = " Repositories " - v.Highlight = true - v.SelBgColor = gocui.ColorCyan - v.SelFgColor = gocui.ColorBlack - } - - // フィルタビューの内容更新 - filterView, err := g.View(viewSelectorFilter) - if err != nil { - return err - } - filterView.Clear() - _, _ = fmt.Fprint(filterView, a.repoFilterQuery) - - // リストビューの内容更新 - listView, err := g.View(viewSelectorList) - if err != nil { - return err - } - listView.Clear() - for _, repo := range a.filteredRepoList { - _, _ = fmt.Fprintln(listView, repo) - } - - // リストビューのカーソル同期 - if err := a.syncSelectorScroll(listView); err != nil { - return err - } - - // ビューを前面に表示 - if _, err := g.SetViewOnTop(viewSelectorFilter); err != nil { - return err - } - if _, err := g.SetViewOnTop(viewSelectorList); err != nil { - return err - } - - // フィルタビューにフォーカス - if _, err := g.SetCurrentView(viewSelectorFilter); err != nil { - return err - } - - return nil -} - -// syncSelectorScroll はセレクタリストビューのスクロール位置を同期する。 -func (a *App) syncSelectorScroll(v *gocui.View) error { - _, viewHeight := v.Size() - if viewHeight <= 0 { - return nil - } - - oy := 0 - cy := a.repoSelectorCursor - if a.repoSelectorCursor >= viewHeight { - oy = a.repoSelectorCursor - viewHeight + 1 - cy = viewHeight - 1 - } - - if err := v.SetOrigin(0, oy); err != nil { - return err - } - return v.SetCursor(0, cy) } diff --git a/main.go b/main.go index 45abc1a..faa4453 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/jroimartin/gocui" + tea "github.com/charmbracelet/bubbletea" "github.com/myuron/graftx/internal/fs" "github.com/myuron/graftx/internal/selector" "github.com/myuron/graftx/internal/ui" @@ -23,19 +23,8 @@ func main() { os.Exit(1) } - g, err := gocui.NewGui(gocui.OutputNormal) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to initialize GUI: %v\n", err) - os.Exit(1) - } - defer g.Close() - - if err := app.SetGui(g); err != nil { - fmt.Fprintf(os.Stderr, "failed to configure GUI: %v\n", err) - os.Exit(1) - } - - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + p := tea.NewProgram(app, tea.WithAltScreen()) + if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } diff --git a/result b/result new file mode 120000 index 0000000..2cf7c41 --- /dev/null +++ b/result @@ -0,0 +1 @@ +/nix/store/xb8v2ix2bv5a5vdi35kkk6aflv1wpyz9-graftx-0.1.0 \ No newline at end of file