okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
4ad1172
main
1// Package terminal owns interactive confirmation, notices, and Bubble Tea launch. 2package terminal 3 4import ( 5"bufio" 6"context" 7"errors" 8"fmt" 9"io" 10"os" 11"runtime/debug" 12"slices" 13"strings" 14 15"albedo/cli/internal/app" 16"albedo/cli/internal/daemon" 17"albedo/cli/internal/tui" 18 19 tea"charm.land/bubbletea/v2" 20"github.com/charmbracelet/x/ansi" 21"github.com/mattn/go-isatty" 22) 23 24type Service struct { 25In io.Reader 26Out ,Err io.Writer 27OpenBrowser func (string ) 28Home string 29Capable bool 30} 31 32func IsTTY (in io.Reader ,out io.Writer )bool { 33input ,inputOK := in .(* os.File ) 34output ,outputOK := out .(* os.File ) 35return inputOK && outputOK && isatty .IsTerminal (input .Fd ())&& isatty .IsTerminal (output .Fd ()) 36} 37 38// announceMigration presents retained daemon notices before the full screen opens. 39func (t * Service )announceMigration (ctx context.Context ,conn * daemon.Connection ,settings daemon.Settings ) (daemon.Settings ,error ) { 40server ,err := daemon .ProbeServer (ctx ,conn ) 41if err != nil { 42return daemon.Settings {},err 43 } 44dismissed := slices .Clone (settings .UI .DismissedNotices ) 45shown := false 46for _ ,notice := range server .Notices { 47if slices .Contains (dismissed ,notice .ID ) { 48continue 49 } 50if _ ,err = fmt .Fprintln (t .Out ,notice .Message );err != nil { 51return daemon.Settings {},err 52 } 53dismissed = append (dismissed ,notice .ID ) 54shown = true 55 } 56if ! shown { 57return settings ,nil 58 } 59if _ ,err = fmt .Fprint (t .Out ,"Press Enter to continue. " );err != nil { 60return daemon.Settings {},err 61 } 62_ ,err = bufio .NewReader (t .In ).ReadString ('\n' ) 63if err != nil && ! errors .Is (err ,io .EOF ) { 64return daemon.Settings {},err 65 } 66if err = daemon .DismissServerNotices (ctx ,conn ,settings .UI .ETag ,dismissed );err != nil { 67return daemon.Settings {},err 68 } 69// Acknowledgement changes the UI validator; seed the TUI from that result. 70return daemon .GetSettings (ctx ,conn ) 71} 72 73// Confirmed reads a [y/N] answer. A late reply to the terminal's startup 74// queries can precede it. 75func Confirmed (answer string )bool { 76switch strings .ToLower (strings .TrimSpace (ansi .Strip (answer ))) { 77case "y" ,"yes" : 78return true 79 } 80return false 81} 82 83func (t * Service )Open (ctx context.Context ,prepared app.PreparedOpen )error { 84final ,err := t .runApp (ctx ,prepared ,func (settings daemon.Settings )* tui.AppModel { 85return tui .NewAppModel (prepared .Connection , tui.Bootstrap {Sessions :prepared .Sessions ,Settings :settings },prepared .Selected ,prepared .Workspace ,prepared .LoginRequired ,t .OpenBrowser ) 86 }) 87if err != nil { 88return err 89 } 90if m ,ok := final .(* tui.AppModel );ok && m .ActiveSession != nil { 91_ ,err = fmt .Fprintf (t .Out ,"To reopen this session, run albedo resume %s\n" ,m .ActiveSession .ID ) 92 } 93return err 94} 95 96func (t * Service )Login (ctx context.Context ,prepared app.PreparedOpen ,workspace ,name string )error { 97_ ,err := t .runApp (ctx ,prepared ,func (settings daemon.Settings )* tui.AppModel { 98return tui .NewLoginAppModel (prepared .Connection , tui.Bootstrap {Settings :settings },workspace ,name ,t .OpenBrowser ) 99 }) 100return err 101} 102 103func (t * Service )runApp (ctx context.Context ,prepared app.PreparedOpen ,build func (daemon.Settings )* tui.AppModel ) (tea.Model ,error ) { 104settings ,err := t .announceMigration (ctx ,prepared .Connection ,prepared .Settings ) 105if err != nil { 106return nil ,err 107 } 108if err = tui .DetectInk ();err != nil { 109return nil ,err 110 } 111leanHeap () 112model := build (settings ) 113program := tea .NewProgram (model ,tea .WithInput (t .In ),tea .WithOutput (t .Out )) 114stopCancellation := context .AfterFunc (ctx ,func () {program .Send (tea .Quit ()) }) 115defer stopCancellation () 116final ,err := program .Run () 117closed ,ok := final .(* tui.AppModel ) 118if ! ok { 119closed = model 120 } 121if cleanup := closed .Close ();cleanup != nil { 122cleanup () 123 } 124if ctx .Err ()!= nil { 125return final ,ctx .Err () 126 } 127return final ,err 128} 129 130func (t * Service )ConfirmCleanup (sessions bool ) (bool ,error ) { 131if ! t .Capable { 132return false ,errors .New ("no files have been removed; run cleanup in a terminal to confirm, or add --yes to approve the changes shown above" ) 133 } 134prompt := "Apply the cleanup shown above? [y/N] " 135if sessions { 136prompt = "Permanently delete the sessions listed above? [y/N] " 137 } 138if _ ,err := fmt .Fprint (t .Out ,prompt );err != nil { 139return false ,err 140 } 141answer ,err := bufio .NewReader (t .In ).ReadString ('\n' ) 142if err != nil && ! errors .Is (err ,io .EOF ) { 143return false ,err 144 } 145return Confirmed (answer ),nil 146} 147 148// leanHeap lets the heap grow half past what it holds before collecting, 149// not the default doubling: a terminal UI idles at a few kilobytes a second, 150// so the extra collections cost nothing a person would see and the resident 151// size drops by a sixth. A GOGC set in the environment still wins. 152func leanHeap () { 153if os .Getenv ("GOGC" )== "" { 154debug .SetGCPercent (50 ) 155 } 156}