Compare commits

..
Author SHA1 Message Date
moloch-- 2af6e11adc Update readline/console deps 2026-01-27 19:55:18 -08:00
Joe a3a98858fd Merge pull request #2135 from BishopFox/fix/sqlite
Add sqlite pragmas to db config
2026-01-21 22:28:56 -07:00
moloch-- d0652f2fd3 Add sqlite pragmas to db config 2026-01-21 16:08:07 -08:00
Joe 936f067363 Merge pull request #2130 from BishopFox/feature/website-info
Feature/website info
2026-01-21 10:25:56 -07:00
moloch-- 041c3c7048 Improve tab completer for websites cmd 2026-01-20 21:29:39 -08:00
moloch-- b48f74af58 Add original file name and sha256 to websites cmd 2026-01-20 19:13:42 -08:00
moloch-- 6ec8849b20 v1.6.6 2026-01-16 09:18:39 -08:00
Joe db7cf1ed62 Merge pull request #2125 from BishopFox/fix/winlsdir
Improved `err` and `nil` checks in `dirListHandler`
2026-01-16 10:17:01 -07:00
30 changed files with 1618 additions and 1154 deletions

No files matched your search

-1
View File
@@ -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.
+39
View File
@@ -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
}
+10 -4
View File
@@ -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 -2
View File
@@ -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
+4 -10
View File
@@ -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=
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
+71 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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,
}
}
+21 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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...)
+5 -4
View File
@@ -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