TUI IDE multiplexer?!
golang tui ide
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add working WIP of progress

kettek e3bb4b54

+1414
+1
.gitignore
··· 1 + ./aight
+167
app.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + 7 + "github.com/gdamore/tcell/v2" 8 + "github.com/godbus/dbus/v5" 9 + "github.com/rivo/tview" 10 + ) 11 + 12 + type App struct { 13 + tview.Application 14 + Conn *dbus.Conn 15 + dbusID string 16 + env []string // Env to pass to editors/shells/etc. 17 + 18 + grid *tview.Grid 19 + 20 + menu *Menu 21 + fm *Term 22 + editors Editors 23 + shells Shells 24 + status *Status 25 + // focusedPanel *Term 26 + lastFocusedEditor int 27 + } 28 + 29 + func NewApp() *App { 30 + app := &App{ 31 + Application: *tview.NewApplication(), 32 + } 33 + 34 + app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 35 + if event.Key() == tcell.KeyCtrlC { 36 + return tcell.NewEventKey(tcell.KeyCtrlC, 0, tcell.ModNone) 37 + } else if event.Key() == tcell.KeyEscape { 38 + bus.Publish(UI_CANCEL, event) 39 + } 40 + return event 41 + }) 42 + 43 + app.editors.env = &app.env 44 + app.editors.setFocusFunc = func(p tview.Primitive) { 45 + app.SetFocus(p) 46 + } 47 + app.shells.env = &app.env 48 + app.shells.setFocusFunc = func(p tview.Primitive) { 49 + app.SetFocus(p) 50 + } 51 + return app 52 + } 53 + 54 + func (a *App) Setup() error { 55 + c, err := loadConfig() 56 + if err != nil { 57 + return err 58 + } 59 + cfg = c 60 + 61 + a.Conn, err = dbus.ConnectSessionBus() 62 + if err != nil { 63 + return err 64 + } 65 + 66 + a.dbusID = a.Conn.Names()[0] 67 + a.editors.conn = a.Conn 68 + a.shells.conn = a.Conn 69 + a.env = append(a.env, fmt.Sprintf("AIGHT_DBUS=%s", a.dbusID)) 70 + a.env = append(a.env, fmt.Sprintf("AIGHT_EDITOR=%s %s", os.Args[0], "--dbus-send --with-newline --with-focus editor/active :open ")) 71 + a.env = append(a.env, fmt.Sprintf("EDITOR=%s %s", os.Args[0], "--dbus-send --with-newline --with-focus editor/active :open ")) 72 + 73 + // Create/run our various components. 74 + if err := a.RunStatus(); err != nil { 75 + return err 76 + } 77 + if err := a.RunFM(); err != nil { 78 + return err 79 + } 80 + 81 + if err := a.editors.Setup(); err != nil { 82 + return err 83 + } 84 + if err := a.editors.AddEditor(); err != nil { 85 + return err 86 + } 87 + 88 + if err := a.shells.Setup(); err != nil { 89 + return err 90 + } 91 + if err := a.shells.AddShell(); err != nil { 92 + return err 93 + } 94 + if err := a.shells.AddShell(); err != nil { 95 + return err 96 + } 97 + if err := a.setupMenu(); err != nil { 98 + return err 99 + } 100 + 101 + shellAndEditors := NewResizable(true) 102 + shellAndEditors.AddItem(&a.editors, 0, true) 103 + shellAndEditors.AddItem(&a.shells, 0, false) 104 + 105 + fmAndSE := NewResizable(false) 106 + fmAndSE.AddItem(a.fm, 0, false) 107 + fmAndSE.AddItem(shellAndEditors, 0, true) 108 + 109 + a.grid = tview.NewGrid(). 110 + SetRows(1, 0, 8, 1). 111 + SetColumns(30, 0). 112 + // SetBorders(true). 113 + AddItem(a.menu, 0, 0, 1, 2, 1, 0, false). 114 + AddItem(a.fm, 1, 0, 2, 1, 0, 0, false). 115 + AddItem(fmAndSE, 1, 0, 2, 2, 0, 0, false). 116 + // AddItem(shellAndEditors, 1, 1, 2, 1, 0, 0, false). 117 + // AddItem(a.shells, 2, 1, 1, 1, 0, 0, false). 118 + // AddItem(a.editors, 1, 1, 1, 1, 0, 0, false). 119 + AddItem(a.status, 3, 0, 1, 2, 0, 0, false) 120 + 121 + return nil 122 + } 123 + 124 + func (a *App) setupMenu() error { 125 + a.menu = NewMenu([]string{"File", "Settings"}) 126 + 127 + return nil 128 + } 129 + 130 + func (a *App) Cleanup() error { 131 + a.status.RemoveFromConn(a.Conn) 132 + a.fm.RemoveFromConn(a.Conn) 133 + if err := a.shells.Cleanup(); err != nil { 134 + return err 135 + } 136 + if err := a.editors.Cleanup(); err != nil { 137 + return err 138 + } 139 + if err := a.Conn.Close(); err != nil { 140 + return err 141 + } 142 + 143 + return nil 144 + } 145 + 146 + func (a *App) RunStatus() error { 147 + a.status = NewStatus() 148 + return a.status.AddToConn(a.Conn) 149 + } 150 + 151 + func (a *App) RunFM() error { 152 + a.fm = NewTerm("fm", cfg.FileManager) 153 + 154 + if err := app.Conn.ExportMethodTable(map[string]any{ 155 + "SetFocus": func() *dbus.Error { 156 + a.SetFocus(a.fm) 157 + return nil 158 + }, 159 + }, dbus.ObjectPath("/panels/fm"), "net.kettek.Aight.Panel"); err != nil { 160 + return err 161 + } 162 + 163 + if err := a.fm.Start(a.env); err != nil { 164 + return err 165 + } 166 + return a.fm.AddToConn(a.Conn) 167 + }
+75
bus.go
··· 1 + package main 2 + 3 + // Bus provides a basic pubsub modelo via callbacks. 4 + type Bus struct { 5 + // subscribers 6 + topics map[string]*Topic 7 + } 8 + 9 + func (b *Bus) Register(name string) *Topic { 10 + b.topics[name] = &Topic{ 11 + name: name, 12 + } 13 + return b.topics[name] 14 + } 15 + 16 + func (b *Bus) Unregister(name string) { 17 + delete(b.topics, name) 18 + } 19 + 20 + func (b *Bus) Publish(topic string, parts ...any) { 21 + if t, ok := b.topics[topic]; ok { 22 + for _, s := range t.subscriptions { 23 + s.callback(parts) 24 + } 25 + } 26 + } 27 + 28 + // Subscribe subscribes the func to the given topic and returns a non-zero ID if it was added. 29 + func (b *Bus) Subscribe(topic string, cb func(...any)) (id int) { 30 + t, ok := b.topics[topic] 31 + 32 + // Eh... just auto-register if it doesn't exist. 33 + if !ok { 34 + t = b.Register(topic) 35 + } 36 + 37 + t.topID++ 38 + id = t.topID 39 + t.subscriptions = append(t.subscriptions, Subscription{ 40 + id: id, 41 + callback: cb, 42 + }) 43 + 44 + return id 45 + } 46 + 47 + // Unsubscribes removes a subscription for a topic using the given id. Returns true on success, false if the topic or id was not found. 48 + func (b *Bus) Unsubscribe(topic string, id int) bool { 49 + if t, ok := b.topics[topic]; ok { 50 + for index, s := range t.subscriptions { 51 + if s.id == id { 52 + t.subscriptions = append(t.subscriptions[:index], t.subscriptions[index+1:]...) 53 + return true 54 + } 55 + } 56 + } 57 + return false 58 + } 59 + 60 + type Subscription struct { 61 + id int 62 + callback func(...any) 63 + } 64 + 65 + type Topic struct { 66 + name string 67 + subscriptions []Subscription 68 + topID int 69 + } 70 + 71 + var bus Bus 72 + 73 + func init() { 74 + bus.topics = make(map[string]*Topic) 75 + }
+27
config.go
··· 1 + package main 2 + 3 + import ( 4 + "log" 5 + 6 + "github.com/gookit/config/v2" 7 + ) 8 + 9 + type Config struct { 10 + FileManager string `default:"treefiddy"` 11 + Editor string `default:"hx"` 12 + Shell string `default:"sh"` 13 + UI struct { 14 + FocusFollowsMouse bool `default:"false"` 15 + } 16 + } 17 + 18 + func loadConfig() (*Config, error) { 19 + c := &Config{} 20 + config.WithOptions(config.ParseDefault) 21 + err := config.BindStruct("", c) 22 + config.LoadOSEnvs(map[string]string{"FILEMANAGER": "FileManager", "EDITOR": "Editor", "SHELL": "Shell"}) 23 + 24 + log.Println(c) 25 + 26 + return c, err 27 + }
+6
econfig.go
··· 1 + package main 2 + 3 + // EmphemeralConfig reprsents values that are not crucially defined by the user, such as current panel sizings and positions. 4 + type EphemeralConfig struct { 5 + panelSizes map[string]int 6 + }
+109
editors.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/godbus/dbus/v5" 7 + "github.com/godbus/dbus/v5/introspect" 8 + "github.com/rivo/tview" 9 + ) 10 + 11 + type Editors struct { 12 + conn *dbus.Conn // Assigned from app. 13 + env *[]string // Assigned from app. 14 + setFocusFunc func(tview.Primitive) // Calls app's SetFocus 15 + 16 + editors []*Term 17 + lastFocusedEditor int 18 + 19 + *tview.Flex 20 + } 21 + 22 + func (s *Editors) Setup() error { 23 + // UI 24 + s.Flex = tview.NewFlex() 25 + 26 + // D-Bus 27 + if err := app.Conn.ExportMethodTable(map[string]any{ 28 + "SetFocus": func() *dbus.Error { 29 + if editor := s.GetLastFocusedEditor(); editor != nil { 30 + s.setFocusFunc(editor) 31 + } 32 + return nil 33 + }, 34 + "SendString": func(str string) *dbus.Error { 35 + if editor := s.GetLastFocusedEditor(); editor != nil { 36 + return editor.SendString(str) 37 + } 38 + return nil 39 + }, 40 + "SendBytes": func(b []byte) *dbus.Error { 41 + if editor := s.GetLastFocusedEditor(); editor != nil { 42 + return editor.SendBytes(b) 43 + } 44 + return nil 45 + }, 46 + }, dbus.ObjectPath("/panels/editor/active"), "net.kettek.Aight.Panel"); err != nil { 47 + return err 48 + } 49 + if err := app.Conn.Export(introspect.Introspectable(termIntro), dbus.ObjectPath("/panels/editor/active"), "org.freedesktop.DBus.Introspectable"); err != nil { 50 + return err 51 + } 52 + 53 + return nil 54 + } 55 + 56 + func (s *Editors) Cleanup() error { 57 + // D-Bus 58 + for _, editor := range s.editors { 59 + editor.RemoveFromConn(s.conn) 60 + } 61 + 62 + s.conn.Export(nil, dbus.ObjectPath("/panels/editor/active"), "org.freedesktop.DBus.Introspectable") 63 + s.conn.Export(nil, dbus.ObjectPath("/panels/editor/active"), "net.kettek.Aight.Status") 64 + 65 + return nil 66 + } 67 + 68 + func (s *Editors) AddEditor() error { 69 + id := len(s.editors) 70 + editor := NewTerm(fmt.Sprintf("editor/%d", id), cfg.Editor) 71 + editor.SetFocusFunc(func() { 72 + s.lastFocusedEditor = id 73 + }) 74 + 75 + // Add focus handler. 76 + if err := s.conn.ExportMethodTable(map[string]any{ 77 + "SetFocus": func() *dbus.Error { 78 + if editor := s.GetEditorByIndex(s.lastFocusedEditor); editor != nil { 79 + s.setFocusFunc(editor) 80 + } 81 + return nil 82 + }, 83 + }, dbus.ObjectPath(fmt.Sprintf("/panels/editor/%d", id)), "net.kettek.Aight.Panel"); err != nil { 84 + return err 85 + } 86 + 87 + // Start 'er up. 88 + if err := editor.Start(*s.env); err != nil { 89 + return err 90 + } 91 + 92 + s.editors = append(s.editors, editor) 93 + 94 + // Add to UI. 95 + s.AddItem(editor, 0, 1, true) 96 + 97 + return editor.AddToConn(s.conn) 98 + } 99 + 100 + func (s *Editors) GetEditorByIndex(id int) *Term { 101 + if id > len(s.editors) { 102 + return nil 103 + } 104 + return s.editors[id] 105 + } 106 + 107 + func (s *Editors) GetLastFocusedEditor() *Term { 108 + return s.GetEditorByIndex(s.lastFocusedEditor) 109 + }
+25
go.mod
··· 1 + module tangled.org/kettek.net/aight 2 + 3 + go 1.26 4 + 5 + require ( 6 + github.com/gdamore/tcell/v2 v2.13.8 7 + github.com/godbus/dbus/v5 v5.2.2 8 + github.com/gookit/config/v2 v2.2.7 9 + github.com/kettek/terminal v0.0.0-20260401081058-301e56decebd 10 + github.com/rivo/tview v0.42.0 11 + ) 12 + 13 + require ( 14 + dario.cat/mergo v1.0.2 // indirect 15 + github.com/creack/pty v1.1.24 // indirect 16 + github.com/gdamore/encoding v1.0.1 // indirect 17 + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 18 + github.com/gookit/goutil v0.7.1 // indirect 19 + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 20 + github.com/rivo/uniseg v0.4.7 // indirect 21 + golang.org/x/sync v0.19.0 // indirect 22 + golang.org/x/sys v0.42.0 // indirect 23 + golang.org/x/term v0.39.0 // indirect 24 + golang.org/x/text v0.33.0 // indirect 25 + )
+73
go.sum
··· 1 + dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 2 + dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 3 + github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 4 + github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 5 + github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= 6 + github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 7 + github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= 8 + github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= 9 + github.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3RlfU= 10 + github.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= 11 + github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 12 + github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 13 + github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= 14 + github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= 15 + github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= 16 + github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= 17 + github.com/gookit/config/v2 v2.2.7 h1:P58/uENzkDp7r7Hp8YSZxOhZ/F5a5Y/AzyhDUkQYa9A= 18 + github.com/gookit/config/v2 v2.2.7/go.mod h1:QST99HmkZXXD/HkZmOm1OXpgdAnc6Rl9syGl+u62Pi8= 19 + github.com/gookit/goutil v0.7.1 h1:AaFJPN9mrdeYBv8HOybri26EHGCC34WJVT7jUStGJsI= 20 + github.com/gookit/goutil v0.7.1/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= 21 + github.com/gookit/ini/v2 v2.3.2 h1:W6tzOGE6zOLQelH2xhcH8BIBZPtnEpJgQ+J6SsAKBSw= 22 + github.com/gookit/ini/v2 v2.3.2/go.mod h1:StKSqY5niArRwYBS8Z71+iWUt5ow47qt359sS9YQLYY= 23 + github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 24 + github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 25 + github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 26 + github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 27 + github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 28 + github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 29 + github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= 30 + github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= 31 + github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 32 + github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 33 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 34 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 35 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 36 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 37 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 38 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 39 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 40 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 41 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 42 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 43 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 45 + golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 46 + golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 47 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 + golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= 54 + golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 55 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 56 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 57 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 58 + golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= 59 + golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 60 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 61 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 62 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 63 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 64 + golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 65 + golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= 66 + golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= 67 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 68 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 69 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 70 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 71 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 72 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 73 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+86
main.go
··· 1 + package main 2 + 3 + import ( 4 + "flag" 5 + "log" 6 + "os" 7 + "strings" 8 + 9 + "github.com/godbus/dbus/v5" 10 + ) 11 + 12 + var app *App 13 + var cfg *Config 14 + 15 + func main() { 16 + // Early handle so we can do DBUS sending. 17 + dbusSend := flag.Bool("dbus-send", false, "Send a DBUS message, requires AIGHT_DBUS env to be set to a DBUS address") 18 + dbusSendWithNewline := flag.Bool("with-newline", true, "Adds a newline to any submitted DBUS message") 19 + dbusSendWithFocus := flag.Bool("with-focus", true, "If the target panel should be focused on send") 20 + flag.Parse() 21 + 22 + if *dbusSend { 23 + id := flag.Arg(0) 24 + arg := strings.Join(flag.Args()[1:], " ") 25 + 26 + if *dbusSendWithNewline { 27 + arg += string([]byte{0x0D}) 28 + } 29 + 30 + if id == "" { 31 + log.Fatalln("id must not be empty") 32 + } 33 + if arg == "" { 34 + log.Fatalln("arg must not be empty") 35 + } 36 + 37 + var dbusID string 38 + for _, v := range os.Environ() { 39 + parts := strings.SplitN(v, "=", 2) 40 + if len(parts) != 2 { 41 + continue 42 + } 43 + if parts[0] == "AIGHT_DBUS" { 44 + dbusID = parts[1] 45 + break 46 + } 47 + } 48 + if dbusID == "" { 49 + log.Fatalln("dbusID must not be missing") 50 + } 51 + conn, err := dbus.ConnectSessionBus() 52 + if err != nil { 53 + log.Fatalln(err) 54 + } 55 + defer conn.Close() 56 + 57 + obj := conn.Object(dbusID, dbus.ObjectPath("/panels/"+id)) 58 + err = obj.Call("net.kettek.Aight.Panel.SendString", 0, arg).Store() 59 + if err != nil { 60 + log.Fatalln(err) 61 + } 62 + 63 + if *dbusSendWithFocus { 64 + err = obj.Call("net.kettek.Aight.Panel.SetFocus", 0).Store() 65 + if err != nil { 66 + log.Fatalln(err) 67 + } 68 + } 69 + 70 + return 71 + } 72 + 73 + // Alright, big chungus, let's fuck. 74 + // panel/<name>/send 75 + 76 + app = NewApp() 77 + 78 + if err := app.Setup(); err != nil { 79 + panic(err) 80 + } 81 + 82 + if err := app.SetRoot(app.grid, true).EnableMouse(true).EnablePaste(true).Run(); err != nil { 83 + panic(err) 84 + } 85 + 86 + }
+75
menu.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/gdamore/tcell/v2" 7 + "github.com/rivo/tview" 8 + ) 9 + 10 + type Menu struct { 11 + *tview.Box 12 + items []string 13 + renderedPositions [][2]int 14 + selectedIndex int 15 + } 16 + 17 + func NewMenu(items []string) *Menu { 18 + return &Menu{ 19 + Box: tview.NewBox(), 20 + items: items, 21 + renderedPositions: make([][2]int, len(items)), 22 + selectedIndex: -1, 23 + } 24 + } 25 + 26 + func (m *Menu) Draw(screen tcell.Screen) { 27 + m.Box.DrawForSubclass(screen, m) 28 + x, y, width, _ := m.GetInnerRect() 29 + 30 + // TODO: attempt to truncate item names to ensure they get at least one character on screen. 31 + px := 0 32 + for index, option := range m.items { 33 + if index >= width { 34 + break 35 + } 36 + var item string 37 + if index == m.selectedIndex { 38 + item = fmt.Sprintf(`[black:white]%s`, option) 39 + } else { 40 + item = fmt.Sprintf(`%s`, option) 41 + } 42 + _, sx := tview.Print(screen, item, x+px, y, len(option), tview.AlignCenter, tcell.ColorYellow) 43 + m.renderedPositions[index][0] = x + px 44 + px += sx + 1 45 + m.renderedPositions[index][1] = x + px 46 + } 47 + } 48 + 49 + func (m *Menu) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { 50 + return m.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { 51 + x, y := event.Position() 52 + if !m.InRect(x, y) { 53 + return false, nil 54 + } 55 + 56 + if action == tview.MouseLeftClick { 57 + setFocus(m) 58 + 59 + hitIndex := -1 60 + for index, pos := range m.renderedPositions { 61 + if x >= pos[0] && x <= pos[1] { 62 + hitIndex = index 63 + break 64 + } 65 + } 66 + 67 + if hitIndex >= 0 { 68 + consumed = true 69 + m.selectedIndex = hitIndex 70 + } 71 + } 72 + 73 + return 74 + }) 75 + }
+204
resizable.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/gdamore/tcell/v2" 7 + "github.com/rivo/tview" 8 + ) 9 + 10 + type Gripper struct { 11 + *tview.Box 12 + isVertical bool 13 + isHeld bool 14 + } 15 + 16 + func NewGripper(isVertical bool) *Gripper { 17 + return &Gripper{ 18 + Box: tview.NewBox(), 19 + isVertical: isVertical, 20 + } 21 + } 22 + 23 + func (g *Gripper) Draw(screen tcell.Screen) { 24 + g.DrawForSubclass(screen, g) 25 + x, y, w, h := g.GetInnerRect() 26 + 27 + ch := '\u2502' 28 + if g.isVertical { 29 + ch = '\u2500' 30 + } 31 + if g.isHeld { 32 + ch = '\u2503' 33 + if g.isVertical { 34 + ch = '\u2501' 35 + } 36 + } 37 + 38 + color := tcell.StyleDefault.Dim(true) 39 + 40 + for x1 := range w { 41 + for y1 := range h { 42 + screen.SetContent(x+x1, y+y1, ch, nil, color) 43 + } 44 + } 45 + } 46 + 47 + // Resizable is a rather hacky wrapper around Flex to allow resizable items controlled by grippers within. 48 + type Resizable struct { 49 + *tview.Flex 50 + items []ResizableItem 51 + grippers []*Gripper 52 + heldGripper *Gripper 53 + heldGripperIndex int 54 + lastX, lastY int 55 + isVertical bool 56 + subscription int 57 + } 58 + 59 + type ResizableItem struct { 60 + item tview.Primitive 61 + size int // static size 62 + proportion int 63 + preferResize bool 64 + } 65 + 66 + func NewResizable(isVertical bool) *Resizable { 67 + r := &Resizable{ 68 + Flex: tview.NewFlex(), 69 + isVertical: isVertical, 70 + } 71 + if r.isVertical { 72 + r.Flex.SetDirection(tview.FlexRow) 73 + } else { 74 + r.Flex.SetDirection(tview.FlexColumn) 75 + } 76 + 77 + // Hook into global bus for convenient program-wide handling. 78 + r.subscription = bus.Subscribe(UI_CANCEL, func(v ...any) { 79 + if r.heldGripper != nil { 80 + r.heldGripper.isHeld = false 81 + r.heldGripper = nil 82 + } 83 + }) 84 + 85 + return r 86 + } 87 + 88 + func (r *Resizable) Cleanup() { 89 + bus.Unsubscribe(UI_CANCEL, r.subscription) 90 + } 91 + 92 + func (r *Resizable) AddItem(item tview.Primitive, size int, preferResize bool) { 93 + ritem := ResizableItem{item: item, proportion: 1000, size: size, preferResize: preferResize} 94 + r.items = append(r.items, ritem) 95 + 96 + if len(r.items) > 1 { 97 + gripper := NewGripper(r.isVertical) 98 + r.Flex.AddItem(gripper, 1, 0, false) 99 + r.grippers = append(r.grippers, gripper) 100 + } 101 + r.Flex.AddItem(ritem.item, ritem.size, ritem.proportion, true) 102 + } 103 + 104 + func (r *Resizable) Draw(screen tcell.Screen) { 105 + r.Flex.Draw(screen) 106 + if r.heldGripper != nil { 107 + startIndex := r.heldGripperIndex 108 + x, y, w, h := r.items[startIndex].item.GetRect() 109 + 110 + _, _, sw, sh := r.GetRect() 111 + var size int 112 + if r.isVertical { 113 + size = sh 114 + } else { 115 + size = sw 116 + } 117 + step := int(1000.0 / float64(size)) 118 + 119 + screen.PutStr(x, y, fmt.Sprintf("%dx%d %d %d %d %d", w, h, r.items[startIndex].proportion, step, size, size*step)) 120 + } 121 + } 122 + 123 + func (r *Resizable) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { 124 + return r.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { 125 + x, y := event.Position() 126 + if !r.InRect(x, y) { 127 + return false, nil 128 + } 129 + 130 + if r.heldGripper != nil { 131 + if action == tview.MouseMove { 132 + startIndex := r.heldGripperIndex 133 + var delta int 134 + var rsize int 135 + 136 + which := startIndex 137 + if r.items[startIndex].preferResize { 138 + which = startIndex + 1 139 + } 140 + 141 + _, _, rw, rh := r.items[which].item.GetRect() 142 + if r.isVertical { 143 + delta = y - r.lastY 144 + rsize = rh 145 + } else { 146 + delta = x - r.lastX 147 + rsize = rw 148 + } 149 + if which == startIndex { 150 + delta *= -1 151 + } 152 + r.items[which].size = rsize - delta 153 + r.Flex.ResizeItem(r.items[which].item, r.items[which].size, r.items[which].proportion) 154 + 155 + r.lastX, r.lastY = x, y 156 + } else if action == tview.MouseLeftUp || action == tview.MouseLeftClick || action == tview.MouseLeftDoubleClick { 157 + r.heldGripper.isHeld = false 158 + r.heldGripper = nil 159 + } else { 160 + return r.MouseHandleItems(action, event, setFocus) 161 + } 162 + return true, r 163 + } 164 + 165 + // This is hacky, but whatever for now. 166 + if action == tview.MouseLeftDown { 167 + var gripper *Gripper 168 + var gripperIndex int 169 + for i, g := range r.grippers { 170 + if g.InRect(x, y) { 171 + gripper = g 172 + gripperIndex = i 173 + break 174 + } 175 + } 176 + 177 + if gripper != nil { 178 + gripper.isHeld = true 179 + r.heldGripper = gripper 180 + r.heldGripperIndex = gripperIndex 181 + r.lastX, r.lastY = x, y 182 + return true, gripper 183 + } 184 + } 185 + 186 + // If we hit here, pass down to actual items. 187 + return r.MouseHandleItems(action, event, setFocus) 188 + }) 189 + } 190 + 191 + func (r *Resizable) MouseHandleItems(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { 192 + for _, item := range r.items { 193 + consumed, capture = item.item.MouseHandler()(action, event, setFocus) 194 + if consumed { 195 + return 196 + } 197 + } 198 + return false, nil 199 + } 200 + 201 + func (r *Resizable) SetRect(x, y, width, height int) { 202 + r.Flex.SetRect(x, y, width, height) 203 + // TODO: Redistribute items. 204 + }
+109
shells.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/godbus/dbus/v5" 7 + "github.com/godbus/dbus/v5/introspect" 8 + "github.com/rivo/tview" 9 + ) 10 + 11 + type Shells struct { 12 + conn *dbus.Conn // Assigned from app. 13 + env *[]string // Assigned from app. 14 + setFocusFunc func(tview.Primitive) // Calls app's SetFocus 15 + 16 + shells []*Term 17 + lastFocusedShell int 18 + 19 + *Resizable 20 + } 21 + 22 + func (s *Shells) Setup() error { 23 + // UI 24 + s.Resizable = NewResizable(false) 25 + 26 + // D-Bus 27 + if err := app.Conn.ExportMethodTable(map[string]any{ 28 + "SetFocus": func() *dbus.Error { 29 + if shell := s.GetLastFocusedShell(); shell != nil { 30 + s.setFocusFunc(shell) 31 + } 32 + return nil 33 + }, 34 + "SendString": func(str string) *dbus.Error { 35 + if shell := s.GetLastFocusedShell(); shell != nil { 36 + return shell.SendString(str) 37 + } 38 + return nil 39 + }, 40 + "SendBytes": func(b []byte) *dbus.Error { 41 + if shell := s.GetLastFocusedShell(); shell != nil { 42 + return shell.SendBytes(b) 43 + } 44 + return nil 45 + }, 46 + }, dbus.ObjectPath("/panels/shell/active"), "net.kettek.Aight.Panel"); err != nil { 47 + return err 48 + } 49 + if err := app.Conn.Export(introspect.Introspectable(termIntro), dbus.ObjectPath("/panels/shell/active"), "org.freedesktop.DBus.Introspectable"); err != nil { 50 + return err 51 + } 52 + 53 + return nil 54 + } 55 + 56 + func (s *Shells) Cleanup() error { 57 + // D-Bus 58 + for _, shell := range s.shells { 59 + shell.RemoveFromConn(s.conn) 60 + } 61 + 62 + s.conn.Export(nil, dbus.ObjectPath("/panels/shell/active"), "org.freedesktop.DBus.Introspectable") 63 + s.conn.Export(nil, dbus.ObjectPath("/panels/shell/active"), "net.kettek.Aight.Status") 64 + 65 + return nil 66 + } 67 + 68 + func (s *Shells) AddShell() error { 69 + id := len(s.shells) 70 + shell := NewTerm(fmt.Sprintf("shell/%d", id), cfg.Shell) 71 + shell.SetFocusFunc(func() { 72 + s.lastFocusedShell = id 73 + }) 74 + 75 + // Add focus handler. 76 + if err := s.conn.ExportMethodTable(map[string]any{ 77 + "SetFocus": func() *dbus.Error { 78 + if shell := s.GetShellByIndex(s.lastFocusedShell); shell != nil { 79 + s.setFocusFunc(shell) 80 + } 81 + return nil 82 + }, 83 + }, dbus.ObjectPath(fmt.Sprintf("/panels/shell/%d", id)), "net.kettek.Aight.Panel"); err != nil { 84 + return err 85 + } 86 + 87 + // Start 'er up. 88 + if err := shell.Start(*s.env); err != nil { 89 + return err 90 + } 91 + 92 + s.shells = append(s.shells, shell) 93 + 94 + // Add to UI. 95 + s.AddItem(shell, 0, false) 96 + 97 + return shell.AddToConn(s.conn) 98 + } 99 + 100 + func (s *Shells) GetShellByIndex(id int) *Term { 101 + if id > len(s.shells) { 102 + return nil 103 + } 104 + return s.shells[id] 105 + } 106 + 107 + func (s *Shells) GetLastFocusedShell() *Term { 108 + return s.GetShellByIndex(s.lastFocusedShell) 109 + }
+112
status.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "slices" 6 + "strings" 7 + 8 + "github.com/godbus/dbus/v5" 9 + "github.com/godbus/dbus/v5/introspect" 10 + "github.com/rivo/tview" 11 + ) 12 + 13 + const statusIntro = ` 14 + <node> 15 + <interface name="net.kettek.Aight.Status"> 16 + <method name="UpdateItem"> 17 + <arg name="ID" direction="in" type="s"/> 18 + <arg name="Name" direction="in" type="s"/> 19 + <arg name="Value" direction="in" type="s"/> 20 + </method> 21 + <method name="RemoveItem"> 22 + <arg name="ID" direction="in" type="s"/> 23 + </method> 24 + <method name="Refresh"></method> 25 + </interface>` + introspect.IntrospectDataString + ` 26 + </node> 27 + ` 28 + 29 + // TODO: Make configurable. 30 + var statusItemString = "[::b]%s[::B]%s\t" 31 + 32 + type StatusItem struct { 33 + ID string // If ID is set, only one instance of this item may exist and any calls to Add will just overwrite. 34 + Name string 35 + Value string 36 + } 37 + 38 + type Status struct { 39 + *tview.TextView 40 + items []StatusItem 41 + } 42 + 43 + func NewStatus() *Status { 44 + view := tview.NewTextView(). 45 + SetWrap(false). 46 + SetScrollable(true). 47 + SetDynamicColors(true) 48 + 49 + status := &Status{ 50 + TextView: view, 51 + } 52 + 53 + return status 54 + } 55 + 56 + func (s *Status) AddToConn(conn *dbus.Conn) error { 57 + if err := app.Conn.Export(s, "/status", "net.kettek.Aight.Status"); err != nil { 58 + return err 59 + } 60 + if err := app.Conn.Export(introspect.Introspectable(statusIntro), "/status", "org.freedesktop.DBus.Introspectable"); err != nil { 61 + return err 62 + } 63 + 64 + // Add DBUS connection ID to the statusbar. 65 + s.Add(StatusItem{"DBUS", "DBUS", conn.Names()[0]}) 66 + return nil 67 + } 68 + 69 + func (s *Status) RemoveFromConn(conn *dbus.Conn) { 70 + conn.Export(nil, "/status", "org.freedesktop.DBus.Introspectable") 71 + conn.Export(nil, "/status", "net.kettek.Aight.Status") 72 + } 73 + 74 + func (s *Status) Add(items ...StatusItem) { 75 + for _, v := range items { 76 + if v.ID != "" { 77 + index := slices.IndexFunc(s.items, func(item StatusItem) bool { 78 + return item.ID == v.ID 79 + }) 80 + if index != -1 { 81 + s.items[index] = v 82 + continue 83 + } 84 + } 85 + s.items = append(s.items, v) 86 + } 87 + s.Refresh() 88 + } 89 + 90 + func (s *Status) UpdateItem(id, name, value string) *dbus.Error { 91 + s.Add(StatusItem{id, name, value}) 92 + return nil 93 + } 94 + 95 + func (s *Status) RemoveItem(id string) *dbus.Error { 96 + if index := slices.IndexFunc(s.items, func(s StatusItem) bool { 97 + return s.ID == id 98 + }); index != -1 { 99 + s.items = append(s.items[:index], s.items[index+1:]...) 100 + return nil 101 + } 102 + return s.Refresh() 103 + } 104 + 105 + func (s *Status) Refresh() *dbus.Error { 106 + var t strings.Builder 107 + for _, item := range s.items { 108 + fmt.Fprintf(&t, statusItemString, item.Name, item.Value) 109 + } 110 + s.SetText(t.String()) 111 + return nil 112 + }
+340
term.go
··· 1 + package main 2 + 3 + import ( 4 + // "fmt" 5 + "errors" 6 + "fmt" 7 + "os" 8 + "os/exec" 9 + "strings" 10 + 11 + "github.com/gdamore/tcell/v2" 12 + "github.com/godbus/dbus/v5" 13 + "github.com/godbus/dbus/v5/introspect" 14 + "github.com/kettek/terminal" 15 + "github.com/rivo/tview" 16 + ) 17 + 18 + const termIntro = ` 19 + <node> 20 + <interface name="net.kettek.Aight.Panel"> 21 + <method name="SetFocus"></method> 22 + <method name="SendString"> 23 + <arg direction="in" type="s"/> 24 + </method> 25 + <method name="SendBytes"> 26 + <arg direction="in" type="ay"/> 27 + </method> 28 + </interface>` + introspect.IntrospectDataString + ` 29 + </node> 30 + ` 31 + 32 + type Term struct { 33 + *tview.Box 34 + id string 35 + command []string 36 + vt *terminal.VT 37 + closed bool 38 + env []string 39 + state terminal.State 40 + lastErr error 41 + lastW, lastH int 42 + } 43 + 44 + func NewTerm(id string, command ...string) *Term { 45 + term := &Term{ 46 + id: id, 47 + Box: tview.NewBox(), 48 + command: command, 49 + } 50 + 51 + if err := app.Conn.Export(term, dbus.ObjectPath("/panels/"+id), "net.kettek.Aight.Panel"); err != nil { 52 + panic(err) 53 + } 54 + if err := app.Conn.Export(introspect.Introspectable(termIntro), dbus.ObjectPath("/panels/"+id), "org.freedesktop.DBus.Introspectable"); err != nil { 55 + panic(err) 56 + } 57 + 58 + return term 59 + } 60 + 61 + func (t *Term) AddToConn(conn *dbus.Conn) error { 62 + if err := app.Conn.Export(t, dbus.ObjectPath("/panels/"+t.id), "net.kettek.Aight.Panel"); err != nil { 63 + panic(err) 64 + } 65 + if err := app.Conn.Export(introspect.Introspectable(termIntro), dbus.ObjectPath("/panels/"+t.id), "org.freedesktop.DBus.Introspectable"); err != nil { 66 + panic(err) 67 + } 68 + return nil 69 + } 70 + 71 + func (t *Term) RemoveFromConn(conn *dbus.Conn) { 72 + conn.Export(nil, dbus.ObjectPath("/panels/"+t.id), "org.freedesktop.DBus.Introspectable") 73 + conn.Export(nil, dbus.ObjectPath("/panels/"+t.id), "net.kettek.Aight.Status") 74 + } 75 + 76 + func (t *Term) Start(env []string) error { 77 + t.env = env 78 + cmd := exec.Command(t.command[0], t.command[1:]...) 79 + cmd.Env = append(cmd.Env, os.Environ()...) 80 + cmd.Env = append(cmd.Env, env...) 81 + 82 + term, _, err := terminal.Start(&t.state, cmd) 83 + if err != nil { 84 + t.lastErr = err 85 + return err 86 + } 87 + t.vt = term 88 + t.vt.Resize(t.lastW, t.lastH) 89 + 90 + go func() { 91 + t.closed = false 92 + for { 93 + err := term.Parse() 94 + if err != nil { 95 + t.lastErr = errors.Join(errors.New("Program exited."), err) 96 + app.QueueUpdateDraw(func() { 97 + t.closed = true 98 + term.File().Close() 99 + term.Close() 100 + }) 101 + break 102 + } 103 + app.QueueUpdateDraw(func() {}) 104 + } 105 + }() 106 + 107 + return nil 108 + } 109 + 110 + func (t *Term) Write(str string) { 111 + t.vt.File().WriteString(str) 112 + } 113 + 114 + func (t *Term) SendString(s string) *dbus.Error { 115 + if _, err := t.vt.File().WriteString(s); err != nil { 116 + return dbus.NewError("Send", []any{err.Error()}) 117 + } 118 + return nil 119 + } 120 + 121 + func (t *Term) SendBytes(b []byte) *dbus.Error { 122 + if _, err := t.vt.File().Write(b); err != nil { 123 + return dbus.NewError("Send", []any{err.Error()}) 124 + } 125 + return nil 126 + } 127 + 128 + func (t *Term) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { 129 + return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { 130 + if t.closed { 131 + t.Start(t.env) 132 + return 133 + } 134 + 135 + key := event.Key() 136 + 137 + switch key { 138 + case tcell.KeyUp: 139 + t.Write(string([]byte{0x1b, 0x5b, 0x41})) 140 + case tcell.KeyDown: 141 + t.Write(string([]byte{0x1b, 0x5b, 0x42})) 142 + case tcell.KeyLeft: 143 + t.Write(string([]byte{0x1b, 0x5b, 0x44})) 144 + case tcell.KeyRight: 145 + t.Write(string([]byte{0x1b, 0x5b, 0x43})) 146 + case tcell.KeyRune: 147 + t.Write(string(event.Rune())) 148 + default: 149 + if key >= tcell.KeyCtrlSpace && key <= tcell.KeyCtrlUnderscore { // This might have to change... 150 + t.Write(string([]byte{byte(key - tcell.KeyCtrlSpace)})) 151 + return 152 + } 153 + // Just send others, I guess... 154 + t.Write(string([]byte{byte(key)})) 155 + } 156 + }) 157 + } 158 + 159 + func (t *Term) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { 160 + return func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { 161 + if t.closed { 162 + if action != tview.MouseMove || cfg.UI.FocusFollowsMouse { 163 + setFocus(t) 164 + consumed = true 165 + } 166 + return 167 + } 168 + 169 + x, y := event.Position() 170 + if !t.InRect(x, y) { 171 + return false, nil 172 + } 173 + 174 + rectX, rectY, _, _ := t.GetInnerRect() 175 + ix, iy := x-rectX+1, y-rectY+1 176 + 177 + if t.state.Mode(terminal.ModeMouseSgr) { 178 + msg := []byte{0x1b, 0x5b, '<'} 179 + var motion bool 180 + var release bool 181 + var btn uint 182 + 183 + switch action { 184 + case tview.MouseMove: 185 + btns := event.Buttons() 186 + if btns&tcell.ButtonPrimary != 0 { 187 + btn = 0 | (1 << 5) 188 + } else if btns&tcell.ButtonSecondary != 0 { 189 + btn = 2 | (1 << 5) 190 + } else if btns&tcell.ButtonMiddle != 0 { 191 + btn = 1 | (1 << 5) 192 + } else { 193 + btn = 0x03 | (1 << 5) 194 + motion = true 195 + } 196 + case tview.MouseLeftDown: 197 + btn = 0 198 + case tview.MouseLeftUp: 199 + btn = 0 200 + release = true 201 + case tview.MouseRightDown: 202 + btn = 2 203 + case tview.MouseRightUp: 204 + btn = 2 205 + release = true 206 + case tview.MouseMiddleDown: 207 + btn = 1 208 + case tview.MouseMiddleUp: 209 + btn = 1 210 + release = true 211 + case tview.MouseScrollUp: 212 + btn = 0x40 213 + case tview.MouseScrollDown: 214 + btn = 0x41 215 + case tview.MouseScrollLeft: 216 + btn = 0x42 217 + case tview.MouseScrollRight: 218 + btn = 0x43 219 + } 220 + if !motion || cfg.UI.FocusFollowsMouse { 221 + setFocus(t) 222 + consumed = true 223 + } 224 + msg = append(msg, fmt.Sprintf("%d;%d;%d", btn, ix, iy)...) 225 + if release { 226 + msg = append(msg, 'm') 227 + } else { 228 + msg = append(msg, 'M') 229 + } 230 + if len(msg) > 3 { 231 + t.SendBytes(msg) 232 + } 233 + } else { 234 + if action == tview.MouseLeftClick { 235 + setFocus(t) 236 + consumed = true 237 + } 238 + } 239 + return 240 + } 241 + } 242 + 243 + func (t *Term) PasteHandler() func(pastedText string, setFocus func(p tview.Primitive)) { 244 + return t.WrapPasteHandler(func(pastedText string, setFocus func(p tview.Primitive)) { 245 + if t.closed { 246 + t.Start(t.env) 247 + return 248 + } 249 + if t.state.Mode(terminal.ModeBracketedPaste) { // Send it as a bracketed paste. 250 + t.Write(string([]byte{0x1b, 0x5b, '2', '0', '0', '~'})) 251 + t.Write(pastedText) 252 + t.Write(string([]byte{0x1b, 0x5b, '2', '0', '1', '~'})) 253 + } else { // Otherwise just write it out! 254 + t.Write(pastedText) 255 + } 256 + }) 257 + } 258 + 259 + func (t *Term) Draw(screen tcell.Screen) { 260 + t.DrawForSubclass(screen, t) 261 + x, y, w, h := t.GetInnerRect() 262 + 263 + if t.closed { 264 + lines := strings.Split(t.lastErr.Error(), "\n") 265 + y1 := 0 266 + for _, line := range lines { 267 + screen.PutStr(x, y+y1, line) 268 + y1++ 269 + } 270 + screen.PutStrStyled(x, y+y1, "Press any key to run again.", tcell.StyleDefault.Bold(true)) 271 + return 272 + } 273 + 274 + if t.lastW != w || t.lastH != h { 275 + t.lastW = w 276 + t.lastH = h 277 + t.vt.Resize(w, h) 278 + } 279 + 280 + if t.state.Changed(terminal.ChangedScreen) { 281 + for x1 := range w { 282 + for y1 := range h { 283 + ch, fg, bg, attr := t.state.Cell(x1, y1) 284 + 285 + var tfg, tbg tcell.Color 286 + 287 + if fg == terminal.DefaultFG { 288 + tfg = tcell.ColorDefault 289 + } else if fg&terminal.RGBColor != 0 { 290 + r, g, b := fg.RGB() 291 + tfg = tcell.NewRGBColor(int32(r), int32(g), int32(b)) 292 + } else { 293 + tfg = tcell.PaletteColor(int(fg)) 294 + } 295 + if bg == terminal.DefaultBG { 296 + tbg = tcell.ColorDefault 297 + } else if bg&terminal.RGBColor != 0 { 298 + r, g, b := bg.RGB() 299 + tbg = tcell.NewRGBColor(int32(r), int32(g), int32(b)) 300 + } else { 301 + tbg = tcell.PaletteColor(int(bg)) 302 + } 303 + 304 + style := (tcell.Style{}).Background(tbg).Foreground(tfg) 305 + 306 + if attr&terminal.AttrBold != 0 { 307 + style = style.Bold(true) 308 + } 309 + if attr&terminal.AttrDim != 0 { 310 + style = style.Dim(true) 311 + } 312 + if attr&terminal.AttrBlink != 0 { 313 + style = style.Blink(true) 314 + } 315 + if attr&terminal.AttrUnderline != 0 { 316 + style = style.Underline(true) 317 + } 318 + if attr&terminal.AttrItalic != 0 { 319 + style = style.Italic(true) 320 + } 321 + if attr&terminal.AttrReverse != 0 { 322 + style = style.Reverse(true) 323 + } 324 + if attr&terminal.AttrStrikethrough != 0 { 325 + style = style.StrikeThrough(true) 326 + } 327 + 328 + screen.SetContent(x+x1, y+y1, ch, nil, style) 329 + } 330 + } 331 + } 332 + 333 + // Draw the cursor, I s'pose. 334 + if t.state.CursorVisible() { 335 + cx, cy := t.state.Cursor() 336 + screen.ShowCursor(x+cx, y+cy) 337 + } else { 338 + screen.ShowCursor(-1, -1) 339 + } 340 + }
+5
topics.go
··· 1 + package main 2 + 3 + const ( 4 + UI_CANCEL = "ui.cancel" 5 + )