diff --git a/cairde.go b/cairde.go index b5e77ca..b4ecf7e 100644 --- a/cairde.go +++ b/cairde.go @@ -134,7 +134,7 @@ type model struct { input textinput.Model hiddenInput string - statusMsgs *[]string + statusBuffer chan string } func newModel(username, homeDir string) model { @@ -144,11 +144,11 @@ func newModel(username, homeDir string) model { input.Focus() return model{ - username: username, - userDir: path.Join(homeDir, "/.cairde/"), - connState: offline, - input: input, - statusMsgs: new([]string), + username: username, + userDir: path.Join(homeDir, "/.cairde/"), + connState: offline, + input: input, + statusBuffer: make(chan string), } } @@ -163,82 +163,89 @@ type appInitialisedMsg struct { acn connectivity.ACN } -func (m model) statusInfo(msg string) { - *m.statusMsgs = append(*m.statusMsgs, msg) +func (m model) initApp() tea.Cmd { + return func() tea.Msg { + mrand.Seed(int64(time.Now().Nanosecond())) + port := mrand.Intn(1000) + 9600 + controlPort := port + 1 + + key := make([]byte, 64) + _, err := rand.Read(key) + if err != nil { + return errMsg{Err: fmt.Errorf("unable to generate control port password: %s", err)} + } + + if err := os.MkdirAll(path.Join(m.userDir, "/.tor", "tor"), 0700); err != nil { + return errMsg{Err: fmt.Errorf("unable to create tor directory: %s", err)} + } + + if err := os.MkdirAll(path.Join(m.userDir, "profiles"), 0700); err != nil { + return errMsg{Err: fmt.Errorf("unable to create profiles directory: %s", err)} + } + + if err := tor.NewTorrc(). + WithSocksPort(port). + WithOnionTrafficOnly(). + WithControlPort(controlPort). + WithHashedPassword(base64.StdEncoding.EncodeToString(key)). + Build(filepath.Join(m.userDir, ".tor", "tor", "torrc")); err != nil { + return errMsg{Err: fmt.Errorf("unable to initialise torrc builder: %s", err)} + } + + m.statusBuffer <- "initialising Tor ACN..." + acn, err := tor.NewTorACNWithAuth( + path.Join(m.userDir, "/.tor"), + "", + path.Join(m.userDir, "/.tor", "data"), + controlPort, + tor.HashedPasswordAuthenticator{ + Password: base64.StdEncoding.EncodeToString(key), + }, + ) + if err != nil { + return errMsg{Err: fmt.Errorf("unable to bootstrap tor: %s", err)} + } + + m.statusBuffer <- "waiting for Tor ACN to bootstrap..." + if err := acn.WaitTillBootstrapped(); err != nil { + return errMsg{Err: fmt.Errorf("unable to initialise tor: %s", err)} + } + + settingsFile, err := settings.InitGlobalSettingsFile(m.userDir, "") + if err != nil { + return errMsg{Err: fmt.Errorf("unable to initialise settings: %s", err)} + } + gSettings := settingsFile.ReadGlobalSettings() + gSettings.ExperimentsEnabled = false + gSettings.DownloadPath = "./" + settingsFile.WriteGlobalSettings(gSettings) + + app := app.NewApp(acn, m.userDir, settingsFile) + + engineHooks := connections.DefaultEngineHooks{} + app.InstallEngineHooks(engineHooks) + + m.statusBuffer <- "the Tor ACN is up" + + return appInitialisedMsg{ + app: app, + acn: acn, + } + } } -func (m model) initApp() tea.Msg { - mrand.Seed(int64(time.Now().Nanosecond())) - port := mrand.Intn(1000) + 9600 - controlPort := port + 1 +type statusMsg struct{ msg string } - key := make([]byte, 64) - _, err := rand.Read(key) - if err != nil { - return errMsg{Err: fmt.Errorf("unable to generate control port password: %s", err)} - } - - if err := os.MkdirAll(path.Join(m.userDir, "/.tor", "tor"), 0700); err != nil { - return errMsg{Err: fmt.Errorf("unable to create tor directory: %s", err)} - } - - if err := os.MkdirAll(path.Join(m.userDir, "profiles"), 0700); err != nil { - return errMsg{Err: fmt.Errorf("unable to create profiles directory: %s", err)} - } - - if err := tor.NewTorrc(). - WithSocksPort(port). - WithOnionTrafficOnly(). - WithControlPort(controlPort). - WithHashedPassword(base64.StdEncoding.EncodeToString(key)). - Build(filepath.Join(m.userDir, ".tor", "tor", "torrc")); err != nil { - return errMsg{Err: fmt.Errorf("unable to initialise torrc builder: %s", err)} - } - - m.statusInfo("initialising Tor ACN...") - acn, err := tor.NewTorACNWithAuth( - path.Join(m.userDir, "/.tor"), - "", - path.Join(m.userDir, "/.tor", "data"), - controlPort, - tor.HashedPasswordAuthenticator{ - Password: base64.StdEncoding.EncodeToString(key), - }, - ) - if err != nil { - return errMsg{Err: fmt.Errorf("unable to bootstrap tor: %s", err)} - } - - m.statusInfo("waiting for Tor ACN to bootstrap...") - if err := acn.WaitTillBootstrapped(); err != nil { - return errMsg{Err: fmt.Errorf("unable to initialise tor: %s", err)} - } - - settingsFile, err := settings.InitGlobalSettingsFile(m.userDir, "") - if err != nil { - return errMsg{Err: fmt.Errorf("unable to initialise settings: %s", err)} - } - gSettings := settingsFile.ReadGlobalSettings() - gSettings.ExperimentsEnabled = false - gSettings.DownloadPath = "./" - settingsFile.WriteGlobalSettings(gSettings) - - app := app.NewApp(acn, m.userDir, settingsFile) - - engineHooks := connections.DefaultEngineHooks{} - app.InstallEngineHooks(engineHooks) - - m.statusInfo("the Tor ACN is up") - - return appInitialisedMsg{ - app: app, - acn: acn, +func (m model) handleStatusMessage() tea.Cmd { + return func() tea.Msg { + return statusMsg{msg: <-m.statusBuffer} } } func (m model) Init() tea.Cmd { return tea.Batch( textinput.Blink, + m.handleStatusMessage(), ) } @@ -269,6 +276,7 @@ func (m model) shutdown() bool { m.app.Shutdown() m.acn.Close() + close(m.statusBuffer) return true } @@ -397,12 +405,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds []tea.Cmd ) - m.input, cmd = m.input.Update(msg) - cmds = append(cmds, cmd) - - m.viewport, cmd = m.viewport.Update(msg) - cmds = append(cmds, cmd) - switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width - 2 @@ -452,7 +454,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case cmdMsg: m.input.Reset() - m.statusInfo(msg.output) + m.statusBuffer <- msg.output case shutdownMsg: m.input.Reset() @@ -465,7 +467,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.input.Reset() m.app.CreateProfile(msg.name, msg.password, false) - m.statusInfo(fmt.Sprintf("created new profile: %s", msg.name)) + m.statusBuffer <- fmt.Sprintf("created new profile: %s", msg.name) profiles := m.app.ListProfiles() // TODO m.profiles = append(m.profiles, profile{ @@ -479,10 +481,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.connState { case offline: - m.statusInfo("offline, run /connect first?") + m.statusBuffer <- "offline, run /connect first?" break case connecting: - m.statusInfo("still connecting, hold on a sec...") + m.statusBuffer <- "still connecting, hold on a sec..." break } @@ -500,7 +502,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { for _, p := range unlocked { names = append(names, p.name) } - m.statusInfo(fmt.Sprintf("unlocked profile(s): %s", strings.Join(names, " "))) + m.statusBuffer <- fmt.Sprintf("unlocked profile(s): %s", strings.Join(names, " ")) } m.profiles = unlocked @@ -508,45 +510,54 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case profileInfoMsg: m.input.Reset() if m.connState != connected { - m.statusInfo("offline, run /connect first?") + m.statusBuffer <- "offline, run /connect first?" break } if len(m.profiles) == 0 { - m.statusInfo("no profiles loaded") + m.statusBuffer <- "no profiles loaded" break } profile := m.profiles[m.profileState] - m.statusInfo(fmt.Sprintf("profile onion: %s", profile.onion)) + m.statusBuffer <- fmt.Sprintf("profile onion: %s", profile.onion) + + case statusMsg: + existing := m.viewport.View() + m.viewport.SetContent(existing + "\n" + msg.msg) + m.viewport.GotoBottom() + cmds = append(cmds, m.handleStatusMessage()) case clearMsg: m.input.Reset() - m.statusMsgs = new([]string) + m.viewport.SetContent("") m.viewport.GotoBottom() case connectMsg: m.input.Reset() - cmds = append(cmds, m.initApp) + cmds = append(cmds, m.initApp()) case appInitialisedMsg: m.app = msg.app m.acn = msg.acn m.connState = connected - m.statusInfo("create or unlock a profile to get started") - m.statusInfo("run /help to see the command listing") + m.statusBuffer <- "create or unlock a profile to get started" + m.statusBuffer <- "run /help to see the command listing" case errMsg: - m.statusInfo(msg.Error()) + m.statusBuffer <- msg.Error() } + m.input, cmd = m.input.Update(msg) + cmds = append(cmds, cmd) + + m.viewport, cmd = m.viewport.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) } func (m model) View() string { body := strings.Builder{} - // TODO: adjust when more panes are available - m.viewport.SetContent(strings.Join(*m.statusMsgs, "\n")) - channelBar := channelBarStyle. Width(m.width). AlignVertical(lipgloss.Center).