mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2af6e11adc | ||
|
|
a3a98858fd | ||
|
|
d0652f2fd3 | ||
|
|
936f067363 | ||
|
|
041c3c7048 | ||
|
|
b48f74af58 | ||
|
|
6ec8849b20 | ||
|
|
db7cf1ed62 |
No files matched your search
@@ -43,4 +43,3 @@ See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
|
||||
|
||||
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package websites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -26,6 +30,41 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
})
|
||||
carapace.Gen(websitesCmd).PositionalCompletion(WebsiteNameCompleter(con))
|
||||
|
||||
websitesListCmd := &cobra.Command{
|
||||
Use: consts.ListStr,
|
||||
Short: "List configured websites",
|
||||
Long: help.GetHelpFor([]string{consts.WebsitesStr, consts.ListStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ListWebsites(cmd, con, args)
|
||||
},
|
||||
}
|
||||
websitesCmd.AddCommand(websitesListCmd)
|
||||
|
||||
websitesShowCmd := &cobra.Command{
|
||||
Use: "show [name]",
|
||||
Short: "Show contents of a website",
|
||||
Long: help.GetHelpFor([]string{consts.WebsitesStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ListWebsiteContent(args[0], con)
|
||||
},
|
||||
}
|
||||
carapace.Gen(websitesShowCmd).PositionalCompletion(WebsiteNameCompleter(con))
|
||||
websitesShowCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
ws, err := con.Rpc.Websites(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
out := []string{}
|
||||
for _, w := range ws.Websites {
|
||||
if strings.HasPrefix(w.Name, toComplete) {
|
||||
out = append(out, w.Name)
|
||||
}
|
||||
}
|
||||
return out, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
websitesCmd.AddCommand(websitesShowCmd)
|
||||
|
||||
websitesRmCmd := &cobra.Command{
|
||||
Use: consts.RmStr,
|
||||
Short: "Remove an entire website and all of its contents",
|
||||
|
||||
@@ -121,9 +121,10 @@ func webAddFile(web *clientpb.WebsiteAddContent, webpath string, contentType str
|
||||
}
|
||||
|
||||
web.Contents[webpath] = &clientpb.WebContent{
|
||||
Path: webpath,
|
||||
ContentType: contentType,
|
||||
Content: data,
|
||||
Path: webpath,
|
||||
ContentType: contentType,
|
||||
OriginalFile: filepath.Base(contentPath),
|
||||
Content: data,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ package websites
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -58,11 +57,14 @@ func ListWebsites(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
con.PrintInfof("No websites\n")
|
||||
return
|
||||
}
|
||||
con.Println("Websites")
|
||||
con.Println(strings.Repeat("=", len("Websites")))
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{"Name", "Objects"})
|
||||
for _, site := range websites.Websites {
|
||||
con.Printf("%s%s%s - %d page(s)\n", console.Bold, site.Name, console.Normal, len(site.Contents))
|
||||
tw.AppendRow(table.Row{site.Name, len(site.Contents)})
|
||||
}
|
||||
|
||||
con.Println(tw.Render())
|
||||
}
|
||||
|
||||
// ListWebsiteContent - List the static contents of a website.
|
||||
@@ -91,6 +93,8 @@ func PrintWebsite(web *clientpb.Website, con *console.SliverClient) {
|
||||
"Path",
|
||||
"Content-type",
|
||||
"Size",
|
||||
"Original File",
|
||||
"SHA256",
|
||||
})
|
||||
sortedContents := []*clientpb.WebContent{}
|
||||
for _, content := range web.Contents {
|
||||
@@ -104,6 +108,8 @@ func PrintWebsite(web *clientpb.Website, con *console.SliverClient) {
|
||||
content.Path,
|
||||
content.ContentType,
|
||||
content.Size,
|
||||
content.OriginalFile,
|
||||
content.Sha256,
|
||||
})
|
||||
}
|
||||
con.Println(tw.Render())
|
||||
|
||||
@@ -4,7 +4,9 @@ go 1.25.5
|
||||
|
||||
replace github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.46.3-0.20231214181515-27e49f3c3b69
|
||||
|
||||
replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d
|
||||
replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989
|
||||
|
||||
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260128035037-680800942770
|
||||
|
||||
require (
|
||||
filippo.io/age v1.3.1
|
||||
@@ -99,7 +101,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/carapace-sh/carapace v1.11.0 // indirect
|
||||
github.com/carapace-sh/carapace v1.11.0
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||
|
||||
@@ -91,12 +91,8 @@ github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xW
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/carapace-sh/carapace v1.9.0 h1:IuEP6YeeK4fhrH7MoOyL0I3GMhvROerkw6YGxkx8Fbw=
|
||||
github.com/carapace-sh/carapace v1.9.0/go.mod h1:Zs3DpsawpFm+8fEfRVYE9EzMBYH8u7S3Aw9yAAKX0JM=
|
||||
github.com/carapace-sh/carapace v1.11.0 h1:dTUFXvIrUTLd9BGLsfDt4wCgEyPl8bJeZOFDmN7fIfo=
|
||||
github.com/carapace-sh/carapace v1.11.0/go.mod h1:NVkuv1s8wyFvu7d98rdgYwqGKMB6GO1MEUZp5RcWri8=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.0 h1:58QSJTUxghpa/rEn6+Z9UM/Hy0ipVHDUn9S1mp4i+ZE=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.0/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1 h1:ccmNeetAYZOk4IcV36youFDsXusT9uCNW2Njkw+QS+Q=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
@@ -354,12 +350,14 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/moloch--/asciicast v0.1.1 h1:btwwniCK9br3ys33XZ71IG/M3ZujSSCcBzsH/sieiXg=
|
||||
github.com/moloch--/asciicast v0.1.1/go.mod h1:OckO16UDLgxVLclrCnbocL1ix15Br/8Xv/caBoYq98o=
|
||||
github.com/moloch--/console v0.0.0-20260128035037-680800942770 h1:JtfE7Od0O5ISD4b9r9AX7FTlAf8sT6O4qr5HV6Ey5U0=
|
||||
github.com/moloch--/console v0.0.0-20260128035037-680800942770/go.mod h1:UdqyqqCSnPu3Zc8Q3kpDszrq1hcyBHsG17hlWlRJmWI=
|
||||
github.com/moloch--/go-keystone v0.0.2 h1:xThbYs45CSzoDUz5kBZxAyTPIpK3t5M7NB1x+NTTRL8=
|
||||
github.com/moloch--/go-keystone v0.0.2/go.mod h1:kpGRJpw9b7Eq3MluHrba+GPKZhSyDrvhLULSwH5nGGM=
|
||||
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e h1:IkFCPlAa0iTiLxck+NqAwBx8JDlnHYm4orOQBbs4BDQ=
|
||||
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e/go.mod h1:eYeI6cQ5YHhHt9i0BBW0zc1DaQnb4ZMXsSPuEV/V5Og=
|
||||
github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d h1:9RbKockJl6ZdgoEh2QqU3An6Ij/NWZtRWS3J6yG0+qo=
|
||||
github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
|
||||
github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989 h1:g0EfxAWzDGYarN0RAPibsjz2M/f0pAqYNiEpCPWRDok=
|
||||
github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
|
||||
github.com/moloch--/sgn v0.0.4 h1:90IC5rx3TmPEfrqPnENB0yyiqcbcNk9LqezXx7uruF0=
|
||||
github.com/moloch--/sgn v0.0.4/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
@@ -401,8 +399,6 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/reeflective/console v0.1.25 h1:do/r6rcKH6pCQDEzh5PhoVFcqHkqcjd0N6tNjpmYWWQ=
|
||||
github.com/reeflective/console v0.1.25/go.mod h1:It/IqEjo0PowaBuxBS9pEi5EYUgmtvQd0ytMtlS77kk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
@@ -423,11 +419,9 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
|
||||
+1027
-1007
File diff suppressed because it is too large.
Load diff
@@ -586,6 +586,9 @@ message WebContent {
|
||||
string ContentType = 4;
|
||||
uint64 Size = 5 [ jstype = JS_STRING ];
|
||||
|
||||
string OriginalFile = 6;
|
||||
string Sha256 = 7;
|
||||
|
||||
bytes Content = 9;
|
||||
}
|
||||
|
||||
@@ -1316,4 +1319,4 @@ message CertificateData {
|
||||
|
||||
message CertificateInfo {
|
||||
repeated CertificateData info = 1;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v6.33.2
|
||||
// protoc v3.15.8
|
||||
// source: commonpb/common.proto
|
||||
|
||||
package commonpb
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v6.33.2
|
||||
// protoc v3.15.8
|
||||
// source: dnspb/dns.proto
|
||||
|
||||
package dnspb
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v6.33.2
|
||||
// protoc v3.15.8
|
||||
// source: rpcpb/services.proto
|
||||
|
||||
package rpcpb
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v6.33.2
|
||||
// protoc v3.15.8
|
||||
// source: sliverpb/sliver.proto
|
||||
|
||||
package sliverpb
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
@@ -47,6 +48,13 @@ var (
|
||||
ErrInvalidDialect = errors.New("invalid SQL Dialect")
|
||||
|
||||
databaseConfigLog = log.NamedLogger("config", "database")
|
||||
|
||||
defaultSQLitePragmas = map[string]string{
|
||||
"journal_mode": "WAL", // reduce writer blocking, better concurrency
|
||||
"busy_timeout": "5000", // wait for locks instead of failing fast (ms)
|
||||
"synchronous": "NORMAL", // faster WAL syncs while retaining durability
|
||||
"temp_store": "MEMORY", // keep temp structures off disk for quicker startup
|
||||
}
|
||||
)
|
||||
|
||||
// GetDatabaseConfigPath - File path to config.json
|
||||
@@ -66,7 +74,8 @@ type DatabaseConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
|
||||
Params map[string]string `json:"params"`
|
||||
Params map[string]string `json:"params"`
|
||||
Pragmas map[string]string `json:"pragmas"`
|
||||
|
||||
MaxIdleConns int `json:"max_idle_conns"`
|
||||
MaxOpenConns int `json:"max_open_conns"`
|
||||
@@ -80,7 +89,7 @@ func (c *DatabaseConfig) DSN() (string, error) {
|
||||
switch c.Dialect {
|
||||
case Sqlite:
|
||||
filePath := filepath.Join(assets.GetRootAppDir(), "sliver.db")
|
||||
params := encodeParams(c.Params)
|
||||
params := encodeSQLiteParams(c.Params, c.Pragmas)
|
||||
return fmt.Sprintf("file:%s?%s", filePath, params), nil
|
||||
case MySQL:
|
||||
user := url.QueryEscape(c.Username)
|
||||
@@ -112,6 +121,44 @@ func encodeParams(rawParams map[string]string) string {
|
||||
return params.Encode()
|
||||
}
|
||||
|
||||
func encodeSQLiteParams(rawParams map[string]string, pragmas map[string]string) string {
|
||||
if rawParams == nil {
|
||||
rawParams = map[string]string{}
|
||||
}
|
||||
params := url.Values{}
|
||||
|
||||
// Preserve any user-provided parameters first.
|
||||
for key, value := range rawParams {
|
||||
params.Add(key, value)
|
||||
}
|
||||
|
||||
// Apply safer defaults only when the user has not set any custom pragma.
|
||||
if _, ok := rawParams["_pragma"]; !ok {
|
||||
selectedPragmas := pragmas
|
||||
if len(selectedPragmas) == 0 {
|
||||
selectedPragmas = defaultSQLitePragmas
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(selectedPragmas))
|
||||
for key := range selectedPragmas {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
value := selectedPragmas[key]
|
||||
params.Add("_pragma", fmt.Sprintf("%s(%s)", key, value))
|
||||
}
|
||||
}
|
||||
|
||||
// Encourage shared cache to play nicer with WAL and multiple connections.
|
||||
if _, ok := rawParams["cache"]; !ok {
|
||||
params.Add("cache", "shared")
|
||||
}
|
||||
|
||||
return params.Encode()
|
||||
}
|
||||
|
||||
// Save - Save config file to disk
|
||||
func (c *DatabaseConfig) Save() error {
|
||||
configPath := GetDatabaseConfigPath()
|
||||
@@ -161,6 +208,8 @@ func GetDatabaseConfig() *DatabaseConfig {
|
||||
config.MaxOpenConns = 1
|
||||
}
|
||||
|
||||
ensureSQLiteDefaults(config)
|
||||
|
||||
err := config.Save() // This updates the config with any missing fields
|
||||
if err != nil {
|
||||
databaseConfigLog.Errorf("Failed to save default config %s", err)
|
||||
@@ -171,9 +220,29 @@ func GetDatabaseConfig() *DatabaseConfig {
|
||||
func getDefaultDatabaseConfig() *DatabaseConfig {
|
||||
return &DatabaseConfig{
|
||||
Dialect: Sqlite,
|
||||
Pragmas: defaultSQLitePragmas,
|
||||
MaxIdleConns: 10,
|
||||
MaxOpenConns: 100,
|
||||
|
||||
LogLevel: "warn",
|
||||
}
|
||||
}
|
||||
|
||||
func ensureSQLiteDefaults(c *DatabaseConfig) {
|
||||
if c.Dialect != Sqlite {
|
||||
return
|
||||
}
|
||||
if c.Params == nil {
|
||||
c.Params = map[string]string{}
|
||||
}
|
||||
if _, ok := c.Params["_pragma"]; ok {
|
||||
// User provided explicit pragmas; leave Pragmas untouched to avoid confusion.
|
||||
return
|
||||
}
|
||||
if len(c.Pragmas) == 0 {
|
||||
c.Pragmas = defaultSQLitePragmas
|
||||
}
|
||||
if _, ok := c.Params["cache"]; !ok {
|
||||
c.Params["cache"] = "shared"
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -789,8 +789,18 @@ func AddContent(pbWebContent *clientpb.WebContent, webContentDir string) (*clien
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
dbWebContent.ContentType = pbWebContent.ContentType
|
||||
dbWebContent.Size = pbWebContent.Size
|
||||
if pbWebContent.ContentType != "" {
|
||||
dbWebContent.ContentType = pbWebContent.ContentType
|
||||
}
|
||||
if pbWebContent.Size != 0 {
|
||||
dbWebContent.Size = pbWebContent.Size
|
||||
}
|
||||
if pbWebContent.OriginalFile != "" {
|
||||
dbWebContent.OriginalFile = pbWebContent.OriginalFile
|
||||
}
|
||||
if pbWebContent.Sha256 != "" {
|
||||
dbWebContent.Sha256 = pbWebContent.Sha256
|
||||
}
|
||||
|
||||
dbModelWebContent := models.WebContentFromProtobuf(dbWebContent)
|
||||
err = Session().Save(&dbModelWebContent).Error
|
||||
|
||||
+20
-14
@@ -67,9 +67,11 @@ type WebContent struct {
|
||||
ID uuid.UUID `gorm:"primaryKey;->;<-:create;type:uuid;"`
|
||||
WebsiteID uuid.UUID `gorm:"type:uuid;"`
|
||||
|
||||
Path string `gorm:"primaryKey"`
|
||||
Size uint64
|
||||
ContentType string
|
||||
Path string `gorm:"primaryKey"`
|
||||
Size uint64
|
||||
ContentType string
|
||||
OriginalFile string
|
||||
Sha256 string
|
||||
}
|
||||
|
||||
// BeforeCreate - GORM hook to automatically set values
|
||||
@@ -81,12 +83,14 @@ func (wc *WebContent) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
// ToProtobuf - Converts to protobuf object
|
||||
func (wc *WebContent) ToProtobuf(content *[]byte) *clientpb.WebContent {
|
||||
return &clientpb.WebContent{
|
||||
ID: wc.ID.String(),
|
||||
WebsiteID: wc.WebsiteID.String(),
|
||||
Path: wc.Path,
|
||||
Size: uint64(wc.Size),
|
||||
ContentType: wc.ContentType,
|
||||
Content: *content,
|
||||
ID: wc.ID.String(),
|
||||
WebsiteID: wc.WebsiteID.String(),
|
||||
Path: wc.Path,
|
||||
Size: uint64(wc.Size),
|
||||
ContentType: wc.ContentType,
|
||||
OriginalFile: wc.OriginalFile,
|
||||
Sha256: wc.Sha256,
|
||||
Content: *content,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +99,12 @@ func WebContentFromProtobuf(pbWebContent *clientpb.WebContent) WebContent {
|
||||
websiteUUID, _ := uuid.FromString(pbWebContent.WebsiteID)
|
||||
|
||||
return WebContent{
|
||||
ID: siteUUID,
|
||||
WebsiteID: websiteUUID,
|
||||
Path: pbWebContent.Path,
|
||||
Size: pbWebContent.Size,
|
||||
ContentType: pbWebContent.ContentType,
|
||||
ID: siteUUID,
|
||||
WebsiteID: websiteUUID,
|
||||
Path: pbWebContent.Path,
|
||||
Size: pbWebContent.Size,
|
||||
ContentType: pbWebContent.ContentType,
|
||||
OriginalFile: pbWebContent.OriginalFile,
|
||||
Sha256: pbWebContent.Sha256,
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ package website
|
||||
*/
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -87,14 +89,30 @@ func AddContent(name string, pbWebContent *clientpb.WebContent) error {
|
||||
pbWebContent.WebsiteID = website.ID
|
||||
}
|
||||
|
||||
if pbWebContent.Size == 0 && len(pbWebContent.Content) > 0 {
|
||||
pbWebContent.Size = uint64(len(pbWebContent.Content))
|
||||
}
|
||||
if pbWebContent.OriginalFile == "" {
|
||||
pbWebContent.OriginalFile = filepath.Base(pbWebContent.Path)
|
||||
}
|
||||
|
||||
if len(pbWebContent.Content) > 0 {
|
||||
sha := sha256.Sum256(pbWebContent.Content)
|
||||
pbWebContent.Sha256 = hex.EncodeToString(sha[:])
|
||||
}
|
||||
|
||||
webContent, err := db.AddContent(pbWebContent, webContentDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write content to disk
|
||||
webContentPath := filepath.Join(webContentDir, webContent.ID)
|
||||
return os.WriteFile(webContentPath, pbWebContent.Content, 0600)
|
||||
// Write content to disk when provided (metadata-only updates skip disk writes)
|
||||
if len(pbWebContent.Content) > 0 {
|
||||
webContentPath := filepath.Join(webContentDir, webContent.ID)
|
||||
return os.WriteFile(webContentPath, pbWebContent.Content, 0600)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveContent - Remove website content for a path
|
||||
|
||||
+78
-2
@@ -1,11 +1,13 @@
|
||||
package readline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/rivo/uniseg"
|
||||
|
||||
@@ -448,8 +450,82 @@ func (rl *Shell) selfInsert() {
|
||||
}
|
||||
|
||||
func (rl *Shell) bracketedPasteBegin() {
|
||||
// keys, _ := rl.Keys.PeekAllBytes()
|
||||
// fmt.Println(string(keys))
|
||||
rl.History.SkipSave()
|
||||
rl.completer.TrimSuffix()
|
||||
|
||||
const endSeq = "\x1b[201~"
|
||||
|
||||
data := rl.Keys.DrainBuffer()
|
||||
|
||||
for {
|
||||
if idx := bytes.Index(data, []byte(endSeq)); idx >= 0 {
|
||||
payload := data[:idx]
|
||||
rest := data[idx+len(endSeq):]
|
||||
if len(rest) > 0 {
|
||||
rl.Keys.PrependBuffer(rest)
|
||||
}
|
||||
|
||||
rl.insertPastedBytes(payload)
|
||||
return
|
||||
}
|
||||
|
||||
more, err := rl.Keys.ReadInput()
|
||||
if err != nil {
|
||||
rl.insertPastedBytes(data)
|
||||
return
|
||||
}
|
||||
|
||||
if len(more) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
data = append(data, more...)
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *Shell) insertPastedBytes(data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
runes := normalizePasteBytes(data)
|
||||
if len(runes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rl.cursor.InsertAt(runes...)
|
||||
rl.Keys.SetMatched(runes...)
|
||||
}
|
||||
|
||||
func normalizePasteBytes(data []byte) []rune {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]rune, 0, len(data))
|
||||
|
||||
for len(data) > 0 {
|
||||
if data[0] == '\r' {
|
||||
if len(data) > 1 && data[1] == '\n' {
|
||||
data = data[1:]
|
||||
}
|
||||
out = append(out, '\n')
|
||||
data = data[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
r, size := utf8.DecodeRune(data)
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
out = append(out, rune(data[0]))
|
||||
data = data[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, r)
|
||||
data = data[size:]
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Drag the character before point forward over the character
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ func Display(eng *Engine, maxRows int) {
|
||||
// little more time. The engine itself is responsible for
|
||||
// deleting those lists when it deems them useless.
|
||||
if eng.Matches() == 0 || eng.skipDisplay {
|
||||
fmt.Print(term.ClearLineAfter)
|
||||
term.WriteString(term.ClearLineAfter)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func Display(eng *Engine, maxRows int) {
|
||||
completions, eng.usedY = eng.cropCompletions(completions, maxRows)
|
||||
|
||||
if completions != "" {
|
||||
fmt.Print(completions)
|
||||
term.WriteString(completions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+84
-1
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
keyScanBufSize = 1024
|
||||
keyScanBufSize = 8192
|
||||
)
|
||||
|
||||
// Stdin is used by the Keys struct to read and write keys.
|
||||
@@ -238,6 +238,89 @@ func FlushUsed(keys *Keys) {
|
||||
defer keys.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Pending returns the number of buffered keys waiting to be processed.
|
||||
func (k *Keys) Pending() int {
|
||||
k.mutex.RLock()
|
||||
defer k.mutex.RUnlock()
|
||||
|
||||
return len(k.buf) + len(k.macroKeys)
|
||||
}
|
||||
|
||||
// BufferSize returns the number of raw input bytes buffered.
|
||||
func (k *Keys) BufferSize() int {
|
||||
k.mutex.RLock()
|
||||
defer k.mutex.RUnlock()
|
||||
|
||||
return len(k.buf)
|
||||
}
|
||||
|
||||
// PeekBuffer returns up to max bytes from the raw input buffer.
|
||||
func (k *Keys) PeekBuffer(max int) []byte {
|
||||
k.mutex.RLock()
|
||||
defer k.mutex.RUnlock()
|
||||
|
||||
if max <= 0 || len(k.buf) <= max {
|
||||
return append([]byte(nil), k.buf...)
|
||||
}
|
||||
|
||||
return append([]byte(nil), k.buf[:max]...)
|
||||
}
|
||||
|
||||
// DrainBuffer returns all buffered raw input bytes and clears the buffer.
|
||||
func (k *Keys) DrainBuffer() []byte {
|
||||
k.mutex.Lock()
|
||||
defer k.mutex.Unlock()
|
||||
|
||||
if len(k.buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := append([]byte(nil), k.buf...)
|
||||
k.buf = nil
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// PrependBuffer pushes raw input bytes back to the front of the buffer.
|
||||
func (k *Keys) PrependBuffer(keys []byte) {
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
k.mutex.Lock()
|
||||
k.buf = append(keys, k.buf...)
|
||||
k.mutex.Unlock()
|
||||
}
|
||||
|
||||
// AppendMatched appends keys to the matched buffer, so they can be recorded by macros.
|
||||
func (k *Keys) AppendMatched(keys ...rune) {
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
k.mutex.Lock()
|
||||
k.matched = append(k.matched, keys...)
|
||||
k.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetMatched replaces the matched buffer with the provided keys.
|
||||
func (k *Keys) SetMatched(keys ...rune) {
|
||||
k.mutex.Lock()
|
||||
if len(keys) == 0 {
|
||||
k.matched = nil
|
||||
k.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
k.matched = append(k.matched[:0], keys...)
|
||||
k.mutex.Unlock()
|
||||
}
|
||||
|
||||
// ReadInput reads raw input bytes from stdin, passing through the input filter.
|
||||
func (k *Keys) ReadInput() ([]byte, error) {
|
||||
return k.readInputFiltered()
|
||||
}
|
||||
|
||||
// ReadKey reads keys from stdin like Read(), but immediately
|
||||
// returns them instead of storing them in the stack, along with
|
||||
// an indication on whether this key is an escape/abort one.
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
@@ -344,7 +343,7 @@ func DisplayLine(l *Line, indent int) {
|
||||
line += term.NewlineReturn
|
||||
}
|
||||
|
||||
fmt.Print(line)
|
||||
term.WriteString(line)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+56
-21
@@ -1,7 +1,7 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/reeflective/readline/inputrc"
|
||||
"github.com/reeflective/readline/internal/color"
|
||||
@@ -32,6 +32,10 @@ type Engine struct {
|
||||
hintRows int
|
||||
compRows int
|
||||
primaryPrinted bool
|
||||
cursorKnown bool
|
||||
cursorDirty bool
|
||||
commentBegin string
|
||||
commentRegexp *regexp.Regexp
|
||||
|
||||
// UI components
|
||||
keys *core.Keys
|
||||
@@ -69,7 +73,10 @@ func Init(e *Engine, highlighter func([]rune) string) {
|
||||
// Refresh recomputes and redisplays the entire readline interface, except
|
||||
// the first lines of the primary prompt when the latter is a multiline one.
|
||||
func (e *Engine) Refresh() {
|
||||
fmt.Print(term.HideCursor)
|
||||
term.BeginBuffer()
|
||||
defer term.EndBuffer()
|
||||
|
||||
term.WriteString(term.HideCursor)
|
||||
|
||||
// Go back to the first column, and if the primary prompt
|
||||
// was not printed yet, back up to the line's beginning row.
|
||||
@@ -82,6 +89,12 @@ func (e *Engine) Refresh() {
|
||||
// Print either all or the last line of the prompt.
|
||||
e.prompt.LastPrint()
|
||||
|
||||
// Flush prompt output before querying the cursor position.
|
||||
if term.ShouldQueryCursorPos() {
|
||||
term.EndBuffer()
|
||||
term.BeginBuffer()
|
||||
}
|
||||
|
||||
// Get all positions required for the redisplay to come:
|
||||
// prompt end (thus indentation), cursor positions, etc.
|
||||
e.computeCoordinates(true)
|
||||
@@ -99,7 +112,7 @@ func (e *Engine) Refresh() {
|
||||
} else {
|
||||
e.lineEndToCursorPos()
|
||||
}
|
||||
fmt.Print(term.ShowCursor)
|
||||
term.WriteString(term.ShowCursor)
|
||||
}
|
||||
|
||||
// PrintPrimaryPrompt redraws the primary prompt.
|
||||
@@ -108,12 +121,13 @@ func (e *Engine) Refresh() {
|
||||
func (e *Engine) PrintPrimaryPrompt() {
|
||||
e.prompt.PrimaryPrint()
|
||||
e.primaryPrinted = true
|
||||
e.MarkCursorDirty()
|
||||
}
|
||||
|
||||
// ClearHelpers clears the hint and completion sections below the line.
|
||||
func (e *Engine) ClearHelpers() {
|
||||
e.CursorBelowLine()
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
term.MoveCursorUp(1)
|
||||
term.MoveCursorUp(e.lineRows)
|
||||
@@ -140,14 +154,15 @@ func (e *Engine) AcceptLine() {
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
term.MoveCursorDown(e.lineRows)
|
||||
term.MoveCursorForwards(e.lineCol)
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
// Reprint the right-side prompt if it's not a tooltip one.
|
||||
e.prompt.RightPrint(e.lineCol, false)
|
||||
|
||||
// Go below this non-suggested line and clear everything.
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
fmt.Print(term.NewlineReturn)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
e.MarkCursorDirty()
|
||||
}
|
||||
|
||||
// RefreshTransient goes back to the first line of the input buffer
|
||||
@@ -164,7 +179,8 @@ func (e *Engine) RefreshTransient() {
|
||||
// And redisplay the transient/primary/line.
|
||||
e.prompt.TransientPrint()
|
||||
e.displayLine()
|
||||
fmt.Print(term.NewlineReturn)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
e.MarkCursorDirty()
|
||||
}
|
||||
|
||||
// CursorToLineStart moves the cursor just after the primary prompt.
|
||||
@@ -183,7 +199,7 @@ func (e *Engine) CursorToLineStart() {
|
||||
func (e *Engine) CursorBelowLine() {
|
||||
term.MoveCursorUp(e.cursorRow)
|
||||
term.MoveCursorDown(e.lineRows)
|
||||
fmt.Print(term.NewlineReturn)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
}
|
||||
|
||||
// lineStartToCursorPos can be used if the cursor is currently
|
||||
@@ -202,6 +218,11 @@ func (e *Engine) cursorHintToLineStart() {
|
||||
e.CursorToLineStart()
|
||||
}
|
||||
|
||||
// MarkCursorDirty forces the next refresh to query the cursor position.
|
||||
func (e *Engine) MarkCursorDirty() {
|
||||
e.cursorDirty = true
|
||||
}
|
||||
|
||||
func (e *Engine) computeCoordinates(suggested bool) {
|
||||
// Get the new input line and auto-suggested one.
|
||||
e.line, e.cursor = e.completer.Line()
|
||||
@@ -211,17 +232,31 @@ func (e *Engine) computeCoordinates(suggested bool) {
|
||||
e.suggested = e.histories.Suggest(e.line)
|
||||
}
|
||||
|
||||
// Get the position of the line's beginning by querying
|
||||
// the terminal for the cursor position.
|
||||
e.startCols, e.startRows = e.keys.GetCursorPos()
|
||||
// Get the position of the line's beginning. When possible, reuse the
|
||||
// cached cursor row to avoid the expensive terminal query.
|
||||
if term.ShouldQueryCursorPos() && (!e.cursorKnown || e.cursorDirty) {
|
||||
e.startCols, e.startRows = e.keys.GetCursorPos()
|
||||
|
||||
if e.startCols > 0 {
|
||||
e.startCols--
|
||||
}
|
||||
if e.startCols > 0 {
|
||||
e.startCols--
|
||||
}
|
||||
|
||||
// Cursor position might be misleading if invalid (negative).
|
||||
if e.startCols == -1 {
|
||||
e.startCols = e.prompt.LastUsed()
|
||||
// Cursor position might be misleading if invalid (negative).
|
||||
if e.startCols == -1 {
|
||||
e.startCols = e.prompt.LastUsed()
|
||||
}
|
||||
|
||||
e.cursorKnown = true
|
||||
e.cursorDirty = false
|
||||
} else {
|
||||
e.startCols = e.prompt.LastCols()
|
||||
if e.startCols == 0 {
|
||||
e.startCols = e.prompt.LastUsed()
|
||||
}
|
||||
if !e.cursorKnown {
|
||||
e.startRows = 0
|
||||
e.cursorKnown = true
|
||||
}
|
||||
}
|
||||
|
||||
e.cursorCol, e.cursorRow = core.CoordinatesCursor(e.cursor, e.startCols)
|
||||
@@ -269,8 +304,8 @@ func (e *Engine) displayLine() {
|
||||
|
||||
// Adjust the cursor if the line fits exactly in the terminal width.
|
||||
if e.lineCol == 0 {
|
||||
fmt.Print(term.NewlineReturn)
|
||||
fmt.Print(term.ClearLineAfter)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
term.WriteString(term.ClearLineAfter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +345,7 @@ func (e *Engine) displayHelpers() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
fmt.Print(term.NewlineReturn)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
|
||||
prevHintRows := e.hintRows
|
||||
prevCompRows := e.compRows
|
||||
@@ -327,7 +362,7 @@ func (e *Engine) displayHelpers() bool {
|
||||
}
|
||||
|
||||
if e.hintRows+e.compRows < prevHintRows+prevCompRows {
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
}
|
||||
|
||||
// Go back to the first line below the input line.
|
||||
|
||||
+28
-15
@@ -19,29 +19,41 @@ func (e *Engine) highlightLine(line []rune, selection core.Selection) string {
|
||||
sorted := sortHighlights(selection)
|
||||
colors := e.getHighlights(line, sorted)
|
||||
|
||||
var highlighted string
|
||||
var highlighted strings.Builder
|
||||
|
||||
// And apply highlighting before each rune.
|
||||
for i, r := range line {
|
||||
if highlight, found := colors[i]; found {
|
||||
highlighted += string(highlight)
|
||||
highlighted.WriteString(highlight)
|
||||
}
|
||||
|
||||
highlighted += string(r)
|
||||
highlighted.WriteRune(r)
|
||||
}
|
||||
|
||||
// Finally, highlight comments using a regex.
|
||||
result := highlighted.String()
|
||||
|
||||
// Finally, highlight comments using a cached regex.
|
||||
comment := strings.Trim(e.opts.GetString("comment-begin"), "\"")
|
||||
commentPattern := fmt.Sprintf(`(^|\s)%s.*`, comment)
|
||||
if comment != e.commentBegin {
|
||||
e.commentBegin = comment
|
||||
e.commentRegexp = nil
|
||||
|
||||
if commentsMatch, err := regexp.Compile(commentPattern); err == nil {
|
||||
commentColor := color.SGRStart + color.Fg + "244" + color.SGREnd
|
||||
highlighted = commentsMatch.ReplaceAllString(highlighted, fmt.Sprintf("%s${0}%s", commentColor, color.Reset))
|
||||
if comment != "" {
|
||||
commentPattern := fmt.Sprintf(`(^|\s)%s.*`, comment)
|
||||
if commentsMatch, err := regexp.Compile(commentPattern); err == nil {
|
||||
e.commentRegexp = commentsMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlighted += color.Reset
|
||||
if e.commentRegexp != nil {
|
||||
commentColor := color.SGRStart + color.Fg + "244" + color.SGREnd
|
||||
result = e.commentRegexp.ReplaceAllString(result, fmt.Sprintf("%s${0}%s", commentColor, color.Reset))
|
||||
}
|
||||
|
||||
return highlighted
|
||||
result += color.Reset
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func sortHighlights(vhl core.Selection) []core.Selection {
|
||||
@@ -95,15 +107,16 @@ func sortHighlights(vhl core.Selection) []core.Selection {
|
||||
return sorted
|
||||
}
|
||||
|
||||
func (e *Engine) getHighlights(line []rune, sorted []core.Selection) map[int][]rune {
|
||||
highlights := make(map[int][]rune)
|
||||
var colorSeqRegexp = regexp.MustCompile(`\x1b\[[0-9;]+m`)
|
||||
|
||||
func (e *Engine) getHighlights(line []rune, sorted []core.Selection) map[int]string {
|
||||
highlights := make(map[int]string)
|
||||
|
||||
// Find any highlighting already applied on the line,
|
||||
// and keep the indexes so that we can skip those.
|
||||
var colors [][]int
|
||||
|
||||
colorMatch := regexp.MustCompile(`\x1b\[[0-9;]+m`)
|
||||
colors = colorMatch.FindAllStringIndex(string(line), -1)
|
||||
colors = colorSeqRegexp.FindAllStringIndex(string(line), -1)
|
||||
|
||||
// marks that started highlighting, but not done yet.
|
||||
regions := make([]core.Selection, 0)
|
||||
@@ -149,7 +162,7 @@ func (e *Engine) getHighlights(line []rune, sorted []core.Selection) map[int][]r
|
||||
// Add to the line, with the raw index since
|
||||
// we must take into account embedded colors.
|
||||
if len(posHl) > 0 {
|
||||
highlights[rawIndex] = posHl
|
||||
highlights[rawIndex] = string(posHl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-21
@@ -1,7 +1,6 @@
|
||||
package keymap
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/reeflective/readline/inputrc"
|
||||
@@ -180,32 +179,19 @@ func (m *Engine) matchBind(keys []byte, binds map[string]inputrc.Bind) (inputrc.
|
||||
var match inputrc.Bind
|
||||
var prefixed []inputrc.Bind
|
||||
|
||||
// Make a sorted list with all keys in the binds map.
|
||||
var sequences []string
|
||||
for sequence := range binds {
|
||||
sequences = append(sequences, sequence)
|
||||
}
|
||||
keysStr := string(keys)
|
||||
|
||||
// Sort the list of sequences by alphabetical order and length.
|
||||
sort.Slice(sequences, func(i, j int) bool {
|
||||
if len(sequences[i]) == len(sequences[j]) {
|
||||
return sequences[i] < sequences[j]
|
||||
}
|
||||
|
||||
return len(sequences[i]) < len(sequences[j])
|
||||
})
|
||||
|
||||
// Iterate over the sorted list of sequences and find all binds
|
||||
// Iterate over the list of sequences and find all binds
|
||||
// that match the sequence either by prefix or exactly.
|
||||
for _, sequence := range sequences {
|
||||
for sequence, bind := range binds {
|
||||
seq := strutil.ConvertMeta([]rune(sequence))
|
||||
|
||||
if len(string(keys)) < len(seq) && strings.HasPrefix(seq, string(keys)) {
|
||||
prefixed = append(prefixed, binds[sequence])
|
||||
if len(keysStr) < len(seq) && strings.HasPrefix(seq, keysStr) {
|
||||
prefixed = append(prefixed, bind)
|
||||
}
|
||||
|
||||
if string(keys) == seq {
|
||||
match = binds[sequence]
|
||||
if keysStr == seq {
|
||||
match = bind
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -11,11 +11,13 @@ const (
|
||||
ClearScreen = "\x1b[2J" // Clears screen, preserving scroll buffer
|
||||
ClearDisplay = "\x1b[3J" // Clears screen fully, wipes the scroll buffer
|
||||
|
||||
CursorTopLeft = "\x1b[H"
|
||||
SaveCursorPos = "\x1b7"
|
||||
RestoreCursorPos = "\x1b8"
|
||||
HideCursor = "\x1b[?25l"
|
||||
ShowCursor = "\x1b[?25h"
|
||||
CursorTopLeft = "\x1b[H"
|
||||
SaveCursorPos = "\x1b7"
|
||||
RestoreCursorPos = "\x1b8"
|
||||
HideCursor = "\x1b[?25l"
|
||||
ShowCursor = "\x1b[?25h"
|
||||
BracketedPasteEnable = "\x1b[?2004h"
|
||||
BracketedPasteDisable = "\x1b[?2004l"
|
||||
)
|
||||
|
||||
// Some core keys needed by some stuff.
|
||||
|
||||
+80
-4
@@ -1,8 +1,11 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Those variables are very important to realine low-level code: all virtual terminal
|
||||
@@ -16,13 +19,19 @@ var (
|
||||
|
||||
func init() {
|
||||
stdoutTerm = os.Stdout
|
||||
stdoutTerm = os.Stderr
|
||||
stderrTerm = os.Stdin
|
||||
stdinTerm = os.Stdin
|
||||
stderrTerm = os.Stderr
|
||||
}
|
||||
|
||||
// fallback terminal width when we can't get it through query.
|
||||
var defaultTermWidth = 80
|
||||
|
||||
var (
|
||||
outputMu sync.Mutex
|
||||
outputDepth int
|
||||
outputBuf *bufio.Writer
|
||||
)
|
||||
|
||||
// GetWidth returns the width of Stdout or 80 if the width cannot be established.
|
||||
func GetWidth() (termWidth int) {
|
||||
var err error
|
||||
@@ -50,6 +59,73 @@ func GetLength() int {
|
||||
}
|
||||
|
||||
func printf(format string, a ...interface{}) {
|
||||
s := fmt.Sprintf(format, a...)
|
||||
fmt.Print(s)
|
||||
Printf(format, a...)
|
||||
}
|
||||
|
||||
// BeginBuffer enables buffered terminal writes until EndBuffer is called.
|
||||
func BeginBuffer() {
|
||||
outputMu.Lock()
|
||||
defer outputMu.Unlock()
|
||||
|
||||
outputDepth++
|
||||
if outputDepth == 1 {
|
||||
outputBuf = bufio.NewWriterSize(stdoutTerm, 64*1024)
|
||||
}
|
||||
}
|
||||
|
||||
// EndBuffer flushes buffered terminal writes when leaving the outermost buffer.
|
||||
func EndBuffer() {
|
||||
outputMu.Lock()
|
||||
defer outputMu.Unlock()
|
||||
|
||||
if outputDepth == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
outputDepth--
|
||||
if outputDepth == 0 && outputBuf != nil {
|
||||
_ = outputBuf.Flush()
|
||||
outputBuf = nil
|
||||
}
|
||||
}
|
||||
|
||||
// WriteString writes to the terminal, using the buffer when enabled.
|
||||
func WriteString(s string) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
|
||||
outputMu.Lock()
|
||||
defer outputMu.Unlock()
|
||||
|
||||
if outputDepth > 0 && outputBuf != nil {
|
||||
_, _ = outputBuf.WriteString(s)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = stdoutTerm.WriteString(s)
|
||||
}
|
||||
|
||||
// Printf formats and writes to the terminal, using the buffer when enabled.
|
||||
func Printf(format string, a ...interface{}) {
|
||||
WriteString(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// ShouldQueryCursorPos reports whether querying the terminal cursor position is allowed.
|
||||
// Some terminals respond slowly or cause visible flicker when queried frequently.
|
||||
func ShouldQueryCursorPos() bool {
|
||||
if v := strings.TrimSpace(os.Getenv("READLINE_CURSOR_POS")); v != "" {
|
||||
switch strings.ToLower(v) {
|
||||
case "0", "false", "no", "off":
|
||||
return false
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("TERM_PROGRAM") == "iTerm.app" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/reeflective/readline/internal/color"
|
||||
@@ -78,7 +77,7 @@ func DisplayHint(hint *Hint) {
|
||||
|
||||
if len(hint.text) == 0 && len(hint.persistent) == 0 {
|
||||
if hint.cleanup {
|
||||
fmt.Print(term.ClearLineAfter)
|
||||
term.WriteString(term.ClearLineAfter)
|
||||
}
|
||||
|
||||
hint.cleanup = false
|
||||
@@ -95,7 +94,7 @@ func DisplayHint(hint *Hint) {
|
||||
text += term.ClearLineAfter + color.Reset
|
||||
|
||||
if len(text) > 0 {
|
||||
fmt.Print(text)
|
||||
term.WriteString(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+43
-18
@@ -20,9 +20,10 @@ const (
|
||||
// Prompt stores all prompt rendering/generation functions and is
|
||||
// in charge of displaying them, as well as computing their offsets.
|
||||
type Prompt struct {
|
||||
primaryF func() string
|
||||
primaryRows int
|
||||
primaryCols int
|
||||
primaryF func() string
|
||||
primaryRows int
|
||||
primaryCols int
|
||||
primaryColsRaw int
|
||||
|
||||
secondaryF func() string
|
||||
transientF func() string
|
||||
@@ -108,14 +109,15 @@ func (p *Prompt) PrimaryPrint() {
|
||||
|
||||
// Print the various lines.
|
||||
if prompt != "" {
|
||||
fmt.Print(prompt)
|
||||
term.WriteString(prompt)
|
||||
}
|
||||
|
||||
fmt.Print(lastPrompt)
|
||||
term.WriteString(lastPrompt)
|
||||
|
||||
// And compute coordinates
|
||||
p.primaryRows = strings.Count(prompt, "\n")
|
||||
p.primaryCols = strutil.RealLength(lastPrompt)
|
||||
p.primaryColsRaw = strutil.RealLength(lastPrompt)
|
||||
p.primaryCols = p.primaryColsRaw
|
||||
|
||||
if p.primaryCols > 0 {
|
||||
p.primaryCols--
|
||||
@@ -149,9 +151,10 @@ func (p *Prompt) LastPrint() {
|
||||
|
||||
prompt := p.formatLastPrompt(lines[len(lines)-1])
|
||||
|
||||
fmt.Print(prompt)
|
||||
term.WriteString(prompt)
|
||||
|
||||
p.primaryCols = strutil.RealLength(prompt)
|
||||
p.primaryColsRaw = strutil.RealLength(prompt)
|
||||
p.primaryCols = p.primaryColsRaw
|
||||
if p.primaryCols > 0 {
|
||||
p.primaryCols--
|
||||
}
|
||||
@@ -175,7 +178,8 @@ func (p *Prompt) LastUsed() int {
|
||||
}
|
||||
|
||||
prompt := p.formatLastPrompt(lines[len(lines)-1])
|
||||
p.primaryCols = strutil.RealLength(prompt)
|
||||
p.primaryColsRaw = strutil.RealLength(prompt)
|
||||
p.primaryCols = p.primaryColsRaw
|
||||
|
||||
if p.primaryCols > 0 {
|
||||
p.primaryCols--
|
||||
@@ -184,15 +188,36 @@ func (p *Prompt) LastUsed() int {
|
||||
return p.primaryCols
|
||||
}
|
||||
|
||||
// LastCols returns the number of terminal columns used by the last prompt line.
|
||||
func (p *Prompt) LastCols() int {
|
||||
if p.primaryF == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
if p.primaryColsRaw != 0 {
|
||||
return p.primaryColsRaw
|
||||
}
|
||||
|
||||
lines := strings.Split(p.primaryF(), "\n")
|
||||
if len(lines) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
prompt := p.formatLastPrompt(lines[len(lines)-1])
|
||||
p.primaryColsRaw = strutil.RealLength(prompt)
|
||||
|
||||
return p.primaryColsRaw
|
||||
}
|
||||
|
||||
// SecondaryPrint prints the last cursor in secondary prompt mode,
|
||||
// which is always activated when the current input line is a multiline one.
|
||||
func (p *Prompt) SecondaryPrint() {
|
||||
if p.secondaryF != nil {
|
||||
fmt.Print(p.secondaryF())
|
||||
term.WriteString(p.secondaryF())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Print(secondaryPromptDefault)
|
||||
term.WriteString(secondaryPromptDefault)
|
||||
}
|
||||
|
||||
// MultilineColumnPrint prints the multiline editor column status indicator.
|
||||
@@ -209,7 +234,7 @@ func (p *Prompt) MultilineColumnPrint() {
|
||||
column += fmt.Sprintf("\n\x1b[1;30m%d\x1b[0m", pos+2)
|
||||
}
|
||||
|
||||
fmt.Print(column)
|
||||
term.WriteString(column)
|
||||
|
||||
case len(custom) > 0:
|
||||
column := ""
|
||||
@@ -217,7 +242,7 @@ func (p *Prompt) MultilineColumnPrint() {
|
||||
column += fmt.Sprintf("\n%s\x1b[0m", custom)
|
||||
}
|
||||
|
||||
fmt.Print(column)
|
||||
term.WriteString(column)
|
||||
|
||||
case defaultCol:
|
||||
column := ""
|
||||
@@ -225,7 +250,7 @@ func (p *Prompt) MultilineColumnPrint() {
|
||||
column += "\n" + multilineColumnDefault
|
||||
}
|
||||
|
||||
fmt.Print(column)
|
||||
term.WriteString(column)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,9 +274,9 @@ func (p *Prompt) RightPrint(startColumn int, force bool) {
|
||||
}
|
||||
|
||||
if prompt, canPrint := p.formatRightPrompt(rprompt, startColumn); canPrint {
|
||||
fmt.Print(prompt)
|
||||
term.WriteString(prompt)
|
||||
} else {
|
||||
fmt.Print(term.ClearLineAfter)
|
||||
term.WriteString(term.ClearLineAfter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,10 +289,10 @@ func (p *Prompt) TransientPrint() {
|
||||
// Clean everything below where the prompt will be printed.
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
term.MoveCursorUp(p.primaryRows)
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
// And print the prompt
|
||||
fmt.Print(p.transientF())
|
||||
term.WriteString(p.transientF())
|
||||
}
|
||||
|
||||
// Refreshing returns true if the prompt is currently redisplaying
|
||||
|
||||
+3
-1
@@ -56,11 +56,13 @@ func (rl *Shell) Readline() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
defer term.Restore(descriptor, state)
|
||||
defer term.WriteString(term.BracketedPasteDisable)
|
||||
term.WriteString(term.BracketedPasteEnable)
|
||||
|
||||
// Prompts and cursor styles
|
||||
rl.Display.PrintPrimaryPrompt()
|
||||
defer rl.Display.RefreshTransient()
|
||||
defer fmt.Print(keymap.CursorStyle("default"))
|
||||
defer term.WriteString(keymap.CursorStyle("default").String())
|
||||
|
||||
rl.init()
|
||||
|
||||
|
||||
+2
-2
@@ -145,7 +145,7 @@ func (rl *Shell) Printf(msg string, args ...any) (n int, err error) {
|
||||
// and clear everything below (hints and completions).
|
||||
rl.Display.CursorBelowLine()
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
// Skip a line, and print the formatted message.
|
||||
n, err = fmt.Printf(msg+"\n", args...)
|
||||
@@ -165,7 +165,7 @@ func (rl *Shell) PrintTransientf(msg string, args ...any) (n int, err error) {
|
||||
rl.Display.CursorToLineStart()
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
term.MoveCursorUp(rl.Prompt.PrimaryUsed())
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
// Print the logged message.
|
||||
n, err = fmt.Printf(msg+"\n", args...)
|
||||
|
||||
Vendored
+5
-4
@@ -581,15 +581,15 @@ github.com/pmezard/go-difflib/difflib
|
||||
# github.com/prometheus-community/pro-bing v0.4.0
|
||||
## explicit; go 1.19
|
||||
github.com/prometheus-community/pro-bing
|
||||
# github.com/reeflective/console v0.1.25
|
||||
## explicit; go 1.23.6
|
||||
# github.com/reeflective/console v0.1.25 => github.com/moloch--/console v0.0.0-20260128035037-680800942770
|
||||
## explicit; go 1.24.0
|
||||
github.com/reeflective/console
|
||||
github.com/reeflective/console/commands/readline
|
||||
github.com/reeflective/console/internal/completion
|
||||
github.com/reeflective/console/internal/line
|
||||
github.com/reeflective/console/internal/strutil
|
||||
github.com/reeflective/console/internal/ui
|
||||
# github.com/reeflective/readline v1.1.4 => github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d
|
||||
# github.com/reeflective/readline v1.1.4 => github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989
|
||||
## explicit; go 1.23.6
|
||||
github.com/reeflective/readline
|
||||
github.com/reeflective/readline/inputrc
|
||||
@@ -1397,4 +1397,5 @@ tailscale.com/wgengine/wgcfg/nmcfg
|
||||
tailscale.com/wgengine/wgint
|
||||
tailscale.com/wgengine/wglog
|
||||
# github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.46.3-0.20231214181515-27e49f3c3b69
|
||||
# github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d
|
||||
# github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989
|
||||
# github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260128035037-680800942770
|
||||
Reference in new issue
Block a user