diff --git a/README.md b/README.md
index f7f4cc13..3d6cb81f 100644
--- a/README.md
+++ b/README.md
@@ -97,8 +97,6 @@ Currently at a **pre-beta** level (**DON'T RECOMMEND USING RIGHT NOW** -- come b
* drag on textview should prevent DND -- dnd not getting "processed"
* TextView:
- + array of select regions -- for search fill 'em all up! draw in a loop with bounds-checking
- + draw bg at least even when empty -- blank proj should look like something is there
+ losing line numbers when typing new text, in some select cases
+ word-level functions: forward, back, delete etc. ctrl+ backspace is back.
+ optimize wraparound multi-span rendering -- if no change in start of line, don't re render!
diff --git a/giv/tabview.go b/giv/tabview.go
index 8afe1e48..f4cc082a 100644
--- a/giv/tabview.go
+++ b/giv/tabview.go
@@ -38,6 +38,15 @@ var TabViewProps = ki.Props{
"height": units.NewValue(10, units.Em),
}
+// NTabs returns number of tabs
+func (tv *TabView) NTabs() int {
+ fr := tv.Frame()
+ if fr == nil {
+ return 0
+ }
+ return len(fr.Kids)
+}
+
// AddTab adds a widget as a new tab, with given tab label, and returns the
// index of that tab
func (tv *TabView) AddTab(widg gi.Node2D, label string) int {
@@ -221,6 +230,9 @@ func (tv *TabView) InitTabView() {
if len(tv.Kids) == 2 {
return
}
+ if tv.Sty.Font.Size.Val == 0 { // not yet styled
+ tv.StyleLayout()
+ }
updt := tv.UpdateStart()
tv.Lay = gi.LayoutVert
@@ -229,6 +241,7 @@ func (tv *TabView) InitTabView() {
tabs.SetStretchMaxWidth()
// tabs.SetStretchMaxHeight()
tabs.SetMinPrefWidth(units.NewValue(10, units.Em))
+ tabs.SetProp("height", units.NewValue(1.8, units.Em))
tabs.SetProp("overflow", "hidden") // no scrollbars!
tabs.SetProp("padding", units.NewValue(0, units.Px))
tabs.SetProp("margin", units.NewValue(0, units.Px))
@@ -277,6 +290,11 @@ func (tv *TabView) RenumberTabs() {
}
}
+func (tv *TabView) Style2D() {
+ tv.InitTabView()
+ tv.Layout.Style2D()
+}
+
////////////////////////////////////////////////////////////////////////////////////////
// TabButton
diff --git a/giv/textbuf.go b/giv/textbuf.go
index 907147bb..c310e203 100644
--- a/giv/textbuf.go
+++ b/giv/textbuf.go
@@ -94,18 +94,19 @@ func (tb *TextBuf) Text() []byte {
return tb.Txt
}
+// Refresh signals any views to refresh views
+func (tb *TextBuf) Refresh() {
+ tb.TextBufSig.Emit(tb.This, int64(TextBufNew), tb.Txt)
+}
+
// todo: use https://github.com/andybalholm/crlf to deal with cr/lf etc --
// internally just use lf = \n
// New initializes a new buffer with n blank lines
func (tb *TextBuf) New(nlines int) {
- if cap(tb.Lines) >= nlines {
- tb.Lines = tb.Lines[:nlines]
- } else {
- tb.Lines = make([][]rune, nlines)
- if nlines == 1 {
- tb.Lines[0] = []rune("")
- }
+ tb.Lines = make([][]rune, nlines)
+ if nlines == 1 {
+ tb.Lines[0] = []rune("")
}
if cap(tb.ByteOffs) >= nlines {
tb.ByteOffs = tb.ByteOffs[:nlines]
@@ -113,6 +114,7 @@ func (tb *TextBuf) New(nlines int) {
tb.ByteOffs = make([]int, nlines)
}
tb.NLines = nlines
+ tb.Refresh()
}
// Open loads text from a file into the buffer
@@ -367,6 +369,16 @@ type TextRegion struct {
End TextPos
}
+// NewTextRegionLen makes a new TextRegion from a starting point and a length
+// along same line
+func NewTextRegionLen(start TextPos, len int) TextRegion {
+ reg := TextRegion{}
+ reg.Start = start
+ reg.End = start
+ reg.End.Ch += len
+ return reg
+}
+
var TextRegionZero = TextRegion{}
// TextBufEdit describes an edit action to a buffer -- this is the data passed
diff --git a/giv/textview.go b/giv/textview.go
index bd1e364d..33487ee9 100644
--- a/giv/textview.go
+++ b/giv/textview.go
@@ -68,12 +68,13 @@ type TextView struct {
CursorCol int `json:"-" xml:"-" desc:"desired cursor column -- where the cursor was last when moved using left / right arrows -- used when doing up / down to not always go to short line columns"`
SelectReg TextRegion `json:"-" xml:"-" desc:"current selection region"`
PrevSelectReg TextRegion `json:"-" xml:"-" desc:"previous selection region, that was actually rendered -- needed to update render"`
+ Highlights []TextRegion `json:"-" xml:"-" desc:"highlighed regions, e.g., for search results"`
SelectMode bool `json:"-" xml:"-" desc:"if true, select text as cursor moves"`
ISearchMode bool `json:"-" xml:"-" desc:"if true, in interactive search mode"`
ISearchString string `json:"-" xml:"-" desc:"current interactive search string"`
ISearchCase bool `json:"-" xml:"-" desc:"pay attention to case in isearch -- triggered by typing an upper-case letter"`
- ISearchMatches []TextPos `json:"-" xml:"-" desc:"current i-search matches"`
- ISearchPos int `json:"-" xml:"-" desc:"position within isearch matches"`
+ SearchMatches []TextPos `json:"-" xml:"-" desc:"current search matches"`
+ SearchPos int `json:"-" xml:"-" desc:"position within isearch matches"`
PrevISearchString string `json:"-" xml:"-" desc:"previous interactive search string"`
PrevISearchCase bool `json:"-" xml:"-" desc:"prev: pay attention to case in isearch -- triggered by typing an upper-case letter"`
TextViewSig ki.Signal `json:"-" xml:"-" view:"-" desc:"signal for text viewt -- see TextViewSignals for the types"`
@@ -121,6 +122,9 @@ var TextViewProps = ki.Props{
TextViewSelectors[TextViewSel]: ki.Props{
"background-color": &gi.Prefs.Colors.Select,
},
+ TextViewSelectors[TextViewHighlight]: ki.Props{
+ "background-color": &gi.Prefs.Colors.Highlight,
+ },
}
// TextViewSignals are signals that text view can send
@@ -158,16 +162,19 @@ const (
// inactive -- not editable
TextViewInactive
- // selected -- for inactive state, can select entire element
+ // selected
TextViewSel
+ // highlighted
+ TextViewHighlight
+
TextViewStatesN
)
//go:generate stringer -type=TextViewStates
// Style selector names for the different states
-var TextViewSelectors = []string{":active", ":focus", ":inactive", ":selected"}
+var TextViewSelectors = []string{":active", ":focus", ":inactive", ":selected", ":highlight"}
// Label returns the display label for this node, satisfying the Labeler interface
func (tv *TextView) Label() string {
@@ -185,10 +192,8 @@ func (tv *TextView) EditDone() {
// Refresh re-displays everything anew from the buffer
func (tv *TextView) Refresh() {
- tv.SelectReset()
tv.LayoutAllLines(false)
- tv.SetFullReRender()
- tv.UpdateSig()
+ tv.RenderAllLines()
}
func (tv *TextView) IsChanged() bool {
@@ -205,7 +210,11 @@ func (tv *TextView) IsChanged() bool {
func (tv *TextView) SetBuf(buf *TextBuf) {
tv.Buf = buf
buf.AddView(tv)
- tv.Refresh()
+ tv.SelectReset()
+ tv.Highlights = nil
+ tv.LayoutAllLines(false)
+ tv.SetFullReRender()
+ tv.UpdateSig()
}
// TextViewBufSigRecv receives a signal from the buffer and updates view accordingly
@@ -386,7 +395,7 @@ func (tv *TextView) LayoutAllLines(inLayout bool) bool {
}
tv.VisSizes()
- sz := tv.RenderSize()
+ sz := tv.RenderSz
// fmt.Printf("rendersize: %v\n", sz)
sty := &tv.Sty
fst := sty.Font
@@ -880,36 +889,58 @@ func (tv *TextView) Redo() {
///////////////////////////////////////////////////////////////////////////////
// Search / Find
+// FindMatches finds the matches with given search string (literal, not regex)
+// and case sensitivity, updates highlights for all. returns false if none
+// found
+func (tv *TextView) FindMatches(find string, useCase bool) bool {
+ fsz := len(find)
+ if fsz == 0 {
+ tv.Highlights = nil
+ return false
+ }
+ if useCase {
+ _, tv.SearchMatches = tv.Buf.Search(find)
+ } else {
+ _, tv.SearchMatches = tv.Buf.SearchCI(find)
+ }
+ matches := tv.SearchMatches
+ if len(matches) == 0 {
+ tv.Highlights = nil
+ return false
+ }
+ hi := make([]TextRegion, len(matches))
+ for i, m := range matches {
+ hi[i] = NewTextRegionLen(m, fsz)
+ }
+ tv.Highlights = hi
+ tv.Refresh()
+ return true
+}
+
+// ISearchMatches finds ISearch matches -- returns true if there are any
+func (tv *TextView) ISearchMatches() bool {
+ return tv.FindMatches(tv.ISearchString, tv.ISearchCase)
+}
+
// ISearchSig sends the signal that ISearch is updated
func (tv *TextView) ISearchSig() {
tv.TextViewSig.Emit(tv.This, int64(TextViewISearch), tv.CursorPos)
}
-// ISearchFindMatches finds the matches with current search string
-func (tv *TextView) ISearchFindMatches() {
- if tv.ISearchCase {
- _, tv.ISearchMatches = tv.Buf.Search(tv.ISearchString)
- } else {
- _, tv.ISearchMatches = tv.Buf.SearchCI(tv.ISearchString)
- }
-}
-
// ISearch is an emacs-style interactive search mode -- this is called when
// the search command itself is entered
func (tv *TextView) ISearch() {
if tv.ISearchMode {
if tv.ISearchString != "" { // already searching -- find next
- sz := len(tv.ISearchMatches)
+ sz := len(tv.SearchMatches)
if sz > 0 {
- if tv.ISearchPos < sz-1 {
- tv.ISearchPos++
+ if tv.SearchPos < sz-1 {
+ tv.SearchPos++
} else {
- tv.ISearchPos = 0
+ tv.SearchPos = 0
}
- pos := tv.ISearchMatches[tv.ISearchPos]
- tv.SelectReg.Start = pos
- tv.SelectReg.End = tv.SelectReg.Start
- tv.SelectReg.End.Ch += len(tv.ISearchString) // todo: select all!
+ pos := tv.SearchMatches[tv.SearchPos]
+ tv.SelectReg = NewTextRegionLen(pos, len(tv.ISearchString))
tv.SetCursor(pos)
tv.ScrollCursorToCenterIfHidden()
tv.RenderSelectLines()
@@ -920,8 +951,8 @@ func (tv *TextView) ISearch() {
tv.ISearchString = tv.PrevISearchString
tv.ISearchCase = tv.PrevISearchCase
tv.PrevISearchString = "" // prevents future resets
- tv.ISearchPos = -1
- tv.ISearchFindMatches()
+ tv.SearchPos = -1
+ tv.ISearchMatches()
tv.ISearch()
}
// nothing..
@@ -929,8 +960,8 @@ func (tv *TextView) ISearch() {
} else {
tv.ISearchMode = true
tv.ISearchCase = false
- tv.ISearchMatches = nil
- tv.ISearchPos = -1
+ tv.SearchMatches = nil
+ tv.SearchPos = -1
tv.ISearchSig()
}
}
@@ -945,20 +976,18 @@ func (tv *TextView) ISearchKeyInput(r rune) {
tv.ISearchCase = true
}
tv.ISearchString += string(r)
- tv.ISearchFindMatches()
- sz := len(tv.ISearchMatches)
+ tv.ISearchMatches()
+ sz := len(tv.SearchMatches)
if sz == 0 {
- tv.ISearchPos = -1
+ tv.SearchPos = -1
tv.ISearchSig()
return
}
got := false
- for i, pos := range tv.ISearchMatches {
+ for i, pos := range tv.SearchMatches {
if pos.Ln >= tv.CursorPos.Ln {
- tv.ISearchPos = i
- tv.SelectReg.Start = pos
- tv.SelectReg.End = tv.SelectReg.Start
- tv.SelectReg.End.Ch += len(tv.ISearchString) // todo: select all!
+ tv.SearchPos = i
+ tv.SelectReg = NewTextRegionLen(pos, len(tv.ISearchString))
tv.SetCursor(pos)
tv.ScrollCursorToCenterIfHidden()
tv.RenderSelectLines()
@@ -968,11 +997,9 @@ func (tv *TextView) ISearchKeyInput(r rune) {
}
}
if !got {
- tv.ISearchPos = 0
- pos := tv.ISearchMatches[0]
- tv.SelectReg.Start = pos
- tv.SelectReg.End = tv.SelectReg.Start
- tv.SelectReg.End.Ch += len(tv.ISearchString) // todo: select all!
+ tv.SearchPos = 0
+ pos := tv.SearchMatches[0]
+ tv.SelectReg = NewTextRegionLen(pos, len(tv.ISearchString))
tv.SetCursor(pos)
tv.ScrollCursorToCenterIfHidden()
tv.RenderSelectLines()
@@ -984,8 +1011,8 @@ func (tv *TextView) ISearchKeyInput(r rune) {
func (tv *TextView) ISearchBackspace() {
if tv.ISearchString == tv.PrevISearchString { // undo starting point
tv.ISearchString = ""
- tv.ISearchMatches = nil
- tv.ISearchPos = -1
+ tv.SearchMatches = nil
+ tv.SearchPos = -1
tv.ISearchSig()
}
if len(tv.ISearchString) <= 1 {
@@ -994,17 +1021,17 @@ func (tv *TextView) ISearchBackspace() {
return
}
tv.ISearchString = tv.ISearchString[:len(tv.ISearchString)-1]
- tv.ISearchFindMatches()
- sz := len(tv.ISearchMatches)
+ tv.ISearchMatches()
+ sz := len(tv.SearchMatches)
if sz == 0 {
- tv.ISearchPos = -1
+ tv.SearchPos = -1
tv.ISearchSig()
return
}
got := false
- for i, pos := range tv.ISearchMatches {
+ for i, pos := range tv.SearchMatches {
if pos.Ln >= tv.CursorPos.Ln {
- tv.ISearchPos = i
+ tv.SearchPos = i
tv.SetCursor(pos)
tv.ScrollCursorToCenterIfHidden()
tv.ISearchSig()
@@ -1013,8 +1040,8 @@ func (tv *TextView) ISearchBackspace() {
}
}
if !got {
- tv.ISearchPos = 0
- pos := tv.ISearchMatches[0]
+ tv.SearchPos = 0
+ pos := tv.SearchMatches[0]
tv.SetCursor(pos)
tv.ScrollCursorToCenterIfHidden()
tv.ISearchSig()
@@ -1031,8 +1058,10 @@ func (tv *TextView) ISearchCancel() {
tv.ISearchString = ""
tv.ISearchCase = false
tv.ISearchMode = false
- tv.ISearchPos = -1
- tv.ISearchMatches = nil
+ tv.SearchPos = -1
+ tv.SearchMatches = nil
+ tv.Highlights = nil
+ tv.Refresh()
tv.ISearchSig()
}
@@ -1673,22 +1702,43 @@ func (tv *TextView) CursorSprite() *gi.Viewport2D {
return sp
}
-// RenderSelect renders the selection region as a highlighted background color
+// RenderSelect renders the selection region as a selected background color
// -- always called within context of outer RenderLines or RenderAllLines
func (tv *TextView) RenderSelect() {
if !tv.HasSelection() {
return
}
+ tv.RenderRegionBox(tv.SelectReg, TextViewSel)
+}
+
+// RenderHighlights renders the highlight regions as a highlighted background
+// color -- always called within context of outer RenderLines or
+// RenderAllLines
+func (tv *TextView) RenderHighlights(stln, edln int) {
+ for _, reg := range tv.Highlights {
+ if stln >= 0 && (reg.Start.Ln > edln || reg.End.Ln < stln) {
+ continue
+ }
+ tv.RenderRegionBox(reg, TextViewHighlight)
+ }
+}
+
+// RenderRegionBox renders a region in background color according to given state style
+func (tv *TextView) RenderRegionBox(reg TextRegion, state TextViewStates) {
+ st := reg.Start
+ ed := reg.End
+ spos := tv.CharStartPos(st)
+ epos := tv.CharEndPos(ed)
+ if int(math32.Ceil(epos.Y)) < tv.VpBBox.Min.Y || int(math32.Floor(spos.Y)) > tv.VpBBox.Max.Y {
+ return
+ }
+
rs := &tv.Viewport.Render
pc := &rs.Paint
- sty := &tv.StateStyles[TextViewSel]
+ sty := &tv.StateStyles[state]
spc := sty.BoxSpace()
- st := tv.SelectReg.Start
- ed := tv.SelectReg.End
ed.Ch-- // end is exclusive
- spos := tv.CharStartPos(st)
- epos := tv.CharEndPos(ed)
rst := tv.RenderStartPos()
// fmt.Printf("select: %v -- %v\n", st, ed)
@@ -1754,6 +1804,9 @@ func (tv *TextView) RenderStartPos() gi.Vec2D {
// VisSizes computes the visible size of view given current parameters
func (tv *TextView) VisSizes() {
+ if tv.Sty.Font.Size.Val == 0 { // not yet styled
+ tv.StyleTextView()
+ }
sty := &tv.Sty
spc := sty.BoxSpace()
sty.Font.OpenFont(&sty.UnContext)
@@ -1773,45 +1826,58 @@ func (tv *TextView) VisSizes() {
} else {
tv.LineNoOff = 0
}
+ tv.RenderSize()
}
-// RenderAllLines displays all the visible lines on the screen -- called
-// during standard render
+// RenderAllLines displays all the visible lines on the screen -- this is
+// called outside of update process and has its own bounds check and updating
func (tv *TextView) RenderAllLines() {
if tv.PushBounds() {
vp := tv.Viewport
updt := vp.Win.UpdateStart()
-
- sty := &tv.Sty
- sty.Font.OpenFont(&sty.UnContext)
- tv.VisSizes()
- tv.RenderStdBox(sty)
- tv.RenderLineNosBoxAll()
- tv.RenderSelect()
- rs := &tv.Viewport.Render
- pos := tv.RenderStartPos()
- for ln := 0; ln < tv.NLines; ln++ {
- lst := pos.Y + tv.Offs[ln]
- led := lst + math32.Max(tv.Renders[ln].Size.Y, tv.LineHeight)
- if int(math32.Ceil(led)) < tv.VpBBox.Min.Y {
- continue
- }
- if int(math32.Floor(lst)) > tv.VpBBox.Max.Y {
- continue
- }
- lp := pos
- lp.Y = lst
- lp.X += tv.LineNoOff
- tv.Renders[ln].Render(rs, lp) // not top pos -- already has baseline offset
- tv.RenderLineNo(ln)
- }
-
+ tv.RenderAllLinesInBounds()
tv.PopBounds()
vp.Win.UploadVpRegion(vp, tv.VpBBox, tv.WinBBox)
vp.Win.UpdateEnd(updt)
}
}
+// RenderAllLinesInBounds displays all the visible lines on the screen --
+// after PushBounds has already been called
+func (tv *TextView) RenderAllLinesInBounds() {
+ rs := &tv.Viewport.Render
+ pc := &rs.Paint
+ sty := &tv.Sty
+ tv.VisSizes()
+ if tv.NLines == 0 {
+ pos := tv.RenderStartPos()
+ pos.X += tv.LineNoOff
+ sz := tv.RenderSz
+ pc.FillBox(rs, pos, sz, &sty.Font.BgColor)
+ } else {
+ tv.RenderStdBox(sty)
+ }
+ tv.RenderLineNosBoxAll()
+ tv.RenderHighlights(-1, -1) // all
+ tv.RenderSelect()
+ pos := tv.RenderStartPos()
+ for ln := 0; ln < tv.NLines; ln++ {
+ lst := pos.Y + tv.Offs[ln]
+ led := lst + math32.Max(tv.Renders[ln].Size.Y, tv.LineHeight)
+ if int(math32.Ceil(led)) < tv.VpBBox.Min.Y {
+ continue
+ }
+ if int(math32.Floor(lst)) > tv.VpBBox.Max.Y {
+ continue
+ }
+ lp := pos
+ lp.Y = lst
+ lp.X += tv.LineNoOff
+ tv.Renders[ln].Render(rs, lp) // not top pos -- already has baseline offset
+ tv.RenderLineNo(ln)
+ }
+}
+
// RenderLineNosBoxAll renders the background for the line numbers in a darker shade
func (tv *TextView) RenderLineNosBoxAll() {
if !tv.Opts.LineNos {
@@ -1908,6 +1974,7 @@ func (tv *TextView) RenderLines(st, ed int) bool {
pc.FillBox(rs, boxMin, boxMax.Sub(boxMin), &sty.Font.BgColor)
// fmt.Printf("lns: st: %v ed: %v vis st: %v ed %v box: min %v max: %v\n", st, ed, visSt, visEd, boxMin, boxMax)
+ tv.RenderHighlights(st, ed)
tv.RenderSelect()
tv.RenderLineNosBox(st, ed)
@@ -2309,14 +2376,8 @@ func (tv *TextView) MouseEvent(me *mouse.Event) {
switch me.Button {
case mouse.Left:
if me.Action == mouse.Press {
- if tv.IsInactive() {
- tv.SetSelectedState(!tv.IsSelected())
- tv.EmitSelectedSignal()
- tv.UpdateSig()
- } else {
- pt := tv.PointToRelPos(me.Pos())
- tv.SetCursorFromPixel(pt, me.SelectMode())
- }
+ pt := tv.PointToRelPos(me.Pos())
+ tv.SetCursorFromPixel(pt, me.SelectMode())
} else if me.Action == mouse.DoubleClick {
me.SetProcessed()
// if tv.HasSelection() {
@@ -2440,6 +2501,12 @@ func (tv *TextView) Render2D() {
if tv.FullReRenderIfNeeded() {
return
}
+ tv.VisSizes()
+ if tv.NLines == 0 {
+ sz := tv.RenderSz.ToPointCeil()
+ tv.VpBBox.Max = tv.VpBBox.Min.Add(sz)
+ tv.WinBBox.Max = tv.WinBBox.Min.Add(sz)
+ }
if tv.PushBounds() {
tv.TextViewEvents()
if tv.IsInactive() {
@@ -2448,6 +2515,8 @@ func (tv *TextView) Render2D() {
} else {
tv.Sty = tv.StateStyles[TextViewInactive]
}
+ } else if tv.NLines == 0 {
+ tv.Sty = tv.StateStyles[TextViewInactive]
} else if tv.HasFocus() {
if tv.FocusActive {
tv.Sty = tv.StateStyles[TextViewFocus]
@@ -2459,7 +2528,7 @@ func (tv *TextView) Render2D() {
} else {
tv.Sty = tv.StateStyles[TextViewActive]
}
- tv.RenderAllLines()
+ tv.RenderAllLinesInBounds()
if tv.HasFocus() && tv.FocusActive {
tv.StartCursor()
} else {
diff --git a/giv/textviewsignals_string.go b/giv/textviewsignals_string.go
index fab95b50..ea4e7fcb 100644
--- a/giv/textviewsignals_string.go
+++ b/giv/textviewsignals_string.go
@@ -7,9 +7,9 @@ import (
"strconv"
)
-const _TextViewSignals_name = "TextViewDoneTextViewSelectedTextViewCursorMovedTextViewSignalsN"
+const _TextViewSignals_name = "TextViewDoneTextViewSelectedTextViewCursorMovedTextViewISearchTextViewSignalsN"
-var _TextViewSignals_index = [...]uint8{0, 12, 28, 47, 63}
+var _TextViewSignals_index = [...]uint8{0, 12, 28, 47, 62, 78}
func (i TextViewSignals) String() string {
if i < 0 || i >= TextViewSignals(len(_TextViewSignals_index)-1) {
diff --git a/giv/textviewstates_string.go b/giv/textviewstates_string.go
index 52007df7..87e23c34 100644
--- a/giv/textviewstates_string.go
+++ b/giv/textviewstates_string.go
@@ -7,9 +7,9 @@ import (
"strconv"
)
-const _TextViewStates_name = "TextViewActiveTextViewFocusTextViewInactiveTextViewSelTextViewStatesN"
+const _TextViewStates_name = "TextViewActiveTextViewFocusTextViewInactiveTextViewSelTextViewHighlightTextViewStatesN"
-var _TextViewStates_index = [...]uint8{0, 14, 27, 43, 54, 69}
+var _TextViewStates_index = [...]uint8{0, 14, 27, 43, 54, 71, 86}
func (i TextViewStates) String() string {
if i < 0 || i >= TextViewStates(len(_TextViewStates_index)-1) {
diff --git a/icons/folder-open.svg b/icons/folder-open.svg
index 63f28367..2ba19fcf 100644
--- a/icons/folder-open.svg
+++ b/icons/folder-open.svg
@@ -1 +1,2 @@
-
\ No newline at end of file
+
diff --git a/icons/terminal.svg b/icons/terminal.svg
new file mode 100644
index 00000000..1ebd5f34
--- /dev/null
+++ b/icons/terminal.svg
@@ -0,0 +1,4 @@
+