mirror of
https://github.com/anotherhadi/ilovetui.git
synced 2026-08-21 12:05:49 +02:00
init layout, notifications, tabs & more
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// Command basic demonstrates the minimum layout needs: a 2x2 grid of plain
|
||||
// panes, no custom border, ctrl+hjkl moving focus between them out of the
|
||||
// box.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
)
|
||||
|
||||
// textPane is the simplest possible Pane: it just reports its own id, size
|
||||
// and focus state. It still renders to exactly the width/height it was
|
||||
// told (via lipgloss's own Width/Height), which is the one rule every Pane
|
||||
// has to follow - layout composes View() output side by side and never
|
||||
// pads it itself.
|
||||
type textPane struct {
|
||||
id string
|
||||
w, h int
|
||||
focused bool
|
||||
}
|
||||
|
||||
func newTextPane(id string) *textPane { return &textPane{id: id} }
|
||||
|
||||
func (p *textPane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *textPane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *textPane) View() string {
|
||||
state := "blurred"
|
||||
if p.focused {
|
||||
state = "focused"
|
||||
}
|
||||
content := fmt.Sprintf("%s\n(%s, %dx%d)", p.id, state, p.w, p.h)
|
||||
return lipgloss.NewStyle().
|
||||
Width(p.w).
|
||||
Height(p.h).
|
||||
AlignHorizontal(lipgloss.Center).
|
||||
AlignVertical(lipgloss.Center).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
// model is the actual top-level tea.Model: layout itself reserves no quit
|
||||
// key (that's an app policy, not layout's to make), so the host wraps it
|
||||
// and handles ctrl+c/q itself, same as any other custom component in this
|
||||
// repo (see examples/tabs).
|
||||
type model struct {
|
||||
layout layout.Model
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return m.layout.Init() }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyPressMsg); ok {
|
||||
switch key.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
updated, cmd := m.layout.Update(msg)
|
||||
m.layout = updated.(layout.Model)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
view := tea.NewView(m.layout.View())
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
|
||||
func main() {
|
||||
root := layout.HSplit(0.5,
|
||||
layout.VSplit(0.5,
|
||||
layout.Leaf("top-left", newTextPane("top-left")),
|
||||
layout.Leaf("bottom-left", newTextPane("bottom-left")),
|
||||
),
|
||||
layout.VSplit(0.5,
|
||||
layout.Leaf("top-right", newTextPane("top-right")),
|
||||
layout.Leaf("bottom-right", newTextPane("bottom-right")),
|
||||
),
|
||||
)
|
||||
m := model{layout: layout.New(root, layout.AsRoot())}
|
||||
|
||||
if _, err := tea.NewProgram(m).Run(); err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Command bordered demonstrates that layout draws no chrome of its own:
|
||||
// each pane here owns its border and picks its color from FocusMsg/BlurMsg,
|
||||
// via the optional layout.Bordered helper.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
)
|
||||
|
||||
type borderedPane struct {
|
||||
id string
|
||||
w, h int
|
||||
focused bool
|
||||
}
|
||||
|
||||
func newBorderedPane(id string) *borderedPane { return &borderedPane{id: id} }
|
||||
|
||||
func (p *borderedPane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *borderedPane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *borderedPane) View() string {
|
||||
inner := lipgloss.NewStyle().
|
||||
Width(p.w - 2).
|
||||
Height(p.h - 2).
|
||||
AlignHorizontal(lipgloss.Center).
|
||||
AlignVertical(lipgloss.Center).
|
||||
Render(p.id)
|
||||
// layout.Bordered already draws the border at exactly p.w x p.h, so the
|
||||
// content it wraps must already be sized to w-2 x h-2 (border eats one
|
||||
// cell on each side) - same rule as any other Pane, just one layer in.
|
||||
return layout.Bordered(p.focused, p.w, p.h, inner)
|
||||
}
|
||||
|
||||
// model is the actual top-level tea.Model: layout itself reserves no quit
|
||||
// key (that's an app policy, not layout's to make), so the host wraps it
|
||||
// and handles ctrl+c/q itself, same as any other custom component in this
|
||||
// repo (see examples/tabs).
|
||||
type model struct {
|
||||
layout layout.Model
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return m.layout.Init() }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyPressMsg); ok {
|
||||
switch key.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
updated, cmd := m.layout.Update(msg)
|
||||
m.layout = updated.(layout.Model)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
view := tea.NewView(m.layout.View())
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
|
||||
func main() {
|
||||
root := layout.HSplit(0.3,
|
||||
layout.Leaf("sidebar", newBorderedPane("sidebar")),
|
||||
layout.VSplit(0.6,
|
||||
layout.Leaf("main", newBorderedPane("main")),
|
||||
layout.Leaf("log", newBorderedPane("log")),
|
||||
),
|
||||
)
|
||||
m := model{layout: layout.New(root, layout.AsRoot())}
|
||||
|
||||
if _, err := tea.NewProgram(m).Run(); err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Command fullapp demonstrates a more realistic app shell: the sidebar
|
||||
// leaf isn't a plain placeholder pane, it's a full layout.Model of its own
|
||||
// (sidebar.New(), a themed list, see sidebar/sidebar.go) embedded exactly
|
||||
// like the "router" one below - layout.Model implements Pane, so any
|
||||
// package can hand back one to be wrapped in a Leaf, no adapter needed.
|
||||
package first
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
)
|
||||
|
||||
type pane struct {
|
||||
id string
|
||||
w, h int
|
||||
focused bool
|
||||
}
|
||||
|
||||
func newPane(id string) *pane { return &pane{id: id} }
|
||||
|
||||
func (p *pane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *pane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *pane) View() string {
|
||||
content := lipgloss.NewStyle().
|
||||
Width(p.w - 2).Height(p.h - 2).
|
||||
AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Center).
|
||||
Render(p.id)
|
||||
return layout.Bordered(p.focused, p.w, p.h, content)
|
||||
}
|
||||
|
||||
// HelpBindings implements layout.HelpProvider, so every pane - at the top
|
||||
// level or nested three levels deep, doesn't matter - shows up correctly
|
||||
// in the single, outer help bar.
|
||||
func (p *pane) HelpBindings() []key.Binding {
|
||||
return []key.Binding{key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "use "+p.id))}
|
||||
}
|
||||
|
||||
// editorPane keeps a counter (press +) that's otherwise irrelevant to the
|
||||
// app but proves the point of router.Model owning pages permanently:
|
||||
// switch to "Second" and back, and it's still there. A plain pane (like
|
||||
// terminal below) would just get reconstructed from scratch every time if
|
||||
// something recreated it on each switch instead of keeping it alive.
|
||||
type editorPane struct {
|
||||
w, h int
|
||||
focused bool
|
||||
count int
|
||||
}
|
||||
|
||||
func newEditorPane() *editorPane { return &editorPane{} }
|
||||
|
||||
func (p *editorPane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *editorPane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
case tea.KeyPressMsg:
|
||||
if msg.String() == "+" {
|
||||
p.count++
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *editorPane) View() string {
|
||||
content := lipgloss.NewStyle().
|
||||
Width(p.w - 2).Height(p.h - 2).
|
||||
AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Center).
|
||||
Render(fmt.Sprintf("editor\n\npress + to increment: %d", p.count))
|
||||
return layout.Bordered(p.focused, p.w, p.h, content)
|
||||
}
|
||||
|
||||
// HelpBindings implements layout.HelpProvider.
|
||||
func (p *editorPane) HelpBindings() []key.Binding {
|
||||
return []key.Binding{key.NewBinding(key.WithKeys("+"), key.WithHelp("+", "increment"))}
|
||||
}
|
||||
|
||||
// NewWorkspace builds the "First" page's own nested layout.Model: note the
|
||||
// lack of layout.AsRoot() here - only the outermost Model (see main) should
|
||||
// render a help bar, or focused help would show up twice. second.
|
||||
// NewWorkspace and sidebar.New() below are built the same way, same reason.
|
||||
//
|
||||
// This Model is built exactly once, by router.New(), and kept alive for
|
||||
// the whole app's lifetime - router.Model just changes which page is
|
||||
// rendered/routed to, it never reconstructs one. That's what lets
|
||||
// editorPane's counter above survive switching to "Second" and back; see
|
||||
// package router's own doc comment for why that ownership lives there
|
||||
// and not in the sidebar.
|
||||
func NewWorkspace() layout.Model {
|
||||
root := layout.VSplit(0.7,
|
||||
layout.Leaf("editor", newEditorPane()),
|
||||
layout.Leaf("terminal", newPane("terminal")),
|
||||
)
|
||||
return layout.New(root)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Command fullapp demonstrates a more realistic app shell: the sidebar
|
||||
// leaf isn't a plain placeholder pane, it's a full layout.Model of its own
|
||||
// (sidebar.New(), a themed list, see sidebar/sidebar.go) embedded exactly
|
||||
// like the "router" one below - layout.Model implements Pane, so any
|
||||
// package can hand back one to be wrapped in a Leaf, no adapter needed.
|
||||
//
|
||||
// It also owns the app's single notification.Model (see second/second.go,
|
||||
// which triggers toasts via notification.Show without ever holding a
|
||||
// reference to it) and composites its toasts over the whole rendered
|
||||
// layout, top-right - notification has no dependency on layout, so this is
|
||||
// the same Render(background string) pattern as examples/notification, just
|
||||
// with layout.Model.View() as the background instead of a plain string.
|
||||
//
|
||||
// Same story for the app's single modal.Model (see third/third.go), except
|
||||
// modal is composited last, on top of notif's own output: a modal is meant
|
||||
// to command the whole screen's attention, so it should flatten any toast
|
||||
// already showing to the same dim gray as everything else, not leave it
|
||||
// floating on top in full color.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/examples/layout/fullapp/router"
|
||||
"github.com/anotherhadi/ilovetui/examples/layout/fullapp/sidebar"
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
"github.com/anotherhadi/ilovetui/modal"
|
||||
"github.com/anotherhadi/ilovetui/notification"
|
||||
)
|
||||
|
||||
// model is the actual top-level tea.Model: layout itself reserves no quit
|
||||
// key (that's an app policy, not layout's to make), so the host wraps it
|
||||
// and handles ctrl+c/q itself, same as any other custom component in this
|
||||
// repo (see examples/tabs).
|
||||
type model struct {
|
||||
layout layout.Model
|
||||
notif notification.Model
|
||||
modal modal.Model
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(m.layout.Init(), m.notif.Init(), m.modal.Init())
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyPressMsg); ok {
|
||||
switch key.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
updated, layoutCmd := m.layout.Update(msg)
|
||||
m.layout = updated.(layout.Model)
|
||||
|
||||
var notifCmd, modalCmd tea.Cmd
|
||||
m.notif, notifCmd = m.notif.Update(msg)
|
||||
m.modal, modalCmd = m.modal.Update(msg)
|
||||
|
||||
return m, tea.Batch(layoutCmd, notifCmd, modalCmd)
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
view := tea.NewView(m.modal.Render(m.notif.Render(m.layout.View())))
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
|
||||
func main() {
|
||||
root := layout.HSplit(0.25,
|
||||
layout.Leaf("sidebar", sidebar.New()),
|
||||
layout.Leaf("router", router.New()),
|
||||
).WithMaximum(20)
|
||||
|
||||
m := model{
|
||||
layout: layout.New(root, layout.AsRoot()),
|
||||
notif: notification.New(notification.WithPosition(notification.TopRight)),
|
||||
modal: modal.New(),
|
||||
}
|
||||
|
||||
if _, err := tea.NewProgram(m).Run(); err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Package router owns every page's own layout.Model for the lifetime of
|
||||
// the whole app, so switching pages never discards their state - only
|
||||
// which one is currently active/rendered changes. It slots into the root
|
||||
// tree's "router" leaf exactly like a single nested layout.Model would
|
||||
// (see examples/layout/nested), because Model implements layout.Navigable
|
||||
// itself on top of layout.Pane, delegating everything to whichever page is
|
||||
// active: ctrl+hjkl, the help bar, and SendMsg/RequestFocusMsg addressed to
|
||||
// ids inside that page all keep working transparently.
|
||||
package router
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/examples/layout/fullapp/first"
|
||||
"github.com/anotherhadi/ilovetui/examples/layout/fullapp/second"
|
||||
"github.com/anotherhadi/ilovetui/examples/layout/fullapp/third"
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
)
|
||||
|
||||
// SelectMsg asks Model to switch its active page, by id ("first", "second"
|
||||
// or "third"). Sent by the sidebar via layout.SendMsg{Target: "router"} -
|
||||
// it never holds a reference to Model either.
|
||||
type SelectMsg struct{ Page string }
|
||||
|
||||
type Model struct {
|
||||
active string
|
||||
pages map[string]layout.Model
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
return &Model{
|
||||
active: "first",
|
||||
pages: map[string]layout.Model{
|
||||
"first": first.NewWorkspace(),
|
||||
"second": second.NewWorkspace(),
|
||||
"third": third.NewWorkspace(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) current() layout.Model { return m.pages[m.active] }
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
for _, p := range m.pages {
|
||||
if cmd := p.Init(); cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
if sel, ok := msg.(SelectMsg); ok {
|
||||
if _, exists := m.pages[sel.Page]; exists {
|
||||
m.active = sel.Page
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if size, ok := msg.(layout.SizeMsg); ok {
|
||||
// Every page needs to stay correctly sized even while hidden - the
|
||||
// outer tree only re-sends SizeMsg when the "router" leaf's own
|
||||
// Rect changes, never on a plain page switch, so a page that was
|
||||
// inactive during a resize must still learn about it here, or it
|
||||
// renders at a stale size whenever it becomes active again.
|
||||
var cmds []tea.Cmd
|
||||
for id, p := range m.pages {
|
||||
updated, cmd := p.Update(size)
|
||||
m.pages[id] = updated.(layout.Model)
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
updated, cmd := m.current().Update(msg)
|
||||
m.pages[m.active] = updated.(layout.Model)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return m.current().View()
|
||||
}
|
||||
|
||||
// Leaves, MoveFocus, Route, Focus and FocusedHelp implement
|
||||
// layout.Navigable, delegating to whichever page is currently active.
|
||||
|
||||
func (m *Model) Leaves() []layout.LeafRect {
|
||||
return m.current().Leaves()
|
||||
}
|
||||
|
||||
func (m *Model) MoveFocus(dir layout.FocusDirection) bool {
|
||||
return m.current().MoveFocus(dir)
|
||||
}
|
||||
|
||||
func (m *Model) Route(target string, msg tea.Msg) (bool, tea.Cmd) {
|
||||
return m.current().Route(target, msg)
|
||||
}
|
||||
|
||||
func (m *Model) Focus(id string) (bool, tea.Cmd) {
|
||||
return m.current().Focus(id)
|
||||
}
|
||||
|
||||
func (m *Model) FocusedHelp() []key.Binding {
|
||||
return m.current().FocusedHelp()
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Package second is the "Second" page's own workspace: deliberately a
|
||||
// different shape from first's (a single pane holding a themed list, not an
|
||||
// editor/terminal split), so swapping between the two via sidebar.Model's
|
||||
// "enter" case is visibly a real change of sub-app, not just a relabeled
|
||||
// copy of first. It also demonstrates notification.Show being called from
|
||||
// deep inside a nested layout.Model, with nothing wiring it to the
|
||||
// notification.Model that actually renders it - see main.go's model.View.
|
||||
package second
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/key"
|
||||
bubbleslist "charm.land/bubbles/v2/list"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/bubbles"
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
"github.com/anotherhadi/ilovetui/notification"
|
||||
)
|
||||
|
||||
// kind is a list entry: a notification.Kind plus the title/message Show
|
||||
// gets called with when it's selected.
|
||||
type kind struct {
|
||||
title string
|
||||
message string
|
||||
kind notification.Kind
|
||||
}
|
||||
|
||||
func (k kind) Title() string { return k.title }
|
||||
func (k kind) Description() string { return "" }
|
||||
func (k kind) FilterValue() string { return k.title }
|
||||
|
||||
type pane struct {
|
||||
list bubbleslist.Model
|
||||
w, h int
|
||||
focused bool
|
||||
}
|
||||
|
||||
func newPane() *pane {
|
||||
items := []bubbleslist.Item{
|
||||
kind{title: "Info", message: "Just so you know.", kind: notification.Info},
|
||||
kind{title: "Success", message: "Config written to disk.", kind: notification.Success},
|
||||
kind{title: "Warning", message: "Disk space getting low on /dev/sda1.", kind: notification.Warning},
|
||||
kind{title: "Error", message: "Failed to reach the remote host.", kind: notification.Error},
|
||||
}
|
||||
list := bubbles.NewList(items, 0, 0)
|
||||
list.Title = "Notify"
|
||||
// Same reasoning as sidebar.Model: the built-in help footer is
|
||||
// redundant with layout's own centralized help bar (see HelpBindings
|
||||
// below).
|
||||
list.SetShowHelp(false)
|
||||
return &pane{list: list}
|
||||
}
|
||||
|
||||
func (p *pane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *pane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
p.list.SetSize(p.w-2, p.h-2)
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
}
|
||||
|
||||
if !p.focused {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
if key, ok := msg.(tea.KeyPressMsg); ok && key.String() == "enter" {
|
||||
if selected, ok := p.list.SelectedItem().(kind); ok {
|
||||
return p, notification.Show(selected.title, selected.message, selected.kind)
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
p.list, cmd = p.list.Update(msg)
|
||||
return p, cmd
|
||||
}
|
||||
|
||||
func (p *pane) View() string {
|
||||
return layout.Bordered(p.focused, p.w, p.h, p.list.View())
|
||||
}
|
||||
|
||||
// HelpBindings implements layout.HelpProvider.
|
||||
func (p *pane) HelpBindings() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "show notification")),
|
||||
key.NewBinding(key.WithKeys("j"), key.WithHelp("j", "go down")),
|
||||
key.NewBinding(key.WithKeys("k"), key.WithHelp("k", "go up")),
|
||||
}
|
||||
}
|
||||
|
||||
// NewWorkspace builds the "Second" page's own nested layout.Model. No
|
||||
// layout.AsRoot() - see first.NewWorkspace's doc comment.
|
||||
func NewWorkspace() layout.Model {
|
||||
return layout.New(layout.Leaf("content", newPane()))
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package sidebar
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/key"
|
||||
bubbleslist "charm.land/bubbles/v2/list"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/bubbles"
|
||||
"github.com/anotherhadi/ilovetui/examples/layout/fullapp/router"
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
)
|
||||
|
||||
// page is a sidebar entry: just an id (matching one of router.Model's
|
||||
// own page keys) and a label. Model has no idea what a page actually is or
|
||||
// how it's built - that's router's business entirely, this only needs
|
||||
// to name it.
|
||||
type page struct {
|
||||
id string
|
||||
title string
|
||||
}
|
||||
|
||||
func (p page) Title() string { return p.title }
|
||||
func (p page) Description() string { return "" }
|
||||
func (p page) FilterValue() string { return p.title }
|
||||
|
||||
type Model struct {
|
||||
id string // learned from SizeMsg.ID, needed as RequestFocusMsg.Source
|
||||
list bubbleslist.Model
|
||||
w, h int
|
||||
focused bool
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
items := []bubbleslist.Item{
|
||||
page{id: "first", title: "First"},
|
||||
page{id: "second", title: "Second"},
|
||||
page{id: "third", title: "Third"},
|
||||
}
|
||||
list := bubbles.NewList(items, 0, 0)
|
||||
list.Title = "Sidebar"
|
||||
// list's own built-in help footer is redundant - Model already feeds
|
||||
// layout's single centralized help bar via HelpBindings below - and at
|
||||
// a narrow sidebar width it word-wraps onto multiple lines, which
|
||||
// list.SetSize doesn't account for: list.View() ends up taller than
|
||||
// the height it was given, breaking layout's "render exactly h" rule
|
||||
// and pushing the border past the bottom of the pane.
|
||||
list.SetShowHelp(false)
|
||||
return &Model{list: list}
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
m.id, m.w, m.h = msg.ID, msg.Width, msg.Height
|
||||
m.list.SetSize(m.w-2, m.h-2)
|
||||
case layout.FocusMsg:
|
||||
m.focused = true
|
||||
case layout.BlurMsg:
|
||||
m.focused = false
|
||||
}
|
||||
|
||||
if !m.focused {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if key, ok := msg.(tea.KeyPressMsg); ok && key.String() == "enter" {
|
||||
if selected, ok := m.list.SelectedItem().(page); ok {
|
||||
// tea.Sequence, not tea.Batch: the page switch must land
|
||||
// before the focus jump, or RequestFocusMsg could cascade
|
||||
// into router while it's still showing the previous page
|
||||
// (Batch runs both concurrently with no ordering guarantee).
|
||||
return m, tea.Sequence(
|
||||
func() tea.Msg {
|
||||
return layout.SendMsg{Target: "router", Msg: router.SelectMsg{Page: selected.id}}
|
||||
},
|
||||
func() tea.Msg {
|
||||
return layout.RequestFocusMsg{Source: m.id, Target: "router"}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return layout.Bordered(m.focused, m.w, m.h, m.list.View())
|
||||
}
|
||||
|
||||
func (m *Model) HelpBindings() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "open page")),
|
||||
key.NewBinding(key.WithKeys("j"), key.WithHelp("j", "go down")),
|
||||
key.NewBinding(key.WithKeys("k"), key.WithHelp("k", "go up")),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Package third is the "Third" page's own workspace: a single pane showing
|
||||
// modal.Show being called from deep inside a nested layout.Model, exactly
|
||||
// like second/second.go does for notification.Show - modal has no
|
||||
// dependency on layout either, so nothing here needs a reference to the
|
||||
// modal.Model that actually renders it (see main.go's model.View).
|
||||
package third
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/key"
|
||||
bubbleslist "charm.land/bubbles/v2/list"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/bubbles"
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
"github.com/anotherhadi/ilovetui/modal"
|
||||
)
|
||||
|
||||
// action is a list entry: the title/content Show gets called with when it's
|
||||
// selected, and whether closing it needs an explicit "y" (a destructive
|
||||
// confirm) or any key at all (a plain info popup).
|
||||
type action struct {
|
||||
title, content string
|
||||
confirm bool
|
||||
}
|
||||
|
||||
func (a action) Title() string { return a.title }
|
||||
func (a action) Description() string { return "" }
|
||||
func (a action) FilterValue() string { return a.title }
|
||||
|
||||
type pane struct {
|
||||
list bubbleslist.Model
|
||||
w, h int
|
||||
focused bool
|
||||
// open/confirm track the modal this pane itself opened, so it knows to
|
||||
// swallow keys instead of forwarding them to the list underneath while
|
||||
// it's up - modal.Model has no notion of "focus" of its own, the pane
|
||||
// that triggered it is responsible for gating input while it's open.
|
||||
open bool
|
||||
confirm bool
|
||||
}
|
||||
|
||||
func newPane() *pane {
|
||||
items := []bubbleslist.Item{
|
||||
action{title: "Delete file", content: "This can't be undone.\n\ny: confirm esc: cancel", confirm: true},
|
||||
action{title: "About", content: "modal renders a centered popup over a\nflat-dimmed background.\n\nesc: close"},
|
||||
}
|
||||
list := bubbles.NewList(items, 0, 0)
|
||||
list.Title = "Modal"
|
||||
// Same reasoning as sidebar/second: redundant with layout's own
|
||||
// centralized help bar (see HelpBindings below).
|
||||
list.SetShowHelp(false)
|
||||
return &pane{list: list}
|
||||
}
|
||||
|
||||
func (p *pane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *pane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
p.list.SetSize(p.w-2, p.h-2)
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
}
|
||||
|
||||
if !p.focused {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
if key, ok := msg.(tea.KeyPressMsg); ok {
|
||||
if p.open {
|
||||
if !p.confirm || key.String() == "y" || key.String() == "esc" {
|
||||
p.open = false
|
||||
return p, modal.Close()
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
if key.String() == "enter" {
|
||||
if selected, ok := p.list.SelectedItem().(action); ok {
|
||||
p.open, p.confirm = true, selected.confirm
|
||||
return p, modal.Show(selected.title+"?", selected.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
p.list, cmd = p.list.Update(msg)
|
||||
return p, cmd
|
||||
}
|
||||
|
||||
func (p *pane) View() string {
|
||||
return layout.Bordered(p.focused, p.w, p.h, p.list.View())
|
||||
}
|
||||
|
||||
// HelpBindings implements layout.HelpProvider.
|
||||
func (p *pane) HelpBindings() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "open modal")),
|
||||
key.NewBinding(key.WithKeys("j"), key.WithHelp("j", "go down")),
|
||||
key.NewBinding(key.WithKeys("k"), key.WithHelp("k", "go up")),
|
||||
}
|
||||
}
|
||||
|
||||
// NewWorkspace builds the "Third" page's own nested layout.Model. No
|
||||
// layout.AsRoot() - see first.NewWorkspace's doc comment.
|
||||
func NewWorkspace() layout.Model {
|
||||
return layout.New(layout.Leaf("content", newPane()))
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Command help demonstrates the dynamic help bar: each pane implements
|
||||
// layout.HelpProvider (or doesn't) and the bottom bar always reflects
|
||||
// whichever one is currently focused, with no wiring beyond that.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
)
|
||||
|
||||
// helpfulPane implements layout.HelpProvider with a binding or two of its
|
||||
// own, on top of the usual Size/Focus/Blur bookkeeping.
|
||||
type helpfulPane struct {
|
||||
id string
|
||||
w, h int
|
||||
focused bool
|
||||
bindings []key.Binding
|
||||
}
|
||||
|
||||
func newHelpfulPane(id string, bindings []key.Binding) *helpfulPane {
|
||||
return &helpfulPane{id: id, bindings: bindings}
|
||||
}
|
||||
|
||||
func (p *helpfulPane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *helpfulPane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *helpfulPane) View() string {
|
||||
content := lipgloss.NewStyle().
|
||||
Width(p.w - 2).Height(p.h - 2).
|
||||
AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Center).
|
||||
Render(p.id)
|
||||
return layout.Bordered(p.focused, p.w, p.h, content)
|
||||
}
|
||||
|
||||
// HelpBindings implements layout.HelpProvider.
|
||||
func (p *helpfulPane) HelpBindings() []key.Binding { return p.bindings }
|
||||
|
||||
// silentPane deliberately does NOT implement layout.HelpProvider, to show
|
||||
// that the help bar just falls back to layout's own controls (ctrl+hjkl,
|
||||
// ?) instead of disappearing or erroring when it's focused.
|
||||
type silentPane struct {
|
||||
w, h int
|
||||
focused bool
|
||||
}
|
||||
|
||||
func (p *silentPane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *silentPane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *silentPane) View() string {
|
||||
content := lipgloss.NewStyle().
|
||||
Width(p.w - 2).Height(p.h - 2).
|
||||
AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Center).
|
||||
Render("no help bindings\n(watch the bar below)")
|
||||
return layout.Bordered(p.focused, p.w, p.h, content)
|
||||
}
|
||||
|
||||
// model is the actual top-level tea.Model: layout itself reserves no quit
|
||||
// key (that's an app policy, not layout's to make), so the host wraps it
|
||||
// and handles ctrl+c/q itself, same as any other custom component in this
|
||||
// repo (see examples/tabs).
|
||||
type model struct {
|
||||
layout layout.Model
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return m.layout.Init() }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyPressMsg); ok {
|
||||
switch key.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
updated, cmd := m.layout.Update(msg)
|
||||
m.layout = updated.(layout.Model)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
view := tea.NewView(m.layout.View())
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
|
||||
func main() {
|
||||
writer := newHelpfulPane("writer", []key.Binding{
|
||||
key.NewBinding(key.WithKeys("ctrl+s"), key.WithHelp("ctrl+s", "save")),
|
||||
key.NewBinding(key.WithKeys("ctrl+z"), key.WithHelp("ctrl+z", "undo")),
|
||||
})
|
||||
browser := newHelpfulPane("browser", []key.Binding{
|
||||
key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "search")),
|
||||
})
|
||||
|
||||
root := layout.HSplit(0.34,
|
||||
layout.Leaf("writer", writer),
|
||||
layout.HSplit(0.5,
|
||||
layout.Leaf("browser", browser),
|
||||
layout.Leaf("scratch", &silentPane{}),
|
||||
),
|
||||
)
|
||||
m := model{layout: layout.New(root, layout.AsRoot())}
|
||||
|
||||
if _, err := tea.NewProgram(m).Run(); err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Command messaging demonstrates the two ways panes talk without holding a
|
||||
// reference to each other: "control" sends arbitrary commands to "editor"
|
||||
// by id via SendMsg (1/2/3 keys), and asks layout to move focus there via
|
||||
// RequestFocusMsg (enter key) - the same pattern a real sidebar would use
|
||||
// to both drive and jump to a content pane it selected.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
)
|
||||
|
||||
// commandMsg is what "control" sends to "editor" - an app-defined message,
|
||||
// entirely opaque to layout itself (see SendMsg).
|
||||
type commandMsg struct{ text string }
|
||||
|
||||
type controlPane struct {
|
||||
id string // learned from SizeMsg.ID, needed as RequestFocusMsg.Source
|
||||
w, h int
|
||||
focused bool
|
||||
}
|
||||
|
||||
func (p *controlPane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *controlPane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.id, p.w, p.h = msg.ID, msg.Width, msg.Height
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "1", "2", "3":
|
||||
text := "command " + msg.String()
|
||||
return p, func() tea.Msg {
|
||||
return layout.SendMsg{Target: "editor", Msg: commandMsg{text: text}}
|
||||
}
|
||||
case "enter":
|
||||
return p, func() tea.Msg {
|
||||
return layout.RequestFocusMsg{Source: p.id, Target: "editor"}
|
||||
}
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *controlPane) View() string {
|
||||
content := "control\n\n1/2/3: send a command\nenter: focus editor"
|
||||
inner := lipgloss.NewStyle().Width(p.w - 2).Height(p.h - 2).Render(content)
|
||||
return layout.Bordered(p.focused, p.w, p.h, inner)
|
||||
}
|
||||
|
||||
type editorPane struct {
|
||||
w, h int
|
||||
focused bool
|
||||
last string
|
||||
}
|
||||
|
||||
func (p *editorPane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *editorPane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
case commandMsg:
|
||||
p.last = msg.text
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *editorPane) View() string {
|
||||
last := p.last
|
||||
if last == "" {
|
||||
last = "(nothing yet)"
|
||||
}
|
||||
content := fmt.Sprintf("editor\n\nlast command received:\n%s", last)
|
||||
inner := lipgloss.NewStyle().Width(p.w - 2).Height(p.h - 2).Render(content)
|
||||
return layout.Bordered(p.focused, p.w, p.h, inner)
|
||||
}
|
||||
|
||||
// model is the actual top-level tea.Model: layout itself reserves no quit
|
||||
// key (that's an app policy, not layout's to make), so the host wraps it
|
||||
// and handles ctrl+c/q itself, same as any other custom component in this
|
||||
// repo (see examples/tabs).
|
||||
type model struct {
|
||||
layout layout.Model
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return m.layout.Init() }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyPressMsg); ok {
|
||||
switch key.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
updated, cmd := m.layout.Update(msg)
|
||||
m.layout = updated.(layout.Model)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
view := tea.NewView(m.layout.View())
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
|
||||
func main() {
|
||||
root := layout.HSplit(0.35,
|
||||
layout.Leaf("control", &controlPane{}),
|
||||
layout.Leaf("editor", &editorPane{}),
|
||||
)
|
||||
m := model{layout: layout.New(root, layout.AsRoot())}
|
||||
|
||||
if _, err := tea.NewProgram(m).Run(); err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Command nested demonstrates composability: "workspace" is itself a full
|
||||
// layout.Model (its own two-pane split) embedded as an ordinary Leaf inside
|
||||
// the outer tree. ctrl+hjkl bubbles in and out of it transparently, and
|
||||
// only the outer Model shows a help bar - the inner one is built without
|
||||
// layout.AsRoot(), see newWorkspace.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/layout"
|
||||
)
|
||||
|
||||
type pane struct {
|
||||
id string
|
||||
w, h int
|
||||
focused bool
|
||||
}
|
||||
|
||||
func newPane(id string) *pane { return &pane{id: id} }
|
||||
|
||||
func (p *pane) Init() tea.Cmd { return nil }
|
||||
|
||||
func (p *pane) Update(msg tea.Msg) (layout.Pane, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case layout.SizeMsg:
|
||||
p.w, p.h = msg.Width, msg.Height
|
||||
case layout.FocusMsg:
|
||||
p.focused = true
|
||||
case layout.BlurMsg:
|
||||
p.focused = false
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *pane) View() string {
|
||||
content := lipgloss.NewStyle().
|
||||
Width(p.w - 2).Height(p.h - 2).
|
||||
AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Center).
|
||||
Render(p.id)
|
||||
return layout.Bordered(p.focused, p.w, p.h, content)
|
||||
}
|
||||
|
||||
// HelpBindings implements layout.HelpProvider, so every pane - at the top
|
||||
// level or nested three levels deep, doesn't matter - shows up correctly
|
||||
// in the single, outer help bar.
|
||||
func (p *pane) HelpBindings() []key.Binding {
|
||||
return []key.Binding{key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "use "+p.id))}
|
||||
}
|
||||
|
||||
// newWorkspace builds the nested layout.Model: note the lack of
|
||||
// layout.AsRoot() here - only the outermost Model (see main) should render
|
||||
// a help bar, or focused help would show up twice.
|
||||
func newWorkspace() layout.Model {
|
||||
root := layout.VSplit(0.7,
|
||||
layout.Leaf("editor", newPane("editor")),
|
||||
layout.Leaf("terminal", newPane("terminal")),
|
||||
)
|
||||
return layout.New(root)
|
||||
}
|
||||
|
||||
// model is the actual top-level tea.Model: layout itself reserves no quit
|
||||
// key (that's an app policy, not layout's to make), so the host wraps it
|
||||
// and handles ctrl+c/q itself, same as any other custom component in this
|
||||
// repo (see examples/tabs).
|
||||
type model struct {
|
||||
layout layout.Model
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return m.layout.Init() }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyPressMsg); ok {
|
||||
switch key.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
updated, cmd := m.layout.Update(msg)
|
||||
m.layout = updated.(layout.Model)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
view := tea.NewView(m.layout.View())
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
|
||||
func main() {
|
||||
root := layout.HSplit(0.25,
|
||||
layout.Leaf("sidebar", newPane("sidebar")).WithMinimum(20),
|
||||
layout.Leaf("workspace", newWorkspace()),
|
||||
)
|
||||
m := model{layout: layout.New(root, layout.AsRoot())}
|
||||
|
||||
if _, err := tea.NewProgram(m).Run(); err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user