mirror of
https://git.sr.ht/~rjarry/aerc
synced 2025-07-09 07:00:21 +02:00

Add a template for the terminal tab title and define two terminal-specific properties: Title which expands to whatever the underlying application requests via OSC and defaults to the command name and Bell which is a boolean indicating whether the application has emitted bell, the flag is cleared when the corresponding tab gains focus and is not set at all if it currently has it. This can among other things be used to highlight the terminal tab when the underlying process prints the bell, such as by setting the template as such: tab-title-terminal={{if .Bell}}[BELL] {{end}}{{.Title}} Implements: https://todo.sr.ht/~rjarry/aerc/138 Changelog-added: A tab-title-terminal setting for customizing the title of tabs in which the terminal widget is run. Requested-by: Drew DeVault <drew@ddevault.org> Signed-off-by: Karel Balej <balejk@matfyz.cz> Acked-by: Robin Jarry <robin@jarry.cc>
224 lines
4.3 KiB
Go
224 lines
4.3 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"os/exec"
|
|
"sync/atomic"
|
|
|
|
"git.sr.ht/~rjarry/aerc/config"
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/state"
|
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
|
"git.sr.ht/~rockorager/vaxis"
|
|
"git.sr.ht/~rockorager/vaxis/widgets/term"
|
|
)
|
|
|
|
type HasTerminal interface {
|
|
Terminal() *Terminal
|
|
}
|
|
|
|
type Terminal struct {
|
|
// Bell was rung in the terminal since it last had focus.
|
|
bell bool
|
|
closed int32
|
|
visible int32 // visible if >0
|
|
cmd *exec.Cmd
|
|
ctx *ui.Context
|
|
focus bool
|
|
vterm *term.Model
|
|
running bool
|
|
tab *ui.Tab
|
|
// The window title set by the application running in the terminal.
|
|
title string
|
|
|
|
OnClose func(err error)
|
|
OnEvent func(event vaxis.Event) bool
|
|
OnStart func()
|
|
OnTitle func(title string)
|
|
}
|
|
|
|
func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
|
|
term := &Terminal{
|
|
cmd: cmd,
|
|
vterm: term.New(),
|
|
visible: 1,
|
|
}
|
|
term.vterm.OSC8 = config.General.EnableOSC8
|
|
term.vterm.TERM = config.General.Term
|
|
return term, nil
|
|
}
|
|
|
|
func (term *Terminal) Close() {
|
|
term.closeErr(nil)
|
|
}
|
|
|
|
// TODO: replace with atomic.Bool when min go version will have it (1.19+)
|
|
const closed int32 = 1
|
|
|
|
func (term *Terminal) isClosed() bool {
|
|
return atomic.LoadInt32(&term.closed) == closed
|
|
}
|
|
|
|
func (term *Terminal) closeErr(err error) {
|
|
if atomic.SwapInt32(&term.closed, closed) == closed {
|
|
return
|
|
}
|
|
if term.vterm != nil {
|
|
// Stop receiving events
|
|
term.vterm.Detach()
|
|
term.vterm.Close()
|
|
if term.ctx != nil {
|
|
term.ctx.HideCursor()
|
|
}
|
|
}
|
|
if term.OnClose != nil {
|
|
term.OnClose(err)
|
|
}
|
|
ui.Invalidate()
|
|
}
|
|
|
|
func (term *Terminal) Destroy() {
|
|
// If we destroy, we don't want to call the OnClose callback
|
|
term.OnClose = nil
|
|
term.closeErr(nil)
|
|
}
|
|
|
|
func (term *Terminal) Invalidate() {
|
|
ui.Invalidate()
|
|
}
|
|
|
|
func (term *Terminal) Draw(ctx *ui.Context) {
|
|
if ctx.Width() == 0 || ctx.Height() == 0 {
|
|
return
|
|
}
|
|
term.ctx = ctx
|
|
if !term.running && term.cmd != nil {
|
|
term.vterm.Attach(term.HandleEvent)
|
|
w, h := ctx.Window().Size()
|
|
if err := term.vterm.StartWithSize(term.cmd, w, h); err != nil {
|
|
log.Errorf("error running terminal: %v", err)
|
|
term.closeErr(err)
|
|
return
|
|
}
|
|
term.running = true
|
|
if term.OnStart != nil {
|
|
term.OnStart()
|
|
}
|
|
}
|
|
term.vterm.Draw(ctx.Window())
|
|
}
|
|
|
|
func (term *Terminal) Show(visible bool) {
|
|
if visible {
|
|
atomic.StoreInt32(&term.visible, 1)
|
|
} else {
|
|
atomic.StoreInt32(&term.visible, 0)
|
|
}
|
|
}
|
|
|
|
func (term *Terminal) Terminal() *Terminal {
|
|
return term
|
|
}
|
|
|
|
func (term *Terminal) MouseEvent(localX int, localY int, event vaxis.Event) {
|
|
ev, ok := event.(vaxis.Mouse)
|
|
if !ok {
|
|
return
|
|
}
|
|
if term.OnEvent != nil {
|
|
term.OnEvent(ev)
|
|
}
|
|
if term.isClosed() {
|
|
return
|
|
}
|
|
ev.Row = localY
|
|
ev.Col = localX
|
|
term.vterm.Update(ev)
|
|
}
|
|
|
|
func (term *Terminal) Focus(focus bool) {
|
|
if term.isClosed() {
|
|
return
|
|
}
|
|
term.focus = focus
|
|
term.bell = false
|
|
if term.focus {
|
|
term.vterm.Focus()
|
|
} else {
|
|
term.vterm.Blur()
|
|
}
|
|
term.setTitle()
|
|
ui.Invalidate()
|
|
}
|
|
|
|
// HandleEvent is used to watch the underlying terminal events
|
|
func (t *Terminal) HandleEvent(ev vaxis.Event) {
|
|
if t.isClosed() {
|
|
return
|
|
}
|
|
switch ev := ev.(type) {
|
|
case vaxis.Redraw:
|
|
if atomic.LoadInt32(&t.visible) > 0 {
|
|
ui.Invalidate()
|
|
}
|
|
case term.EventTitle:
|
|
t.title = string(ev)
|
|
t.setTitle()
|
|
ui.Invalidate()
|
|
if t.OnTitle != nil {
|
|
t.OnTitle(string(ev))
|
|
}
|
|
case term.EventClosed:
|
|
t.Close()
|
|
ui.Invalidate()
|
|
case term.EventBell:
|
|
if !t.focus {
|
|
t.bell = true
|
|
t.setTitle()
|
|
ui.Invalidate()
|
|
}
|
|
aerc.Beep()
|
|
}
|
|
}
|
|
|
|
func (term *Terminal) Event(event vaxis.Event) bool {
|
|
if term.OnEvent != nil {
|
|
if term.OnEvent(event) {
|
|
return true
|
|
}
|
|
}
|
|
if term.isClosed() {
|
|
return false
|
|
}
|
|
term.vterm.Update(event)
|
|
return true
|
|
}
|
|
|
|
// Renders the terminal tab title based on the template, falls back to the
|
|
// unformatted title in case of errors.
|
|
func (t *Terminal) RenderTitle() string {
|
|
title := t.title
|
|
if title == "" {
|
|
title = t.cmd.Path
|
|
}
|
|
|
|
data := state.NewDataSetter()
|
|
data.SetTitle(title)
|
|
data.SetBell(t.bell)
|
|
var buf bytes.Buffer
|
|
if err := config.Ui.TabTitleTerminal.Execute(&buf, data); err != nil {
|
|
PushError("failed to render terminal tab title: " + err.Error())
|
|
return title
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func (t *Terminal) SetTab(tab *ui.Tab) {
|
|
t.tab = tab
|
|
}
|
|
|
|
func (t *Terminal) setTitle() {
|
|
if t.tab != nil {
|
|
t.tab.SetTitle(t.RenderTitle())
|
|
}
|
|
}
|