summary refs log tree commit diff stats
path: root/TODO
blob: 55b31290319c5cbb54dcb4c8138cceac7426b611 (plain) (blame)
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
Console

   (X) #0   09/12/06  console commands
   (X) #1   09/12/06  quick find
   (X) #2   09/12/06  open with
   (X) #4   09/12/06  history for console
   (X) #13  09/12/27  display docstring of a command


General

   (X) #5   09/12/06  move code from fm into objects
   (X) #6   09/12/06  move main to __init__
   (X) #7   09/12/06  cooler titlebar
   (X) #8   09/12/17  Add operations to modify files/directories
   (X) #9   09/12/24  add a widget for managing running operations
   (X) #10  09/12/24  sorting
   (X) #11  09/12/27  filter
   (X) #12  09/12/27  jump through the list in a specific order
   (X) #14  09/12/29  make filelists inherit from pagers
   (X) #15  09/12/29  better way of running processes!!~
   (X) #16  10/01/01  list of bookmarks
   (X) #21  10/01/01  write help!
   (X) #22  10/01/03  add getopt options to change flags/mode
   (X) #29  10/01/06  add chmod command
   (X) #30  10/01/06  add a way to create symlinks
   (X) #32  10/01/08  place the (hidden) cursor to a meaningful position
   (X) #34  10/01/09  display free disk space
   (X) #35  10/01/09  display disk usage of files in current directory
   ( ) #36  10/01/11  help coloring is terribly inefficient
   (X) #37  10/01/13  better tab completion for OpenConsole
   ( ) #38  10/01/16  searching in pager
   (X) #39  10/01/17  flushinput not always good
   (X) #42  10/01/17  memorize directory for `` when using :cd
   (X) #43  10/01/18  internally treat the bookmarks ` and ' the same
   ( ) #44  10/01/18  more error messages :P
   (X) #47  10/01/19  less restricive auto preview
   (X) #48  10/01/19  abbreviate commands with first unambiguous substring
   ( ) #50  10/01/19  add more unit tests
   ( ) #51  10/01/21  remove directory.marked_items ?
   ( ) #55  10/01/24  allow change of filename when pasting
   ( ) #56  10/01/30  warn before deleting mount points
   ( ) #57  10/01/30  warn before deleting unseen marked files
   (X) #58  10/02/04  change the title of the terminal
   ( ) #61  10/02/09  show sum of size of marked files


Bugs

   (X) #17  10/01/01  why do bookmarks disappear sometimes?
   (X) #18  10/01/01  fix notify widget (by adding a LogView?)
   (X) #19  10/01/01  resizing after pressing g
   (X) #23  10/01/04  stop dir loading with ^C -> wont load anymore
   (X) #25  10/01/06  directories sometimes dont reload correctly
   (X) #26  10/01/06  :delete on symlinks of directories fails
   (X) #31  10/01/06  ^C breaks cd-after-exit by stopping sourced shell script
   ( ) #40  10/01/17  freeze with unavailable sshfs
   (X) #41  10/01/17  capital file extensions are not recognized
   ( ) #46  10/01/19  old username displayed after using su
   (X) #49  10/01/19  fix unit tests :'(
   ( ) #52  10/01/23  special characters in tab completion
   (X) #54  10/01/23  max_dirsize_for_autopreview not working
   ( ) #60  10/02/05  utf support improvable


Ideas

   ( ) #20  10/01/01  use inotify to monitor filesystem changes
   ( ) #24  10/01/06  progress bar
   (X) #27  10/01/06  hide bookmarks in list which contain hidden dir
   (X) #28  10/01/06  use regexp instead of string for searching
   ( ) #33  10/01/08  accelerate mousewheel speed
   ( ) #45  10/01/18  hooks for events like setting changes
   ( ) #53  10/01/23  merge fm and environment


Goals for next minor version

   (X) #54  10/01/23  max_dirsize_for_autopreview not working
   ( ) #55  10/01/24  allow change of filename when pasting
   ( ) #61  10/02/09  show sum of size of marked files
href='#n348'>348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
package widgets

import (
	gocolor "image/color"
	"os"
	"os/exec"
	"sync"

	"git.sr.ht/~sircmpwn/aerc2/lib/ui"

	"git.sr.ht/~sircmpwn/go-libvterm"
	"github.com/gdamore/tcell"
	"github.com/kr/pty"
)

type vtermKey struct {
	Key  vterm.Key
	Rune rune
	Mod  vterm.Modifier
}

var keyMap map[tcell.Key]vtermKey

func directKey(key vterm.Key) vtermKey {
	return vtermKey{key, 0, vterm.ModNone}
}

func runeMod(r rune, mod vterm.Modifier) vtermKey {
	return vtermKey{vterm.KeyNone, r, mod}
}

func keyMod(key vterm.Key, mod vterm.Modifier) vtermKey {
	return vtermKey{key, 0, mod}
}

func init() {
	keyMap = make(map[tcell.Key]vtermKey)
	keyMap[tcell.KeyCtrlSpace] = runeMod(' ', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlA] = runeMod('a', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlB] = runeMod('b', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlC] = runeMod('c', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlD] = runeMod('d', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlE] = runeMod('e', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlF] = runeMod('f', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlG] = runeMod('g', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlH] = runeMod('h', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlI] = runeMod('i', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlJ] = runeMod('j', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlK] = runeMod('k', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlL] = runeMod('l', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlM] = runeMod('m', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlN] = runeMod('n', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlO] = runeMod('o', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlP] = runeMod('p', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlQ] = runeMod('q', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlR] = runeMod('r', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlS] = runeMod('s', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlT] = runeMod('t', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlU] = runeMod('u', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlV] = runeMod('v', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlW] = runeMod('w', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlX] = runeMod('x', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlY] = runeMod('y', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlZ] = runeMod('z', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlBackslash] = runeMod('\\', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlCarat] = runeMod('^', vterm.ModCtrl)
	keyMap[tcell.KeyCtrlUnderscore] = runeMod('_', vterm.ModCtrl)
	keyMap[tcell.KeyEnter] = directKey(vterm.KeyEnter)
	keyMap[tcell.KeyTab] = directKey(vterm.KeyTab)
	keyMap[tcell.KeyBackspace] = directKey(vterm.KeyBackspace)
	keyMap[tcell.KeyEscape] = directKey(vterm.KeyEscape)
	keyMap[tcell.KeyUp] = directKey(vterm.KeyUp)
	keyMap[tcell.KeyDown] = directKey(vterm.KeyDown)
	keyMap[tcell.KeyLeft] = directKey(vterm.KeyLeft)
	keyMap[tcell.KeyRight] = directKey(vterm.KeyRight)
	keyMap[tcell.KeyInsert] = directKey(vterm.KeyIns)
	keyMap[tcell.KeyDelete] = directKey(vterm.KeyDel)
	keyMap[tcell.KeyHome] = directKey(vterm.KeyHome)
	keyMap[tcell.KeyEnd] = directKey(vterm.KeyEnd)
	keyMap[tcell.KeyPgUp] = directKey(vterm.KeyPageUp)
	keyMap[tcell.KeyPgDn] = directKey(vterm.KeyPageDown)
	for i := 0; i < 64; i++ {
		keyMap[tcell.Key(int(tcell.KeyF1)+i)] =
			directKey(vterm.Key(int(vterm.KeyFunction0) + i))
	}
	keyMap[tcell.KeyTAB] = directKey(vterm.KeyTab)
	keyMap[tcell.KeyESC] = directKey(vterm.KeyEscape)
	keyMap[tcell.KeyDEL] = directKey(vterm.KeyBackspace)
}

type Terminal struct {
	closed       bool
	cmd          *exec.Cmd
	colors       map[tcell.Color]tcell.Color
	ctx          *ui.Context
	cursorPos    vterm.Pos
	cursorShown  bool
	damage       []vterm.Rect
	err          error
	focus        bool
	mutex        sync.Mutex
	onInvalidate func(d ui.Drawable)
	pty          *os.File
	start        chan interface{}
	vterm        *vterm.VTerm

	OnClose func(err error)
	OnTitle func(title string)
}

func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
	term := &Terminal{
		cursorShown: true,
	}
	term.cmd = cmd
	term.vterm = vterm.New(24, 80)
	term.vterm.SetUTF8(true)
	term.start = make(chan interface{})
	screen := term.vterm.ObtainScreen()
	go func() {
		<-term.start
		buf := make([]byte, 4096)
		for {
			n, err := term.pty.Read(buf)
			if err != nil || term.closed {
				// These are generally benine errors when the process exits
				term.Close(nil)
				return
			}
			n, err = term.vterm.Write(buf[:n])
			if err != nil {
				term.Close(err)
				return
			}
			screen.Flush()
			term.flushTerminal()
			term.Invalidate()
		}
	}()
	screen.OnDamage = term.onDamage
	screen.OnMoveCursor = term.onMoveCursor
	screen.OnSetTermProp = term.onSetTermProp
	screen.EnableAltScreen(true)
	screen.Reset(true)

	state := term.vterm.ObtainState()
	term.colors = make(map[tcell.Color]tcell.Color)
	for i := 0; i < 256; i += 1 {
		tcolor := tcell.Color(i)
		var r uint8 = 0
		var g uint8 = 0
		var b uint8 = uint8(i + 1)
		if i < 16 {
			// Set the first 16 colors to predictable near-black RGB values
			state.SetPaletteColor(i,
				vterm.NewVTermColorRGB(gocolor.RGBA{r, g, b, 255}))
		} else {
			// The rest use RGB
			vcolor := state.GetPaletteColor(i)
			r, g, b = vcolor.GetRGB()
		}
		term.colors[tcell.NewRGBColor(int32(r), int32(g), int32(b))] = tcolor
	}
	fg, bg := state.GetDefaultColors()
	r, g, b := bg.GetRGB()
	term.colors[tcell.NewRGBColor(
		int32(r), int32(g), int32(b))] = tcell.ColorDefault
	r, g, b = fg.GetRGB()
	term.colors[tcell.NewRGBColor(
		int32(r), int32(g), int32(b))] = tcell.ColorDefault

	return term, nil
}

func (term *Terminal) flushTerminal() {
	buf := make([]byte, 4096)
	for {
		n, err := term.vterm.Read(buf)
		if err != nil {
			term.Close(err)
			return
		}
		if n == 0 {
			break
		}
		n, err = term.pty.Write(buf[:n])
		if err != nil {
			term.Close(err)
			return
		}
	}
}

func (term *Terminal) Close(err error) {
	term.mutex.Lock()
	defer term.mutex.Unlock()
	term.err = err
	if term.vterm != nil {
		term.vterm.Close()
		term.vterm = nil
	}
	if term.pty != nil {
		term.pty.Close()
		term.pty = nil
	}
	if term.cmd != nil && term.cmd.Process != nil {
		term.cmd.Process.Kill()
		term.cmd = nil
	}
	if !term.closed && term.OnClose != nil {
		term.OnClose(err)
	}
	term.closed = true
	term.ctx.HideCursor()
}

func (term *Terminal) OnInvalidate(cb func(d ui.Drawable)) {
	term.onInvalidate = cb
}

func (term *Terminal) Invalidate() {
	if term.onInvalidate != nil {
		term.onInvalidate(term)
	}
}

func (term *Terminal) Draw(ctx *ui.Context) {
	if term.closed {
		if term.err != nil {
			ui.NewText(term.err.Error()).Strategy(ui.TEXT_CENTER).Draw(ctx)
		} else {
			ui.NewText("Terminal closed").Strategy(ui.TEXT_CENTER).Draw(ctx)
		}
		return
	}

	term.mutex.Lock()
	defer term.mutex.Unlock()

	winsize := pty.Winsize{
		Cols: uint16(ctx.Width()),
		Rows: uint16(ctx.Height()),
	}

	if term.pty == nil {
		term.vterm.SetSize(ctx.Height(), ctx.Width())
		tty, err := pty.StartWithSize(term.cmd, &winsize)
		term.pty = tty
		if err != nil {
			term.mutex.Unlock()
			term.Close(err)
			return
		}
		term.start <- nil
	}

	term.ctx = ctx // gross

	rows, cols, err := pty.Getsize(term.pty)
	if err != nil {
		return
	}
	if ctx.Width() != cols || ctx.Height() != rows {
		pty.Setsize(term.pty, &winsize)
		term.vterm.SetSize(ctx.Height(), ctx.Width())
		return
	}

	screen := term.vterm.ObtainScreen()

	type coords struct {
		x int
		y int
	}

	// naive optimization
	visited := make(map[coords]interface{})

	for _, rect := range term.damage {
		for x := rect.StartCol(); x < rect.EndCol() && x < ctx.Width(); x += 1 {

			for y := rect.StartCol(); y < rect.EndCol() && y < ctx.Height(); y += 1 {

				coords := coords{x, y}
				if _, ok := visited[coords]; ok {
					continue
				}
				visited[coords] = nil

				cell, err := screen.GetCellAt(y, x)
				if err != nil {
					continue
				}
				style := term.styleFromCell(cell)
				ctx.Printf(x, y, style, "%s", string(cell.Chars()))
			}
		}
	}

	if term.focus {
		if !term.cursorShown {
			ctx.HideCursor()
		} else {
			state := term.vterm.ObtainState()
			row, col := state.GetCursorPos()
			ctx.SetCursor(col, row)
		}
	}
}

func (term *Terminal) Focus(focus bool) {
	term.focus = focus
	if term.ctx != nil {
		if !term.focus {
			term.ctx.HideCursor()
		} else {
			state := term.vterm.ObtainState()
			row, col := state.GetCursorPos()
			term.ctx.SetCursor(col, row)
		}
	}
}

func convertMods(mods tcell.ModMask) vterm.Modifier {
	var (
		ret  uint = 0
		mask uint = uint(mods)
	)
	if mask&uint(tcell.ModShift) > 0 {
		ret |= uint(vterm.ModShift)
	}
	if mask&uint(tcell.ModCtrl) > 0 {
		ret |= uint(vterm.ModCtrl)
	}
	if mask&uint(tcell.ModAlt) > 0 {
		ret |= uint(vterm.ModAlt)
	}
	return vterm.Modifier(ret)
}

func (term *Terminal) Event(event tcell.Event) bool {
	switch event := event.(type) {
	case *tcell.EventKey:
		if event.Key() == tcell.KeyRune {
			term.vterm.KeyboardUnichar(
				event.Rune(), convertMods(event.Modifiers()))
		} else {
			if key, ok := keyMap[event.Key()]; ok {
				if key.Key == vterm.KeyNone {
					term.vterm.KeyboardUnichar(
						key.Rune, key.Mod)
				} else if key.Mod == vterm.ModNone {
					term.vterm.KeyboardKey(key.Key,
						convertMods(event.Modifiers()))
				} else {
					term.vterm.KeyboardKey(key.Key, key.Mod)
				}
			}
		}
		term.flushTerminal()
	}
	return false
}

func (term *Terminal) styleFromCell(cell *vterm.ScreenCell) tcell.Style {
	style := tcell.StyleDefault

	background := cell.Bg()
	r, g, b := background.GetRGB()
	bg := tcell.NewRGBColor(int32(r), int32(g), int32(b))
	foreground := cell.Fg()
	r, g, b = foreground.GetRGB()
	fg := tcell.NewRGBColor(int32(r), int32(g), int32(b))

	if color, ok := term.colors[bg]; ok {
		style = style.Background(color)
	} else {
		style = style.Background(bg)
	}
	if color, ok := term.colors[fg]; ok {
		style = style.Foreground(color)
	} else {
		style = style.Foreground(fg)
	}

	if cell.Attrs().Bold != 0 {
		style = style.Bold(true)
	}
	if cell.Attrs().Underline != 0 {
		style = style.Underline(true)
	}
	if cell.Attrs().Blink != 0 {
		style = style.Blink(true)
	}
	if cell.Attrs().Reverse != 0 {
		style = style.Reverse(true)
	}
	return style
}

func (term *Terminal) onDamage(rect *vterm.Rect) int {
	term.damage = append(term.damage, *rect)
	term.Invalidate()
	return 1
}

func (term *Terminal) onMoveCursor(old *vterm.Pos,
	pos *vterm.Pos, visible bool) int {

	rows, cols, _ := pty.Getsize(term.pty)
	if pos.Row() >= rows || pos.Col() >= cols {
		return 1
	}

	term.cursorPos = *pos
	term.Invalidate()
	return 1
}

func (term *Terminal) onSetTermProp(prop int, val *vterm.VTermValue) int {
	switch prop {
	case vterm.VTERM_PROP_TITLE:
		if term.OnTitle != nil {
			term.OnTitle(val.String)
		}
	case vterm.VTERM_PROP_CURSORVISIBLE:
		term.cursorShown = val.Boolean
		term.Invalidate()
	}
	return 1
}