turbo-editors/turbo-corepublic Fork 0
main
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.

terminals.go · 134 lines · 4.1 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 7h ago1// Terminal windows: a shell running in a pseudo-terminal, in a window of the
2// editor's own.
3
4package app
5
6import (
7 "errors"
8 "os"
9 "path/filepath"
10
📦 Turbo Core f3ade8d k33g 16m ago11 "rickub.com/turbo-editors/turbo-core/editor"
12 "rickub.com/turbo-editors/turbo-core/terminal"
13 "rickub.com/turbo-editors/turbo-core/ui"
🛟 Updated. 28d5985 k33g 7h ago14)
15
16// NewTerminal opens a window running a shell.
17//
18// The shell starts in the directory of the file being edited, which is where
19// you want to be for `go build` or `git`. With no file open it starts wherever
20// the editor was started.
21//
22// On a platform without pseudo-terminal support this reports so and changes
23// nothing: the rest of the editor is unaffected.
24func (a *App) NewTerminal() {
25 // The shell writes from a goroutine of its own, so neither callback may
26 // touch the desktop: they only ask the event loop to come round again. They
27 // are options rather than fields because NewView starts that goroutine.
28 view, err := terminal.NewView(terminal.ViewOptions{
29 Options: terminal.Options{Dir: a.terminalDirectory()},
30 OnChange: a.wake,
31 OnExit: a.wake,
32 })
33 if err != nil {
34 a.reportTerminalFailure(err)
35 return
36 }
37
38 window := ui.NewWindow(view.Title(), view)
39 window.SetBounds(a.newWindowBounds())
40 window.OnClose = func() bool { a.closeTerminal(window, view); return true }
41
42 a.desktop.Add(window)
43 a.windowsOpened++
44 a.terminals[window] = view
45}
46
47// reportTerminalFailure explains why a terminal could not be opened.
48func (a *App) reportTerminalFailure(err error) {
49 if errors.Is(err, terminal.ErrUnsupported) {
50 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.")
51 return
52 }
53 a.ShowMessage("Terminal", err.Error())
54}
55
56// terminalDirectory returns where a new shell should start: beside the file
57// being edited, or the editor's own working directory.
58func (a *App) terminalDirectory() string {
59 if view := a.activeView(); view != nil && view.Buffer().Path() != "" {
60 if absolute, err := filepath.Abs(view.Buffer().Path()); err == nil {
61 return filepath.Dir(absolute)
62 }
63 }
64 if working, err := os.Getwd(); err == nil {
65 return working
66 }
67 return ""
68}
69
70// closeTerminal ends a shell and takes its window away.
71//
72// A terminal is not asked about unsaved work: there is none. What it holds is
73// a running process, and closing the window is how you say you have finished
74// with it.
75func (a *App) closeTerminal(window *ui.Window, view *terminal.View) {
76 _ = view.Close()
77 delete(a.terminals, window)
78 a.desktop.Remove(window)
79 a.completion.Hide()
80}
81
82// terminalView returns the terminal in a window, if that is what it holds.
83func (a *App) terminalView(window *ui.Window) (*terminal.View, bool) {
84 view, ok := a.terminals[window]
85 return view, ok
86}
87
88// activeTerminal returns the terminal in the front window, or nil when the
89// front window holds a file.
90func (a *App) activeTerminal() *terminal.View {
91 window := a.desktop.Active()
92 if window == nil {
93 return nil
94 }
95 if view, ok := a.terminals[window]; ok {
96 return view
97 }
98 return nil
99}
100
101// refreshTerminalTitles keeps each terminal window's title in step with what
102// the program inside it calls itself.
103//
104// It runs on every turn of the event loop rather than on a notification,
105// because the title changes on the shell's goroutine and nothing else would
106// notice.
107func (a *App) refreshTerminalTitles() {
108 for window, view := range a.terminals {
109 if title := view.Title(); title != window.Title() {
110 window.SetTitle(title)
111 }
112 }
113}
114
115// closeTerminals ends every shell, which is what leaving the editor does.
116func (a *App) closeTerminals() {
117 for window, view := range a.terminals {
118 _ = view.Close()
119 delete(a.terminals, window)
120 }
121}
122
123// isTerminalWindow reports whether a window holds a terminal rather than a
124// file. The file actions use it to leave terminals alone.
125func (a *App) isTerminalWindow(window *ui.Window) bool {
126 _, ok := a.terminals[window]
127 return ok
128}
129
130// editorViewOf returns the editor in a window, if that is what it holds.
131func editorViewOf(window *ui.Window) (*editor.View, bool) {
132 view, ok := window.Content().(*editor.View)
133 return view, ok
134}