Beispiel #1
0
// 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)
		}
	}
}
Beispiel #2
0
// 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)
		}
	}
}
Beispiel #3
0
// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
	// prof := profile.Start(profile.ProfilePath("/tmp/"))
	<-t.startChan
	{ // Late initialization
		intChan := make(chan os.Signal, 1)
		signal.Notify(intChan, os.Interrupt, os.Kill, syscall.SIGTERM)
		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.resizeWindows()
		t.printPrompt()
		t.placeCursor()
		t.refresh()
		t.printInfo()
		t.printHeader()
		t.mutex.Unlock()
		go func() {
			timer := time.NewTimer(t.initDelay)
			<-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)
			}
		}()
	}

	if t.hasPreviewWindow() {
		go func() {
			for {
				var request *Item
				t.previewBox.Wait(func(events *util.Events) {
					for req, value := range *events {
						switch req {
						case reqPreviewEnqueue:
							request = value.(*Item)
						}
					}
					events.Clear()
				})
				if request != nil {
					command := replacePlaceholder(t.preview.command,
						t.ansi, t.delimiter, string(t.input), []*Item{request})
					cmd := util.ExecCommand(command)
					out, _ := cmd.CombinedOutput()
					t.reqBox.Set(reqPreviewDisplay, string(out))
				} else {
					t.reqBox.Set(reqPreviewDisplay, "")
				}
			}
		}()
	}

	exit := func(code int) {
		if code <= exitNoMatch && t.history != nil {
			t.history.append(string(t.input))
		}
		// prof.Stop()
		os.Exit(code)
	}

	go func() {
		var focused *Item
		for {
			t.reqBox.Wait(func(events *util.Events) {
				defer events.Clear()
				t.mutex.Lock()
				for req, value := range *events {
					switch req {
					case reqPrompt:
						t.printPrompt()
						if t.inlineInfo {
							t.printInfo()
						}
					case reqInfo:
						t.printInfo()
					case reqList:
						t.printList()
						cnt := t.merger.Length()
						var currentFocus *Item
						if cnt > 0 && cnt > t.cy {
							currentFocus = t.currentItem()
						} else {
							currentFocus = nil
						}
						if currentFocus != focused {
							focused = currentFocus
							if t.isPreviewEnabled() {
								t.previewBox.Set(reqPreviewEnqueue, focused)
							}
						}
					case reqJump:
						if t.merger.Length() == 0 {
							t.jumping = jumpDisabled
						}
						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 reqPreviewDisplay:
						t.previewer.text = value.(string)
						t.previewer.lines = strings.Count(t.previewer.text, "\n")
						t.previewer.offset = 0
						t.printPreview()
					case reqPreviewRefresh:
						t.printPreview()
					case reqPrintQuery:
						C.Close()
						t.printer(string(t.input))
						exit(exitOk)
					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}
				return true
			}
			return false
		}
		toggleY := func(y int) {
			item := t.merger.Get(y).item
			if !selectItem(item) {
				delete(t.selected, item.Index())
			}
		}
		toggle := func() {
			if t.cy < t.merger.Length() {
				toggleY(t.cy)
				req(reqInfo)
			}
		}
		scrollPreview := func(amount int) {
			t.previewer.offset = util.Constrain(
				t.previewer.offset+amount, 0, t.previewer.lines-1)
			req(reqPreviewRefresh)
		}
		for key, ret := range t.expect {
			if keyMatch(key, event) {
				t.pressed = ret
				t.reqBox.Set(reqClose, nil)
				t.mutex.Unlock()
				return
			}
		}

		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() {
					t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()})
				}
			case actExecuteMulti:
				if len(t.selected) > 0 {
					sels := make([]*Item, len(t.selected))
					for i, sel := range t.sortSelected() {
						sels[i] = sel.item
					}
					t.executeCommand(t.execmap[mapkey], sels)
				} else {
					return doAction(actExecute, mapkey)
				}
			case actInvalid:
				t.mutex.Unlock()
				return false
			case actTogglePreview:
				if t.hasPreviewWindow() {
					t.previewer.enabled = !t.previewer.enabled
					t.resizeWindows()
					cnt := t.merger.Length()
					if t.previewer.enabled && cnt > 0 && cnt > t.cy {
						t.previewBox.Set(reqPreviewEnqueue, t.currentItem())
					}
					req(reqList, reqInfo)
				}
			case actToggleSort:
				t.sort = !t.sort
				t.eventBox.Set(EvtSearchNew, t.sort)
				t.mutex.Unlock()
				return false
			case actPreviewUp:
				if t.isPreviewEnabled() {
					scrollPreview(-1)
				}
			case actPreviewDown:
				if t.isPreviewEnabled() {
					scrollPreview(1)
				}
			case actPreviewPageUp:
				if t.isPreviewEnabled() {
					scrollPreview(-t.pwindow.Height)
				}
			case actPreviewPageDown:
				if t.isPreviewEnabled() {
					scrollPreview(t.pwindow.Height)
				}
			case actBeginningOfLine:
				t.cx = 0
			case actBackwardChar:
				if t.cx > 0 {
					t.cx--
				}
			case actPrintQuery:
				req(reqPrintQuery)
			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).item
						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 actToggleIn:
				if t.reverse {
					return doAction(actToggleUp, mapkey)
				}
				return doAction(actToggleDown, mapkey)
			case actToggleOut:
				if t.reverse {
					return doAction(actToggleDown, mapkey)
				}
				return doAction(actToggleUp, mapkey)
			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 actJump:
				t.jumping = jumpEnabled
				req(reqJump)
			case actJumpAccept:
				t.jumping = jumpAcceptEnabled
				req(reqJump)
			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.window.Enclose(my, mx) && t.merger.Length() > 0 {
						if t.multi && me.Mod {
							toggle()
						}
						t.vmove(me.S)
						req(reqList)
					} else if t.isPreviewEnabled() && t.pwindow.Enclose(my, mx) {
						scrollPreview(-me.S)
					}
				} else if t.window.Enclose(my, mx) {
					mx -= t.window.Left
					my -= t.window.Top
					mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input))
					if !t.reverse {
						my = t.window.Height - 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
		}
		changed := false
		mapkey := event.Type
		if t.jumping == jumpDisabled {
			action := t.keymap[mapkey]
			if mapkey == C.Rune {
				mapkey = int(event.Char) + int(C.AltZ)
				if act, prs := t.keymap[mapkey]; prs {
					action = act
				}
			}
			if !doAction(action, mapkey) {
				continue
			}
			// Truncate the query if it's too long
			if len(t.input) > maxPatternLength {
				t.input = t.input[:maxPatternLength]
				t.cx = util.Constrain(t.cx, 0, maxPatternLength)
			}
			changed = string(previousInput) != string(t.input)
		} else {
			if mapkey == C.Rune {
				if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
					t.cy = idx + t.offset
					if t.jumping == jumpAcceptEnabled {
						req(reqClose)
					}
				}
			}
			t.jumping = jumpDisabled
			req(reqList)
		}
		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)
		}
	}
}