resolve conflict

This commit is contained in:
屈轩
2018-09-30 12:10:58 +08:00
61 changed files with 2018 additions and 1625 deletions
Generated
+3 -3
View File
@@ -87,12 +87,12 @@
version = "v0.5.0"
[[projects]]
digest = "1:a2486f94bda33b301614ff5cfce102b7eed4c923fa650c2b83dba1fcf2117466"
digest = "1:1659cb76cbd08a29826688d006e7a3d9279d6ca8a12155acb7b20164958987d3"
name = "github.com/c-bata/go-prompt"
packages = ["."]
pruneopts = "UT"
revision = "c52492ff1b386e5c0ba5271b5eaad165fab09eca"
version = "v0.2.2"
revision = "e99fbc797b795e0a7a94affc8d44f6a0350d85f0"
version = "v0.2.1"
[[projects]]
digest = "1:40098afbdd06a76dee4b6bcb85a22fed81ac9d2ebaf775c91e558c80f57aaff1"
+1 -1
View File
@@ -27,7 +27,7 @@
[[constraint]]
name = "github.com/c-bata/go-prompt"
version = "0.2.2"
version = "=0.2.1"
[[constraint]]
branch = "master"
+31
View File
@@ -0,0 +1,31 @@
package shell
import (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
func init() {
type BaremetalAgentListOptions struct {
options.BaseListOptions
}
R(&BaremetalAgentListOptions{}, "baremetal-agent-list", "List baremetal agent", func(s *mcclient.ClientSession, args *BaremetalAgentListOptions) error {
var params *jsonutils.JSONDict
{
var err error
params, err = args.BaseListOptions.Params()
if err != nil {
return err
}
}
result, err := modules.Baremetalagents.List(s, params)
if err != nil {
return err
}
printList(result, modules.Baremetalagents.GetColumns(s))
return nil
})
}
+9
View File
@@ -65,4 +65,13 @@ func init() {
handleResult(args.WebConsoleOptions, ret)
return nil
})
R(&o.WebConsoleSshOptions{}, "webconsole-ssh", "Connect ssh webconsole", func(s *mcclient.ClientSession, args *o.WebConsoleSshOptions) error {
ret, err := modules.WebConsole.DoSshConnect(s, args.IP)
if err != nil {
return err
}
handleResult(args.WebConsoleOptions, ret)
return nil
})
}
+1
View File
@@ -26,6 +26,7 @@ type Options struct {
AdminProject string `help:"Admin project" default:"system" alias:"admin-tenant-name"`
CorsHosts []string `help:"List of hostname that allow CORS"`
AuthTokenCacheSize uint32 `help:"Auth token Cache Size" default:"2048"`
TempPath string `help:"Path for store temp file, at least 40G space" default:"/opt/yunion/tmp"`
ApplicationID string `help:"Application ID"`
RequestWorkerCount int `default:"4" help:"Request worker thread count, default is 4"`
+1
View File
@@ -493,6 +493,7 @@ func (v *ValidatorStruct) Validate(data *jsonutils.JSONDict) error {
return err
}
}
data.Set(v.Key, jsonutils.Marshal(v.Value))
return nil
}
+66 -50
View File
@@ -546,19 +546,22 @@ func TestIPv4Validator(t *testing.T) {
}
type TestStruct struct {
F0 string
F1 int
F2 bool
Name string
F0 string
F1 int
F2 bool
}
type TestVStruct TestStruct
func (v *TestVStruct) Validate(data *jsonutils.JSONDict) error {
if v.F2 {
return nil
} else {
return newInvalidValueError("F2", v.F0)
switch v.Name {
case "bad":
return newInvalidValueError("Name", v.Name)
case "setDefault":
v.Name = "defaultVal"
}
return nil
}
func TestStructValidator(t *testing.T) {
@@ -566,29 +569,7 @@ func TestStructValidator(t *testing.T) {
*C
Value interface{}
}
defaultVal := &TestStruct{
F0: "holy",
F1: 100,
F2: true,
}
var defaultValMiss *TestStruct
{
defaultValMissCopy := *defaultVal
defaultValMissCopy.F2 = false
defaultValMiss = &defaultValMissCopy
}
defaultValJsonStr := `{s: {"F0": "holy", "F1": 100, "F2": true}}`
defaultValJsonStrMiss := `{s: {"F0": "holy", "F1": 100}}`
defaultValJsonStrMore := `{s: {"F0": "holy", "F1": 100, "F2": true, "Foo": "bar"}}`
defaultVVal := (*TestVStruct)(defaultVal)
var defaultVValBad *TestVStruct
{
defaultVValBadCopy := *defaultVVal
defaultVValBadCopy.F2 = false
defaultVValBad = &defaultVValBadCopy
}
defaultVValJsonStrBad := `{s: {"F0": "holy", "F1": 100, "F2": false}}`
cases := []*StructC{
{
C: &C{
@@ -612,47 +593,82 @@ func TestStructValidator(t *testing.T) {
},
{
C: &C{
Name: "valid",
In: defaultValJsonStr,
Out: defaultValJsonStr,
ValueWant: defaultVal,
Name: "valid",
In: `{s: {"F0": "holy", "F1": 100, "F2": true}}`,
Out: `{s: {"f0": "holy", "f1": 100, "f2": true}}`,
ValueWant: &TestStruct{
F0: "holy",
F1: 100,
F2: true,
},
},
Value: &TestStruct{},
},
{
C: &C{
Name: "valid (missing fields)",
In: defaultValJsonStrMiss,
Out: defaultValJsonStrMiss,
ValueWant: defaultValMiss,
Name: "valid (missing fields)",
In: `{s: {"F0": "holy", "F1": 100}}`,
Out: `{s: {"f0": "holy", "f1": 100, "f2": false}}`,
ValueWant: &TestStruct{
F0: "holy",
F1: 100,
F2: false,
},
},
Value: &TestStruct{},
},
{
C: &C{
Name: "valid (more fields)",
In: defaultValJsonStrMore,
Out: defaultValJsonStrMore,
ValueWant: defaultVal,
Name: "valid (more fields)",
In: `{s: {"F0": "holy", "F1": 100, "F2": true, "Foo": "bar"}}`,
Out: `{s: {"f0": "holy", "f1": 100, "f2": true}}`,
ValueWant: &TestStruct{
F0: "holy",
F1: 100,
F2: true,
},
},
Value: &TestStruct{},
},
{
C: &C{
Name: "valid (struct says valid)",
In: defaultValJsonStr,
Out: defaultValJsonStr,
ValueWant: defaultVVal,
Name: "valid (struct says valid)",
In: `{s: {"F0": "holy", "F1": 100, "F2": true}}`,
Out: `{s: {"f0": "holy", "f1": 100, "f2": true}}`,
ValueWant: &TestVStruct{
F0: "holy",
F1: 100,
F2: true,
},
},
Value: &TestVStruct{},
},
{
C: &C{
Name: "invalid (struct says invalid)",
In: defaultVValJsonStrBad,
Out: defaultVValJsonStrBad,
ValueWant: defaultVValBad,
Err: ERR_INVALID_VALUE,
Name: "valid (with default initialized)",
In: `{s: {Name: "setDefault", "F0": "holy", "F1": 100, "F2": false}}`,
Out: `{s: {name: "defaultVal", "f0": "holy", "f1": 100, "f2": false}}`,
ValueWant: &TestVStruct{
Name: "defaultVal",
F0: "holy",
F1: 100,
F2: false,
},
},
Value: &TestVStruct{},
},
{
C: &C{
Name: "invalid (struct says invalid)",
In: `{s: {Name: "bad", "F0": "holy", "F1": 100, "F2": false}}`,
Out: `{s: {Name: "bad", "F0": "holy", "F1": 100, "F2": false}}`,
ValueWant: &TestVStruct{
Name: "bad",
F0: "holy",
F1: 100,
F2: false,
},
Err: ERR_INVALID_VALUE,
},
Value: &TestVStruct{},
},
+1 -1
View File
@@ -81,7 +81,7 @@ type ICloudStoragecache interface {
CreateIImage(snapshotId, imageName, osType, imageDesc string) (ICloudImage, error)
DownloadImage(userCred mcclient.TokenCredential, imageId string, extId string) (jsonutils.JSONObject, error)
DownloadImage(userCred mcclient.TokenCredential, imageId string, extId string, path string) (jsonutils.JSONObject, error)
UploadImage(userCred mcclient.TokenCredential, imageId string, osArch, osType, osDist string, extId string, isForce bool) (string, error)
}
+8 -1
View File
@@ -3,6 +3,7 @@ package hostdrivers
import (
"context"
"fmt"
"os"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
@@ -10,6 +11,7 @@ import (
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/cloudprovider"
"yunion.io/x/onecloud/pkg/compute/models"
"yunion.io/x/onecloud/pkg/compute/options"
"yunion.io/x/onecloud/pkg/httperrors"
)
@@ -96,7 +98,12 @@ func (self *SAliyunHostDriver) RequestSaveUploadImageOnHost(ctx context.Context,
return nil, err
} else {
scimg.SetExternalId(iImage.GetId())
if result, err := iStoragecache.DownloadImage(task.GetUserCred(), imageId, iImage.GetId()); err != nil {
if _, err := os.Stat(options.Options.TempPath); os.IsNotExist(err) {
if err = os.MkdirAll(options.Options.TempPath, 0755); err != nil {
return nil, err
}
}
if result, err := iStoragecache.DownloadImage(task.GetUserCred(), imageId, iImage.GetId(), options.Options.TempPath); err != nil {
scimg.SetStatus(task.GetUserCred(), models.CACHED_IMAGE_STATUS_CACHE_FAILED, err.Error())
return nil, err
} else {
+8 -1
View File
@@ -3,6 +3,7 @@ package hostdrivers
import (
"context"
"fmt"
"os"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
@@ -10,6 +11,7 @@ import (
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/cloudprovider"
"yunion.io/x/onecloud/pkg/compute/models"
"yunion.io/x/onecloud/pkg/compute/options"
"yunion.io/x/onecloud/pkg/httperrors"
)
@@ -169,7 +171,12 @@ func (self *SAzureHostDriver) RequestSaveUploadImageOnHost(ctx context.Context,
return nil, err
} else {
scimg.SetExternalId(iImage.GetId())
if result, err := iStoragecache.DownloadImage(task.GetUserCred(), imageId, iImage.GetId()); err != nil {
if _, err := os.Stat(options.Options.TempPath); os.IsNotExist(err) {
if err = os.MkdirAll(options.Options.TempPath, 0755); err != nil {
return nil, err
}
}
if result, err := iStoragecache.DownloadImage(task.GetUserCred(), imageId, iImage.GetId(), options.Options.TempPath); err != nil {
scimg.SetStatus(task.GetUserCred(), models.CACHED_IMAGE_STATUS_CACHE_FAILED, err.Error())
return nil, err
} else {
+7
View File
@@ -71,6 +71,11 @@ func (self *SCloudprovider) ValidateDeleteCondition(ctx context.Context) error {
return self.SEnabledStatusStandaloneResourceBase.ValidateDeleteCondition(ctx)
}
func (self *SCloudprovider) GetGuestCount() int {
sq := HostManager.Query("id").Equals("manager_id", self.Id)
return GuestManager.Query().In("host_id", sq).Count()
}
func (self *SCloudprovider) GetHostCount() int {
return HostManager.Query().Equals("manager_id", self.Id).Count()
}
@@ -397,6 +402,7 @@ func (manager *SCloudproviderManager) FetchCloudproviderByIdOrName(providerId st
}
type SCloudproviderUsage struct {
GuestCount int
HostCount int
VpcCount int
StorageCount int
@@ -425,6 +431,7 @@ func (usage *SCloudproviderUsage) isEmpty() bool {
func (self *SCloudprovider) getUsage() *SCloudproviderUsage {
usage := SCloudproviderUsage{}
usage.GuestCount = self.GetGuestCount()
usage.HostCount = self.GetHostCount()
usage.VpcCount = self.getVpcCount()
usage.StorageCount = self.getStorageCount()
+20
View File
@@ -3,6 +3,7 @@ package models
import (
"context"
"database/sql"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
@@ -70,6 +71,23 @@ func (self *SCloudregion) GetZoneCount() int {
}
}
func (self *SCloudregion) GetGuestCount(increment bool) int {
zoneTable := ZoneManager.Query("id").Equals("cloudregion_id", self.Id)
if self.Id == "default" {
zoneTable = ZoneManager.Query("id").Filter(sqlchemy.OR(sqlchemy.IsNull(zoneTable.Field("cloudregion_id")),
sqlchemy.IsEmpty(zoneTable.Field("cloudregion_id")),
sqlchemy.Equals(zoneTable.Field("cloudregion_id"), self.Id)))
}
sq := HostManager.Query("id").In("zone_id", zoneTable)
query := GuestManager.Query().In("host_id", sq)
if increment {
year, month, _ := time.Now().UTC().Date()
startOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
query.GE("created_at", startOfMonth)
}
return query.Count()
}
func (self *SCloudregion) GetVpcCount() int {
vpcs := VpcManager.Query()
if self.Id == "default" {
@@ -84,6 +102,8 @@ func (self *SCloudregion) GetVpcCount() int {
func (self *SCloudregion) getMoreDetails(extra *jsonutils.JSONDict) *jsonutils.JSONDict {
extra.Add(jsonutils.NewInt(int64(self.GetVpcCount())), "vpc_count")
extra.Add(jsonutils.NewInt(int64(self.GetZoneCount())), "zone_count")
extra.Add(jsonutils.NewInt(int64(self.GetGuestCount(false))), "guest_count")
extra.Add(jsonutils.NewInt(int64(self.GetGuestCount(true))), "guest_increment_count")
return extra
}
+4 -4
View File
@@ -1296,7 +1296,7 @@ func (self *SGuest) GetSecRules() []secrules.SecurityRule {
func (self *SGuest) getSecRules() []secrules.SecurityRule {
if secgrp := self.getSecgroup(); secgrp != nil {
return secgrp.getSecRules()
return secgrp.getSecRules("")
}
if rule, err := secrules.ParseSecurityRule(options.Options.DefaultSecurityRules); err == nil {
return []secrules.SecurityRule{*rule}
@@ -1309,7 +1309,7 @@ func (self *SGuest) getSecRules() []secrules.SecurityRule {
func (self *SGuest) getSecurityRules() string {
secgrp := self.getSecgroup()
if secgrp != nil {
return secgrp.getSecurityRuleString()
return secgrp.getSecurityRuleString("")
} else {
return options.Options.DefaultSecurityRules
}
@@ -1318,7 +1318,7 @@ func (self *SGuest) getSecurityRules() string {
func (self *SGuest) getAdminSecurityRules() string {
secgrp := self.getAdminSecgroup()
if secgrp != nil {
return secgrp.getSecurityRuleString()
return secgrp.getSecurityRuleString("")
} else {
return options.Options.DefaultAdminSecurityRules
}
@@ -3438,7 +3438,7 @@ func (self *SGuest) GetJsonDescAtHypervisor(ctx context.Context, host *SHost) *j
srs := self.getSecurityRuleSet()
if srs.estimatedSinglePortRuleCount() <= options.FirewallFlowCountLimit {
*/
rules := self.getSecurityRules()
if len(rules) > 0 {
desc.Add(jsonutils.NewString(rules), "security_rules")
+14 -8
View File
@@ -10,6 +10,7 @@ import (
"yunion.io/x/log"
"yunion.io/x/pkg/util/compare"
"yunion.io/x/pkg/util/secrules"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
@@ -64,7 +65,7 @@ func (self *SSecurityGroup) GetGuests() []SGuest {
func (self *SSecurityGroup) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) *jsonutils.JSONDict {
extra := self.SSharableVirtualResourceBase.GetExtraDetails(ctx, userCred, query)
extra.Add(jsonutils.NewInt(int64(len(self.GetGuests()))), "guest_cnt")
extra.Add(jsonutils.NewString(self.getSecurityRuleString()), "rules")
extra.Add(jsonutils.NewString(self.getSecurityRuleString("")), "rules")
return extra
}
@@ -73,6 +74,8 @@ func (self *SSecurityGroup) GetCustomizeColumns(ctx context.Context, userCred mc
extra.Add(jsonutils.NewInt(int64(len(self.GetGuests()))), "guest_cnt")
extra.Add(jsonutils.NewTimeString(self.CreatedAt), "created_at")
extra.Add(jsonutils.NewString(self.Description), "description")
extra.Add(jsonutils.NewString(self.getSecurityRuleString("in")), "in_rules")
extra.Add(jsonutils.NewString(self.getSecurityRuleString("out")), "out_rules")
return extra
}
@@ -97,9 +100,12 @@ func (manager *SSecurityGroupManager) FetchSecgroupById(secId string) *SSecurity
return nil
}
func (self *SSecurityGroup) getSecurityRules() (rules []SSecurityGroupRule) {
func (self *SSecurityGroup) getSecurityRules(direction string) (rules []SSecurityGroupRule) {
secgrouprules := SecurityGroupRuleManager.Query().SubQuery()
sql := secgrouprules.Query().Filter(sqlchemy.Equals(secgrouprules.Field("secgroup_id"), self.Id))
sql := secgrouprules.Query().Filter(sqlchemy.Equals(secgrouprules.Field("secgroup_id"), self.Id)).Desc("priority")
if len(direction) > 0 && utils.IsInStringArray(direction, []string{"in", "out"}) {
sql = sql.Equals("direction", direction)
}
if err := db.FetchModelObjects(SecurityGroupRuleManager, sql, &rules); err != nil {
log.Errorf("GetGuests fail %s", err)
return
@@ -107,9 +113,9 @@ func (self *SSecurityGroup) getSecurityRules() (rules []SSecurityGroupRule) {
return
}
func (self *SSecurityGroup) getSecRules() []secrules.SecurityRule {
func (self *SSecurityGroup) getSecRules(direction string) []secrules.SecurityRule {
rules := make([]secrules.SecurityRule, 0)
for _, _rule := range self.getSecurityRules() {
for _, _rule := range self.getSecurityRules(direction) {
singleRules, err := _rule.SingleRules()
if err != nil {
log.Errorf(err.Error())
@@ -119,8 +125,8 @@ func (self *SSecurityGroup) getSecRules() []secrules.SecurityRule {
return rules
}
func (self *SSecurityGroup) getSecurityRuleString() string {
secgrouprules := self.getSecurityRules()
func (self *SSecurityGroup) getSecurityRuleString(direction string) string {
secgrouprules := self.getSecurityRules(direction)
var rules []string
for _, rule := range secgrouprules {
rules = append(rules, rule.String())
@@ -158,7 +164,7 @@ func (self *SSecurityGroup) PerformClone(ctx context.Context, userCred mcclient.
return nil, err
//db.OpsLog.LogCloneEvent(self, secgroup, userCred, nil)
}
secgrouprules := self.getSecurityRules()
secgrouprules := self.getSecurityRules("")
for _, rule := range secgrouprules {
secgrouprule := &SSecurityGroupRule{}
secgrouprule.SetModelManager(SecurityGroupRuleManager)
+8 -3
View File
@@ -62,12 +62,17 @@ func (self *GuestDeleteTask) OnEipDissociateComplete(ctx context.Context, obj db
}
}
log.Debugf("XXXXXXX Do real delete on guest ... XXXXXXX")
self.OnGuestStopCompleteFailed(ctx, guest, data)
self.doStartDeleteGuest(ctx, guest)
}
func (self *GuestDeleteTask) OnGuestStopCompleteFailed(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
func (self *GuestDeleteTask) OnGuestStopCompleteFailed(ctx context.Context, obj db.IStandaloneModel, err jsonutils.JSONObject) {
guest := obj.(*models.SGuest)
guest.SetStatus(self.UserCred, models.VM_DELETING, "delete anyway")
self.OnFailed(ctx, guest, err)
}
func (self *GuestDeleteTask) doStartDeleteGuest(ctx context.Context, obj db.IStandaloneModel) {
guest := obj.(*models.SGuest)
guest.SetStatus(self.UserCred, models.VM_DELETING, "delete server after stop")
db.OpsLog.LogEvent(guest, db.ACT_DELOCATING, nil, self.UserCred)
self.StartDeleteGuest(ctx, guest)
}
@@ -0,0 +1,15 @@
package modules
var (
Baremetalagents ResourceManager
)
func init() {
Baremetalagents = NewComputeManager(
"baremetalagent",
"baremetalagents",
[]string{"ID", "Name", "Access_ip", "Manager_URI", "Status"},
[]string{},
)
registerCompute(&Baremetalagents)
}
+3 -1
View File
@@ -6,7 +6,9 @@ var (
func init() {
Cloudproviders = NewComputeManager("cloudprovider", "cloudproviders",
[]string{"ID", "Name", "Enabled", "Status", "Access_url", "Account", "Last_sync", "Provider"},
[]string{"ID", "Name", "Enabled", "Status", "Access_url", "Account",
"Last_sync", "Provider", "guest_count", "host_count", "vpc_count",
"storage_count", "storage_cache_count", "eip_count"},
[]string{})
registerCompute(&Cloudproviders)
+2 -1
View File
@@ -6,7 +6,8 @@ var (
func init() {
Cloudregions = NewComputeManager("cloudregion", "cloudregions",
[]string{"ID", "Name", "Enabled", "Status", "Provider", "Latitude", "Longitude"},
[]string{"ID", "Name", "Enabled", "Status", "Provider", "Latitude", "Longitude",
"vpc_count", "zone_count", "guest_count", "guest_increment_count"},
[]string{})
registerCompute(&Cloudregions)
+1 -1
View File
@@ -8,7 +8,7 @@ func init() {
SecGroups = NewComputeManager("secgroup", "secgroups",
[]string{"ID", "Name", "Rules",
"Is_public", "Created_at",
"Guest_cnt", "Description"},
"Guest_cnt", "Description", "in_rules", "out_rules"},
[]string{})
registerCompute(&SecGroups)
+4
View File
@@ -69,3 +69,7 @@ func (m WebConsoleManager) DoK8sLogConnect(
func (m WebConsoleManager) DoBaremetalConnect(s *mcclient.ClientSession, id string) (jsonutils.JSONObject, error) {
return m.DoConnect(s, "baremetal", id, "", nil)
}
func (m WebConsoleManager) DoSshConnect(s *mcclient.ClientSession, id string) (jsonutils.JSONObject, error) {
return m.DoConnect(s, "ssh", id, "", nil)
}
+1
View File
@@ -102,6 +102,7 @@ type Manager interface {
BatchPatchInContext(session *mcclient.ClientSession, idlist []string, params jsonutils.JSONObject, ctx Manager, ctxid string) []SubmitResult
BatchPatchInContexts(session *mcclient.ClientSession, idlist []string, params jsonutils.JSONObject, ctxs []ManagerContext) []SubmitResult
PerformAction(session *mcclient.ClientSession, id string, action string, params jsonutils.JSONObject) (jsonutils.JSONObject, error)
PerformClassAction(session *mcclient.ClientSession, action string, params jsonutils.JSONObject) (jsonutils.JSONObject, error)
PerformActionInContext(session *mcclient.ClientSession, id string, action string, params jsonutils.JSONObject, ctx Manager, ctxid string) (jsonutils.JSONObject, error)
PerformActionInContexts(session *mcclient.ClientSession, id string, action string, params jsonutils.JSONObject, ctxs []ManagerContext) (jsonutils.JSONObject, error)
BatchPerformAction(session *mcclient.ClientSession, idlist []string, action string, params jsonutils.JSONObject) []SubmitResult
+5
View File
@@ -36,3 +36,8 @@ type WebConsoleBaremetalOptions struct {
func (opt *WebConsoleBaremetalOptions) Params() (*jsonutils.JSONDict, error) {
return StructToParams(opt)
}
type WebConsoleSshOptions struct {
WebConsoleOptions
IP string `help:"IP to connect"`
}
+1
View File
@@ -5,6 +5,7 @@ import (
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/cloudprovider"
+11 -7
View File
@@ -2,6 +2,7 @@ package aliyun
import (
"fmt"
"io/ioutil"
"os"
"strings"
"time"
@@ -253,8 +254,8 @@ func (self *SRegion) createIImage(snapshoutId, imageName, imageDesc string) (str
}
}
func (self *SStoragecache) DownloadImage(userCred mcclient.TokenCredential, imageId string, extId string) (jsonutils.JSONObject, error) {
return self.downloadImage(userCred, imageId, extId)
func (self *SStoragecache) DownloadImage(userCred mcclient.TokenCredential, imageId string, extId string, path string) (jsonutils.JSONObject, error) {
return self.downloadImage(userCred, imageId, extId, path)
}
// 定义进度条监听器。
@@ -280,9 +281,12 @@ func (listener *OssProgressListener) ProgressChanged(event *oss.ProgressEvent) {
}
}
func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imageId string, extId string) (jsonutils.JSONObject, error) {
tmpImageFile := fmt.Sprintf("/opt/cloud/workspace/data/glance/image-cache/%s", extId)
defer os.Remove(tmpImageFile)
func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imageId string, extId string, path string) (jsonutils.JSONObject, error) {
tmpImageFile, err := ioutil.TempFile(path, extId)
if err != nil {
return nil, err
}
defer os.Remove(tmpImageFile.Name())
bucketName := strings.ToLower(fmt.Sprintf("imgcache-%s", self.region.GetId()))
if bucket, err := self.region.checkBucket(bucketName); err != nil {
return nil, err
@@ -296,12 +300,12 @@ func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imag
return nil, err
} else if len(imageList.Objects) != 1 {
return nil, httperrors.NewResourceNotFoundError("exported image not find")
} else if err := bucket.DownloadFile(imageList.Objects[0].Key, tmpImageFile, 12*1024*1024, oss.Routines(3), oss.Progress(&OssProgressListener{})); err != nil {
} else if err := bucket.DownloadFile(imageList.Objects[0].Key, tmpImageFile.Name(), 12*1024*1024, oss.Routines(3), oss.Progress(&OssProgressListener{})); err != nil {
return nil, err
} else {
s := auth.GetAdminSession(options.Options.Region, "")
params := jsonutils.Marshal(map[string]string{"image_id": imageId, "disk-format": "raw"})
if file, err := os.Open(tmpImageFile); err != nil {
if file, err := os.Open(tmpImageFile.Name()); err != nil {
return nil, err
} else if result, err := modules.Images.Upload(s, params, file, imageList.Objects[0].Size); err != nil {
return nil, err
+11 -7
View File
@@ -3,6 +3,7 @@ package azure
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
@@ -171,11 +172,11 @@ func (self *SStoragecache) CreateIImage(snapshotId, imageName, osType, imageDesc
}
}
func (self *SStoragecache) DownloadImage(userCred mcclient.TokenCredential, imageId string, extId string) (jsonutils.JSONObject, error) {
return self.downloadImage(userCred, imageId, extId)
func (self *SStoragecache) DownloadImage(userCred mcclient.TokenCredential, imageId string, extId string, path string) (jsonutils.JSONObject, error) {
return self.downloadImage(userCred, imageId, extId, path)
}
func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imageId string, extId string) (jsonutils.JSONObject, error) {
func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imageId string, extId string, path string) (jsonutils.JSONObject, error) {
if image, err := self.region.GetImage(extId); err != nil {
return nil, err
} else if snapshotId := image.Properties.StorageProfile.OsDisk.Snapshot.ID; len(snapshotId) == 0 {
@@ -186,9 +187,12 @@ func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imag
return nil, err
} else {
_, _, snapshot := pareResourceGroupWithName(snapshotId, SNAPSHOT_RESOURCE)
tmpImageFile := fmt.Sprintf("/opt/cloud/workspace/data/glance/image-cache/%s", snapshot)
defer os.Remove(tmpImageFile)
if f, err := os.Create(tmpImageFile); err != nil {
tmpImageFile, err := ioutil.TempFile(path, snapshot)
if err != nil {
return nil, err
}
defer os.Remove(tmpImageFile.Name())
if f, err := os.Open(tmpImageFile.Name()); err != nil {
return nil, err
} else {
readed, writed, skiped := 0, 0, 0
@@ -229,7 +233,7 @@ func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imag
s := auth.GetAdminSession(options.Options.Region, "")
params := jsonutils.Marshal(map[string]string{"image_id": imageId, "disk-format": "raw"})
if file, err := os.Open(tmpImageFile); err != nil {
if file, err := os.Open(tmpImageFile.Name()); err != nil {
return nil, err
} else if result, err := modules.Images.Upload(s, params, file, resp.ContentLength); err != nil {
return nil, err
+10
View File
@@ -15,6 +15,8 @@ type ICommand interface {
GetProtocol() string
GetCommand() *exec.Cmd
Cleanup() error
GetData(string) (isShow bool, ouput string, command string)
ShowInfo() string
}
type BaseCommand struct {
@@ -40,6 +42,14 @@ func (c BaseCommand) GetCommand() *exec.Cmd {
return exec.Command(c.name, c.args...)
}
func (c BaseCommand) GetData(comand string) (isShow bool, ouput string, command string) {
return true, "", ""
}
func (c BaseCommand) ShowInfo() string {
return ""
}
func (c BaseCommand) Cleanup() error {
log.Infof("BaseCommand Cleanup do nothing")
return nil
+72
View File
@@ -0,0 +1,72 @@
package command
import (
"fmt"
"net"
"os/exec"
"time"
"yunion.io/x/log"
o "yunion.io/x/onecloud/pkg/webconsole/options"
)
type SSHtoolSol struct {
*BaseCommand
IP string
Username string
reTry int
showInfo string
}
func NewSSHtoolSolCommand(ip string) (*SSHtoolSol, error) {
if conn, err := net.DialTimeout("tcp", ip+":22", time.Second*2); err != nil {
return nil, fmt.Errorf("IPAddress %s not accessable", ip)
} else {
conn.Close()
return &SSHtoolSol{
BaseCommand: nil,
IP: ip,
Username: "",
reTry: 0,
showInfo: fmt.Sprintf("%s login:", ip),
}, nil
}
}
func (c *SSHtoolSol) GetCommand() *exec.Cmd {
return nil
}
func (c *SSHtoolSol) Cleanup() error {
log.Infof("SSHtoolSol Cleanup do nothing")
return nil
}
func (c *SSHtoolSol) GetProtocol() string {
return PROTOCOL_TTY
}
func (c *SSHtoolSol) GetData(data string) (isShow bool, ouput string, command string) {
if len(c.Username) == 0 {
if len(data) == 0 {
//用户名不能为空
return true, c.showInfo, ""
}
c.Username = data
return false, "Password:", ""
} else {
return true, "", fmt.Sprintf("%s -p %s %s %s@%s", o.Options.SshpassToolPath, data, o.Options.SshToolPath, c.Username, c.IP)
}
}
func (c *SSHtoolSol) ShowInfo() string {
c.Username = ""
c.reTry++
if c.reTry == 3 {
c.reTry = 0
//清屏
time.Sleep(1 * time.Second)
return "\033c " + c.showInfo
}
return c.showInfo
}
+16 -1
View File
@@ -31,6 +31,7 @@ func InitHandlers(app *appsrv.Application) {
app.AddHandler("POST", ApiPathPrefix+"k8s/<podName>/shell", auth.Authenticate(handleK8sShell))
app.AddHandler("POST", ApiPathPrefix+"k8s/<podName>/log", auth.Authenticate(handleK8sLog))
app.AddHandler("POST", ApiPathPrefix+"baremetal/<id>", auth.Authenticate(handleBaremetalShell))
app.AddHandler("POST", ApiPathPrefix+"ssh/<ip>", auth.Authenticate(handleSshShell))
}
func fetchEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) {
@@ -122,7 +123,7 @@ func fetchCloudEnv(ctx context.Context, w http.ResponseWriter, r *http.Request)
if userCred == nil {
return nil, httperrors.NewUnauthorizedError("No token founded")
}
s := auth.Client().NewSession(o.Options.Region, "", "internal", userCred, "")
s := auth.Client().NewSession(o.Options.Region, "", "internal", userCred, "v2")
return &CloudEnv{
ClientSessin: s,
Params: params,
@@ -156,6 +157,20 @@ func handleK8sLog(ctx context.Context, w http.ResponseWriter, r *http.Request) {
handleK8sCommand(ctx, w, r, command.NewPodLogCommand)
}
func handleSshShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
cmd, err := command.NewSSHtoolSolCommand(env.Params["<ip>"])
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
handleCommandSession(cmd, w)
}
func handleBaremetalShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
+5 -3
View File
@@ -11,7 +11,9 @@ var (
type WebConsoleOptions struct {
cloudcommon.Options
ApiServer string `help:"API server url to handle websocket connection, usually with public access" default:"http://webconsole.yunion.io"`
KubectlPath string `help:"kubectl binary path used to connect k8s cluster" default:"/usr/bin/kubectl"`
IpmitoolPath string `help:"ipmitool binary path used to connect baremetal sol" default:"/usr/bin/ipmitool"`
ApiServer string `help:"API server url to handle websocket connection, usually with public access" default:"http://webconsole.yunion.io"`
KubectlPath string `help:"kubectl binary path used to connect k8s cluster" default:"/usr/bin/kubectl"`
IpmitoolPath string `help:"ipmitool binary path used to connect baremetal sol" default:"/usr/bin/ipmitool"`
SshToolPath string `help:"sshtool binary path used to connect server sol" default:"/usr/bin/ssh"`
SshpassToolPath string `help:"sshpass tool binary path used to connect server sol" default:"/usr/local/bin/sshpass"`
}
+60 -6
View File
@@ -1,6 +1,11 @@
package server
import (
"os/exec"
"strconv"
"strings"
"time"
socketio "github.com/googollee/go-socket.io"
"github.com/kr/pty"
@@ -56,19 +61,67 @@ func initSocketHandler(so socketio.Socket, p *session.Pty) {
go func() {
buf := make([]byte, 1024)
for {
n, err := p.Pty.Read(buf)
if err != nil {
log.Errorf("Failed to read from pty master: %v", err)
cleanUp(so, p)
if p.IsOk {
if p.Cmd == nil || p.Cmd.Process == nil {
p.IsOk = false
} else if p.Pty == nil {
p.IsOk = false
} else if n, err := p.Pty.Read(buf); err != nil {
p.IsOk = false
} else {
so.Emit(OUTPUT_EVENT, string(buf[0:n]))
}
if !p.IsOk {
p.Stop()
if info := p.Session.ShowInfo(); len(info) > 0 {
so.Emit(OUTPUT_EVENT, info)
}
}
} else if p.Exit {
return
} else {
//避免goroutine死循环导致主进程卡死
time.Sleep(time.Microsecond * 50)
}
so.Emit(OUTPUT_EVENT, string(buf[0:n]))
}
}()
// handle write
so.On(INPUT_EVENT, func(data string) {
p.Pty.Write([]byte(data))
if !p.IsOk {
if data == "\r" {
p.Show, p.Output, p.Command = p.Session.GetData(p.Buffer)
so.Emit(OUTPUT_EVENT, "\r\n")
if len(p.Output) > 0 {
so.Emit(OUTPUT_EVENT, p.Output)
}
if len(p.Command) > 0 {
log.Infof("exec: %s", p.Command)
args := strings.Split(p.Command, " ")
cmd := exec.Command(args[0], args[1:]...)
if _pty, err := pty.Start(cmd); err != nil {
so.Emit(OUTPUT_EVENT, err.Error()+"\r\n")
log.Errorf("exec error: %v", err)
} else {
p.Pty, p.Cmd, p.IsOk = _pty, cmd, true
}
}
p.Buffer, data = "", ""
} else if data == "\u007f" {
//退格处理
if len(p.Buffer) > 0 {
p.Buffer = p.Buffer[:len(p.Buffer)-1]
data = "\b \b"
}
} else if strconv.IsPrint([]rune(data)[0]) {
p.Buffer += data
}
if p.Show && len(data) > 0 {
so.Emit(OUTPUT_EVENT, data)
}
} else {
p.Pty.Write([]byte(data))
}
})
// handle resize
@@ -106,4 +159,5 @@ func initSocketHandler(so socketio.Socket, p *session.Pty) {
func cleanUp(so socketio.Socket, p *session.Pty) {
so.Disconnect()
p.Stop()
p.Exit = true
}
+1 -1
View File
@@ -34,7 +34,7 @@ func StartService() {
log.Fatalf("invalid --api-server %s", o.Options.ApiServer)
}
for _, binPath := range []string{o.Options.KubectlPath, o.Options.IpmitoolPath} {
for _, binPath := range []string{o.Options.KubectlPath, o.Options.IpmitoolPath, o.Options.SshToolPath, o.Options.SshpassToolPath} {
ensureBinExists(binPath)
}
+31 -18
View File
@@ -17,6 +17,12 @@ type Pty struct {
Pty *os.File
sizeCh chan os.Signal
size *pty.Winsize
Show bool
IsOk bool
Buffer string
Output string
Command string
Exit bool
}
func NewPty(session *SSession) (p *Pty, err error) {
@@ -24,11 +30,17 @@ func NewPty(session *SSession) (p *Pty, err error) {
p = &Pty{
Session: session,
Cmd: cmd,
Show: true,
IsOk: true,
Exit: false,
Pty: nil,
}
log.Debugf("[session %s] Start command: %#v", session.Id, cmd)
p.Pty, err = pty.Start(p.Cmd)
if err != nil {
return
if cmd != nil {
p.Pty, err = pty.Start(p.Cmd)
if err != nil {
return
}
}
p.sizeCh = make(chan os.Signal, 1)
p.size = &pty.Winsize{}
@@ -40,10 +52,12 @@ func NewPty(session *SSession) (p *Pty, err error) {
func (p *Pty) startResizeMonitor() {
go func() {
for range p.sizeCh {
if err := pty.Setsize(p.Pty, p.size); err != nil {
log.Errorf("Resize pty error: %v", err)
} else {
log.Debugf("Resize pty to %#v, cmd: %#v", p.size, p.Cmd)
if p.Pty != nil {
if err := pty.Setsize(p.Pty, p.size); err != nil {
log.Errorf("Resize pty error: %v", err)
} else {
log.Debugf("Resize pty to %#v, cmd: %#v", p.size, p.Cmd)
}
}
}
}()
@@ -55,18 +69,17 @@ func (p *Pty) Resize(size *pty.Winsize) {
}
func (p *Pty) Stop() {
var err error
err = p.Pty.Close()
if err != nil {
log.Errorf("Close PTY error: %v", err)
if p.Pty != nil {
if err := p.Pty.Close(); err != nil {
log.Errorf("Close PTY error: %v", err)
}
}
err = p.Cmd.Process.Signal(os.Kill)
if err != nil {
log.Errorf("Kill command process error: %v", err)
}
err = p.Cmd.Wait()
if err != nil {
log.Errorf("Wait command error: %v", err)
if p.Cmd != nil && p.Cmd.Process != nil {
if err := p.Cmd.Process.Signal(os.Kill); err != nil {
log.Errorf("Kill command process error: %v", err)
} else if err := p.Cmd.Wait(); err != nil {
log.Errorf("Wait command error: %v", err)
}
}
p.Session.Close()
}
+1 -22
View File
@@ -1,31 +1,10 @@
# Change Log
## v0.3.0 (2018/??/??)
next release.
## v0.2.2 (2018/06/28)
### What's new?
* Support CJK(Chinese, Japanese and Korean) and Cyrillic characters.
* Add OptionCompletionWordSeparator(x string) to customize insertion points for completions.
* To support this, text query functions by arbitrary word separator are added in Document (please see [here](https://github.com/c-bata/go-prompt/pull/79) for more details).
* Add FilePathCompleter to complete file path on your system.
* Add option to customize ascii code key bindings.
* Add GetWordAfterCursor method in Document.
### Removed or Deprecated
* SetColor method in ConsoleWriter is deprecated. Please use SetDisplayAttributes instead.
* prompt.Choose shortcut function is deprecated.
## v0.2.1 (2018/02/14)
### What's New?
* ~~It seems that windows support is almost perfect.~~
* A critical bug is found :( When you change a terminal window size, the layout will be broken because current implementation cannot catch signal for updating window size on Windows.
* It seems that windows support is almost perfect.
### Fixed
+155
View File
@@ -0,0 +1,155 @@
# Developer Guide
## Getting Started
The most simple example is below.
```go
package main
import (
"fmt"
"github.com/c-bata/go-prompt"
)
// executor executes command and print the output.
func executor(in string) {
fmt.Println("Your input: " + in)
}
// completer returns the completion items from user input.
func completer(d prompt.Document) []prompt.Suggest {
s := []prompt.Suggest{
{Text: "users", Description: "user table"},
{Text: "sites", Description: "sites table"},
{Text: "articles", Description: "articles table"},
{Text: "comments", Description: "comments table"},
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
func main() {
p := prompt.New(
executor,
completer,
prompt.OptionPrefix(">>> "),
prompt.OptionTitle("sql-prompt"),
)
p.Run()
}
```
If you want to create CLI using go-prompt, I recommend you to look at the [source code of kube-prompt](https://github.com/c-bata/kube-prompt).
It is the most practical example.
## Options
go-prompt has many color options.
It is difficult to describe by text. So please see below figure:
![options](https://github.com/c-bata/assets/raw/master/go-prompt/prompt-options.png)
* **OptionPrefixTextColor(prompt.Color)** : default `prompt.Blue`
* **OptionPrefixBackgroundColor(prompt.Color)** : default `prompt.DefaultColor`
* **OptionInputTextColor(prompt.Color)** : default `prompt.DefaultColor`
* **OptionInputBGColor(prompt.Color)** : default `prompt.DefaultColor`
* **OptionPreviewSuggestionTextColor(prompt.Color)** : default `prompt.Green`
* **OptionPreviewSuggestionBGColor(prompt.Color)** : default `prompt.DefaultColor`
* **OptionSuggestionTextColor(prompt.Color)** : default `prompt.White`
* **OptionSuggestionBGColor(prompt.Color)** : default `prompt.Cyan`
* **OptionSelectedSuggestionTextColor(prompt.Color)** : `default prompt.Black`
* **OptionSelectedSuggestionBGColor(prompt.Color)** : `default prompt.DefaultColor`
* **OptionDescriptionTextColor(prompt.Color)** : default `prompt.Black`
* **OptionDescriptionBGColor(prompt.Color)** : default `prompt.Turquoise`
* **OptionSelectedDescriptionTextColor(prompt.Color)** : default `prompt.White`
* **OptionSelectedDescriptionBGColor(prompt.Color)** : default `prompt.Cyan`
* **OptionScrollbarThumbColor** : `prompt.DarkGray`
* **OptionScrollbarBGColor** : `prompt.Cyan`
**Other Options**
#### `OptionTitle(string)` : default `""`
Option to set a title that wll be displayed on the header bar of terminal.
#### `OptionHistory([]string)` : default `[]string{}`
Option to set history.
#### `OptionPrefix(string)` : default `"> "`
Option to set prefix string.
#### `OptionLivePrefix(func() (prefix string, useLivePrefix bool))` : default `nil`
Option to set a callback function for updating prefix string dynamically.
#### `OptionMaxSuggestions(x uint16)` : default `6`
The max number of displayed suggestions.
#### `OptionParser(prompt.ConsoleParser)` : default `VT100Parser`
To set a custom ConsoleParser object.
The argument should implement ConsoleParser interface.
#### `OptionWriter(prompt.ConsoleWriter)` : default `VT100Writer`
To set a custom ConsoleWriter object.
The argument should implement ConsoleWriter interface.
#### `SwitchKeyBindMode(prompt.KeyBindMode)` : default `prompt.EmacsKeyBindMode`
To set a key bind mode.
#### `OptionAddKeyBind(...KeyBind)` : default `[]KeyBind{}`
To set a custom key bind.
## Architecture of go-prompt
*Caution: This section is WIP.*
This is a short description of go-prompt implementation.
go-prompt consists of three parts.
1. Input parser
2. Emulate user input with Buffer object.
3. Render buffer object.
### Input Parser
![input-parser animation](https://github.com/c-bata/assets/raw/master/go-prompt/input-parser.gif)
Input Parser only supports vt100-compatible console now.
* Set as the raw mode.
* Read a standard input.
* Parse to byte array
### Emulate user input
go-prompt contains Buffer class.
It represents input state by handling a key input by user.
`Buffer` object has text and cursor position.
**TODO prepare the sample of buffer**
```go
package main
import "github.com/c-bata/go-prompt"
func main() {
b := prompt.NewBuffer()
... wip
}
```
### Renderer
`Renderer` object renders a buffer object.
**TODO prepare the sample of brender**
```go
package main
```
the output is below:
**TODO prepare a screen shot**
+4 -10
View File
@@ -13,33 +13,27 @@
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
branch = "master"
name = "github.com/mattn/go-runewidth"
packages = ["."]
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
[[projects]]
branch = "master"
name = "github.com/mattn/go-tty"
packages = ["."]
revision = "931426f7535ac39720c8909d70ece5a41a2502a6"
revision = "c1750293025292316a611ae8f632d9791515e51c"
[[projects]]
branch = "master"
name = "github.com/pkg/term"
packages = ["termios"]
revision = "cda20d4ac917ad418d86e151eff439648b06185b"
revision = "b1f72af2d63057363398bec5873d16a98b453312"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "ad87a3a340fa7f3bed189293fbfa7a9b7e021ae1"
revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "d6a0ea9e49092cfd8cb3d6077c97a938de7c39195b83828dae2a0befdd207ffd"
inputs-digest = "6c442f55617e93df75aa1ee2fd2b36cfab23003fded4723be56c6b6fd0545c56"
solver-name = "gps-cdcl"
solver-version = 1
-4
View File
@@ -32,7 +32,3 @@
[[constraint]]
branch = "master"
name = "github.com/pkg/term"
[[constraint]]
branch = "master"
name = "github.com/mattn/go-runewidth"
-5
View File
@@ -21,11 +21,6 @@ lint: ## Run golint and go vet.
test: ## Run the tests.
@go test .
.PHONY: coverage
cover: ## Run the tests.
@go test -coverprofile=coverage.o
@go tool cover -func=coverage.o
.PHONY: race-test
race-test: ## Checking the race condition.
@go test -race .
+31 -31
View File
@@ -1,10 +1,7 @@
# go-prompt
[![Go Report Card](https://goreportcard.com/badge/github.com/c-bata/go-prompt)](https://goreportcard.com/report/github.com/c-bata/go-prompt)
![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)
A library for building powerful interactive prompts inspired by [python-prompt-toolkit](https://github.com/jonathanslenders/python-prompt-toolkit),
making it easier to build cross-platform command line tools using Go.
Library for building a powerful interactive prompt, inspired by [python-prompt-toolkit](https://github.com/jonathanslenders/python-prompt-toolkit).
Easy building a multi-platform binary of the command line tools because written in Golang.
```go
package main
@@ -30,15 +27,15 @@ func main() {
}
```
#### Projects using go-prompt
* [c-bata/kube-prompt : An interactive kubernetes client featuring auto-complete written in Go.](https://github.com/c-bata/kube-prompt)
* [rancher/cli : The Rancher Command Line Interface (CLI)is a unified tool to manage your Rancher server](https://github.com/rancher/cli)
* [kubicorn/kubicorn : Simple, cloud native infrastructure for Kubernetes.](https://github.com/kubicorn/kubicorn)
* [kris-nova/kubicorn : Simple. Cloud Native. Kubernetes. Infrastructure.](https://github.com/kris-nova/kubicorn)
* [cch123/asm-cli : Interactive shell of assembly language(X86/X64) based on unicorn and rasm2](https://github.com/cch123/asm-cli)
* [ktr0731/evans : more expressive universal gRPC client](https://github.com/ktr0731/evans)
* [CrushedPixel/moshpit: A Command-line tool for datamoshing.](https://github.com/CrushedPixel/moshpit)
* (If you create a CLI utility using go-prompt and want your own project to be listed here, please submit a GitHub issue.)
* (If you create a CLI using go-prompt and want your own project to be listed here, Please submit a Github Issue.)
## Features
@@ -50,52 +47,54 @@ func main() {
### Flexible options
go-prompt provides many options. Please check [option section of GoDoc](https://godoc.org/github.com/c-bata/go-prompt#Option) for more details.
go-prompt provides many options. All options are listed in [Developer Guide](./DEVELOPER_GUIDE.md).
[![options](https://github.com/c-bata/assets/raw/master/go-prompt/prompt-options.png)](#flexible-options)
### Keyboard Shortcuts
Emacs-like keyboard shortcuts are available by default (these also are the default shortcuts in Bash shell).
Emacs-like keyboard shortcut is available by default (it's also default shortcuts in Bash shell).
You can customize and expand these shortcuts.
[![keyboard shortcuts](https://github.com/c-bata/assets/raw/master/go-prompt/keyboard-shortcuts.gif)](#keyboard-shortcuts)
Key Binding | Description
---------------------|---------------------------------------------------------
<kbd>Ctrl + A</kbd> | Go to the beginning of the line (Home)
<kbd>Ctrl + E</kbd> | Go to the end of the line (End)
<kbd>Ctrl + P</kbd> | Previous command (Up arrow)
<kbd>Ctrl + N</kbd> | Next command (Down arrow)
<kbd>Ctrl + F</kbd> | Forward one character
<kbd>Ctrl + B</kbd> | Backward one character
<kbd>Ctrl + D</kbd> | Delete character under the cursor
<kbd>Ctrl + H</kbd> | Delete character before the cursor (Backspace)
<kbd>Ctrl + W</kbd> | Cut the word before the cursor to the clipboard
<kbd>Ctrl + K</kbd> | Cut the line after the cursor to the clipboard
<kbd>Ctrl + U</kbd> | Cut the line before the cursor to the clipboard
<kbd>Ctrl + L</kbd> | Clear the screen
KeyBinding | Description
--------------------|---------------------------------------------------------
<kbd>Ctrl + A</kbd> | Go to the beginning of the line (Home)
<kbd>Ctrl + E</kbd> | Go to the End of the line (End)
<kbd>Ctrl + P</kbd> | Previous command (Up arrow)
<kbd>Ctrl + N</kbd> | Next command (Down arrow)
<kbd>Ctrl + F</kbd> | Forward one character
<kbd>Ctrl + B</kbd> | Backward one character
<kbd>Ctrl + D</kbd> | Delete character under the cursor
<kbd>Ctrl + H</kbd> | Delete character before the cursor (Backspace)
<kbd>Ctrl + W</kbd> | Cut the Word before the cursor to the clipboard.
<kbd>Ctrl + K</kbd> | Cut the Line after the cursor to the clipboard.
<kbd>Ctrl + U</kbd> | Cut/delete the Line before the cursor to the clipboard.
<kbd>Ctrl + L</kbd> | Clear the screen
### History
You can use <kbd>Up arrow</kbd> and <kbd>Down arrow</kbd> to walk through the history of commands executed.
You can use up-arrow and down-arrow to walk through the history of commands executed.
[![History](https://github.com/c-bata/assets/raw/master/go-prompt/history.gif)](#history)
### Multiple platform support
We have confirmed go-prompt works fine in the following terminals:
We confirmed following terminals
* iTerm2 (macOS)
* Terminal.app (macOS)
* Command Prompt (Windows)
* gnome-terminal (Ubuntu)
* GNU Terminal (Ubuntu)
## Links
* [Developer Guide](./DEVELOPER_GUIDE.md).
* [Change Log](./CHANGELOG.md)
* [GoDoc](http://godoc.org/github.com/c-bata/go-prompt)
* [gocover.io](https://gocover.io/github.com/c-bata/go-prompt)
* [GoDoc](http://godoc.org/github.com/c-bata/go-prompt).
## Author
@@ -105,6 +104,7 @@ Masashi Shibata
* Github: [@c-bata](https://github.com/c-bata/)
* Facebook: [Masashi Shibata](https://www.facebook.com/masashi.cbata)
## License
## LICENSE
This software is licensed under the MIT License (See [LICENSE](./LICENSE) ).
This software is licensed under the MIT license, see [LICENSE](./LICENSE) for more information.
+27
View File
@@ -0,0 +1,27 @@
package prompt
import "sort"
// BisectLeft to Locate the insertion point for v in a to maintain sorted order.
func BisectLeft(a []int, v int) int {
return bisectLeftRange(a, v, 0, len(a))
}
func bisectLeftRange(a []int, v int, lo, hi int) int {
s := a[lo:hi]
return sort.Search(len(s), func(i int) bool {
return s[i] >= v
})
}
// BisectRight to Locate the insertion point for v in a to maintain sorted order.
func BisectRight(a []int, v int) int {
return bisectRightRange(a, v, 0, len(a))
}
func bisectRightRange(a []int, v int, lo, hi int) int {
s := a[lo:hi]
return sort.Search(len(s), func(i int) bool {
return s[i] > v
})
}
+27 -33
View File
@@ -9,7 +9,7 @@ import (
type Buffer struct {
workingLines []string // The working lines. Similar to history
workingIndex int
cursorPosition int
CursorPosition int
cacheDocument *Document
preferredColumn int // Remember the original column for the next up/down movement.
}
@@ -23,25 +23,19 @@ func (b *Buffer) Text() string {
func (b *Buffer) Document() (d *Document) {
if b.cacheDocument == nil ||
b.cacheDocument.Text != b.Text() ||
b.cacheDocument.cursorPosition != b.cursorPosition {
b.cacheDocument.CursorPosition != b.CursorPosition {
b.cacheDocument = &Document{
Text: b.Text(),
cursorPosition: b.cursorPosition,
CursorPosition: b.CursorPosition,
}
}
return b.cacheDocument
}
// DisplayCursorPosition returns the cursor position on rendered text on terminal emulators.
// So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
func (b *Buffer) DisplayCursorPosition() int {
return b.Document().DisplayCursorPosition()
}
// InsertText insert string from current line.
func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) {
or := []rune(b.Text())
oc := b.cursorPosition
oc := b.CursorPosition
if overwrite {
overwritten := string(or[oc : oc+len(v)])
@@ -55,15 +49,15 @@ func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) {
}
if moveCursor {
b.cursorPosition += len([]rune(v))
b.CursorPosition += len([]rune(v))
}
}
// SetText method to set text and update cursorPosition.
// SetText method to set text and update CursorPosition.
// (When doing this, make sure that the cursor_position is valid for this text.
// text/cursor_position should be consistent at any time, otherwise set a Document instead.)
func (b *Buffer) setText(v string) {
if b.cursorPosition > len([]rune(v)) {
if b.CursorPosition > len([]rune(v)) {
log.Print("[ERROR] The length of input value should be shorter than the position of cursor.")
}
o := b.workingLines[b.workingIndex]
@@ -78,11 +72,11 @@ func (b *Buffer) setText(v string) {
// Set cursor position. Return whether it changed.
func (b *Buffer) setCursorPosition(p int) {
o := b.cursorPosition
o := b.CursorPosition
if p > 0 {
b.cursorPosition = p
b.CursorPosition = p
} else {
b.cursorPosition = 0
b.CursorPosition = 0
}
if p != o {
// Cursor position is changed.
@@ -92,21 +86,21 @@ func (b *Buffer) setCursorPosition(p int) {
func (b *Buffer) setDocument(d *Document) {
b.cacheDocument = d
b.setCursorPosition(d.cursorPosition) // Call before setText because setText check the relation between cursorPosition and line length.
b.setCursorPosition(d.CursorPosition) // Call before setText because setText check the relation between cursorPosition and line length.
b.setText(d.Text)
}
// CursorLeft move to left on the current line.
func (b *Buffer) CursorLeft(count int) {
l := b.Document().GetCursorLeftPosition(count)
b.cursorPosition += l
b.CursorPosition += l
return
}
// CursorRight move to right on the current line.
func (b *Buffer) CursorRight(count int) {
l := b.Document().GetCursorRightPosition(count)
b.cursorPosition += l
b.CursorPosition += l
return
}
@@ -117,7 +111,7 @@ func (b *Buffer) CursorUp(count int) {
if b.preferredColumn == -1 { // -1 means nil
orig = b.Document().CursorPositionCol()
}
b.cursorPosition += b.Document().GetCursorUpPosition(count, orig)
b.CursorPosition += b.Document().GetCursorUpPosition(count, orig)
// Remember the original column for the next up/down movement.
b.preferredColumn = orig
@@ -130,7 +124,7 @@ func (b *Buffer) CursorDown(count int) {
if b.preferredColumn == -1 { // -1 means nil
orig = b.Document().CursorPositionCol()
}
b.cursorPosition += b.Document().GetCursorDownPosition(count, orig)
b.CursorPosition += b.Document().GetCursorDownPosition(count, orig)
// Remember the original column for the next up/down movement.
b.preferredColumn = orig
@@ -143,15 +137,15 @@ func (b *Buffer) DeleteBeforeCursor(count int) (deleted string) {
}
r := []rune(b.Text())
if b.cursorPosition > 0 {
start := b.cursorPosition - count
if b.CursorPosition > 0 {
start := b.CursorPosition - count
if start < 0 {
start = 0
}
deleted = string(r[start:b.cursorPosition])
deleted = string(r[start:b.CursorPosition])
b.setDocument(&Document{
Text: string(r[:start]) + string(r[b.cursorPosition:]),
cursorPosition: b.cursorPosition - len([]rune(deleted)),
Text: string(r[:start]) + string(r[b.CursorPosition:]),
CursorPosition: b.CursorPosition - len([]rune(deleted)),
})
}
return
@@ -169,9 +163,9 @@ func (b *Buffer) NewLine(copyMargin bool) {
// Delete specified number of characters and Return the deleted text.
func (b *Buffer) Delete(count int) (deleted string) {
r := []rune(b.Text())
if b.cursorPosition < len(r) {
if b.CursorPosition < len(r) {
deleted = b.Document().TextAfterCursor()[:count]
b.setText(string(r[:b.cursorPosition]) + string(r[b.cursorPosition+len(deleted):]))
b.setText(string(r[:b.CursorPosition]) + string(r[b.CursorPosition+len(deleted):]))
}
return
}
@@ -179,7 +173,7 @@ func (b *Buffer) Delete(count int) (deleted string) {
// JoinNextLine joins the next line to the current one by deleting the line ending after the current line.
func (b *Buffer) JoinNextLine(separator string) {
if !b.Document().OnLastLine() {
b.cursorPosition += b.Document().GetEndOfLinePosition()
b.CursorPosition += b.Document().GetEndOfLinePosition()
b.Delete(1)
// Remove spaces
b.setText(b.Document().TextBeforeCursor() + separator + strings.TrimLeft(b.Document().TextAfterCursor(), " "))
@@ -188,10 +182,10 @@ func (b *Buffer) JoinNextLine(separator string) {
// SwapCharactersBeforeCursor swaps the last two characters before the cursor.
func (b *Buffer) SwapCharactersBeforeCursor() {
if b.cursorPosition >= 2 {
x := b.Text()[b.cursorPosition-2 : b.cursorPosition-1]
y := b.Text()[b.cursorPosition-1 : b.cursorPosition]
b.setText(b.Text()[:b.cursorPosition-2] + y + x + b.Text()[b.cursorPosition:])
if b.CursorPosition >= 2 {
x := b.Text()[b.CursorPosition-2 : b.CursorPosition-1]
y := b.Text()[b.CursorPosition-1 : b.CursorPosition]
b.setText(b.Text()[:b.CursorPosition-2] + y + x + b.Text()[b.CursorPosition:])
}
}
+15 -32
View File
@@ -3,21 +3,16 @@ package prompt
import (
"log"
"strings"
"github.com/mattn/go-runewidth"
)
const (
shortenSuffix = "..."
leftPrefix = " "
leftSuffix = " "
rightPrefix = " "
rightSuffix = " "
)
var (
leftMargin = runewidth.StringWidth(leftPrefix + leftSuffix)
rightMargin = runewidth.StringWidth(rightPrefix + rightSuffix)
shortenSuffix = "..."
leftPrefix = " "
leftSuffix = " "
rightPrefix = " "
rightSuffix = " "
leftMargin = len(leftPrefix + leftSuffix)
rightMargin = len(rightPrefix + rightSuffix)
completionMargin = leftMargin + rightMargin
)
@@ -35,7 +30,6 @@ type CompletionManager struct {
completer Completer
verticalScroll int
wordSeparator string
}
// GetSelectedSuggestion returns the selected item.
@@ -108,26 +102,17 @@ func (c *CompletionManager) update() {
}
}
func deleteBreakLineCharacters(s string) string {
s = strings.Replace(s, "\n", "", -1)
s = strings.Replace(s, "\r", "", -1)
return s
}
func formatTexts(o []string, max int, prefix, suffix string) (new []string, width int) {
l := len(o)
n := make([]string, l)
lenPrefix := runewidth.StringWidth(prefix)
lenSuffix := runewidth.StringWidth(suffix)
lenShorten := runewidth.StringWidth(shortenSuffix)
lenPrefix := len([]rune(prefix))
lenSuffix := len([]rune(suffix))
lenShorten := len(shortenSuffix)
min := lenPrefix + lenSuffix + lenShorten
for i := 0; i < l; i++ {
o[i] = deleteBreakLineCharacters(o[i])
w := runewidth.StringWidth(o[i])
if width < w {
width = w
if width < len([]rune(o[i])) {
width = len([]rune(o[i]))
}
}
@@ -143,15 +128,13 @@ func formatTexts(o []string, max int, prefix, suffix string) (new []string, widt
}
for i := 0; i < l; i++ {
x := runewidth.StringWidth(o[i])
r := []rune(o[i])
x := len(r)
if x <= width {
spaces := strings.Repeat(" ", width-x)
n[i] = prefix + o[i] + spaces + suffix
} else if x > width {
x := runewidth.Truncate(o[i], width, shortenSuffix)
// When calling runewidth.Truncate("您好xxx您好xxx", 11, "...") returns "您好xxx..."
// But the length of this result is 10. So we need fill right using runewidth.FillRight.
n[i] = prefix + runewidth.FillRight(x, width) + suffix
n[i] = prefix + string(r[:width-lenShorten]) + shortenSuffix + suffix
}
}
return n, lenPrefix + width + lenSuffix
@@ -1,90 +1,63 @@
package prompt
// DisplayAttribute represents display attributes like Blinking, Bold, Italic and so on.
type DisplayAttribute int
const (
// DisplayReset reset all display attributes.
DisplayReset DisplayAttribute = iota
// DisplayBold set bold or increases intensity.
DisplayBold
// DisplayLowIntensity decreases intensity. Not widely supported.
DisplayLowIntensity
// DisplayItalic set italic. Not widely supported.
DisplayItalic
// DisplayUnderline set underline
DisplayUnderline
// DisplayBlink set blink (less than 150 per minute).
DisplayBlink
// DisplayRapidBlink set blink (more than 150 per minute). Not widely supported.
DisplayRapidBlink
// DisplayReverse swap foreground and background colors.
DisplayReverse
// DisplayInvisible set invisible. Not widely supported.
DisplayInvisible
// DisplayCrossedOut set characters legible, but marked for deletion. Not widely supported.
DisplayCrossedOut
// DisplayDefaultFont set primary(default) font
DisplayDefaultFont
)
// WinSize represents the width and height of terminal.
type WinSize struct {
Row uint16
Col uint16
}
// Color represents color on terminal.
type Color int
const (
// DefaultColor represents a default color.
DefaultColor Color = iota
// Low intensity
// Black represents a black.
Black
// DarkRed represents a dark red.
DarkRed
// DarkGreen represents a dark green.
DarkGreen
// Brown represents a brown.
Brown
// DarkBlue represents a dark blue.
DarkBlue
// Purple represents a purple.
Purple
// Cyan represents a cyan.
Cyan
// LightGray represents a light gray.
LightGray
// High intensity
// DarkGray represents a dark gray.
DarkGray
// Red represents a red.
Red
// Green represents a green.
Green
// Yellow represents a yellow.
Yellow
// Blue represents a blue.
Blue
// Fuchsia represents a fuchsia.
Fuchsia
// Turquoise represents a turquoise.
Turquoise
// White represents a white.
White
)
// ConsoleParser is an interface to abstract input layer.
type ConsoleParser interface {
// Setup should be called before starting input
Setup() error
// TearDown should be called after stopping input
TearDown() error
// GetKey returns Key correspond to input byte codes.
GetKey(b []byte) Key
// GetWinSize returns WinSize object to represent width and height of terminal.
GetWinSize() *WinSize
// Read returns byte array.
Read() ([]byte, error)
}
// ConsoleWriter is an interface to abstract output layer.
type ConsoleWriter interface {
/* Write */
// WriteRaw to write raw byte array.
WriteRaw(data []byte)
// Write to write safety byte array by removing control sequences.
// Write to write byte array without control sequences.
Write(data []byte)
// WriteStr to write raw string.
WriteRawStr(data string)
// WriteStr to write safety string by removing control sequences.
// WriteStr to write string without control sequences.
WriteStr(data string)
// Flush to flush buffer.
Flush() error
+29 -280
View File
@@ -1,41 +1,24 @@
package prompt
import (
"sort"
"strings"
"unicode/utf8"
"github.com/mattn/go-runewidth"
)
// Document has text displayed in terminal and cursor position.
type Document struct {
Text string
// This represents a index in a rune array of Document.Text.
// So if Document is "日本(cursor)語", cursorPosition is 2.
// But DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
cursorPosition int
Text string
CursorPosition int
}
// NewDocument return the new empty document.
func NewDocument() *Document {
return &Document{
Text: "",
cursorPosition: 0,
CursorPosition: 0,
}
}
// DisplayCursorPosition returns the cursor position on rendered text on terminal emulators.
// So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
func (d *Document) DisplayCursorPosition() int {
var position int
runes := []rune(d.Text)[:d.cursorPosition]
for i := range runes {
position += runewidth.RuneWidth(runes[i])
}
return position
}
// GetCharRelativeToCursor return character relative to cursor position, or empty string
func (d *Document) GetCharRelativeToCursor(offset int) (r rune) {
s := d.Text
@@ -44,7 +27,7 @@ func (d *Document) GetCharRelativeToCursor(offset int) (r rune) {
for len(s) > 0 {
cnt++
r, size := utf8.DecodeRuneInString(s)
if cnt == d.cursorPosition+offset {
if cnt == d.CursorPosition+offset {
return r
}
s = s[size:]
@@ -55,13 +38,13 @@ func (d *Document) GetCharRelativeToCursor(offset int) (r rune) {
// TextBeforeCursor returns the text before the cursor.
func (d *Document) TextBeforeCursor() string {
r := []rune(d.Text)
return string(r[:d.cursorPosition])
return string(r[:d.CursorPosition])
}
// TextAfterCursor returns the text after the cursor.
func (d *Document) TextAfterCursor() string {
r := []rune(d.Text)
return string(r[d.cursorPosition:])
return string(r[d.CursorPosition:])
}
// GetWordBeforeCursor returns the word before the cursor.
@@ -71,13 +54,6 @@ func (d *Document) GetWordBeforeCursor() string {
return x[d.FindStartOfPreviousWord():]
}
// GetWordAfterCursor returns the word after the cursor.
// If we have whitespace after the cursor this returns an empty string.
func (d *Document) GetWordAfterCursor() string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWord()]
}
// GetWordBeforeCursorWithSpace returns the word before the cursor.
// Unlike GetWordBeforeCursor, it returns string containing space
func (d *Document) GetWordBeforeCursorWithSpace() string {
@@ -85,46 +61,16 @@ func (d *Document) GetWordBeforeCursorWithSpace() string {
return x[d.FindStartOfPreviousWordWithSpace():]
}
// GetWordAfterCursorWithSpace returns the word after the cursor.
// Unlike GetWordAfterCursor, it returns string containing space
func (d *Document) GetWordAfterCursorWithSpace() string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWordWithSpace()]
}
// GetWordBeforeCursorUntilSeparator returns the text before the cursor until next separator.
func (d *Document) GetWordBeforeCursorUntilSeparator(sep string) string {
x := d.TextBeforeCursor()
return x[d.FindStartOfPreviousWordUntilSeparator(sep):]
}
// GetWordAfterCursorUntilSeparator returns the text after the cursor until next separator.
func (d *Document) GetWordAfterCursorUntilSeparator(sep string) string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWordUntilSeparator(sep)]
}
// GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor returns the word before the cursor.
// Unlike GetWordBeforeCursor, it returns string containing space
func (d *Document) GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
x := d.TextBeforeCursor()
return x[d.FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep):]
}
// GetWordAfterCursorUntilSeparatorIgnoreNextToCursor returns the word after the cursor.
// Unlike GetWordAfterCursor, it returns string containing space
func (d *Document) GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep)]
}
// FindStartOfPreviousWord returns an index relative to the cursor position
// pointing to the start of the previous word. Return 0 if nothing was found.
// pointing to the start of the previous word. Return `None` if nothing was found.
func (d *Document) FindStartOfPreviousWord() int {
// Reverse the text before the cursor, in order to do an efficient backwards search.
x := d.TextBeforeCursor()
i := strings.LastIndexByte(x, ' ')
if i != -1 {
return i + 1
l := len(x)
for i := l; i > 0; i-- {
if x[i-1:i] == " " {
return i
}
}
return 0
}
@@ -132,119 +78,21 @@ func (d *Document) FindStartOfPreviousWord() int {
// FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord.
// The only difference is to ignore contiguous spaces.
func (d *Document) FindStartOfPreviousWordWithSpace() int {
// Reverse the text before the cursor, in order to do an efficient backwards search.
x := d.TextBeforeCursor()
end := lastIndexByteNot(x, ' ')
if end == -1 {
return 0
}
start := strings.LastIndexByte(x[:end], ' ')
if start == -1 {
return 0
}
return start + 1
}
// FindStartOfPreviousWordUntilSeparator is almost the same as FindStartOfPreviousWord.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) int {
if sep == "" {
return d.FindStartOfPreviousWord()
}
x := d.TextBeforeCursor()
i := strings.LastIndexAny(x, sep)
if i != -1 {
return i + 1
l := len(x)
appear := false
for i := l; i > 0; i-- {
if x[i-1:i] != " " {
appear = true
}
if x[i-1:i] == " " && appear {
return i
}
}
return 0
}
// FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor is almost the same as FindStartOfPreviousWordWithSpace.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep string) int {
if sep == "" {
return d.FindStartOfPreviousWordWithSpace()
}
x := d.TextBeforeCursor()
end := lastIndexAnyNot(x, sep)
if end == -1 {
return 0
}
start := strings.LastIndexAny(x[:end], sep)
if start == -1 {
return 0
}
return start + 1
}
// FindEndOfCurrentWord returns an index relative to the cursor position.
// pointing to the end of the current word. Return 0 if nothing was found.
func (d *Document) FindEndOfCurrentWord() int {
x := d.TextAfterCursor()
i := strings.IndexByte(x, ' ')
if i != -1 {
return i
}
return len(x)
}
// FindEndOfCurrentWordWithSpace is almost the same as FindEndOfCurrentWord.
// The only difference is to ignore contiguous spaces.
func (d *Document) FindEndOfCurrentWordWithSpace() int {
x := d.TextAfterCursor()
start := indexByteNot(x, ' ')
if start == -1 {
return len(x)
}
end := strings.IndexByte(x[start:], ' ')
if end == -1 {
return len(x)
}
return start + end
}
// FindEndOfCurrentWordUntilSeparator is almost the same as FindEndOfCurrentWord.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindEndOfCurrentWordUntilSeparator(sep string) int {
if sep == "" {
return d.FindEndOfCurrentWord()
}
x := d.TextAfterCursor()
i := strings.IndexAny(x, sep)
if i != -1 {
return i
}
return len(x)
}
// FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor is almost the same as FindEndOfCurrentWordWithSpace.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep string) int {
if sep == "" {
return d.FindEndOfCurrentWordWithSpace()
}
x := d.TextAfterCursor()
start := indexAnyNot(x, sep)
if start == -1 {
return len(x)
}
end := strings.IndexAny(x[start:], sep)
if end == -1 {
return len(x)
}
return start + end
}
// CurrentLineBeforeCursor returns the text from the start of the line until the cursor.
func (d *Document) CurrentLineBeforeCursor() string {
s := strings.Split(d.TextBeforeCursor(), "\n")
@@ -292,14 +140,14 @@ func (d *Document) lineStartIndexes() []int {
// the first character on that line.
func (d *Document) findLineStartIndex(index int) (pos int, lineStartIndex int) {
indexes := d.lineStartIndexes()
pos = bisectRight(indexes, index) - 1
pos = BisectRight(indexes, index) - 1
lineStartIndex = indexes[pos]
return
}
// CursorPositionRow returns the current row. (0-based.)
func (d *Document) CursorPositionRow() (row int) {
row, _ = d.findLineStartIndex(d.cursorPosition)
row, _ = d.findLineStartIndex(d.CursorPosition)
return
}
@@ -307,8 +155,8 @@ func (d *Document) CursorPositionRow() (row int) {
func (d *Document) CursorPositionCol() (col int) {
// Don't use self.text_before_cursor to calculate this. Creating substrings
// and splitting is too expensive for getting the cursor position.
_, index := d.findLineStartIndex(d.cursorPosition)
col = d.cursorPosition - index
_, index := d.findLineStartIndex(d.CursorPosition)
col = d.CursorPosition - index
return
}
@@ -348,7 +196,7 @@ func (d *Document) GetCursorUpPosition(count int, preferredColumn int) int {
if row < 0 {
row = 0
}
return d.TranslateRowColToIndex(row, col) - d.cursorPosition
return d.TranslateRowColToIndex(row, col) - d.CursorPosition
}
// GetCursorDownPosition return the relative cursor position (character index) where we would be if the
@@ -361,7 +209,7 @@ func (d *Document) GetCursorDownPosition(count int, preferredColumn int) int {
col = preferredColumn
}
row := d.CursorPositionRow() + count
return d.TranslateRowColToIndex(row, col) - d.cursorPosition
return d.TranslateRowColToIndex(row, col) - d.CursorPosition
}
// Lines returns the array of all the lines.
@@ -432,102 +280,3 @@ func (d *Document) leadingWhitespaceInCurrentLine() (margin string) {
margin = d.CurrentLine()[:len(d.CurrentLine())-len(trimmed)]
return
}
// bisectRight to Locate the insertion point for v in a to maintain sorted order.
func bisectRight(a []int, v int) int {
return bisectRightRange(a, v, 0, len(a))
}
func bisectRightRange(a []int, v int, lo, hi int) int {
s := a[lo:hi]
return sort.Search(len(s), func(i int) bool {
return s[i] > v
})
}
func indexByteNot(s string, c byte) int {
n := len(s)
for i := 0; i < n; i++ {
if s[i] != c {
return i
}
}
return -1
}
func lastIndexByteNot(s string, c byte) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] != c {
return i
}
}
return -1
}
type asciiSet [8]uint32
func (as *asciiSet) notContains(c byte) bool {
return (as[c>>5] & (1 << uint(c&31))) == 0
}
func makeASCIISet(chars string) (as asciiSet, ok bool) {
for i := 0; i < len(chars); i++ {
c := chars[i]
if c >= utf8.RuneSelf {
return as, false
}
as[c>>5] |= 1 << uint(c&31)
}
return as, true
}
func indexAnyNot(s, chars string) int {
if len(chars) > 0 {
if len(s) > 8 {
if as, isASCII := makeASCIISet(chars); isASCII {
for i := 0; i < len(s); i++ {
if as.notContains(s[i]) {
return i
}
}
return -1
}
}
for i := 0; i < len(s); {
// I don't know why strings.IndexAny doesn't add rune count here.
r, size := utf8.DecodeRuneInString(s[i:])
i += size
for _, c := range chars {
if r != c {
return i
}
}
}
}
return -1
}
func lastIndexAnyNot(s, chars string) int {
if len(chars) > 0 {
if len(s) > 8 {
if as, isASCII := makeASCIISet(chars); isASCII {
for i := len(s) - 1; i >= 0; i-- {
if as.notContains(s[i]) {
return i
}
}
return -1
}
}
for i := len(s); i > 0; {
r, size := utf8.DecodeLastRuneInString(s[:i])
i -= size
for _, c := range chars {
if r != c {
return i
}
}
}
}
return -1
}
-1
View File
@@ -113,7 +113,6 @@ var emacsKeyBindings = []KeyBind{
out := NewStandardOutputWriter()
out.EraseScreen()
out.CursorGoTo(0, 0)
out.Flush()
},
},
}
+35 -151
View File
@@ -1,158 +1,42 @@
package prompt
// WinSize represents the width and height of terminal.
type WinSize struct {
Row uint16
Col uint16
func dummyExecutor(in string) { return }
// Input get the input data from the user and return it.
func Input(prefix string, completer Completer, opts ...Option) string {
pt := New(dummyExecutor, completer)
pt.renderer.prefixTextColor = DefaultColor
pt.renderer.prefix = prefix
for _, opt := range opts {
if err := opt(pt); err != nil {
panic(err)
}
}
return pt.Input()
}
// ConsoleParser is an interface to abstract input layer.
type ConsoleParser interface {
// Setup should be called before starting input
Setup() error
// TearDown should be called after stopping input
TearDown() error
// GetKey returns Key correspond to input byte codes.
GetKey(b []byte) Key
// GetWinSize returns WinSize object to represent width and height of terminal.
GetWinSize() *WinSize
// Read returns byte array.
Read() ([]byte, error)
// Choose to the shortcut of input function to select from string array.
func Choose(prefix string, choices []string, opts ...Option) string {
completer := newChoiceCompleter(choices, FilterHasPrefix)
pt := New(dummyExecutor, completer)
pt.renderer.prefixTextColor = DefaultColor
pt.renderer.prefix = prefix
for _, opt := range opts {
if err := opt(pt); err != nil {
panic(err)
}
}
return pt.Input()
}
var asciiSequences = []*ASCIICode{
{Key: Escape, ASCIICode: []byte{0x1b}},
{Key: ControlSpace, ASCIICode: []byte{0x00}},
{Key: ControlA, ASCIICode: []byte{0x1}},
{Key: ControlB, ASCIICode: []byte{0x2}},
{Key: ControlC, ASCIICode: []byte{0x3}},
{Key: ControlD, ASCIICode: []byte{0x4}},
{Key: ControlE, ASCIICode: []byte{0x5}},
{Key: ControlF, ASCIICode: []byte{0x6}},
{Key: ControlG, ASCIICode: []byte{0x7}},
{Key: ControlH, ASCIICode: []byte{0x8}},
//{Key: ControlI, ASCIICode: []byte{0x9}},
//{Key: ControlJ, ASCIICode: []byte{0xa}},
{Key: ControlK, ASCIICode: []byte{0xb}},
{Key: ControlL, ASCIICode: []byte{0xc}},
{Key: ControlM, ASCIICode: []byte{0xd}},
{Key: ControlN, ASCIICode: []byte{0xe}},
{Key: ControlO, ASCIICode: []byte{0xf}},
{Key: ControlP, ASCIICode: []byte{0x10}},
{Key: ControlQ, ASCIICode: []byte{0x11}},
{Key: ControlR, ASCIICode: []byte{0x12}},
{Key: ControlS, ASCIICode: []byte{0x13}},
{Key: ControlT, ASCIICode: []byte{0x14}},
{Key: ControlU, ASCIICode: []byte{0x15}},
{Key: ControlV, ASCIICode: []byte{0x16}},
{Key: ControlW, ASCIICode: []byte{0x17}},
{Key: ControlX, ASCIICode: []byte{0x18}},
{Key: ControlY, ASCIICode: []byte{0x19}},
{Key: ControlZ, ASCIICode: []byte{0x1a}},
{Key: ControlBackslash, ASCIICode: []byte{0x1c}},
{Key: ControlSquareClose, ASCIICode: []byte{0x1d}},
{Key: ControlCircumflex, ASCIICode: []byte{0x1e}},
{Key: ControlUnderscore, ASCIICode: []byte{0x1f}},
{Key: Backspace, ASCIICode: []byte{0x7f}},
{Key: Up, ASCIICode: []byte{0x1b, 0x5b, 0x41}},
{Key: Down, ASCIICode: []byte{0x1b, 0x5b, 0x42}},
{Key: Right, ASCIICode: []byte{0x1b, 0x5b, 0x43}},
{Key: Left, ASCIICode: []byte{0x1b, 0x5b, 0x44}},
{Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x48}},
{Key: Home, ASCIICode: []byte{0x1b, 0x30, 0x48}},
{Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x46}},
{Key: End, ASCIICode: []byte{0x1b, 0x30, 0x46}},
{Key: Enter, ASCIICode: []byte{0xa}},
{Key: Delete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x7e}},
{Key: ShiftDelete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x3b, 0x32, 0x7e}},
{Key: ControlDelete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x3b, 0x35, 0x7e}},
{Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x7e}},
{Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x34, 0x7e}},
{Key: PageUp, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x7e}},
{Key: PageDown, ASCIICode: []byte{0x1b, 0x5b, 0x36, 0x7e}},
{Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x37, 0x7e}},
{Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x38, 0x7e}},
{Key: Tab, ASCIICode: []byte{0x9}},
{Key: BackTab, ASCIICode: []byte{0x1b, 0x5b, 0x5a}},
{Key: Insert, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x7e}},
{Key: F1, ASCIICode: []byte{0x1b, 0x4f, 0x50}},
{Key: F2, ASCIICode: []byte{0x1b, 0x4f, 0x51}},
{Key: F3, ASCIICode: []byte{0x1b, 0x4f, 0x52}},
{Key: F4, ASCIICode: []byte{0x1b, 0x4f, 0x53}},
{Key: F1, ASCIICode: []byte{0x1b, 0x4f, 0x50, 0x41}}, // Linux console
{Key: F2, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x42}}, // Linux console
{Key: F3, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x43}}, // Linux console
{Key: F4, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x44}}, // Linux console
{Key: F5, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x45}}, // Linux console
{Key: F1, ASCIICode: []byte{0x1b, 0x5b, 0x11, 0x7e}}, // rxvt-unicode
{Key: F2, ASCIICode: []byte{0x1b, 0x5b, 0x12, 0x7e}}, // rxvt-unicode
{Key: F3, ASCIICode: []byte{0x1b, 0x5b, 0x13, 0x7e}}, // rxvt-unicode
{Key: F4, ASCIICode: []byte{0x1b, 0x5b, 0x14, 0x7e}}, // rxvt-unicode
{Key: F5, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x35, 0x7e}},
{Key: F6, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x37, 0x7e}},
{Key: F7, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x38, 0x7e}},
{Key: F8, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x39, 0x7e}},
{Key: F9, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x30, 0x7e}},
{Key: F10, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x31, 0x7e}},
{Key: F11, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x32, 0x7e}},
{Key: F12, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x34, 0x7e, 0x8}},
{Key: F13, ASCIICode: []byte{0x1b, 0x5b, 0x25, 0x7e}},
{Key: F14, ASCIICode: []byte{0x1b, 0x5b, 0x26, 0x7e}},
{Key: F15, ASCIICode: []byte{0x1b, 0x5b, 0x28, 0x7e}},
{Key: F16, ASCIICode: []byte{0x1b, 0x5b, 0x29, 0x7e}},
{Key: F17, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x7e}},
{Key: F18, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x7e}},
{Key: F19, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x7e}},
{Key: F20, ASCIICode: []byte{0x1b, 0x5b, 0x34, 0x7e}},
// Xterm
{Key: F13, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x50}},
{Key: F14, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x51}},
// &ASCIICode{Key: F15, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x52}}, // Conflicts with CPR response
{Key: F16, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x52}},
{Key: F17, ASCIICode: []byte{0x1b, 0x5b, 0x15, 0x3b, 0x32, 0x7e}},
{Key: F18, ASCIICode: []byte{0x1b, 0x5b, 0x17, 0x3b, 0x32, 0x7e}},
{Key: F19, ASCIICode: []byte{0x1b, 0x5b, 0x18, 0x3b, 0x32, 0x7e}},
{Key: F20, ASCIICode: []byte{0x1b, 0x5b, 0x19, 0x3b, 0x32, 0x7e}},
{Key: F21, ASCIICode: []byte{0x1b, 0x5b, 0x20, 0x3b, 0x32, 0x7e}},
{Key: F22, ASCIICode: []byte{0x1b, 0x5b, 0x21, 0x3b, 0x32, 0x7e}},
{Key: F23, ASCIICode: []byte{0x1b, 0x5b, 0x23, 0x3b, 0x32, 0x7e}},
{Key: F24, ASCIICode: []byte{0x1b, 0x5b, 0x24, 0x3b, 0x32, 0x7e}},
{Key: ControlUp, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x41}},
{Key: ControlDown, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x42}},
{Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x43}},
{Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x44}},
{Key: ShiftUp, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x41}},
{Key: ShiftDown, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x42}},
{Key: ShiftRight, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x43}},
{Key: ShiftLeft, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x44}},
// Tmux sends following keystrokes when control+arrow is pressed, but for
// Emacs ansi-term sends the same sequences for normal arrow keys. Consider
// it a normal arrow press, because that's more important.
{Key: Up, ASCIICode: []byte{0x1b, 0x4f, 0x41}},
{Key: Down, ASCIICode: []byte{0x1b, 0x4f, 0x42}},
{Key: Right, ASCIICode: []byte{0x1b, 0x4f, 0x43}},
{Key: Left, ASCIICode: []byte{0x1b, 0x4f, 0x44}},
{Key: ControlUp, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x41}},
{Key: ControlDown, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x42}},
{Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x43}},
{Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x44}},
{Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x4f, 0x63}}, // rxvt
{Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x4f, 0x64}}, // rxvt
{Key: Ignore, ASCIICode: []byte{0x1b, 0x5b, 0x45}}, // Xterm
{Key: Ignore, ASCIICode: []byte{0x1b, 0x5b, 0x46}}, // Linux console
func newChoiceCompleter(choices []string, filter Filter) Completer {
s := make([]Suggest, len(choices))
for i := range choices {
s[i] = Suggest{Text: choices[i]}
}
return func(x Document) []Suggest {
return filter(s, x.GetWordBeforeCursor(), true)
}
}
-128
View File
@@ -1,128 +0,0 @@
// +build !windows
package prompt
import (
"bytes"
"log"
"syscall"
"unsafe"
"github.com/pkg/term/termios"
)
const maxReadBytes = 1024
// PosixParser is a ConsoleParser implementation for POSIX environment.
type PosixParser struct {
fd int
origTermios syscall.Termios
}
// Setup should be called before starting input
func (t *PosixParser) Setup() error {
// Set NonBlocking mode because if syscall.Read block this goroutine, it cannot receive data from stopCh.
if err := syscall.SetNonblock(t.fd, true); err != nil {
log.Println("[ERROR] Cannot set non blocking mode.")
return err
}
if err := t.setRawMode(); err != nil {
log.Println("[ERROR] Cannot set raw mode.")
return err
}
return nil
}
// TearDown should be called after stopping input
func (t *PosixParser) TearDown() error {
if err := syscall.SetNonblock(t.fd, false); err != nil {
log.Println("[ERROR] Cannot set blocking mode.")
return err
}
if err := t.resetRawMode(); err != nil {
log.Println("[ERROR] Cannot reset from raw mode.")
return err
}
return nil
}
// Read returns byte array.
func (t *PosixParser) Read() ([]byte, error) {
buf := make([]byte, maxReadBytes)
n, err := syscall.Read(syscall.Stdin, buf)
if err != nil {
return []byte{}, err
}
return buf[:n], nil
}
func (t *PosixParser) setRawMode() error {
x := t.origTermios.Lflag
if x &^= syscall.ICANON; x != 0 && x == t.origTermios.Lflag {
// fd is already raw mode
return nil
}
var n syscall.Termios
if err := termios.Tcgetattr(uintptr(t.fd), &t.origTermios); err != nil {
return err
}
n = t.origTermios
// "&^=" used like: https://play.golang.org/p/8eJw3JxS4O
n.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.IEXTEN | syscall.ISIG
n.Cc[syscall.VMIN] = 1
n.Cc[syscall.VTIME] = 0
termios.Tcsetattr(uintptr(t.fd), termios.TCSANOW, &n)
return nil
}
func (t *PosixParser) resetRawMode() error {
if t.origTermios.Lflag == 0 {
return nil
}
return termios.Tcsetattr(uintptr(t.fd), termios.TCSANOW, &t.origTermios)
}
// GetKey returns Key correspond to input byte codes.
func (t *PosixParser) GetKey(b []byte) Key {
for _, k := range asciiSequences {
if bytes.Equal(k.ASCIICode, b) {
return k.Key
}
}
return NotDefined
}
// winsize is winsize struct got from the ioctl(2) system call.
type ioctlWinsize struct {
Row uint16
Col uint16
X uint16 // pixel value
Y uint16 // pixel value
}
// GetWinSize returns WinSize object to represent width and height of terminal.
func (t *PosixParser) GetWinSize() *WinSize {
ws := &ioctlWinsize{}
retCode, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
uintptr(t.fd),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(ws)))
if int(retCode) == -1 {
panic(errno)
}
return &WinSize{
Row: ws.Row,
Col: ws.Col,
}
}
var _ ConsoleParser = &PosixParser{}
// NewStandardInputParser returns ConsoleParser object to read from stdin.
func NewStandardInputParser() *PosixParser {
return &PosixParser{
fd: syscall.Stdin,
}
}
-94
View File
@@ -1,94 +0,0 @@
// +build windows
package prompt
import (
"bytes"
"errors"
"syscall"
"unicode/utf8"
"unsafe"
"github.com/mattn/go-tty"
)
const maxReadBytes = 1024
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var procGetNumberOfConsoleInputEvents = kernel32.NewProc("GetNumberOfConsoleInputEvents")
// WindowsParser is a ConsoleParser implementation for Win32 console.
type WindowsParser struct {
tty *tty.TTY
}
// Setup should be called before starting input
func (p *WindowsParser) Setup() error {
t, err := tty.Open()
if err != nil {
return err
}
p.tty = t
return nil
}
// TearDown should be called after stopping input
func (p *WindowsParser) TearDown() error {
return p.tty.Close()
}
// GetKey returns Key correspond to input byte codes.
func (p *WindowsParser) GetKey(b []byte) Key {
for _, k := range asciiSequences {
if bytes.Compare(k.ASCIICode, b) == 0 {
return k.Key
}
}
return NotDefined
}
// Read returns byte array.
func (p *WindowsParser) Read() ([]byte, error) {
var ev uint32
r0, _, err := procGetNumberOfConsoleInputEvents.Call(p.tty.Input().Fd(), uintptr(unsafe.Pointer(&ev)))
if r0 == 0 {
return nil, err
}
if ev == 0 {
return nil, errors.New("EAGAIN")
}
r, err := p.tty.ReadRune()
if err != nil {
return nil, err
}
buf := make([]byte, maxReadBytes)
n := utf8.EncodeRune(buf[:], r)
for p.tty.Buffered() && n < maxReadBytes {
r, err := p.tty.ReadRune()
if err != nil {
break
}
n += utf8.EncodeRune(buf[n:], r)
}
return buf[:n], nil
}
// GetWinSize returns WinSize object to represent width and height of terminal.
func (p *WindowsParser) GetWinSize() *WinSize {
w, h, err := p.tty.Size()
if err != nil {
panic(err)
}
return &WinSize{
Row: uint16(h),
Col: uint16(w),
}
}
// NewStandardInputParser returns ConsoleParser object to read from stdin.
func NewStandardInputParser() *WindowsParser {
return &WindowsParser{}
}
-3
View File
@@ -1,6 +1,3 @@
// Code generated "This is a fake comment to avoid golint errors"; DO NOT EDIT.
// FIXME: This is a little bit stupid, but there are many public constants which is no value for writing godoc comment.
package prompt
// Key is the type express the key inserted from user.
+21 -18
View File
@@ -1,59 +1,62 @@
package prompt
// KeyBindFunc receives buffer and processed it.
type KeyBindFunc func(*Buffer)
// KeyBind represents which key should do what operation.
type KeyBind struct {
Key Key
Fn KeyBindFunc
}
// ASCIICodeBind represents which []byte should do what operation
type ASCIICodeBind struct {
ASCIICode []byte
Fn KeyBindFunc
}
// KeyBindMode to switch a key binding flexibly.
type KeyBindMode string
const (
// CommonKeyBind is a mode without any keyboard shortcut
CommonKeyBind KeyBindMode = "common"
// EmacsKeyBind is a mode to use emacs-like keyboard shortcut
EmacsKeyBind KeyBindMode = "emacs"
EmacsKeyBind KeyBindMode = "emacs"
)
var commonKeyBindings = []KeyBind{
// Go to the End of the line
{
Key: End,
Fn: GoLineEnd,
Fn: func(buf *Buffer) {
x := []rune(buf.Document().TextAfterCursor())
buf.CursorRight(len(x))
},
},
// Go to the beginning of the line
{
Key: Home,
Fn: GoLineBeginning,
Fn: func(buf *Buffer) {
x := []rune(buf.Document().TextBeforeCursor())
buf.CursorLeft(len(x))
},
},
// Delete character under the cursor
{
Key: Delete,
Fn: DeleteChar,
Fn: func(buf *Buffer) {
buf.Delete(1)
},
},
// Backspace
{
Key: Backspace,
Fn: DeleteBeforeChar,
Fn: func(buf *Buffer) {
buf.DeleteBeforeCursor(1)
},
},
// Right allow: Forward one character
{
Key: Right,
Fn: GoRightChar,
Fn: func(buf *Buffer) {
buf.CursorRight(1)
},
},
// Left allow: Backward one character
{
Key: Left,
Fn: GoLeftChar,
Fn: func(buf *Buffer) {
buf.CursorLeft(1)
},
},
}
-48
View File
@@ -1,48 +0,0 @@
package prompt
// GoLineEnd Go to the End of the line
func GoLineEnd(buf *Buffer) {
x := []rune(buf.Document().TextAfterCursor())
buf.CursorRight(len(x))
}
// GoLineBeginning Go to the beginning of the line
func GoLineBeginning(buf *Buffer) {
x := []rune(buf.Document().TextBeforeCursor())
buf.CursorLeft(len(x))
}
// DeleteChar Delete character under the cursor
func DeleteChar(buf *Buffer) {
buf.Delete(1)
}
// DeleteWord Delete word before the cursor
func DeleteWord(buf *Buffer) {
buf.DeleteBeforeCursor(len([]rune(buf.Document().TextBeforeCursor())) - buf.Document().FindStartOfPreviousWordWithSpace())
}
// DeleteBeforeChar Go to Backspace
func DeleteBeforeChar(buf *Buffer) {
buf.DeleteBeforeCursor(1)
}
// GoRightChar Forward one character
func GoRightChar(buf *Buffer) {
buf.CursorRight(1)
}
// GoLeftChar Backward one character
func GoLeftChar(buf *Buffer) {
buf.CursorLeft(1)
}
// GoRightWord Forward one word
func GoRightWord(buf *Buffer) {
buf.CursorRight(buf.Document().FindEndOfCurrentWordWithSpace())
}
// GoLeftWord Backward one word
func GoLeftWord(buf *Buffer) {
buf.CursorLeft(len([]rune(buf.Document().TextBeforeCursor())) - buf.Document().FindStartOfPreviousWordWithSpace())
}
+1 -33
View File
@@ -12,7 +12,7 @@ func OptionParser(x ConsoleParser) Option {
}
}
// OptionWriter to set a custom ConsoleWriter object. An argument should implement ConsoleWriter interface.
// OptionWriter to set a custom ConsoleWriter object. An argument should implement ConsoleWriter interace.
func OptionWriter(x ConsoleWriter) Option {
return func(p *Prompt) error {
p.renderer.out = x
@@ -36,14 +36,6 @@ func OptionPrefix(x string) Option {
}
}
// OptionCompletionWordSeparator to set word separators. Enable only ' ' if empty.
func OptionCompletionWordSeparator(x string) Option {
return func(p *Prompt) error {
p.completion.wordSeparator = x
return nil
}
}
// OptionLivePrefix to change the prefix dynamically by callback function
func OptionLivePrefix(f func() (prefix string, useLivePrefix bool)) Option {
return func(p *Prompt) error {
@@ -52,7 +44,6 @@ func OptionLivePrefix(f func() (prefix string, useLivePrefix bool)) Option {
}
}
// OptionPrefixTextColor change a text color of prefix string
func OptionPrefixTextColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.prefixTextColor = x
@@ -60,7 +51,6 @@ func OptionPrefixTextColor(x Color) Option {
}
}
// OptionPrefixBackgroundColor to change a background color of prefix string
func OptionPrefixBackgroundColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.prefixBGColor = x
@@ -68,7 +58,6 @@ func OptionPrefixBackgroundColor(x Color) Option {
}
}
// OptionInputTextColor to change a color of text which is input by user
func OptionInputTextColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.inputTextColor = x
@@ -76,7 +65,6 @@ func OptionInputTextColor(x Color) Option {
}
}
// OptionInputBGColor to change a color of background which is input by user
func OptionInputBGColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.inputBGColor = x
@@ -84,7 +72,6 @@ func OptionInputBGColor(x Color) Option {
}
}
// OptionPreviewSuggestionTextColor to change a text color which is completed
func OptionPreviewSuggestionTextColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.previewSuggestionTextColor = x
@@ -92,7 +79,6 @@ func OptionPreviewSuggestionTextColor(x Color) Option {
}
}
// OptionPreviewSuggestionBGColor to change a background color which is completed
func OptionPreviewSuggestionBGColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.previewSuggestionBGColor = x
@@ -100,7 +86,6 @@ func OptionPreviewSuggestionBGColor(x Color) Option {
}
}
// OptionSuggestionTextColor to change a text color in drop down suggestions.
func OptionSuggestionTextColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.suggestionTextColor = x
@@ -108,7 +93,6 @@ func OptionSuggestionTextColor(x Color) Option {
}
}
// OptionSuggestionBGColor change a background color in drop down suggestions.
func OptionSuggestionBGColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.suggestionBGColor = x
@@ -116,7 +100,6 @@ func OptionSuggestionBGColor(x Color) Option {
}
}
// OptionSelectedSuggestionTextColor to change a text color for completed text which is selected inside suggestions drop down box.
func OptionSelectedSuggestionTextColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.selectedSuggestionTextColor = x
@@ -124,7 +107,6 @@ func OptionSelectedSuggestionTextColor(x Color) Option {
}
}
// OptionSelectedSuggestionBGColor to change a background color for completed text which is selected inside suggestions drop down box.
func OptionSelectedSuggestionBGColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.selectedSuggestionBGColor = x
@@ -132,7 +114,6 @@ func OptionSelectedSuggestionBGColor(x Color) Option {
}
}
// OptionDescriptionTextColor to change a background color of description text in drop down suggestions.
func OptionDescriptionTextColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.descriptionTextColor = x
@@ -140,7 +121,6 @@ func OptionDescriptionTextColor(x Color) Option {
}
}
// OptionDescriptionBGColor to change a background color of description text in drop down suggestions.
func OptionDescriptionBGColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.descriptionBGColor = x
@@ -148,7 +128,6 @@ func OptionDescriptionBGColor(x Color) Option {
}
}
// OptionSelectedDescriptionTextColor to change a text color of description which is selected inside suggestions drop down box.
func OptionSelectedDescriptionTextColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.selectedDescriptionTextColor = x
@@ -156,7 +135,6 @@ func OptionSelectedDescriptionTextColor(x Color) Option {
}
}
// OptionSelectedDescriptionBGColor to change a background color of description which is selected inside suggestions drop down box.
func OptionSelectedDescriptionBGColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.selectedDescriptionBGColor = x
@@ -164,7 +142,6 @@ func OptionSelectedDescriptionBGColor(x Color) Option {
}
}
// OptionScrollbarThumbColor to change a thumb color on scrollbar.
func OptionScrollbarThumbColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.scrollbarThumbColor = x
@@ -172,7 +149,6 @@ func OptionScrollbarThumbColor(x Color) Option {
}
}
// OptionScrollbarBGColor to change a background color of scrollbar.
func OptionScrollbarBGColor(x Color) Option {
return func(p *Prompt) error {
p.renderer.scrollbarBGColor = x
@@ -217,14 +193,6 @@ func OptionAddKeyBind(b ...KeyBind) Option {
}
}
// OptionAddASCIICodeBind to set a custom key bind.
func OptionAddASCIICodeBind(b ...ASCIICodeBind) Option {
return func(p *Prompt) error {
p.ASCIICodeBindings = append(p.ASCIICodeBindings, b...)
return nil
}
}
// New returns a Prompt with powerful auto-completion.
func New(executor Executor, completer Completer, opts ...Option) *Prompt {
pt := &Prompt{
-35
View File
@@ -1,35 +0,0 @@
// +build !windows
package prompt
import (
"syscall"
)
// PosixWriter is a ConsoleWriter implementation for POSIX environment.
// To control terminal emulator, this outputs VT100 escape sequences.
type PosixWriter struct {
VT100Writer
fd int
}
// Flush to flush buffer
func (w *PosixWriter) Flush() error {
_, err := syscall.Write(w.fd, w.buffer)
if err != nil {
return err
}
w.buffer = []byte{}
return nil
}
var _ ConsoleWriter = &PosixWriter{}
// NewStandardOutputWriter returns ConsoleWriter object to write to stdout.
// This generates VT100 escape sequences because almost terminal emulators
// in POSIX OS built on top of a VT100 specification.
func NewStandardOutputWriter() *PosixWriter {
return &PosixWriter{
fd: syscall.Stdout,
}
}
-333
View File
@@ -1,333 +0,0 @@
package prompt
import (
"bytes"
"strconv"
)
// VT100Writer generates VT100 escape sequences.
type VT100Writer struct {
buffer []byte
}
// WriteRaw to write raw byte array
func (w *VT100Writer) WriteRaw(data []byte) {
w.buffer = append(w.buffer, data...)
return
}
// Write to write safety byte array by removing control sequences.
func (w *VT100Writer) Write(data []byte) {
w.WriteRaw(bytes.Replace(data, []byte{0x1b}, []byte{'?'}, -1))
return
}
// WriteRawStr to write raw string
func (w *VT100Writer) WriteRawStr(data string) {
w.WriteRaw([]byte(data))
return
}
// WriteStr to write safety string by removing control sequences.
func (w *VT100Writer) WriteStr(data string) {
w.Write([]byte(data))
return
}
/* Erase */
// EraseScreen erases the screen with the background colour and moves the cursor to home.
func (w *VT100Writer) EraseScreen() {
w.WriteRaw([]byte{0x1b, '[', '2', 'J'})
return
}
// EraseUp erases the screen from the current line up to the top of the screen.
func (w *VT100Writer) EraseUp() {
w.WriteRaw([]byte{0x1b, '[', '1', 'J'})
return
}
// EraseDown erases the screen from the current line down to the bottom of the screen.
func (w *VT100Writer) EraseDown() {
w.WriteRaw([]byte{0x1b, '[', 'J'})
return
}
// EraseStartOfLine erases from the current cursor position to the start of the current line.
func (w *VT100Writer) EraseStartOfLine() {
w.WriteRaw([]byte{0x1b, '[', '1', 'K'})
return
}
// EraseEndOfLine erases from the current cursor position to the end of the current line.
func (w *VT100Writer) EraseEndOfLine() {
w.WriteRaw([]byte{0x1b, '[', 'K'})
return
}
// EraseLine erases the entire current line.
func (w *VT100Writer) EraseLine() {
w.WriteRaw([]byte{0x1b, '[', '2', 'K'})
return
}
/* Cursor */
// ShowCursor stops blinking cursor and show.
func (w *VT100Writer) ShowCursor() {
w.WriteRaw([]byte{0x1b, '[', '?', '1', '2', 'l', 0x1b, '[', '?', '2', '5', 'h'})
}
// HideCursor hides cursor.
func (w *VT100Writer) HideCursor() {
w.WriteRaw([]byte{0x1b, '[', '?', '2', '5', 'l'})
return
}
// CursorGoTo sets the cursor position where subsequent text will begin.
func (w *VT100Writer) CursorGoTo(row, col int) {
if row == 0 && col == 0 {
// If no row/column parameters are provided (ie. <ESC>[H), the cursor will move to the home position.
w.WriteRaw([]byte{0x1b, '[', 'H'})
return
}
r := strconv.Itoa(row)
c := strconv.Itoa(col)
w.WriteRaw([]byte{0x1b, '['})
w.WriteRaw([]byte(r))
w.WriteRaw([]byte{';'})
w.WriteRaw([]byte(c))
w.WriteRaw([]byte{'H'})
return
}
// CursorUp moves the cursor up by 'n' rows; the default count is 1.
func (w *VT100Writer) CursorUp(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorDown(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, '['})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{'A'})
return
}
// CursorDown moves the cursor down by 'n' rows; the default count is 1.
func (w *VT100Writer) CursorDown(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorUp(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, '['})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{'B'})
return
}
// CursorForward moves the cursor forward by 'n' columns; the default count is 1.
func (w *VT100Writer) CursorForward(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorBackward(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, '['})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{'C'})
return
}
// CursorBackward moves the cursor backward by 'n' columns; the default count is 1.
func (w *VT100Writer) CursorBackward(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorForward(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, '['})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{'D'})
return
}
// AskForCPR asks for a cursor position report (CPR).
func (w *VT100Writer) AskForCPR() {
// CPR: Cursor Position Request.
w.WriteRaw([]byte{0x1b, '[', '6', 'n'})
return
}
// SaveCursor saves current cursor position.
func (w *VT100Writer) SaveCursor() {
w.WriteRaw([]byte{0x1b, '[', 's'})
return
}
// UnSaveCursor restores cursor position after a Save Cursor.
func (w *VT100Writer) UnSaveCursor() {
w.WriteRaw([]byte{0x1b, '[', 'u'})
return
}
/* Scrolling */
// ScrollDown scrolls display down one line.
func (w *VT100Writer) ScrollDown() {
w.WriteRaw([]byte{0x1b, 'D'})
return
}
// ScrollUp scroll display up one line.
func (w *VT100Writer) ScrollUp() {
w.WriteRaw([]byte{0x1b, 'M'})
return
}
/* Title */
// SetTitle sets a title of terminal window.
func (w *VT100Writer) SetTitle(title string) {
titleBytes := []byte(title)
patterns := []struct {
from []byte
to []byte
}{
{
from: []byte{0x13},
to: []byte{},
},
{
from: []byte{0x07},
to: []byte{},
},
}
for i := range patterns {
titleBytes = bytes.Replace(titleBytes, patterns[i].from, patterns[i].to, -1)
}
w.WriteRaw([]byte{0x1b, ']', '2', ';'})
w.WriteRaw(titleBytes)
w.WriteRaw([]byte{0x07})
return
}
// ClearTitle clears a title of terminal window.
func (w *VT100Writer) ClearTitle() {
w.WriteRaw([]byte{0x1b, ']', '2', ';', 0x07})
return
}
/* Font */
// SetColor sets text and background colors. and specify whether text is bold.
func (w *VT100Writer) SetColor(fg, bg Color, bold bool) {
if bold {
w.SetDisplayAttributes(fg, bg, DisplayBold)
} else {
w.SetDisplayAttributes(fg, bg, DisplayDefaultFont)
}
return
}
// SetDisplayAttributes to set VT100 display attributes.
func (w *VT100Writer) SetDisplayAttributes(fg, bg Color, attrs ...DisplayAttribute) {
w.WriteRaw([]byte{0x1b, '['}) // control sequence introducer
defer w.WriteRaw([]byte{'m'}) // final character
var separator byte = ';'
for i := range attrs {
p, ok := displayAttributeParameters[attrs[i]]
if !ok {
continue
}
w.WriteRaw(p)
w.WriteRaw([]byte{separator})
}
f, ok := foregroundANSIColors[fg]
if !ok {
f = foregroundANSIColors[DefaultColor]
}
w.WriteRaw(f)
w.WriteRaw([]byte{separator})
b, ok := backgroundANSIColors[bg]
if !ok {
b = backgroundANSIColors[DefaultColor]
}
w.WriteRaw(b)
return
}
var displayAttributeParameters = map[DisplayAttribute][]byte{
DisplayReset: {'0'},
DisplayBold: {'1'},
DisplayLowIntensity: {'2'},
DisplayItalic: {'3'},
DisplayUnderline: {'4'},
DisplayBlink: {'5'},
DisplayRapidBlink: {'6'},
DisplayReverse: {'7'},
DisplayInvisible: {'8'},
DisplayCrossedOut: {'9'},
DisplayDefaultFont: {'1', '0'},
}
var foregroundANSIColors = map[Color][]byte{
DefaultColor: {'3', '9'},
// Low intensity.
Black: {'3', '0'},
DarkRed: {'3', '1'},
DarkGreen: {'3', '2'},
Brown: {'3', '3'},
DarkBlue: {'3', '4'},
Purple: {'3', '5'},
Cyan: {'3', '6'},
LightGray: {'3', '7'},
// High intensity.
DarkGray: {'9', '0'},
Red: {'9', '1'},
Green: {'9', '2'},
Yellow: {'9', '3'},
Blue: {'9', '4'},
Fuchsia: {'9', '5'},
Turquoise: {'9', '6'},
White: {'9', '7'},
}
var backgroundANSIColors = map[Color][]byte{
DefaultColor: {'4', '9'},
// Low intensity.
Black: {'4', '0'},
DarkRed: {'4', '1'},
DarkGreen: {'4', '2'},
Brown: {'4', '3'},
DarkBlue: {'4', '4'},
Purple: {'4', '5'},
Cyan: {'4', '6'},
LightGray: {'4', '7'},
// High intensity
DarkGray: {'1', '0', '0'},
Red: {'1', '0', '1'},
Green: {'1', '0', '2'},
Yellow: {'1', '0', '3'},
Blue: {'1', '0', '4'},
Fuchsia: {'1', '0', '5'},
Turquoise: {'1', '0', '6'},
White: {'1', '0', '7'},
}
-36
View File
@@ -1,36 +0,0 @@
// +build windows
package prompt
import (
"io"
"github.com/mattn/go-colorable"
)
// WindowsWriter is a ConsoleWriter implementation for Win32 console.
// Output is converted from VT100 escape sequences by mattn/go-colorable.
type WindowsWriter struct {
VT100Writer
out io.Writer
}
// Flush to flush buffer
func (w *WindowsWriter) Flush() error {
_, err := w.out.Write(w.buffer)
if err != nil {
return err
}
w.buffer = []byte{}
return nil
}
var _ ConsoleWriter = &WindowsWriter{}
// NewStandardOutputWriter returns ConsoleWriter object to write to stdout.
// This generates win32 control sequences.
func NewStandardOutputWriter() *WindowsWriter {
return &WindowsWriter{
out: colorable.NewColorableStdout(),
}
}
+265
View File
@@ -0,0 +1,265 @@
// +build !windows
package prompt
import (
"bytes"
"log"
"syscall"
"unsafe"
"github.com/pkg/term/termios"
)
const maxReadBytes = 1024
// PosixParser is a ConsoleParser implementation for POSIX environment.
type PosixParser struct {
fd int
origTermios syscall.Termios
}
// Setup should be called before starting input
func (t *PosixParser) Setup() error {
// Set NonBlocking mode because if syscall.Read block this goroutine, it cannot receive data from stopCh.
if err := syscall.SetNonblock(t.fd, true); err != nil {
log.Println("[ERROR] Cannot set non blocking mode.")
return err
}
if err := t.setRawMode(); err != nil {
log.Println("[ERROR] Cannot set raw mode.")
return err
}
return nil
}
// TearDown should be called after stopping input
func (t *PosixParser) TearDown() error {
if err := syscall.SetNonblock(t.fd, false); err != nil {
log.Println("[ERROR] Cannot set blocking mode.")
return err
}
if err := t.resetRawMode(); err != nil {
log.Println("[ERROR] Cannot reset from raw mode.")
return err
}
return nil
}
// Read returns byte array.
func (t *PosixParser) Read() ([]byte, error) {
buf := make([]byte, maxReadBytes)
n, err := syscall.Read(syscall.Stdin, buf)
if err != nil {
return []byte{}, err
}
return buf[:n], nil
}
func (t *PosixParser) setRawMode() error {
x := t.origTermios.Lflag
if x &^= syscall.ICANON; x != 0 && x == t.origTermios.Lflag {
// fd is already raw mode
return nil
}
var n syscall.Termios
if err := termios.Tcgetattr(uintptr(t.fd), &t.origTermios); err != nil {
return err
}
n = t.origTermios
// "&^=" used like: https://play.golang.org/p/8eJw3JxS4O
n.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.IEXTEN | syscall.ISIG
n.Cc[syscall.VMIN] = 1
n.Cc[syscall.VTIME] = 0
termios.Tcsetattr(uintptr(t.fd), termios.TCSANOW, &n)
return nil
}
func (t *PosixParser) resetRawMode() error {
if t.origTermios.Lflag == 0 {
return nil
}
return termios.Tcsetattr(uintptr(t.fd), termios.TCSANOW, &t.origTermios)
}
// GetKey returns Key correspond to input byte codes.
func (t *PosixParser) GetKey(b []byte) Key {
for _, k := range asciiSequences {
if bytes.Equal(k.ASCIICode, b) {
return k.Key
}
}
return NotDefined
}
// winsize is winsize struct got from the ioctl(2) system call.
type ioctlWinsize struct {
Row uint16
Col uint16
X uint16 // pixel value
Y uint16 // pixel value
}
// GetWinSize returns WinSize object to represent width and height of terminal.
func (t *PosixParser) GetWinSize() *WinSize {
ws := &ioctlWinsize{}
retCode, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
uintptr(t.fd),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(ws)))
if int(retCode) == -1 {
panic(errno)
}
return &WinSize{
Row: ws.Row,
Col: ws.Col,
}
}
var asciiSequences = []*ASCIICode{
{Key: Escape, ASCIICode: []byte{0x1b}},
{Key: ControlSpace, ASCIICode: []byte{0x00}},
{Key: ControlA, ASCIICode: []byte{0x1}},
{Key: ControlB, ASCIICode: []byte{0x2}},
{Key: ControlC, ASCIICode: []byte{0x3}},
{Key: ControlD, ASCIICode: []byte{0x4}},
{Key: ControlE, ASCIICode: []byte{0x5}},
{Key: ControlF, ASCIICode: []byte{0x6}},
{Key: ControlG, ASCIICode: []byte{0x7}},
{Key: ControlH, ASCIICode: []byte{0x8}},
//{Key: ControlI, ASCIICode: []byte{0x9}},
//{Key: ControlJ, ASCIICode: []byte{0xa}},
{Key: ControlK, ASCIICode: []byte{0xb}},
{Key: ControlL, ASCIICode: []byte{0xc}},
{Key: ControlM, ASCIICode: []byte{0xd}},
{Key: ControlN, ASCIICode: []byte{0xe}},
{Key: ControlO, ASCIICode: []byte{0xf}},
{Key: ControlP, ASCIICode: []byte{0x10}},
{Key: ControlQ, ASCIICode: []byte{0x11}},
{Key: ControlR, ASCIICode: []byte{0x12}},
{Key: ControlS, ASCIICode: []byte{0x13}},
{Key: ControlT, ASCIICode: []byte{0x14}},
{Key: ControlU, ASCIICode: []byte{0x15}},
{Key: ControlV, ASCIICode: []byte{0x16}},
{Key: ControlW, ASCIICode: []byte{0x17}},
{Key: ControlX, ASCIICode: []byte{0x18}},
{Key: ControlY, ASCIICode: []byte{0x19}},
{Key: ControlZ, ASCIICode: []byte{0x1a}},
{Key: ControlBackslash, ASCIICode: []byte{0x1c}},
{Key: ControlSquareClose, ASCIICode: []byte{0x1d}},
{Key: ControlCircumflex, ASCIICode: []byte{0x1e}},
{Key: ControlUnderscore, ASCIICode: []byte{0x1f}},
{Key: Backspace, ASCIICode: []byte{0x7f}},
{Key: Up, ASCIICode: []byte{0x1b, 0x5b, 0x41}},
{Key: Down, ASCIICode: []byte{0x1b, 0x5b, 0x42}},
{Key: Right, ASCIICode: []byte{0x1b, 0x5b, 0x43}},
{Key: Left, ASCIICode: []byte{0x1b, 0x5b, 0x44}},
{Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x48}},
{Key: Home, ASCIICode: []byte{0x1b, 0x30, 0x48}},
{Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x46}},
{Key: End, ASCIICode: []byte{0x1b, 0x30, 0x46}},
{Key: Enter, ASCIICode: []byte{0xa}},
{Key: Delete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x7e}},
{Key: ShiftDelete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x3b, 0x32, 0x7e}},
{Key: ControlDelete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x3b, 0x35, 0x7e}},
{Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x7e}},
{Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x34, 0x7e}},
{Key: PageUp, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x7e}},
{Key: PageDown, ASCIICode: []byte{0x1b, 0x5b, 0x36, 0x7e}},
{Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x37, 0x7e}},
{Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x38, 0x7e}},
{Key: Tab, ASCIICode: []byte{0x9}},
{Key: BackTab, ASCIICode: []byte{0x1b, 0x5b, 0x5a}},
{Key: Insert, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x7e}},
{Key: F1, ASCIICode: []byte{0x1b, 0x4f, 0x50}},
{Key: F2, ASCIICode: []byte{0x1b, 0x4f, 0x51}},
{Key: F3, ASCIICode: []byte{0x1b, 0x4f, 0x52}},
{Key: F4, ASCIICode: []byte{0x1b, 0x4f, 0x53}},
{Key: F1, ASCIICode: []byte{0x1b, 0x4f, 0x50, 0x41}}, // Linux console
{Key: F2, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x42}}, // Linux console
{Key: F3, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x43}}, // Linux console
{Key: F4, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x44}}, // Linux console
{Key: F5, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x45}}, // Linux console
{Key: F1, ASCIICode: []byte{0x1b, 0x5b, 0x11, 0x7e}}, // rxvt-unicode
{Key: F2, ASCIICode: []byte{0x1b, 0x5b, 0x12, 0x7e}}, // rxvt-unicode
{Key: F3, ASCIICode: []byte{0x1b, 0x5b, 0x13, 0x7e}}, // rxvt-unicode
{Key: F4, ASCIICode: []byte{0x1b, 0x5b, 0x14, 0x7e}}, // rxvt-unicode
{Key: F5, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x35, 0x7e}},
{Key: F6, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x37, 0x7e}},
{Key: F7, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x38, 0x7e}},
{Key: F8, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x39, 0x7e}},
{Key: F9, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x30, 0x7e}},
{Key: F10, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x31, 0x7e}},
{Key: F11, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x32, 0x7e}},
{Key: F12, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x34, 0x7e, 0x8}},
{Key: F13, ASCIICode: []byte{0x1b, 0x5b, 0x25, 0x7e}},
{Key: F14, ASCIICode: []byte{0x1b, 0x5b, 0x26, 0x7e}},
{Key: F15, ASCIICode: []byte{0x1b, 0x5b, 0x28, 0x7e}},
{Key: F16, ASCIICode: []byte{0x1b, 0x5b, 0x29, 0x7e}},
{Key: F17, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x7e}},
{Key: F18, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x7e}},
{Key: F19, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x7e}},
{Key: F20, ASCIICode: []byte{0x1b, 0x5b, 0x34, 0x7e}},
// Xterm
{Key: F13, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x50}},
{Key: F14, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x51}},
// &ASCIICode{Key: F15, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x52}}, // Conflicts with CPR response
{Key: F16, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x52}},
{Key: F17, ASCIICode: []byte{0x1b, 0x5b, 0x15, 0x3b, 0x32, 0x7e}},
{Key: F18, ASCIICode: []byte{0x1b, 0x5b, 0x17, 0x3b, 0x32, 0x7e}},
{Key: F19, ASCIICode: []byte{0x1b, 0x5b, 0x18, 0x3b, 0x32, 0x7e}},
{Key: F20, ASCIICode: []byte{0x1b, 0x5b, 0x19, 0x3b, 0x32, 0x7e}},
{Key: F21, ASCIICode: []byte{0x1b, 0x5b, 0x20, 0x3b, 0x32, 0x7e}},
{Key: F22, ASCIICode: []byte{0x1b, 0x5b, 0x21, 0x3b, 0x32, 0x7e}},
{Key: F23, ASCIICode: []byte{0x1b, 0x5b, 0x23, 0x3b, 0x32, 0x7e}},
{Key: F24, ASCIICode: []byte{0x1b, 0x5b, 0x24, 0x3b, 0x32, 0x7e}},
{Key: ControlUp, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x41}},
{Key: ControlDown, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x42}},
{Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x43}},
{Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x44}},
{Key: ShiftUp, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x41}},
{Key: ShiftDown, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x42}},
{Key: ShiftRight, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x43}},
{Key: ShiftLeft, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x44}},
// Tmux sends following keystrokes when control+arrow is pressed, but for
// Emacs ansi-term sends the same sequences for normal arrow keys. Consider
// it a normal arrow press, because that's more important.
{Key: Up, ASCIICode: []byte{0x1b, 0x4f, 0x41}},
{Key: Down, ASCIICode: []byte{0x1b, 0x4f, 0x42}},
{Key: Right, ASCIICode: []byte{0x1b, 0x4f, 0x43}},
{Key: Left, ASCIICode: []byte{0x1b, 0x4f, 0x44}},
{Key: ControlUp, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x41}},
{Key: ControlDown, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x42}},
{Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x43}},
{Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x44}},
{Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x4f, 0x63}}, // rxvt
{Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x4f, 0x64}}, // rxvt
{Key: Ignore, ASCIICode: []byte{0x1b, 0x5b, 0x45}}, // Xterm
{Key: Ignore, ASCIICode: []byte{0x1b, 0x5b, 0x46}}, // Linux console
}
var _ ConsoleParser = &PosixParser{}
// NewStandardInputParser returns ConsoleParser object to read from stdin.
func NewStandardInputParser() *PosixParser {
return &PosixParser{
fd: syscall.Stdin,
}
}
+335
View File
@@ -0,0 +1,335 @@
// +build !windows
package prompt
import (
"strconv"
"syscall"
)
// PosixWriter is a ConsoleWriter implementation for POSIX environment.
// To control terminal emulator, this outputs VT100 escape sequences.
type PosixWriter struct {
fd int
buffer []byte
}
// WriteRaw to write raw byte array
func (w *PosixWriter) WriteRaw(data []byte) {
w.buffer = append(w.buffer, data...)
// Flush because sometimes the render is broken when a large amount data in buffer.
w.Flush()
return
}
// Write to write byte array without control sequences
func (w *PosixWriter) Write(data []byte) {
w.WriteRaw(byteFilter(data, writeFilter))
return
}
// WriteRawStr to write raw string
func (w *PosixWriter) WriteRawStr(data string) {
w.WriteRaw([]byte(data))
return
}
// WriteStr to write string without control sequences
func (w *PosixWriter) WriteStr(data string) {
w.Write([]byte(data))
return
}
// Flush to flush buffer
func (w *PosixWriter) Flush() error {
_, err := syscall.Write(w.fd, w.buffer)
if err != nil {
return err
}
w.buffer = []byte{}
return nil
}
/* Erase */
// EraseScreen erases the screen with the background colour and moves the cursor to home.
func (w *PosixWriter) EraseScreen() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x32, 0x4a})
return
}
// EraseUp erases the screen from the current line up to the top of the screen.
func (w *PosixWriter) EraseUp() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x31, 0x4a})
return
}
// EraseDown erases the screen from the current line down to the bottom of the screen.
func (w *PosixWriter) EraseDown() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x4a})
return
}
// EraseStartOfLine erases from the current cursor position to the start of the current line.
func (w *PosixWriter) EraseStartOfLine() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x31, 0x4b})
return
}
// EraseEndOfLine erases from the current cursor position to the end of the current line.
func (w *PosixWriter) EraseEndOfLine() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x4b})
return
}
// EraseLine erases the entire current line.
func (w *PosixWriter) EraseLine() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x32, 0x4b})
return
}
/* Cursor */
// ShowCursor stops blinking cursor and show.
func (w *PosixWriter) ShowCursor() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x3f, 0x31, 0x32, 0x6c, 0x1b, 0x5b, 0x3f, 0x32, 0x35, 0x68})
}
// HideCursor hides cursor.
func (w *PosixWriter) HideCursor() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x3f, 0x32, 0x35, 0x6c})
return
}
// CursorGoTo sets the cursor position where subsequent text will begin.
func (w *PosixWriter) CursorGoTo(row, col int) {
if row == 0 && col == 0 {
// If no row/column parameters are provided (ie. <ESC>[H), the cursor will move to the home position.
w.WriteRaw([]byte{0x1b, 0x5b, 0x3b, 0x48})
return
}
r := strconv.Itoa(row)
c := strconv.Itoa(col)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(r))
w.WriteRaw([]byte{0x3b})
w.WriteRaw([]byte(c))
w.WriteRaw([]byte{0x48})
return
}
// CursorUp moves the cursor up by 'n' rows; the default count is 1.
func (w *PosixWriter) CursorUp(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorDown(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{0x41})
return
}
// CursorDown moves the cursor down by 'n' rows; the default count is 1.
func (w *PosixWriter) CursorDown(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorUp(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{0x42})
return
}
// CursorForward moves the cursor forward by 'n' columns; the default count is 1.
func (w *PosixWriter) CursorForward(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorBackward(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{0x43})
return
}
// CursorBackward moves the cursor backward by 'n' columns; the default count is 1.
func (w *PosixWriter) CursorBackward(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorForward(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{0x44})
return
}
// AskForCPR asks for a cursor position report (CPR).
func (w *PosixWriter) AskForCPR() {
// CPR: Cursor Position Request.
w.WriteRaw([]byte{0x1b, 0x5b, 0x36, 0x6e})
w.Flush()
return
}
// SaveCursor saves current cursor position.
func (w *PosixWriter) SaveCursor() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x73})
return
}
// UnSaveCursor restores cursor position after a Save Cursor.
func (w *PosixWriter) UnSaveCursor() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x75})
return
}
/* Scrolling */
// ScrollDown scrolls display down one line.
func (w *PosixWriter) ScrollDown() {
w.WriteRaw([]byte{0x1b, 0x44})
return
}
// ScrollUp scroll display up one line.
func (w *PosixWriter) ScrollUp() {
w.WriteRaw([]byte{0x1b, 0x4d})
return
}
/* Title */
// SetTitle sets a title of terminal window.
func (w *PosixWriter) SetTitle(title string) {
w.WriteRaw([]byte{0x1b, 0x5d, 0x32, 0x3b})
w.WriteRaw(byteFilter([]byte(title), setTextFilter))
w.WriteRaw([]byte{0x07})
return
}
// ClearTitle clears a title of terminal window.
func (w *PosixWriter) ClearTitle() {
w.WriteRaw([]byte{0x1b, 0x5d, 0x32, 0x3b, 0x07})
return
}
/* Font */
// SetColor sets text and background colors. and specify whether text is bold.
func (w *PosixWriter) SetColor(fg, bg Color, bold bool) {
f, ok := foregroundANSIColors[fg]
if !ok {
f = foregroundANSIColors[DefaultColor]
}
b, ok := backgroundANSIColors[bg]
if !ok {
b = backgroundANSIColors[DefaultColor]
}
syscall.Write(syscall.Stdout, []byte{0x1b, 0x5b, 0x33, 0x39, 0x3b, 0x34, 0x39, 0x6d})
w.WriteRaw([]byte{0x1b, 0x5b})
if !bold {
w.WriteRaw([]byte{0x30, 0x3b})
}
w.WriteRaw(f)
w.WriteRaw([]byte{0x3b})
w.WriteRaw(b)
if bold {
w.WriteRaw([]byte{0x3b, 0x31})
}
w.WriteRaw([]byte{0x6d})
return
}
var foregroundANSIColors = map[Color][]byte{
DefaultColor: {0x33, 0x39}, // 39
// Low intensity.
Black: {0x33, 0x30}, // 30
DarkRed: {0x33, 0x31}, // 31
DarkGreen: {0x33, 0x32}, // 32
Brown: {0x33, 0x33}, // 33
DarkBlue: {0x33, 0x34}, // 34
Purple: {0x33, 0x35}, // 35
Cyan: {0x33, 0x36}, //36
LightGray: {0x33, 0x37}, //37
// High intensity.
DarkGray: {0x39, 0x30}, // 90
Red: {0x39, 0x31}, // 91
Green: {0x39, 0x32}, // 92
Yellow: {0x39, 0x33}, // 93
Blue: {0x39, 0x34}, // 94
Fuchsia: {0x39, 0x35}, // 95
Turquoise: {0x39, 0x36}, // 96
White: {0x39, 0x37}, // 97
}
var backgroundANSIColors = map[Color][]byte{
DefaultColor: {0x34, 0x39}, // 49
// Low intensity.
Black: {0x34, 0x30}, // 40
DarkRed: {0x34, 0x31}, // 41
DarkGreen: {0x34, 0x32}, // 42
Brown: {0x34, 0x33}, // 43
DarkBlue: {0x34, 0x34}, // 44
Purple: {0x34, 0x35}, // 45
Cyan: {0x34, 0x36}, // 46
LightGray: {0x34, 0x37}, // 47
// High intensity
DarkGray: {0x31, 0x30, 0x30}, // 100
Red: {0x31, 0x30, 0x31}, // 101
Green: {0x31, 0x30, 0x32}, // 102
Yellow: {0x31, 0x30, 0x33}, // 103
Blue: {0x31, 0x30, 0x34}, // 104
Fuchsia: {0x31, 0x30, 0x35}, // 105
Turquoise: {0x31, 0x30, 0x36}, // 106
White: {0x31, 0x30, 0x37}, // 107
}
func writeFilter(buf byte) bool {
return buf != 0x1b && buf != 0x3f
}
func setTextFilter(buf byte) bool {
return buf != 0x1b && buf != 0x07
}
func byteFilter(buf []byte, fn ...func(b byte) bool) []byte {
if len(fn) == 0 {
return buf
}
ret := make([]byte, 0, len(buf))
f := fn[0]
for i, n := range buf {
if f(n) {
ret = append(ret, buf[i])
}
}
return byteFilter(ret, fn[1:]...)
}
var _ ConsoleWriter = &PosixWriter{}
// NewStandardOutputWriter returns ConsoleWriter object to write to stdout.
func NewStandardOutputWriter() *PosixWriter {
return &PosixWriter{
fd: syscall.Stdout,
}
}
+37 -61
View File
@@ -1,7 +1,6 @@
package prompt
import (
"bytes"
"io/ioutil"
"log"
"os"
@@ -20,15 +19,14 @@ type Completer func(Document) []Suggest
// Prompt is core struct of go-prompt.
type Prompt struct {
in ConsoleParser
buf *Buffer
renderer *Render
executor Executor
history *History
completion *CompletionManager
keyBindings []KeyBind
ASCIICodeBindings []ASCIICodeBind
keyBindMode KeyBindMode
in ConsoleParser
buf *Buffer
renderer *Render
executor Executor
history *History
completion *CompletionManager
keyBindings []KeyBind
keyBindMode KeyBindMode
}
// Exec is the struct contains user input context.
@@ -67,8 +65,6 @@ func (p *Prompt) Run() {
case b := <-bufCh:
if shouldExit, e := p.feed(b); shouldExit {
p.renderer.BreakLine(p.buf)
stopReadBufCh <- struct{}{}
stopHandleSignalCh <- struct{}{}
return
} else if e != nil {
// Stop goroutine to run readBuffer function
@@ -109,7 +105,31 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) {
// completion
completing := p.completion.Completing()
p.handleCompletionKeyBinding(key, completing)
switch key {
case Down:
if completing {
p.completion.Next()
}
case Tab, ControlI:
p.completion.Next()
case Up:
if completing {
p.completion.Previous()
}
case BackTab:
p.completion.Previous()
case ControlSpace:
return
default:
if s, ok := p.completion.GetSelectedSuggestion(); ok {
w := p.buf.Document().GetWordBeforeCursor()
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(s.Text, false, true)
}
p.completion.Reset()
}
switch key {
case Enter, ControlJ, ControlM:
@@ -144,43 +164,10 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) {
return
}
case NotDefined:
if p.handleASCIICodeBinding(b) {
return
}
p.buf.InsertText(string(b), false, true)
}
p.handleKeyBinding(key)
return
}
func (p *Prompt) handleCompletionKeyBinding(key Key, completing bool) {
switch key {
case Down:
if completing {
p.completion.Next()
}
case Tab, ControlI:
p.completion.Next()
case Up:
if completing {
p.completion.Previous()
}
case BackTab:
p.completion.Previous()
default:
if s, ok := p.completion.GetSelectedSuggestion(); ok {
w := p.buf.Document().GetWordBeforeCursorUntilSeparator(p.completion.wordSeparator)
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(s.Text, false, true)
}
p.completion.Reset()
}
}
func (p *Prompt) handleKeyBinding(key Key) {
// Key bindings
for i := range commonKeyBindings {
kb := commonKeyBindings[i]
if kb.Key == key {
@@ -204,17 +191,7 @@ func (p *Prompt) handleKeyBinding(key Key) {
kb.Fn(p.buf)
}
}
}
func (p *Prompt) handleASCIICodeBinding(b []byte) bool {
checked := false
for _, kb := range p.ASCIICodeBindings {
if bytes.Compare(kb.ASCIICode, b) == 0 {
kb.Fn(p.buf)
checked = true
}
}
return checked
return
}
// Input just returns user input text.
@@ -242,7 +219,6 @@ func (p *Prompt) Input() string {
case b := <-bufCh:
if shouldExit, e := p.feed(b); shouldExit {
p.renderer.BreakLine(p.buf)
stopReadBufCh <- struct{}{}
return ""
} else if e != nil {
// Stop goroutine to run readBuffer function
@@ -261,16 +237,16 @@ func (p *Prompt) Input() string {
func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) {
log.Printf("[INFO] readBuffer start")
for {
time.Sleep(10 * time.Millisecond)
select {
case <-stopCh:
log.Print("[INFO] stop readBuffer")
return
default:
if b, err := p.in.Read(); err == nil && !(len(b) == 1 && b[0] == 0) {
if b, err := p.in.Read(); err == nil {
bufCh <- b
}
}
time.Sleep(10 * time.Millisecond)
}
}
+36 -58
View File
@@ -1,11 +1,5 @@
package prompt
import (
"runtime"
"github.com/mattn/go-runewidth"
)
// Render to render prompt information from state of Buffer.
type Render struct {
out ConsoleWriter
@@ -88,6 +82,7 @@ func (r *Render) renderWindowTooSmall() {
r.out.EraseScreen()
r.out.SetColor(DarkRed, White, false)
r.out.WriteStr("Your console window is too small...")
r.out.Flush()
return
}
@@ -99,7 +94,7 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
prefix := r.getCurrentPrefix()
formatted, width := formatSuggestions(
suggestions,
int(r.col)-runewidth.StringWidth(prefix)-1, // -1 means a width of scrollbar
int(r.col)-len(prefix)-1, // -1 means a width of scrollbar
)
// +1 means a width of scrollbar.
width++
@@ -111,10 +106,10 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight]
r.prepareArea(windowHeight)
cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(buf.Document().TextBeforeCursor())
cursor := len(prefix) + len(buf.Document().TextBeforeCursor())
x, _ := r.toPos(cursor)
if x+width >= int(r.col) {
cursor = r.backward(cursor, x+width-int(r.col))
r.out.CursorBackward(x + width - int(r.col))
}
contentHeight := len(completions.tmp)
@@ -153,10 +148,7 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
r.out.SetColor(DefaultColor, r.scrollbarBGColor, false)
}
r.out.WriteStr(" ")
r.out.SetColor(DefaultColor, DefaultColor, false)
r.lineWrap(cursor + width)
r.backward(cursor+width, width)
r.out.CursorBackward(width)
}
if x+width >= int(r.col) {
@@ -170,64 +162,53 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
// Render renders to the console.
func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
// In situations where a pseudo tty is allocated (e.g. within a docker container),
// window size via TIOCGWINSZ is not immediately available and will result in 0,0 dimensions.
if r.col == 0 {
return
}
defer r.out.Flush()
r.move(r.previousCursor, 0)
line := buffer.Text()
prefix := r.getCurrentPrefix()
cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(line)
cursor := len(prefix) + len(line)
// prepare area
_, y := r.toPos(cursor)
// In situations where a psuedo tty is allocated (e.g. within a docker container),
// window size via TIOCGWINSZ is not immediately available and will result in 0,0 dimensions.
if r.col > 0 {
// Erasing
r.clear(r.previousCursor)
h := y + 1 + int(completion.max)
if h > int(r.row) || completionMargin > int(r.col) {
r.renderWindowTooSmall()
return
// prepare area
_, y := r.toPos(cursor)
h := y + 1 + int(completion.max)
if h > int(r.row) || completionMargin > int(r.col) {
r.renderWindowTooSmall()
return
}
}
// Rendering
r.out.HideCursor()
defer r.out.ShowCursor()
r.renderPrefix()
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
r.out.WriteStr(line)
r.out.SetColor(DefaultColor, DefaultColor, false)
r.lineWrap(cursor)
r.out.EraseDown()
cursor = r.backward(cursor, runewidth.StringWidth(line)-buffer.DisplayCursorPosition())
cursor = r.backward(cursor, len(line)-buffer.CursorPosition)
r.renderCompletion(buffer, completion)
if suggest, ok := completion.GetSelectedSuggestion(); ok {
cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator)))
cursor = r.backward(cursor, len(buffer.Document().GetWordBeforeCursor()))
r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
r.out.WriteStr(suggest.Text)
r.out.SetColor(DefaultColor, DefaultColor, false)
cursor += runewidth.StringWidth(suggest.Text)
rest := buffer.Document().TextAfterCursor()
r.out.WriteStr(rest)
cursor += runewidth.StringWidth(rest)
r.lineWrap(cursor)
cursor = r.backward(cursor, runewidth.StringWidth(rest))
cursor += len(suggest.Text)
}
r.out.Flush()
r.previousCursor = cursor
}
// BreakLine to break line.
func (r *Render) BreakLine(buffer *Buffer) {
// Erasing and Render
cursor := runewidth.StringWidth(buffer.Document().TextBeforeCursor()) + runewidth.StringWidth(r.getCurrentPrefix())
cursor := len(buffer.Document().TextBeforeCursor()) + len(r.getCurrentPrefix())
r.clear(cursor)
r.renderPrefix()
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
@@ -238,40 +219,37 @@ func (r *Render) BreakLine(buffer *Buffer) {
r.previousCursor = 0
}
// clear erases the screen from a beginning of input
// even if there is line break which means input length exceeds a window's width.
func (r *Render) clear(cursor int) {
r.move(cursor, 0)
r.backward(cursor, cursor)
r.out.EraseDown()
}
// backward moves cursor to backward from a current cursor position
// regardless there is a line break.
func (r *Render) backward(from, n int) int {
return r.move(from, from-n)
}
// move moves cursor to specified position from the beginning of input
// even if there is a line break.
func (r *Render) move(from, to int) int {
fromX, fromY := r.toPos(from)
_, fromY := r.toPos(from)
toX, toY := r.toPos(to)
r.out.CursorUp(fromY - toY)
r.out.CursorBackward(fromX - toX)
r.out.WriteRaw([]byte{'\r'})
r.out.CursorForward(toX)
return to
}
// toPos returns the relative position from the beginning of the string.
// the coordinate system with the beginning of the string as (0,0) and the width as r.col.
// the cursor points to the next character, but it points to that character only at the right end (x == r.col - 1).
// x will not return 0 except for the first row.
func (r *Render) toPos(cursor int) (x, y int) {
col := int(r.col)
return cursor % col, cursor / col
}
func (r *Render) lineWrap(cursor int) {
if runtime.GOOS != "windows" && cursor > 0 && cursor%int(r.col) == 0 {
r.out.WriteRaw([]byte{'\n'})
if cursor > 0 && cursor%col == 0 {
return col - 1, cursor/col - 1
}
return cursor % col, cursor / col
}
func clamp(high, low, x float64) float64 {
-43
View File
@@ -1,43 +0,0 @@
package prompt
func dummyExecutor(in string) { return }
// Input get the input data from the user and return it.
func Input(prefix string, completer Completer, opts ...Option) string {
pt := New(dummyExecutor, completer)
pt.renderer.prefixTextColor = DefaultColor
pt.renderer.prefix = prefix
for _, opt := range opts {
if err := opt(pt); err != nil {
panic(err)
}
}
return pt.Input()
}
// Choose to the shortcut of input function to select from string array.
// Deprecated: Maybe anyone want to use this.
func Choose(prefix string, choices []string, opts ...Option) string {
completer := newChoiceCompleter(choices, FilterHasPrefix)
pt := New(dummyExecutor, completer)
pt.renderer.prefixTextColor = DefaultColor
pt.renderer.prefix = prefix
for _, opt := range opts {
if err := opt(pt); err != nil {
panic(err)
}
}
return pt.Input()
}
func newChoiceCompleter(choices []string, filter Filter) Completer {
s := make([]Suggest, len(choices))
for i := range choices {
s[i] = Suggest{Text: choices[i]}
}
return func(x Document) []Suggest {
return filter(s, x.GetWordBeforeCursor(), true)
}
}
+214
View File
@@ -0,0 +1,214 @@
// +build windows
package prompt
import (
"bytes"
"unicode/utf8"
"github.com/mattn/go-tty"
)
const maxReadBytes = 1024
// WindowsParser is a ConsoleParser implementation for Win32 console.
type WindowsParser struct {
tty *tty.TTY
}
// Setup should be called before starting input
func (p *WindowsParser) Setup() error {
t, err := tty.Open()
if err != nil {
return err
}
p.tty = t
return nil
}
// TearDown should be called after stopping input
func (p *WindowsParser) TearDown() error {
return p.tty.Close()
}
// GetKey returns Key correspond to input byte codes.
func (p *WindowsParser) GetKey(b []byte) Key {
for _, k := range asciiSequences {
if bytes.Compare(k.ASCIICode, b) == 0 {
return k.Key
}
}
return NotDefined
}
// Read returns byte array.
func (p *WindowsParser) Read() ([]byte, error) {
buf := make([]byte, maxReadBytes)
r, err := p.tty.ReadRune()
if err != nil {
return []byte{}, err
}
n := utf8.EncodeRune(buf[:], r)
for p.tty.Buffered() && n < maxReadBytes {
r, err := p.tty.ReadRune()
if err != nil {
break
}
n += utf8.EncodeRune(buf[n:], r)
}
return buf[:n], nil
}
// GetWinSize returns WinSize object to represent width and height of terminal.
func (p *WindowsParser) GetWinSize() *WinSize {
w, h, err := p.tty.Size()
if err != nil {
panic(err)
}
return &WinSize{
Row: uint16(h),
Col: uint16(w),
}
}
var asciiSequences []*ASCIICode = []*ASCIICode{
{Key: Escape, ASCIICode: []byte{0x1b}},
{Key: ControlSpace, ASCIICode: []byte{0x00}},
{Key: ControlA, ASCIICode: []byte{0x1}},
{Key: ControlB, ASCIICode: []byte{0x2}},
{Key: ControlC, ASCIICode: []byte{0x3}},
{Key: ControlD, ASCIICode: []byte{0x4}},
{Key: ControlE, ASCIICode: []byte{0x5}},
{Key: ControlF, ASCIICode: []byte{0x6}},
{Key: ControlG, ASCIICode: []byte{0x7}},
{Key: ControlH, ASCIICode: []byte{0x8}},
//{Key: ControlI, ASCIICode: []byte{0x9}},
//{Key: ControlJ, ASCIICode: []byte{0xa}},
{Key: ControlK, ASCIICode: []byte{0xb}},
{Key: ControlL, ASCIICode: []byte{0xc}},
{Key: ControlM, ASCIICode: []byte{0xd}},
{Key: ControlN, ASCIICode: []byte{0xe}},
{Key: ControlO, ASCIICode: []byte{0xf}},
{Key: ControlP, ASCIICode: []byte{0x10}},
{Key: ControlQ, ASCIICode: []byte{0x11}},
{Key: ControlR, ASCIICode: []byte{0x12}},
{Key: ControlS, ASCIICode: []byte{0x13}},
{Key: ControlT, ASCIICode: []byte{0x14}},
{Key: ControlU, ASCIICode: []byte{0x15}},
{Key: ControlV, ASCIICode: []byte{0x16}},
{Key: ControlW, ASCIICode: []byte{0x17}},
{Key: ControlX, ASCIICode: []byte{0x18}},
{Key: ControlY, ASCIICode: []byte{0x19}},
{Key: ControlZ, ASCIICode: []byte{0x1a}},
{Key: ControlBackslash, ASCIICode: []byte{0x1c}},
{Key: ControlSquareClose, ASCIICode: []byte{0x1d}},
{Key: ControlCircumflex, ASCIICode: []byte{0x1e}},
{Key: ControlUnderscore, ASCIICode: []byte{0x1f}},
{Key: Backspace, ASCIICode: []byte{0x7f}},
{Key: Up, ASCIICode: []byte{0x1b, 0x5b, 0x41}},
{Key: Down, ASCIICode: []byte{0x1b, 0x5b, 0x42}},
{Key: Right, ASCIICode: []byte{0x1b, 0x5b, 0x43}},
{Key: Left, ASCIICode: []byte{0x1b, 0x5b, 0x44}},
{Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x48}},
{Key: Home, ASCIICode: []byte{0x1b, 0x4f, 0x48}},
{Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x70}},
{Key: End, ASCIICode: []byte{0x1b, 0x4f, 0x70}},
{Key: Enter, ASCIICode: []byte{0xa}},
{Key: Delete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x7e}},
{Key: ShiftDelete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x3b, 0x02, 0x7e}},
{Key: ControlDelete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x3b, 0x05, 0x7e}},
{Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x7e}},
{Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x04, 0x7e}},
{Key: PageUp, ASCIICode: []byte{0x1b, 0x5b, 0x05, 0x7e}},
{Key: PageDown, ASCIICode: []byte{0x1b, 0x5b, 0x06, 0x7e}},
{Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x07, 0x7e}},
{Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x09, 0x7e}},
{Key: Tab, ASCIICode: []byte{0x9}},
{Key: BackTab, ASCIICode: []byte{0x1b, 0x5b, 0x5a}},
{Key: Insert, ASCIICode: []byte{0x1b, 0x5b, 0x02, 0x7e}},
{Key: F1, ASCIICode: []byte{0x1b, 0x4f, 0x50}},
{Key: F2, ASCIICode: []byte{0x1b, 0x4f, 0x51}},
{Key: F3, ASCIICode: []byte{0x1b, 0x4f, 0x52}},
{Key: F4, ASCIICode: []byte{0x1b, 0x4f, 0x53}},
{Key: F1, ASCIICode: []byte{0x1b, 0x4f, 0x50, 0x41}}, // Linux console
{Key: F2, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x42}}, // Linux console
{Key: F3, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x43}}, // Linux console
{Key: F4, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x44}}, // Linux console
{Key: F5, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x45}}, // Linux console
{Key: F1, ASCIICode: []byte{0x1b, 0x5b, 0x11, 0x7e}}, // rxvt-unicode
{Key: F2, ASCIICode: []byte{0x1b, 0x5b, 0x12, 0x7e}}, // rxvt-unicode
{Key: F3, ASCIICode: []byte{0x1b, 0x5b, 0x13, 0x7e}}, // rxvt-unicode
{Key: F4, ASCIICode: []byte{0x1b, 0x5b, 0x14, 0x7e}}, // rxvt-unicode
{Key: F5, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x35, 0x7e}},
{Key: F6, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x37, 0x7e}},
{Key: F7, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x38, 0x7e}},
{Key: F8, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x39, 0x7e}},
{Key: F9, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x30, 0x7e}},
{Key: F10, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x31, 0x7e}},
{Key: F11, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x32, 0x7e}},
{Key: F12, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x34, 0x7e, 0x8}},
{Key: F13, ASCIICode: []byte{0x1b, 0x5b, 0x25, 0x7e}},
{Key: F14, ASCIICode: []byte{0x1b, 0x5b, 0x26, 0x7e}},
{Key: F15, ASCIICode: []byte{0x1b, 0x5b, 0x28, 0x7e}},
{Key: F16, ASCIICode: []byte{0x1b, 0x5b, 0x29, 0x7e}},
{Key: F17, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x7e}},
{Key: F18, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x7e}},
{Key: F19, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x7e}},
{Key: F20, ASCIICode: []byte{0x1b, 0x5b, 0x34, 0x7e}},
// Xterm
{Key: F13, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x3b, 0x02, 0x50}},
{Key: F14, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x3b, 0x02, 0x51}},
// &ASCIICode{Key: F15, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x3b, 0x02, 0x52}}, // Conflicts with CPR response
{Key: F16, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x3b, 0x02, 0x52}},
{Key: F17, ASCIICode: []byte{0x1b, 0x5b, 0x15, 0x3b, 0x02, 0x7e}},
{Key: F18, ASCIICode: []byte{0x1b, 0x5b, 0x17, 0x3b, 0x02, 0x7e}},
{Key: F19, ASCIICode: []byte{0x1b, 0x5b, 0x18, 0x3b, 0x02, 0x7e}},
{Key: F20, ASCIICode: []byte{0x1b, 0x5b, 0x19, 0x3b, 0x02, 0x7e}},
{Key: F21, ASCIICode: []byte{0x1b, 0x5b, 0x20, 0x3b, 0x02, 0x7e}},
{Key: F22, ASCIICode: []byte{0x1b, 0x5b, 0x21, 0x3b, 0x02, 0x7e}},
{Key: F23, ASCIICode: []byte{0x1b, 0x5b, 0x23, 0x3b, 0x02, 0x7e}},
{Key: F24, ASCIICode: []byte{0x1b, 0x5b, 0x24, 0x3b, 0x02, 0x7e}},
{Key: ControlUp, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x3b, 0x5a}},
{Key: ControlDown, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x3b, 0x5b}},
{Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x3b, 0x5c}},
{Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x3b, 0x5d}},
{Key: ShiftUp, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x2a}},
{Key: ShiftDown, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x2b}},
{Key: ShiftRight, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x2c}},
{Key: ShiftLeft, ASCIICode: []byte{0x1b, 0x5b, 0x01, 0x2d}},
// Tmux sends following keystrokes when control+arrow is pressed, but for
// Emacs ansi-term sends the same sequences for normal arrow keys. Consider
// it a normal arrow press, because that's more important.
{Key: Up, ASCIICode: []byte{0x1b, 0x4f, 0x41}},
{Key: Down, ASCIICode: []byte{0x1b, 0x4f, 0x42}},
{Key: Right, ASCIICode: []byte{0x1b, 0x4f, 0x43}},
{Key: Left, ASCIICode: []byte{0x1b, 0x4f, 0x44}},
{Key: ControlUp, ASCIICode: []byte{0x1b, 0x5b, 0x05, 0x41}},
{Key: ControlDown, ASCIICode: []byte{0x1b, 0x5b, 0x05, 0x42}},
{Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x05, 0x43}},
{Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x05, 0x44}},
{Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x4f, 0x63}}, // rxvt
{Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x4f, 0x64}}, // rxvt
{Key: Ignore, ASCIICode: []byte{0x1b, 0x5b, 0x45}}, // Xterm
{Key: Ignore, ASCIICode: []byte{0x1b, 0x5b, 0x46}}, // Linux console
}
// NewStandardInputParser returns ConsoleParser object to read from stdin.
func NewStandardInputParser() *WindowsParser {
return &WindowsParser{}
}
+333
View File
@@ -0,0 +1,333 @@
// +build windows
package prompt
import (
"io"
"strconv"
"github.com/mattn/go-colorable"
)
// WindowsWriter is a ConsoleWriter implementation for Win32 console.
// Output is converted from VT100 escape sequences by mattn/go-colorable.
type WindowsWriter struct {
out io.Writer
buffer []byte
}
// WriteRaw to write raw byte array
func (w *WindowsWriter) WriteRaw(data []byte) {
w.buffer = append(w.buffer, data...)
// Flush because sometimes the render is broken when a large amount data in buffer.
w.Flush()
return
}
// Write to write byte array without control sequences
func (w *WindowsWriter) Write(data []byte) {
w.WriteRaw(byteFilter(data, writeFilter))
return
}
// WriteRawStr to write raw string
func (w *WindowsWriter) WriteRawStr(data string) {
w.WriteRaw([]byte(data))
return
}
// WriteStr to write string without control sequences
func (w *WindowsWriter) WriteStr(data string) {
w.Write([]byte(data))
return
}
// Flush to flush buffer
func (w *WindowsWriter) Flush() error {
_, err := w.out.Write(w.buffer)
if err != nil {
return err
}
w.buffer = []byte{}
return nil
}
/* Erase */
// EraseScreen erases the screen with the background colour and moves the cursor to home.
func (w *WindowsWriter) EraseScreen() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x32, 0x4a})
return
}
// EraseUp erases the screen from the current line up to the top of the screen.
func (w *WindowsWriter) EraseUp() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x31, 0x4a})
return
}
// EraseDown erases the screen from the current line down to the bottom of the screen.
func (w *WindowsWriter) EraseDown() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x4a})
return
}
// EraseStartOfLine erases from the current cursor position to the start of the current line.
func (w *WindowsWriter) EraseStartOfLine() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x31, 0x4b})
return
}
// EraseEndOfLine erases from the current cursor position to the end of the current line.
func (w *WindowsWriter) EraseEndOfLine() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x4b})
return
}
// EraseLine erases the entire current line.
func (w *WindowsWriter) EraseLine() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x32, 0x4b})
return
}
/* Cursor */
// ShowCursor stops blinking cursor and show.
func (w *WindowsWriter) ShowCursor() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x3f, 0x31, 0x32, 0x6c, 0x1b, 0x5b, 0x3f, 0x32, 0x35, 0x68})
}
// HideCursor hides cursor.
func (w *WindowsWriter) HideCursor() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x3f, 0x32, 0x35, 0x6c})
return
}
// CursorGoTo sets the cursor position where subsequent text will begin.
func (w *WindowsWriter) CursorGoTo(row, col int) {
if row == 0 && col == 0 {
// If no row/column parameters are provided (ie. <ESC>[H), the cursor will move to the home position.
w.WriteRaw([]byte{0x1b, 0x5b, 0x3b, 0x48})
return
}
r := strconv.Itoa(row)
c := strconv.Itoa(col)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(r))
w.WriteRaw([]byte{0x3b})
w.WriteRaw([]byte(c))
w.WriteRaw([]byte{0x48})
return
}
// CursorUp moves the cursor up by 'n' rows; the default count is 1.
func (w *WindowsWriter) CursorUp(n int) {
if n < 0 {
w.CursorDown(n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{0x41})
return
}
// CursorDown moves the cursor down by 'n' rows; the default count is 1.
func (w *WindowsWriter) CursorDown(n int) {
if n < 0 {
w.CursorUp(n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{0x42})
return
}
// CursorForward moves the cursor forward by 'n' columns; the default count is 1.
func (w *WindowsWriter) CursorForward(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorBackward(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{0x43})
return
}
// CursorBackward moves the cursor backward by 'n' columns; the default count is 1.
func (w *WindowsWriter) CursorBackward(n int) {
if n == 0 {
return
} else if n < 0 {
w.CursorForward(-n)
return
}
s := strconv.Itoa(n)
w.WriteRaw([]byte{0x1b, 0x5b})
w.WriteRaw([]byte(s))
w.WriteRaw([]byte{0x44})
return
}
// AskForCPR asks for a cursor position report (CPR).
func (w *WindowsWriter) AskForCPR() {
// CPR: Cursor Position Request.
w.WriteRaw([]byte{0x1b, 0x5b, 0x36, 0x6e})
w.Flush()
return
}
// SaveCursor saves current cursor position.
func (w *WindowsWriter) SaveCursor() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x73})
return
}
// UnSaveCursor restores cursor position after a Save Cursor.
func (w *WindowsWriter) UnSaveCursor() {
w.WriteRaw([]byte{0x1b, 0x5b, 0x75})
return
}
/* Scrolling */
// ScrollDown scrolls display down one line.
func (w *WindowsWriter) ScrollDown() {
w.WriteRaw([]byte{0x1b, 0x44})
return
}
// ScrollUp scroll display up one line.
func (w *WindowsWriter) ScrollUp() {
w.WriteRaw([]byte{0x1b, 0x4d})
return
}
/* Title */
// SetTitle sets a title of terminal window.
func (w *WindowsWriter) SetTitle(title string) {
w.WriteRaw([]byte{0x1b, 0x5d, 0x32, 0x3b})
w.WriteRaw(byteFilter([]byte(title), setTextFilter))
w.WriteRaw([]byte{0x07})
return
}
// ClearTitle clears a title of terminal window.
func (w *WindowsWriter) ClearTitle() {
w.WriteRaw([]byte{0x1b, 0x5d, 0x32, 0x3b, 0x07})
return
}
/* Font */
// SetColor sets text and background colors. and specify whether text is bold.
func (w *WindowsWriter) SetColor(fg, bg Color, bold bool) {
f, ok := foregroundANSIColors[fg]
if !ok {
f, _ = foregroundANSIColors[DefaultColor]
}
b, ok := backgroundANSIColors[bg]
if !ok {
b, _ = backgroundANSIColors[DefaultColor]
}
w.out.Write([]byte{0x1b, 0x5b, 0x33, 0x39, 0x3b, 0x34, 0x39, 0x6d})
w.WriteRaw([]byte{0x1b, 0x5b})
if !bold {
w.WriteRaw([]byte{0x30, 0x3b})
}
w.WriteRaw(f)
w.WriteRaw([]byte{0x3b})
w.WriteRaw(b)
if bold {
w.WriteRaw([]byte{0x3b, 0x31})
}
w.WriteRaw([]byte{0x6d})
return
}
var foregroundANSIColors = map[Color][]byte{
DefaultColor: {0x33, 0x39}, // 39
// Low intensity.
Black: {0x33, 0x30}, // 30
DarkRed: {0x33, 0x31}, // 31
DarkGreen: {0x33, 0x32}, // 32
Brown: {0x33, 0x33}, // 33
DarkBlue: {0x33, 0x34}, // 34
Purple: {0x33, 0x35}, // 35
Cyan: {0x33, 0x36}, //36
LightGray: {0x33, 0x37}, //37
// High intensity.
DarkGray: {0x39, 0x30}, // 90
Red: {0x39, 0x31}, // 91
Green: {0x39, 0x32}, // 92
Yellow: {0x39, 0x33}, // 93
Blue: {0x39, 0x34}, // 94
Fuchsia: {0x39, 0x35}, // 95
Turquoise: {0x39, 0x36}, // 96
White: {0x39, 0x37}, // 97
}
var backgroundANSIColors = map[Color][]byte{
DefaultColor: {0x34, 0x39}, // 49
// Low intensity.
Black: {0x34, 0x30}, // 40
DarkRed: {0x34, 0x31}, // 41
DarkGreen: {0x34, 0x32}, // 42
Brown: {0x34, 0x33}, // 43
DarkBlue: {0x34, 0x34}, // 44
Purple: {0x34, 0x35}, // 45
Cyan: {0x34, 0x36}, // 46
LightGray: {0x34, 0x37}, // 47
// High intensity
DarkGray: {0x31, 0x30, 0x30}, // 100
Red: {0x31, 0x30, 0x31}, // 101
Green: {0x31, 0x30, 0x32}, // 102
Yellow: {0x31, 0x30, 0x33}, // 103
Blue: {0x31, 0x30, 0x34}, // 104
Fuchsia: {0x31, 0x30, 0x35}, // 105
Turquoise: {0x31, 0x30, 0x36}, // 106
White: {0x31, 0x30, 0x37}, // 107
}
func writeFilter(buf byte) bool {
return buf != 0x1b && buf != 0x3f
}
func setTextFilter(buf byte) bool {
return buf != 0x1b && buf != 0x07
}
func byteFilter(buf []byte, fn ...func(b byte) bool) []byte {
if len(fn) == 0 {
return buf
}
ret := make([]byte, 0, len(buf))
f := fn[0]
for i, n := range buf {
if f(n) {
ret = append(ret, buf[i])
}
}
return byteFilter(ret, fn[1:]...)
}
var _ ConsoleWriter = &WindowsWriter{}
// NewStandardOutputWriter returns ConsoleWriter object to write to stdout.
func NewStandardOutputWriter() *WindowsWriter {
return &WindowsWriter{
out: colorable.NewColorableStdout(),
}
}