func (t *Terminal) printHeader() { if len(t.header) == 0 { return } max := C.MaxY() var state *ansiState for idx, lineStr := range t.header { if !t.reverse { idx = len(t.header) - idx - 1 } line := idx + 2 if t.inlineInfo { line -= 1 } if line >= max { continue } trimmed, colors, newState := extractColor(&lineStr, state) state = newState item := &Item{ text: trimmed, index: 0, colors: colors, rank: Rank{0, 0, 0}} t.move(line, 2, true) t.printHighlighted(item, false, C.ColHeader, 0, false) } }
func (t *Terminal) calculateMargins() { screenWidth := C.MaxX() screenHeight := C.MaxY() for idx, str := range t.margin { if str == "0" { t.marginInt[idx] = 0 } else if strings.HasSuffix(str, "%") { num, _ := strconv.ParseFloat(str[:len(str)-1], 64) var val float64 if idx%2 == 0 { val = float64(screenHeight) } else { val = float64(screenWidth) } t.marginInt[idx] = int(val * num * 0.01) } else { num, _ := strconv.Atoi(str) t.marginInt[idx] = num } } adjust := func(idx1 int, idx2 int, max int, min int) { if max >= min { margin := t.marginInt[idx1] + t.marginInt[idx2] if max-margin < min { desired := max - min t.marginInt[idx1] = desired * t.marginInt[idx1] / margin t.marginInt[idx2] = desired * t.marginInt[idx2] / margin } } } adjust(1, 3, screenWidth, minWidth) adjust(0, 2, screenHeight, minHeight) }
func (t *Terminal) maxItems() int { max := C.MaxY() - 2 - len(t.header) if t.inlineInfo { max += 1 } return util.Max(max, 0) }
func (t *Terminal) move(y int, x int, clear bool) { maxy := C.MaxY() if !t.reverse { y = maxy - y - 1 } if clear { C.MoveAndClear(y, x) } else { C.Move(y, x) } }
func (t *Terminal) move(y int, x int, clear bool) { x += t.marginInt[3] maxy := C.MaxY() if !t.reverse { y = maxy - y - 1 - t.marginInt[2] } else { y += t.marginInt[0] } if clear { C.MoveAndClear(y, x) } else { C.Move(y, x) } }
func (t *Terminal) constrain() { count := t.merger.Length() height := C.MaxY() - 2 diffpos := t.cy - t.offset t.cy = util.Constrain(t.cy, 0, count-1) if t.cy > t.offset+(height-1) { // Ceil t.offset = t.cy - (height - 1) } else if t.offset > t.cy { // Floor t.offset = t.cy } // Adjustment if count-t.offset < height { t.offset = util.Max(0, count-height) t.cy = util.Constrain(t.offset+diffpos, 0, count-1) } }
// Loop is called to start Terminal I/O func (t *Terminal) Loop() { <-t.startChan { // Late initialization intChan := make(chan os.Signal, 1) signal.Notify(intChan, os.Interrupt, os.Kill) go func() { <-intChan t.reqBox.Set(reqQuit, nil) }() resizeChan := make(chan os.Signal, 1) signal.Notify(resizeChan, syscall.SIGWINCH) go func() { for { <-resizeChan t.reqBox.Set(reqRedraw, nil) } }() t.mutex.Lock() t.initFunc() t.calculateMargins() t.printPrompt() t.placeCursor() C.Refresh() t.printInfo() t.printHeader() t.mutex.Unlock() go func() { timer := time.NewTimer(initialDelay) <-timer.C t.reqBox.Set(reqRefresh, nil) }() // Keep the spinner spinning go func() { for { t.mutex.Lock() reading := t.reading t.mutex.Unlock() if !reading { break } time.Sleep(spinnerDuration) t.reqBox.Set(reqInfo, nil) } }() } exit := func(code int) { if code <= exitNoMatch && t.history != nil { t.history.append(string(t.input)) } os.Exit(code) } go func() { for { t.reqBox.Wait(func(events *util.Events) { defer events.Clear() t.mutex.Lock() for req := range *events { switch req { case reqPrompt: t.printPrompt() if t.inlineInfo { t.printInfo() } case reqInfo: t.printInfo() case reqList: t.printList() case reqHeader: t.printHeader() case reqRefresh: t.suppress = false case reqRedraw: C.Clear() C.Endwin() C.Refresh() t.printAll() case reqClose: C.Close() if t.output() { exit(exitOk) } exit(exitNoMatch) case reqQuit: C.Close() exit(exitInterrupt) } } t.placeCursor() t.mutex.Unlock() }) t.refresh() } }() looping := true for looping { event := C.GetChar() t.mutex.Lock() previousInput := t.input events := []util.EventType{reqPrompt} req := func(evts ...util.EventType) { for _, event := range evts { events = append(events, event) if event == reqClose || event == reqQuit { looping = false } } } selectItem := func(item *Item) bool { if _, found := t.selected[item.index]; !found { t.selected[item.index] = selectedItem{time.Now(), item.StringPtr(t.ansi)} return true } return false } toggleY := func(y int) { item := t.merger.Get(y) if !selectItem(item) { delete(t.selected, item.index) } } toggle := func() { if t.cy < t.merger.Length() { toggleY(t.cy) req(reqInfo) } } for key, ret := range t.expect { if keyMatch(key, event) { t.pressed = ret req(reqClose) break } } var doAction func(actionType, int) bool doAction = func(action actionType, mapkey int) bool { switch action { case actIgnore: case actExecute: if t.cy >= 0 && t.cy < t.merger.Length() { item := t.merger.Get(t.cy) executeCommand(t.execmap[mapkey], item.AsString(t.ansi)) } case actInvalid: t.mutex.Unlock() return false case actToggleSort: t.sort = !t.sort t.eventBox.Set(EvtSearchNew, t.sort) t.mutex.Unlock() return false case actBeginningOfLine: t.cx = 0 case actBackwardChar: if t.cx > 0 { t.cx-- } case actAbort: req(reqQuit) case actDeleteChar: t.delChar() case actDeleteCharEOF: if !t.delChar() && t.cx == 0 { req(reqQuit) } case actEndOfLine: t.cx = len(t.input) case actCancel: if len(t.input) == 0 { req(reqQuit) } else { t.yanked = t.input t.input = []rune{} t.cx = 0 } case actForwardChar: if t.cx < len(t.input) { t.cx++ } case actBackwardDeleteChar: if t.cx > 0 { t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) t.cx-- } case actSelectAll: if t.multi { for i := 0; i < t.merger.Length(); i++ { item := t.merger.Get(i) selectItem(item) } req(reqList, reqInfo) } case actDeselectAll: if t.multi { for i := 0; i < t.merger.Length(); i++ { item := t.merger.Get(i) delete(t.selected, item.index) } req(reqList, reqInfo) } case actToggle: if t.multi && t.merger.Length() > 0 { toggle() req(reqList) } case actToggleAll: if t.multi { for i := 0; i < t.merger.Length(); i++ { toggleY(i) } req(reqList, reqInfo) } case actToggleDown: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(-1) req(reqList) } case actToggleUp: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(1) req(reqList) } case actDown: t.vmove(-1) req(reqList) case actUp: t.vmove(1) req(reqList) case actAccept: req(reqClose) case actClearScreen: req(reqRedraw) case actUnixLineDiscard: if t.cx > 0 { t.yanked = copySlice(t.input[:t.cx]) t.input = t.input[t.cx:] t.cx = 0 } case actUnixWordRubout: if t.cx > 0 { t.rubout("\\s\\S") } case actBackwardKillWord: if t.cx > 0 { t.rubout("[^[:alnum:]][[:alnum:]]") } case actYank: suffix := copySlice(t.input[t.cx:]) t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) t.cx += len(t.yanked) case actPageUp: t.vmove(t.maxItems() - 1) req(reqList) case actPageDown: t.vmove(-(t.maxItems() - 1)) req(reqList) case actBackwardWord: t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 case actForwardWord: t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 case actKillWord: ncx := t.cx + findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 if ncx > t.cx { t.yanked = copySlice(t.input[t.cx:ncx]) t.input = append(t.input[:t.cx], t.input[ncx:]...) } case actKillLine: if t.cx < len(t.input) { t.yanked = copySlice(t.input[t.cx:]) t.input = t.input[:t.cx] } case actRune: prefix := copySlice(t.input[:t.cx]) t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.cx++ case actPreviousHistory: if t.history != nil { t.history.override(string(t.input)) t.input = []rune(t.history.previous()) t.cx = len(t.input) } case actNextHistory: if t.history != nil { t.history.override(string(t.input)) t.input = []rune(t.history.next()) t.cx = len(t.input) } case actMouse: me := event.MouseEvent mx, my := me.X, me.Y if me.S != 0 { // Scroll if t.merger.Length() > 0 { if t.multi && me.Mod { toggle() } t.vmove(me.S) req(reqList) } } else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] && my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { mx -= t.marginInt[3] my -= t.marginInt[0] mx = util.Constrain(mx-len(t.prompt), 0, len(t.input)) if !t.reverse { my = t.maxHeight() - my - 1 } min := 2 + len(t.header) if t.inlineInfo { min-- } if me.Double { // Double-click if my >= min { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { return doAction(t.keymap[C.DoubleClick], C.DoubleClick) } } } else if me.Down { if my == 0 && mx >= 0 { // Prompt t.cx = mx } else if my >= min { // List if t.vset(t.offset+my-min) && t.multi && me.Mod { toggle() } req(reqList) } } } } return true } action := t.keymap[event.Type] mapkey := event.Type if event.Type == C.Rune { mapkey = int(event.Char) + int(C.AltZ) if act, prs := t.keymap[mapkey]; prs { action = act } } if !doAction(action, mapkey) { continue } changed := string(previousInput) != string(t.input) t.mutex.Unlock() // Must be unlocked before touching reqBox if changed { t.eventBox.Set(EvtSearchNew, t.sort) } for _, event := range events { t.reqBox.Set(event, nil) } } }
func (t *Terminal) maxHeight() int { return C.MaxY() - t.marginInt[0] - t.marginInt[2] }
func maxItems() int { return C.MaxY() - 2 }
// Loop is called to start Terminal I/O func (t *Terminal) Loop() { <-t.startChan { // Late initialization t.mutex.Lock() t.initFunc() t.printPrompt() t.placeCursor() C.Refresh() t.printInfo() t.mutex.Unlock() go func() { timer := time.NewTimer(initialDelay) <-timer.C t.reqBox.Set(reqRefresh, nil) }() resizeChan := make(chan os.Signal, 1) signal.Notify(resizeChan, syscall.SIGWINCH) go func() { for { <-resizeChan t.reqBox.Set(reqRedraw, nil) } }() } go func() { for { t.reqBox.Wait(func(events *util.Events) { defer events.Clear() t.mutex.Lock() for req := range *events { switch req { case reqPrompt: t.printPrompt() case reqInfo: t.printInfo() case reqList: t.printList() case reqRefresh: t.suppress = false case reqRedraw: C.Clear() C.Endwin() C.Refresh() t.printAll() case reqClose: C.Close() t.output() os.Exit(0) case reqQuit: C.Close() os.Exit(1) } } t.placeCursor() t.mutex.Unlock() }) t.refresh() } }() looping := true for looping { event := C.GetChar() t.mutex.Lock() previousInput := t.input events := []util.EventType{reqPrompt} req := func(evts ...util.EventType) { for _, event := range evts { events = append(events, event) if event == reqClose || event == reqQuit { looping = false } } } toggle := func() { if t.cy < t.merger.Length() { item := t.merger.Get(t.cy) if _, found := t.selected[item.text]; !found { var strptr *string if item.origText != nil { strptr = item.origText } else { strptr = item.text } t.selected[item.text] = selectedItem{time.Now(), strptr} } else { delete(t.selected, item.text) } req(reqInfo) } } switch event.Type { case C.Invalid: t.mutex.Unlock() continue case C.CtrlA: t.cx = 0 case C.CtrlB: if t.cx > 0 { t.cx-- } case C.CtrlC, C.CtrlG, C.CtrlQ, C.ESC: req(reqQuit) case C.CtrlD: if !t.delChar() && t.cx == 0 { req(reqQuit) } case C.CtrlE: t.cx = len(t.input) case C.CtrlF: if t.cx < len(t.input) { t.cx++ } case C.CtrlH: if t.cx > 0 { t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) t.cx-- } case C.Tab: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(-1) req(reqList) } case C.BTab: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(1) req(reqList) } case C.CtrlJ, C.CtrlN: t.vmove(-1) req(reqList) case C.CtrlK, C.CtrlP: t.vmove(1) req(reqList) case C.CtrlM: req(reqClose) case C.CtrlL: req(reqRedraw) case C.CtrlU: if t.cx > 0 { t.yanked = copySlice(t.input[:t.cx]) t.input = t.input[t.cx:] t.cx = 0 } case C.CtrlW: if t.cx > 0 { t.rubout("\\s\\S") } case C.AltBS: if t.cx > 0 { t.rubout("[^[:alnum:]][[:alnum:]]") } case C.CtrlY: suffix := copySlice(t.input[t.cx:]) t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) t.cx += len(t.yanked) case C.Del: t.delChar() case C.PgUp: t.vmove(maxItems() - 1) req(reqList) case C.PgDn: t.vmove(-(maxItems() - 1)) req(reqList) case C.AltB: t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 case C.AltF: t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 case C.AltD: ncx := t.cx + findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 if ncx > t.cx { t.yanked = copySlice(t.input[t.cx:ncx]) t.input = append(t.input[:t.cx], t.input[ncx:]...) } case C.Rune: prefix := copySlice(t.input[:t.cx]) t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.cx++ case C.Mouse: me := event.MouseEvent mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y if !t.reverse { my = C.MaxY() - my - 1 } if me.S != 0 { // Scroll if t.merger.Length() > 0 { if t.multi && me.Mod { toggle() } t.vmove(me.S) req(reqList) } } else if me.Double { // Double-click if my >= 2 { if t.vset(my-2) && t.cy < t.merger.Length() { req(reqClose) } } } else if me.Down { if my == 0 && mx >= 0 { // Prompt t.cx = mx } else if my >= 2 { // List if t.vset(t.offset+my-2) && t.multi && me.Mod { toggle() } req(reqList) } } } changed := string(previousInput) != string(t.input) t.mutex.Unlock() // Must be unlocked before touching reqBox if changed { t.eventBox.Set(EvtSearchNew, nil) } for _, event := range events { t.reqBox.Set(event, nil) } } }
func (t *Terminal) resizeWindows() { screenWidth := C.MaxX() screenHeight := C.MaxY() marginInt := [4]int{} for idx, sizeSpec := range t.margin { if sizeSpec.percent { var max float64 if idx%2 == 0 { max = float64(screenHeight) } else { max = float64(screenWidth) } marginInt[idx] = int(max * sizeSpec.size * 0.01) } else { marginInt[idx] = int(sizeSpec.size) } } adjust := func(idx1 int, idx2 int, max int, min int) { if max >= min { margin := marginInt[idx1] + marginInt[idx2] if max-margin < min { desired := max - min marginInt[idx1] = desired * marginInt[idx1] / margin marginInt[idx2] = desired * marginInt[idx2] / margin } } } minAreaWidth := minWidth minAreaHeight := minHeight if t.isPreviewEnabled() { switch t.preview.position { case posUp, posDown: minAreaHeight *= 2 case posLeft, posRight: minAreaWidth *= 2 } } adjust(1, 3, screenWidth, minAreaWidth) adjust(0, 2, screenHeight, minAreaHeight) if t.window != nil { t.window.Close() } if t.bwindow != nil { t.bwindow.Close() t.pwindow.Close() } width := screenWidth - marginInt[1] - marginInt[3] height := screenHeight - marginInt[0] - marginInt[2] if t.isPreviewEnabled() { createPreviewWindow := func(y int, x int, w int, h int) { t.bwindow = C.NewWindow(y, x, w, h, true) t.pwindow = C.NewWindow(y+1, x+2, w-4, h-2, false) } switch t.preview.position { case posUp: pheight := calculateSize(height, t.preview.size, minHeight, 3) t.window = C.NewWindow( marginInt[0]+pheight, marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0], marginInt[3], width, pheight) case posDown: pheight := calculateSize(height, t.preview.size, minHeight, 3) t.window = C.NewWindow( marginInt[0], marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) case posLeft: pwidth := calculateSize(width, t.preview.size, minWidth, 5) t.window = C.NewWindow( marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) case posRight: pwidth := calculateSize(width, t.preview.size, minWidth, 5) t.window = C.NewWindow( marginInt[0], marginInt[3], width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) } } else { t.window = C.NewWindow( marginInt[0], marginInt[3], width, height, false) } }
func (t *Terminal) maxItems() int { if t.inlineInfo { return C.MaxY() - 1 } return C.MaxY() - 2 }