turbo-editors/turbo-corepublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Turbo Core f3ade8d · on v1.0.2 · k33g · 10h ago
terminals.go · 134 lines · 4.1 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Terminal windows: a shell running in a pseudo-terminal, in a window of the
// editor's own.

package app

import (
	"errors"
	"os"
	"path/filepath"

	"rickub.com/turbo-editors/turbo-core/editor"
	"rickub.com/turbo-editors/turbo-core/terminal"
	"rickub.com/turbo-editors/turbo-core/ui"
)

// NewTerminal opens a window running a shell.
//
// The shell starts in the directory of the file being edited, which is where
// you want to be for `go build` or `git`. With no file open it starts wherever
// the editor was started.
//
// On a platform without pseudo-terminal support this reports so and changes
// nothing: the rest of the editor is unaffected.
func (a *App) NewTerminal() {
	// The shell writes from a goroutine of its own, so neither callback may
	// touch the desktop: they only ask the event loop to come round again. They
	// are options rather than fields because NewView starts that goroutine.
	view, err := terminal.NewView(terminal.ViewOptions{
		Options:  terminal.Options{Dir: a.terminalDirectory()},
		OnChange: a.wake,
		OnExit:   a.wake,
	})
	if err != nil {
		a.reportTerminalFailure(err)
		return
	}

	window := ui.NewWindow(view.Title(), view)
	window.SetBounds(a.newWindowBounds())
	window.OnClose = func() bool { a.closeTerminal(window, view); return true }

	a.desktop.Add(window)
	a.windowsOpened++
	a.terminals[window] = view
}

// reportTerminalFailure explains why a terminal could not be opened.
func (a *App) reportTerminalFailure(err error) {
	if errors.Is(err, terminal.ErrUnsupported) {
		a.ShowMessage("Terminal", "Terminal windows are not supported on this\nplatform yet — Linux, macOS and Windows have them.\n\nEverything else in the editor works as usual.")
		return
	}
	a.ShowMessage("Terminal", err.Error())
}

// terminalDirectory returns where a new shell should start: beside the file
// being edited, or the editor's own working directory.
func (a *App) terminalDirectory() string {
	if view := a.activeView(); view != nil && view.Buffer().Path() != "" {
		if absolute, err := filepath.Abs(view.Buffer().Path()); err == nil {
			return filepath.Dir(absolute)
		}
	}
	if working, err := os.Getwd(); err == nil {
		return working
	}
	return ""
}

// closeTerminal ends a shell and takes its window away.
//
// A terminal is not asked about unsaved work: there is none. What it holds is
// a running process, and closing the window is how you say you have finished
// with it.
func (a *App) closeTerminal(window *ui.Window, view *terminal.View) {
	_ = view.Close()
	delete(a.terminals, window)
	a.desktop.Remove(window)
	a.completion.Hide()
}

// terminalView returns the terminal in a window, if that is what it holds.
func (a *App) terminalView(window *ui.Window) (*terminal.View, bool) {
	view, ok := a.terminals[window]
	return view, ok
}

// activeTerminal returns the terminal in the front window, or nil when the
// front window holds a file.
func (a *App) activeTerminal() *terminal.View {
	window := a.desktop.Active()
	if window == nil {
		return nil
	}
	if view, ok := a.terminals[window]; ok {
		return view
	}
	return nil
}

// refreshTerminalTitles keeps each terminal window's title in step with what
// the program inside it calls itself.
//
// It runs on every turn of the event loop rather than on a notification,
// because the title changes on the shell's goroutine and nothing else would
// notice.
func (a *App) refreshTerminalTitles() {
	for window, view := range a.terminals {
		if title := view.Title(); title != window.Title() {
			window.SetTitle(title)
		}
	}
}

// closeTerminals ends every shell, which is what leaving the editor does.
func (a *App) closeTerminals() {
	for window, view := range a.terminals {
		_ = view.Close()
		delete(a.terminals, window)
	}
}

// isTerminalWindow reports whether a window holds a terminal rather than a
// file. The file actions use it to leave terminals alone.
func (a *App) isTerminalWindow(window *ui.Window) bool {
	_, ok := a.terminals[window]
	return ok
}

// editorViewOf returns the editor in a window, if that is what it holds.
func editorViewOf(window *ui.Window) (*editor.View, bool) {
	view, ok := window.Content().(*editor.View)
	return view, ok
}