Compare commits

..

No commits in common. "master" and "v2.1.0.beta.10" have entirely different histories.

276 changed files with 839 additions and 44468 deletions

View File

@ -1,74 +0,0 @@
package astro
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
)
//go:embed city.json
var cityByte []byte
type City struct {
Code int `json:"code"`
CityName string `json:"city_name"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
var lon, lat, height, loc float64
var city string
func SetLonLatHeight(lo, la, he float64) error {
lon, lat, height = lo, la, he
return os.WriteFile(".ast.env", []byte(fmt.Sprintf("%f,%f,%f,%f", lon, lat, height, loc)), 0644)
}
func LoadLonLatHeight() error {
if _, err := os.Stat(".ast.env"); errors.Is(err, os.ErrNotExist) {
return nil
}
b, err := os.ReadFile(".ast.env")
if err != nil {
return err
}
_, err = fmt.Sscanf(string(b), "%f,%f,%f,%f", &lon, &lat, &height, &loc)
return err
}
func GetLonLatHeight() (float64, float64, float64) {
return lon, lat, height
}
func Lon() float64 {
return lon
}
func Lat() float64 {
return lat
}
func Height() float64 {
return height
}
func GetFromCity(name string) bool {
var c []City
err := json.Unmarshal(cityByte, &c)
if err != nil {
return false
}
for _, v := range c {
if strings.Contains(v.CityName, name) {
lon, lat = v.Lon, v.Lat
loc = 8
time.Local = time.FixedZone("Local", int(loc*3600))
return true
}
}
return false
}

View File

@ -1,431 +0,0 @@
package astro
import (
"b612.me/astro/calendar"
"b612.me/staros"
"fmt"
"github.com/spf13/cobra"
"golang.org/x/term"
"os"
"strconv"
"strings"
"time"
)
var year int
var nowDay string
var isTimestamp bool
var isLunar, isLive, isLeap bool
func init() {
CmdDateInfo.Flags().StringVarP(&nowDay, "now", "n", "", "指定现在的时间")
CmdDateInfo.Flags().BoolVarP(&isTimestamp, "timestamp", "t", false, "是否为时间戳")
CmdDateInfo.Flags().BoolVarP(&isLunar, "lunar", "l", false, "是否为农历")
CmdDateInfo.Flags().BoolVarP(&isLive, "live", "v", false, "是否为实时")
CmdDateInfo.Flags().BoolVarP(&isLeap, "leap", "L", false, "是否为农历闰月")
CmdHoliday.Flags().IntVarP(&year, "year", "y", 0, "年份")
CmdCal.AddCommand(CmdHoliday, CmdDateInfo)
}
var CmdCal = &cobra.Command{
Use: "cal",
Args: cobra.MaximumNArgs(100),
Short: "简洁日历与日期相关方法",
Long: "简洁日历,支持多个月份,多个年份,支持年份月份混合输入,日期方法请查看子命令",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
now := time.Now()
fmt.Println(now.Format("2006年01月"))
fmt.Println("------------------------")
ShowCalendar(now.Year(), int(now.Month()))
return
}
for _, v := range args {
if len(v) < 4 {
fmt.Println("输入错误年份至少4位")
return
}
year, err := strconv.Atoi(v[:4])
if err != nil {
fmt.Println("输入错误:", err)
return
}
if len(v) == 4 {
for i := 1; i <= 12; i++ {
fmt.Printf("%d年%d月\n", year, i)
fmt.Println("------------------------")
ShowCalendar(year, i)
fmt.Println()
fmt.Println()
}
continue
}
month, err := strconv.Atoi(v[4:])
if err != nil {
fmt.Println("输入错误:", err)
return
}
fmt.Printf("%d年%d月\n", year, month)
fmt.Println("------------------------")
ShowCalendar(year, month)
fmt.Println()
}
},
}
var CmdHoliday = &cobra.Command{
Use: "hol",
Short: "中国法定节日放假安排",
Long: "中国法定节日放假安排",
Run: func(cmd *cobra.Command, args []string) {
if year == 0 {
year = time.Now().Year()
}
fmt.Printf("%d年放假安排\n", year)
for _, v := range args {
fmt.Println("------------------------")
var d Holiday
switch v {
case "1", "元旦":
d = YuanDan(year)
case "2", "春节":
d = ChunJie(year)
case "3", "清明", "清明节":
d = QingMing(year)
case "4", "劳动", "劳动节":
d = LaoDongJie(year)
case "5", "端午", "端午节":
d = DuanWu(year)
case "6", "中秋", "中秋节":
d = ZhongQiu(year)
case "7", "国庆", "国庆节":
d = GuoQing(year)
}
d.BasicInfo()
d.Detail()
}
if len(args) == 0 {
for _, v := range ChineseHoliday(year) {
fmt.Println("------------------------")
v.BasicInfo()
v.Detail()
fmt.Println(" ")
}
}
},
}
var CmdDateInfo = &cobra.Command{
Use: "info",
Short: "指定时刻的详情",
Long: "指定时刻的详情格式info [时间] [选项]",
Run: func(cmd *cobra.Command, args []string) {
var now = time.Now()
var target = now
var err error
if nowDay != "" {
now, err = parseDate(time.Time{}, nowDay, isTimestamp)
if err != nil {
fmt.Println(err)
return
}
}
if len(args) > 0 {
target, err = parseDate(now, args[0], isTimestamp)
if err != nil {
fmt.Println(err)
return
}
}
for {
if isLunar {
LDateInfo(now, target, isLeap)
} else {
DateInfo(now, target)
}
if !isLive {
break
}
time.Sleep(time.Nanosecond*
time.Duration(1000000000-time.Now().Nanosecond()) + 1)
if nowDay == "" {
now = time.Now()
now = now.Add(time.Duration(now.Nanosecond()*-1) * time.Nanosecond)
} else {
now = now.Add(time.Second)
}
ClearScreen()
}
},
}
func ClearScreen() {
fmt.Print("\033[H\033[2J")
}
func GenerateCalendar(year int, month int) []string {
var days []string
date := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local)
firstWeekday := int(date.Weekday()) - 1
if firstWeekday < 0 {
firstWeekday += 7
}
count := 0
for i := 0; i < firstWeekday; i++ {
days = append(days, " ")
count++
}
for i := 1; i <= 32; i++ {
insertLunar := func(start int) {
if start < 0 {
start += 7
}
for j := start; j >= 0; j-- {
if i-j < 1 {
days = append(days, "\u3000\u3000")
continue
}
date = time.Date(year, time.Month(month), i-j, 0, 0, 0, 0, time.Local)
_, d, _, str := calendar.RapidSolarToLunar(date)
if d != 1 {
str = str[strings.Index(str, "月")+3:]
} else {
str = str[:strings.Index(str, "月")+3]
}
days = append(days, str)
}
}
date = time.Date(year, time.Month(month), i, 0, 0, 0, 0, time.Local)
if date.Month() != time.Month(month) {
if (count)%7 != 0 {
i--
insertLunar(count%7 - 1)
}
break
}
days = append(days, fmt.Sprintf("%d", i))
count++
if count%7 == 0 {
insertLunar(6)
}
}
return days
}
func ShowCalendar(year int, month int) {
days := GenerateCalendar(year, month)
fd := int(os.Stdout.Fd())
width, _, err := term.GetSize(fd)
if err != nil {
fmt.Println("Error getting terminal size:", err)
return
}
spark := 4
if width > 45 {
spark = 5
}
if width < 30 {
fmt.Println("Terminal too small")
return
}
for _, v := range []string{"\u3000一", "\u3000二", "\u3000三", "\u3000四", "\u3000五", "\u3000六", "\u3000日"} {
fmt.Printf("%*s", spark-1, v)
}
fmt.Println()
ct := 0
doBreak := false
for i, v := range days {
if len(days)-i < 7 && i%7 == len(days)-i && !doBreak {
doBreak = true
ct++
if ct%2 == 1 {
spark -= 2
} else {
spark += 2
}
fmt.Println()
fmt.Print(" ")
}
if i%7 == 0 && i != 0 && !doBreak {
fmt.Println()
ct++
if ct%2 == 1 {
fmt.Print(" ")
spark -= 2
} else {
spark += 2
}
}
fmt.Printf("%*s ", spark, v)
}
}
func LDateInfo(now, date time.Time, isLeap bool) {
sdate := calendar.RapidLunarToSolar(date.Year(), int(date.Month()), date.Day(), isLeap)
DateInfo(now, sdate)
}
func DateInfo(now, date time.Time) {
if now.IsZero() {
now = time.Now()
}
_, m, _, str := calendar.RapidSolarToLunar(date)
gz := calendar.GanZhi(date.Year())
if m > 10 && int(date.Month()) < 3 {
gz = calendar.GanZhi(date.Year() - 1)
}
fmt.Println("现在:", now.Format("2006年01月02日 15:04:05"))
fmt.Println("-------------------------")
xq := []string{"日", "一", "二", "三", "四", "五", "六"}
fmt.Printf("公历:%s 星期%s\n", date.Format("2006年01月02日 15:04:05"), xq[date.Weekday()])
fmt.Println("农历:", gz+str)
fmt.Printf("时间戳:%v\n", date.Unix())
fmt.Println("-------------------------")
diff := date.Sub(now)
fmt.Printf("距今: %.5f秒\n", diff.Seconds())
fmt.Printf("距今: %.5f分钟\n", diff.Minutes())
fmt.Printf("距今: %.5f小时\n", diff.Hours())
fmt.Printf("距今: %.5f天\n", diff.Hours()/24)
fmt.Printf("距今: %.5f年\n", diff.Hours()/24/365.2425)
fmt.Println("距今:", dayDiff(now, date))
}
func dayDiff(date1, date2 time.Time) string {
// 提取年、月、日
pr := ""
if date1.After(date2) {
pr = "-"
date1, date2 = date2, date1
}
years, months, days := date2.Date()
yearDiff := years - date1.Year()
monthDiff := int(months) - int(date1.Month())
dayDiff := days - date1.Day()
// 处理负的月份和日期差
if dayDiff < 0 {
monthDiff--
// 计算上个月的天数
prevMonth := date2.AddDate(0, -1, 0)
dayDiff += time.Date(prevMonth.Year(), prevMonth.Month(), 0, 0, 0, 0, 0, time.UTC).Day()
}
if monthDiff < 0 {
yearDiff--
monthDiff += 12
}
// 提取小时、分钟和秒
hours := date2.Hour() - date1.Hour()
minutes := date2.Minute() - date1.Minute()
seconds := date2.Second() - date1.Second()
// 处理负的小时、分钟和秒差
if seconds < 0 {
minutes--
seconds += 60
}
if minutes < 0 {
hours--
minutes += 60
}
if hours < 0 {
days--
hours += 24
}
return fmt.Sprintf("%s%d年%d月%d日%d时%d分%d秒", pr, yearDiff, monthDiff, dayDiff, hours, minutes, seconds)
}
func parseDate(base time.Time, date string, isTimestamp bool) (time.Time, error) {
if isTimestamp {
i, err := strconv.Atoi(date)
if err != nil {
return time.Time{}, err
}
return time.Unix(int64(i), 0), nil
}
if base.IsZero() {
base = time.Now()
}
if strings.HasPrefix(date, "+") || strings.HasPrefix(date, "p") {
val, err := staros.Calc(date[1:])
if err != nil {
return time.Time{}, err
}
for val > 9.22e09 {
val = val - 9.22e09
base = base.Add(9.22e09 * time.Second)
}
return base.Add(time.Second * time.Duration(int(val))), nil
}
if strings.HasPrefix(date, "-") || strings.HasPrefix(date, "—") ||
strings.HasPrefix(date, "~") || strings.HasPrefix(date, "!") ||
strings.HasPrefix(date, "m") {
val, err := staros.Calc(date[1:])
if err != nil {
return time.Time{}, err
}
for val > 9.22e09 {
val = val - 9.22e09
base = base.Add(-9.22e09 * time.Second)
}
return base.Add(-1 * time.Second * time.Duration(int(val))), nil
}
if strings.Contains(date, "-") {
d, err := time.ParseInLocation("2006-01-02 15:04:05", date, time.Local)
if err == nil {
return d, nil
}
d, err = time.ParseInLocation("2006-01-02", date, time.Local)
if err == nil {
return d, nil
}
d, err = time.ParseInLocation("2006-01-02T15:04:05-0700", date, time.Local)
if err == nil {
return d, nil
}
d, err = time.ParseInLocation("2006-01-02T15:04:05Z", date, time.Local)
if err == nil {
return d, nil
}
return time.Time{}, err
}
if strings.Contains(date, "/") {
d, err := time.ParseInLocation("2006/01/02 15:04:05", date, time.Local)
if err == nil {
return d, nil
}
d, err = time.ParseInLocation("2006/01/02", date, time.Local)
if err == nil {
return d, nil
}
d, err = time.ParseInLocation("02/01/2006 15:04:05", date, time.Local)
if err == nil {
return d, nil
}
d, err = time.ParseInLocation("02/01/2006", date, time.Local)
if err == nil {
return d, nil
}
d, err = time.ParseInLocation("01/02/2006 15:04:05", date, time.Local)
if err == nil {
return d, nil
}
d, err = time.ParseInLocation("01/02/2006", date, time.Local)
if err == nil {
return d, nil
}
return time.Time{}, err
}
d, err := time.ParseInLocation("20060102150405", date, time.Local)
if err == nil {
return d, nil
}
d, err = time.ParseInLocation("20060102", date, time.Local)
if err == nil {
return d, nil
}
return time.Time{}, fmt.Errorf("无法解析日期")
}

View File

@ -1,37 +0,0 @@
package astro
import (
"fmt"
"testing"
"time"
)
func TestCal(t *testing.T) {
//LDateInfo(time.Time{}, time.Date(2021, 1, 1, 0, 0, 0, 0, time.Local), false)
//DateInfo(time.Time{}, time.Now().Add(time.Second*-5))
for y := 2008; y < 2028; y++ {
zq := GuoQing(y)
zq.BasicInfo()
zq.Detail()
fmt.Println("--------")
}
}
func TestChineseHoliday(t *testing.T) {
legalData := [][]Holiday{
//year 2000
{
{
Start: time.Date(1999, 12, 31, 0, 0, 0, 0, time.Local),
End: time.Date(2000, 1, 2, 0, 0, 0, 0, time.Local),
Core: time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local),
Total: 3,
Instead: nil,
Name: "元旦",
LegalDays: 1,
},
},
}
_ = legalData
}

View File

@ -1,691 +0,0 @@
package astro
import (
"b612.me/astro/calendar"
"fmt"
"time"
)
type Holiday struct {
//假日开始时间
Start time.Time
//假日结束时间
End time.Time
//节日所在日期
Core time.Time
//假日总天数
Total int
//调休日期
Instead []time.Time
//假日名称
Name string
//法定假日天数
LegalDays int
Comment string
}
func (h Holiday) BasicInfo() {
xq := []string{"日", "一", "二", "三", "四", "五", "六"}
_, m, _, str := calendar.RapidSolarToLunar(h.Core)
gz := calendar.GanZhi(h.Core.Year())
if m > 10 && int(h.Core.Month()) < 3 {
gz = calendar.GanZhi(h.Core.Year() - 1)
}
if h.Start.IsZero() {
fmt.Println(h.Name + "无假期")
return
}
fmt.Printf("节日: %s 法定假期: %d天 放假天数: %d天\n", h.Name, h.LegalDays, h.Total)
fmt.Println("公历:", h.Core.Format("2006年01月02日"), "星期"+xq[h.Core.Weekday()])
fmt.Println("农历:", gz+str)
if h.Comment != "" {
fmt.Println(h.Comment)
}
}
func (h Holiday) Detail() {
if h.Start.IsZero() {
return
}
xq := []string{"日", "一", "二", "三", "四", "五", "六"}
if h.Start.Equal(h.End) || h.End.IsZero() {
fmt.Printf("假期: %s(星期%s) 共%d天\n",
h.Start.Format("2006年01月02日"), xq[h.Start.Weekday()], h.Total)
} else {
fmt.Printf("假期: %s(星期%s) - %s(星期%s) 共%d天\n",
h.Start.Format("2006年01月02日"), xq[h.Start.Weekday()],
h.End.Format("2006年01月02日"), xq[h.End.Weekday()], h.Total)
}
if len(h.Instead) > 0 {
fmt.Print("调休: ")
for idx, v := range h.Instead {
fmt.Printf("%s(星期%s)", v.Format("01月02日"), xq[v.Weekday()])
if idx != len(h.Instead)-1 {
fmt.Print("、")
}
}
fmt.Println(" 上班")
return
}
fmt.Println("不调休")
}
func target3Day1(date time.Time) Holiday {
switch date.Weekday() {
case 0:
return Holiday{
Core: date,
Start: date.Add(-24 * time.Hour),
End: date.Add(24 * time.Hour),
Total: 3,
LegalDays: 1,
}
case 1:
return Holiday{
Core: date,
Start: date.Add(-48 * time.Hour),
End: date,
Total: 3,
LegalDays: 1,
}
case 5, 6:
return Holiday{
Core: date,
Start: date,
End: date.Add(48 * time.Hour),
Total: 3,
LegalDays: 1,
}
case 3:
return Holiday{
Core: date,
Start: date,
End: date,
Total: 1,
LegalDays: 1,
}
case 2:
return Holiday{
Core: date,
Start: date.Add(-48 * time.Hour),
End: date,
Total: 3,
Instead: []time.Time{date.Add(-72 * time.Hour)},
LegalDays: 1,
}
case 4:
return Holiday{
Core: date,
Start: date,
End: date.Add(48 * time.Hour),
Total: 3,
Instead: []time.Time{date.Add(72 * time.Hour)},
LegalDays: 1,
}
}
return Holiday{}
}
func target5Day1(date time.Time) Holiday {
switch date.Weekday() {
case 1:
return Holiday{
Start: date.Add(time.Hour * -48),
End: date.Add(time.Hour * 48),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 8), date.Add(time.Hour * 24 * 5)},
Name: "",
LegalDays: 1,
}
case 2:
return Holiday{
Start: date,
End: date.Add(time.Hour * -24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 2), date.Add(time.Hour * 24 * 5)},
Name: "",
LegalDays: 1,
}
case 3:
return Holiday{
Start: date,
End: date.Add(time.Hour * 24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 3), date.Add(time.Hour * 24 * 10)},
Name: "",
LegalDays: 1,
}
case 4:
return Holiday{
Start: date,
End: date.Add(time.Hour * 24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 4), date.Add(time.Hour * 24 * 9)},
Name: "",
LegalDays: 1,
}
case 5:
return Holiday{
Start: date,
End: date.Add(time.Hour * 24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 5), date.Add(time.Hour * 24 * 8)},
Name: "",
LegalDays: 1,
}
case 6:
return Holiday{
Start: date,
End: date.Add(time.Hour * 24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 6), date.Add(time.Hour * 24 * 7)},
Name: "",
LegalDays: 1,
}
case 0:
return Holiday{
Start: date.Add(time.Hour * -24),
End: date.Add(time.Hour * 24 * 3),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 7), date.Add(time.Hour * 24 * 6)},
Name: "",
LegalDays: 1,
}
}
return Holiday{}
}
func target5Day2(date time.Time) Holiday {
switch date.Weekday() {
case 1:
return Holiday{
Start: date.Add(time.Hour * -48),
End: date.Add(time.Hour * 48),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * 24 * 5)},
Name: "",
LegalDays: 2,
}
case 2:
return Holiday{
Start: date,
End: date.Add(time.Hour * -24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 2)},
Name: "",
LegalDays: 2,
}
case 3:
return Holiday{
Start: date,
End: date.Add(time.Hour * 24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 3)},
Name: "",
LegalDays: 2,
}
case 4:
return Holiday{
Start: date,
End: date.Add(time.Hour * 24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * -24 * 4)},
Name: "",
LegalDays: 2,
}
case 5:
return Holiday{
Start: date,
End: date.Add(time.Hour * 24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * 24 * 8)},
Name: "",
LegalDays: 2,
}
case 6:
return Holiday{
Start: date,
End: date.Add(time.Hour * 24 * 4),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * 24 * 7)},
Name: "",
LegalDays: 2,
}
case 0:
return Holiday{
Start: date.Add(time.Hour * -24),
End: date.Add(time.Hour * 24 * 3),
Core: date,
Total: 5,
Instead: []time.Time{date.Add(time.Hour * 24 * 6)},
Name: "",
LegalDays: 2,
}
}
return Holiday{}
}
func target7Day3(date time.Time) Holiday {
switch date.Weekday() {
case 1:
return Holiday{
Start: date,
End: date.Add(time.Hour * 6 * 24),
Core: date,
Total: 7,
Instead: []time.Time{date.Add(time.Hour * 24 * -2), date.Add(time.Hour * 24 * -1)},
Name: "",
LegalDays: 3,
}
case 2:
return Holiday{
Start: date,
End: date.Add(time.Hour * 6 * 24),
Core: date,
Total: 7,
Instead: []time.Time{date.Add(time.Hour * 24 * -2), date.Add(time.Hour * 24 * 11)},
Name: "",
LegalDays: 3,
}
case 3:
return Holiday{
Start: date,
End: date.Add(time.Hour * 6 * 24),
Core: date,
Total: 7,
Instead: []time.Time{date.Add(time.Hour * 24 * -3), date.Add(time.Hour * 24 * 10)},
Name: "",
LegalDays: 3,
}
case 4:
return Holiday{
Start: date,
End: date.Add(time.Hour * 6 * 24),
Core: date,
Total: 7,
Instead: []time.Time{date.Add(time.Hour * 24 * -4), date.Add(time.Hour * 24 * 9)},
Name: "",
LegalDays: 3,
}
case 5:
return Holiday{
Start: date,
End: date.Add(time.Hour * 6 * 24),
Core: date,
Total: 7,
Instead: []time.Time{date.Add(time.Hour * 24 * -5), date.Add(time.Hour * 24 * 8)},
Name: "",
LegalDays: 3,
}
case 6:
return Holiday{
Start: date,
End: date.Add(time.Hour * 6 * 24),
Core: date,
Total: 7,
Instead: []time.Time{date.Add(time.Hour * 24 * 7), date.Add(time.Hour * 24 * 8)},
Name: "",
LegalDays: 3,
}
case 0:
return Holiday{
Start: date,
End: date.Add(time.Hour * 6 * 24),
Core: date,
Total: 7,
Instead: []time.Time{date.Add(time.Hour * 24 * -1), date.Add(time.Hour * 24 * 7)},
Name: "",
LegalDays: 3,
}
}
return Holiday{}
}
func target8Day4(date time.Time) Holiday {
switch date.Weekday() {
case 1:
return Holiday{
Start: date,
End: date.Add(time.Hour * 7 * 24),
Core: date,
Total: 8,
Instead: []time.Time{date.Add(time.Hour * 24 * -2), date.Add(time.Hour * 24 * -1)},
Name: "",
LegalDays: 4,
}
case 2:
return Holiday{
Start: date,
End: date.Add(time.Hour * 7 * 24),
Core: date,
Total: 8,
Instead: []time.Time{date.Add(time.Hour * 24 * -2), date.Add(time.Hour * 24 * 11)},
Name: "",
LegalDays: 4,
}
case 3:
return Holiday{
Start: date,
End: date.Add(time.Hour * 7 * 24),
Core: date,
Total: 8,
Instead: []time.Time{date.Add(time.Hour * 24 * -3), date.Add(time.Hour * 24 * 10)},
Name: "",
LegalDays: 4,
}
case 4:
return Holiday{
Start: date,
End: date.Add(time.Hour * 7 * 24),
Core: date,
Total: 8,
Instead: []time.Time{date.Add(time.Hour * 24 * -4), date.Add(time.Hour * 24 * 9)},
Name: "",
LegalDays: 4,
}
case 5:
if date.Year() >= 2025 {
return Holiday{
Start: date,
End: date.Add(time.Hour * 8 * 24),
Core: date,
Total: 9,
Instead: []time.Time{date.Add(time.Hour * 24 * -5), date.Add(time.Hour * 24 * 9)},
Name: "",
LegalDays: 4,
}
}
return Holiday{
Start: date,
End: date.Add(time.Hour * 7 * 24),
Core: date,
Total: 8,
Instead: []time.Time{date.Add(time.Hour * 24 * 8), date.Add(time.Hour * 24 * 9)},
Name: "",
LegalDays: 4,
}
case 6:
return Holiday{
Start: date,
End: date.Add(time.Hour * 7 * 24),
Core: date,
Total: 8,
Instead: []time.Time{date.Add(time.Hour * 24 * -6), date.Add(time.Hour * 24 * 8)},
Name: "",
LegalDays: 2,
}
case 0:
return Holiday{
Start: date,
End: date.Add(time.Hour * 7 * 24),
Core: date,
Total: 8,
Instead: []time.Time{date.Add(time.Hour * 24 * -1)},
Name: "",
LegalDays: 2,
}
}
return Holiday{}
}
// 元旦
func YuanDan(year int) Holiday {
name := "元旦"
date := time.Date(year, 1, 1, 0, 0, 0, 0, time.Local)
if year > 2007 || year == 2005 {
d := target3Day1(date)
d.Name = name
return d
}
switch year {
case 2007:
return Holiday{
Start: date,
End: date.Add(3 * 24 * time.Hour),
Core: date,
Total: 3,
Instead: []time.Time{date.Add(-24 * time.Hour), date.Add(-2 * 24 * time.Hour)},
Name: name,
LegalDays: 1,
}
case 2006:
return Holiday{
Start: date,
End: date.Add(3 * 24 * time.Hour),
Core: date,
Total: 3,
Instead: []time.Time{date.Add(-24 * time.Hour)},
Name: name,
LegalDays: 1,
}
case 2004, 2003:
return Holiday{
Start: date,
End: date,
Core: date,
Total: 1,
Instead: nil,
Name: name,
LegalDays: 1,
}
case 2002:
return Holiday{
Start: date,
End: date.Add(3 * 24 * time.Hour),
Core: date,
Total: 3,
Instead: []time.Time{date.Add(-3 * 24 * time.Hour), date.Add(-2 * 24 * time.Hour)},
Name: name,
LegalDays: 1,
}
}
return Holiday{Name: name}
}
// 春节
func ChunJie(year int) Holiday {
name := "春节"
chineseNewYear := calendar.RapidLunarToSolar(year, 1, 1, false)
chuxi := chineseNewYear.AddDate(0, 0, -1)
if year <= 2007 || year == 2014 {
d := target7Day3(chineseNewYear)
d.Name = name
return d
}
if year >= 2008 && year < 2024 {
d := target7Day3(chuxi)
d.Name = name
if year == 2020 {
d.Comment = "因新冠疫情防控2020年春节延长至2月2日"
}
return d
}
if year == 2024 {
d := target8Day4(chineseNewYear)
d.LegalDays = 3
d.Name = name
return d
}
d := target8Day4(chuxi)
d.Name = name
return d
}
func QingMing(year int) Holiday {
name := "清明节"
if year < 2008 {
return Holiday{Name: name}
}
qingming := calendar.JieQi(year, calendar.JQ_清明)
d := target3Day1(qingming)
d.Name = name
return d
}
func LaoDongJie(year int) Holiday {
name := "劳动节"
date := time.Date(year, 5, 1, 0, 0, 0, 0, time.Local)
if year < 2008 {
d := target7Day3(date)
d.Name = name
return d
}
if year == 2019 {
return Holiday{
Start: date,
End: date.Add(4 * 24 * time.Hour),
Core: date,
Total: 4,
Instead: []time.Time{date.Add(-24 * 3 * time.Hour), date.Add(24 * time.Hour * 5)},
Name: name,
LegalDays: 1,
Comment: "",
}
}
if year >= 2008 && year < 2020 {
d := target3Day1(date)
d.Name = name
return d
}
if year >= 2020 && year < 2025 {
d := target5Day1(date)
d.Name = name
return d
}
d := target5Day2(date)
d.Name = name
return d
}
func DuanWu(year int) Holiday {
name := "端午节"
if year < 2008 {
return Holiday{Name: name}
}
date := calendar.RapidLunarToSolar(year, 5, 5, false)
d := target3Day1(date)
d.Name = name
return d
}
func ZhongQiu(year int) Holiday {
name := "中秋节"
if year < 2008 {
return Holiday{Name: name}
}
date := calendar.RapidLunarToSolar(year, 8, 15, false)
if date.Month() == 9 && date.Day() >= 28 {
d := target8Day4(date)
d.Name = "国庆中秋连假"
return d
} else if date.Month() == 10 {
d := target8Day4(time.Date(year, 10, 1, 0, 0, 0, 0, time.Local))
d.Name = "国庆中秋连假"
return d
}
d := target3Day1(date)
d.Name = name
if date.Month() == 9 && date.Day() <= 20 {
return d
}
if date.Weekday() == 3 {
d.Instead = []time.Time{date.Add(-24 * 3 * time.Hour), date.Add(24 * 3 * time.Hour)}
d.End = date.Add(2 * 24 * time.Hour)
d.Total = 3
}
gq := target7Day3(time.Date(year, 10, 1, 0, 0, 0, 0, time.Local))
for _, v := range gq.Instead {
if v.Before(d.End) || v.Equal(d.End) {
if v.Equal(date) {
d.Total = d.Total - 1
d.End = d.End.Add(-24 * time.Hour)
} else {
if v.Equal(d.Start) {
d.Total = d.Total - 1
d.Start = d.Start.Add(24 * time.Hour)
} else if v.Equal(d.End) {
d.Total = d.Total - 1
d.End = d.End.Add(-24 * time.Hour)
}
}
}
}
return d
}
func GuoQing(year int) Holiday {
name := "国庆节"
date := time.Date(year, 10, 1, 0, 0, 0, 0, time.Local)
zq := calendar.RapidLunarToSolar(year, 8, 15, false)
if year == 2008 {
return Holiday{
Start: date.Add(-24 * 2 * time.Hour),
End: date.Add(4 * 24 * time.Hour),
Core: date,
Total: 7,
Instead: []time.Time{date.Add(-24 * 4 * time.Hour), date.Add(24 * -3 * time.Hour)},
Name: name,
LegalDays: 3,
}
}
if year < 2008 || (zq.Month() == 9 && zq.Day() <= 20) {
d := target7Day3(date)
d.Name = name
return d
}
if zq.Month() == 9 && zq.Day() >= 28 {
d := target8Day4(zq)
d.Name = "国庆中秋连假"
return d
} else if zq.Month() == 10 {
d := target8Day4(date)
d.Name = "国庆中秋连假"
return d
}
zqd := target3Day1(zq)
if date.Weekday() == 3 {
zqd.Instead = []time.Time{date.Add(-24 * 3 * time.Hour), date.Add(24 * 3 * time.Hour)}
zqd.End = date.Add(2 * 24 * time.Hour)
zqd.Total = 3
}
d := target7Day3(date)
d.Name = name
for k, v := range d.Instead {
if v.Before(zqd.End) || v.Equal(zqd.End) {
if v.Equal(zq) {
d.Instead = append(d.Instead[:k], d.Instead[k+1:]...)
}
}
}
return d
}
func ChineseHoliday(year int) []Holiday {
d := []Holiday{
YuanDan(year),
ChunJie(year),
QingMing(year),
LaoDongJie(year),
DuanWu(year),
ZhongQiu(year),
GuoQing(year),
}
if d[len(d)-1].Name == "国庆中秋连假" {
return d[:len(d)-1]
}
return d
}

File diff suppressed because it is too large Load Diff

View File

@ -1,253 +0,0 @@
package astro
import (
"fmt"
"github.com/spf13/cobra"
"time"
)
var isFormat bool
var jieqi string
func init() {
Cmd.PersistentFlags().Float64Var(&lon, "lon", -273, "经度,WGS84坐标系")
Cmd.PersistentFlags().Float64Var(&lat, "lat", -273, "纬度,WGS84坐标系")
Cmd.PersistentFlags().Float64Var(&height, "height", 0, "海拔高度")
CmdSun.Flags().StringVarP(&nowDay, "now", "n", "", "指定现在的时间")
CmdSun.Flags().BoolVarP(&isTimestamp, "timestamp", "t", false, "是否为时间戳")
CmdSun.Flags().BoolVarP(&isLive, "live", "v", false, "是否为实时")
CmdSun.Flags().BoolVarP(&isFormat, "format", "f", false, "格式化输出")
CmdSun.Flags().StringVarP(&city, "city", "c", "", "城市名")
CmdMoon.Flags().StringVarP(&nowDay, "now", "n", "", "指定现在的时间")
CmdMoon.Flags().BoolVarP(&isTimestamp, "timestamp", "t", false, "是否为时间戳")
CmdMoon.Flags().BoolVarP(&isLive, "live", "v", false, "是否为实时")
CmdMoon.Flags().BoolVarP(&isFormat, "format", "f", false, "格式化输出")
CmdMoon.Flags().StringVarP(&city, "city", "c", "", "城市名")
CmdStar.Flags().StringVarP(&nowDay, "now", "n", "", "指定现在的时间")
CmdStar.Flags().BoolVarP(&isTimestamp, "timestamp", "t", false, "是否为时间戳")
CmdStar.Flags().BoolVarP(&isLive, "live", "v", false, "是否为实时")
CmdStar.Flags().BoolVarP(&isFormat, "format", "f", false, "格式化输出")
CmdStar.Flags().StringVarP(&city, "city", "c", "", "城市名")
Cmd.AddCommand(CmdCal, CmdSun, CmdMoon, CmdStar)
}
var Cmd = &cobra.Command{
Use: "astro",
Short: "天文计算",
}
var CmdSun = &cobra.Command{
Use: "sun",
Short: "太阳计算",
Run: func(cmd *cobra.Command, args []string) {
format := 0
if isFormat {
format = 1
}
isSet := CliLoadLonLatHeight()
var now = time.Now()
var err error
if nowDay != "" {
now, err = parseDate(now, nowDay, isTimestamp)
if err != nil {
fmt.Println(err)
return
}
}
for {
if isSet {
fmt.Printf("经度: %f 纬度: %f 海拔: %f\n", lon, lat, height)
}
BasicSun(now, uint8(format))
if isSet {
SunDetail(now, lon, lat, height, uint8(format))
}
if !isLive {
break
}
time.Sleep(time.Nanosecond*
time.Duration(1000000000-time.Now().Nanosecond()) + 1)
if nowDay == "" {
now = time.Now()
now = now.Add(time.Duration(now.Nanosecond()*-1) * time.Nanosecond)
} else {
now = now.Add(time.Second)
}
ClearScreen()
}
},
}
var CmdMoon = &cobra.Command{
Use: "moon",
Short: "月亮计算",
Run: func(cmd *cobra.Command, args []string) {
format := 0
if isFormat {
format = 1
}
isSet := CliLoadLonLatHeight()
var now = time.Now()
var err error
if nowDay != "" {
now, err = parseDate(now, nowDay, isTimestamp)
if err != nil {
fmt.Println(err)
return
}
}
for {
if isSet {
fmt.Printf("经度: %f 纬度: %f 海拔: %f\n", lon, lat, height)
}
BasicMoon(now, uint8(format))
if isSet {
MoonDetail(now, lon, lat, height, uint8(format))
}
if !isLive {
break
}
time.Sleep(time.Nanosecond*
time.Duration(1000000000-time.Now().Nanosecond()) + 1)
if nowDay == "" {
now = time.Now()
now = now.Add(time.Duration(now.Nanosecond()*-1) * time.Nanosecond)
} else {
now = now.Add(time.Second)
}
ClearScreen()
}
},
}
var CmdStar = &cobra.Command{
Use: "star",
Short: "星星计算",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
fmt.Println("请输入星星名字")
return
}
format := 0
if isFormat {
format = 1
}
isSet := CliLoadLonLatHeight()
var now = time.Now()
var err error
if nowDay != "" {
now, err = parseDate(now, nowDay, isTimestamp)
if err != nil {
fmt.Println(err)
return
}
}
for {
if isSet {
fmt.Printf("经度: %f 纬度: %f 海拔: %f\n", lon, lat, height)
}
BasicStar(now, args[0], uint8(format))
if isSet {
StarDetail(now, args[0], lon, lat, height, uint8(format))
}
if !isLive {
break
}
time.Sleep(time.Nanosecond*
time.Duration(1000000000-time.Now().Nanosecond()) + 1)
if nowDay == "" {
now = time.Now()
now = now.Add(time.Duration(now.Nanosecond()*-1) * time.Nanosecond)
} else {
now = now.Add(time.Second)
}
ClearScreen()
}
},
}
var CmdJieqi = &cobra.Command{
Use: "jq",
Short: "节气计算",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
fmt.Println("请输入年份或节气")
return
}
year := args[0]
if year == "" {
year = time.Now().Format("2006")
}
year = year[:4]
fmt.Println("年份: ", year)
/*
var jqname = map[string]int{
"春分": 0,
"清明": 15,
"谷雨": 30,
"立夏": 45,
"小满": 60,
"芒种": 75,
"夏至": 90,
"小暑": 105,
"大暑": 120,
"立秋": 135,
"处暑": 150,
"白露": 165,
"秋分": 180,
"寒露": 195,
"霜降": 210,
"立冬": 225,
"小雪": 240,
"大雪": 255,
"冬至": 270,
"小寒": 285,
"大寒": 300,
"立春": 315,
"雨水": 330,
"惊蛰": 345,
}
if jieqi != "" {
if v, ok := jqname[jieqi]; !ok {
fmt.Println("节气名错误")
return
} else {
fmt.Println("节气名: ", jieqi)
fmt.Println("时间: ", calendar.JieQi(year, v))
}
}
*/
},
}
func CliLoadLonLatHeight() bool {
if city != "" {
if !GetFromCity(city) {
fmt.Println("城市名错误")
return false
}
fmt.Println("城市名: ", city)
SetLonLatHeight(lon, lat, height)
return true
}
tlon, tlat, theight := lon, lat, height
LoadLonLatHeight()
if lon == -273 && lon != tlon {
lon = tlon
}
if lat == -273 && lat != tlat {
lat = tlat
}
if height == 0 && height != theight {
height = theight
}
SetLonLatHeight(lon, lat, height)
if lon == -273 || lat == -273 {
return false
}
return true
}

View File

@ -1 +0,0 @@
package astro

View File

@ -1,130 +0,0 @@
package astro
import (
"b612.me/astro/moon"
"b612.me/astro/star"
"b612.me/astro/sun"
"b612.me/astro/tools"
"fmt"
"time"
)
func BasicMoon(date time.Time, format uint8) {
fmt.Printf("时间: %s\n", date.Format("2006-01-02 15:04:05"))
lo := moon.ApparentLo(date)
eo := moon.Phase(date)
bo := moon.TrueBo(date)
fmt.Println("月亮")
fmt.Println("------------------------")
switch format {
case 0:
fmt.Printf("黄经: %f\n", lo)
fmt.Printf("黄纬: %f\n", bo)
case 1:
flo := tools.Format(lo, 0)
fbo := tools.Format(bo, 0)
fmt.Printf("黄经: %s\n", flo)
fmt.Printf("黄纬: %s\n", fbo)
}
phaseStr := ""
mslo := tools.Limit360(lo - sun.ApparentLo(date))
if mslo >= 0 && mslo <= 30 {
phaseStr = "新月"
} else if mslo > 30 && mslo <= 75 {
phaseStr = "上峨眉月"
} else if mslo > 75 && mslo <= 135 {
phaseStr = "上弦月"
} else if mslo > 135 && mslo < 170 {
phaseStr = "盈凸月"
} else if mslo >= 170 && mslo <= 190 {
phaseStr = "满月"
} else if mslo > 190 && mslo < 225 {
phaseStr = "亏凸月"
} else if mslo >= 225 && mslo < 285 {
phaseStr = "下弦月"
} else if mslo >= 285 && mslo < 330 {
phaseStr = "下峨眉月"
} else {
phaseStr = "残月"
}
fmt.Printf("月相: %.2f%% %s\n", eo*100, phaseStr)
sx := moon.NextShangXianYue(date)
xx := moon.NextXiaXianYue(date)
wang := moon.NextWangYue(date)
shuo := moon.NextShuoYue(date)
fmt.Printf("朔月: %s\n", shuo.Format("2006-01-02 15:04:05"))
fmt.Printf("上弦: %s\n", sx.Format("2006-01-02 15:04:05"))
fmt.Printf("望月: %s\n", wang.Format("2006-01-02 15:04:05"))
fmt.Printf("下弦: %s\n", xx.Format("2006-01-02 15:04:05"))
}
func MoonDetail(date time.Time, lon, lat, height float64, format uint8) {
var err error
ra, dec := moon.ApparentRaDec(date, lon, lat)
tmp := new(time.Time)
*tmp, err = moon.RiseTime(date, lon, lat, height, true)
if err != nil {
if err == moon.ERR_NOT_TODAY {
*tmp, err = moon.RiseTime(date.AddDate(0, 0, -1), lon, lat, 0, true)
if err != nil {
*tmp = time.Time{}
}
}
}
rise := *tmp
tmp = new(time.Time)
*tmp, err = moon.DownTime(date, lon, lat, 0, true)
if err != nil {
if err == moon.ERR_NOT_TODAY {
*tmp, err = moon.DownTime(date.AddDate(0, 0, 1), lon, lat, 0, true)
if err != nil {
*tmp = time.Time{}
}
}
}
if tmp.Before(rise) {
tmp = new(time.Time)
*tmp, err = moon.DownTime(date.AddDate(0, 0, 1), lon, lat, 0, true)
if err != nil {
if err == moon.ERR_NOT_TODAY {
*tmp, err = moon.DownTime(date.AddDate(0, 0, 2), lon, lat, 0, true)
if err != nil {
*tmp = time.Time{}
}
}
}
}
set := tmp
cst := star.Constellation(ra, dec, date)
switch format {
case 0:
fmt.Printf("视赤经: %s\n", ra)
fmt.Printf("视赤纬: %s\n", dec)
case 1:
fra := tools.Format(ra/15, 1)
fdec := tools.Format(dec, 0)
fmt.Printf("视赤经: %s\n", fra)
fmt.Printf("视赤纬: %s\n", fdec)
}
fmt.Printf("星座: %s\n", cst)
fmt.Printf("升起: %s\n", rise.Format("2006-01-02 15:04:05"))
fmt.Printf("落下: %s\n", set.Format("2006-01-02 15:04:05"))
az := moon.Azimuth(date, lon, lat)
alt := moon.Zenith(date, lon, lat)
ta := moon.HourAngle(date, lon, lat)
switch format {
case 0:
fmt.Printf("方位角: %f\n", az)
fmt.Printf("高度角: %f\n", alt)
fmt.Printf("时角: %f\n", ta)
case 1:
faz := tools.Format(az, 0)
falt := tools.Format(alt, 0)
fta := tools.Format(ta/15, 1)
fmt.Printf("方位角: %s\n", faz)
fmt.Printf("高度角: %s\n", falt)
fmt.Printf("时角: %s\n", fta)
}
}

View File

@ -1,101 +0,0 @@
package astro
import (
"b612.me/astro/star"
"b612.me/astro/tools"
"fmt"
"time"
)
func BasicStar(date time.Time, name string, format uint8) {
fmt.Printf("时间: %s\n", date.Format("2006-01-02 15:04:05"))
s, err := star.StarDataByName(name)
if err != nil {
fmt.Println(err)
return
}
ra, dec := s.RaDecByDate(date)
fmt.Printf("%s %s 星等:%.2f\n", s.ChineseName, s.Name, s.Mag)
fmt.Println("------------------------")
switch format {
case 0:
fmt.Printf("视赤经: %f\n", ra)
fmt.Printf("视赤纬: %f\n", dec)
case 1:
fra := tools.Format(ra/15, 1)
fdec := tools.Format(dec, 0)
fmt.Printf("视赤经: %s\n", fra)
fmt.Printf("视赤纬: %s\n", fdec)
}
cst := star.Constellation(ra, dec, date)
fmt.Printf("星座: %s\n", cst)
}
func StarDetail(date time.Time, name string, lon, lat, height float64, format uint8) {
s, err := star.StarDataByName(name)
if err != nil {
fmt.Println(err)
return
}
ra, dec := s.RaDecByDate(date)
alt := star.Zenith(date, ra, dec, lon, lat)
ta := star.HourAngle(date, ra, lon)
zt := star.CulminationTime(date, ra, lon)
az := star.Azimuth(date, ra, dec, lon, lat)
var rise, set time.Time
var duration time.Duration
for {
nk := date.Add(duration)
rise, err = star.RiseTime(nk, ra, dec, lon, lat, height, true)
if err != nil {
fmt.Println(err)
}
if alt > 0 && rise.After(nk) {
rise, err = star.RiseTime(nk.Add(time.Hour*-24), ra, dec, lon, lat, height, true)
if err != nil {
fmt.Println(err)
}
} else if alt < 0 && rise.Before(nk) {
rise, err = star.RiseTime(nk.Add(time.Hour*24), ra, dec, lon, lat, height, true)
if err != nil {
fmt.Println(err)
}
}
set, err = star.DownTime(nk, ra, dec, lon, lat, height, true)
if err != nil {
fmt.Println(err)
}
if set.Before(rise) {
set, err = star.DownTime(nk.Add(time.Hour*24), ra, dec, lon, lat, height, true)
if err != nil {
fmt.Println(err)
}
}
if set.Before(date) {
duration += time.Hour * 24
continue
}
if zt.Before(rise) {
zt = star.CulminationTime(nk.Add(time.Hour*24), ra, lon)
}
break
}
fmt.Printf("升起: %s\n", rise.Format("2006-01-02 15:04:05"))
fmt.Printf("中天: %s\n", zt.Format("2006-01-02 15:04:05"))
fmt.Printf("落下: %s\n", set.Format("2006-01-02 15:04:05"))
switch format {
case 0:
fmt.Printf("方位角: %f\n", az)
fmt.Printf("高度角: %f\n", alt)
fmt.Printf("时角: %f\n", ta)
case 1:
faz := tools.Format(az, 0)
falt := tools.Format(alt, 0)
fta := tools.Format(ta/15, 1)
fmt.Printf("方位角: %s\n", faz)
fmt.Printf("高度角: %s\n", falt)
fmt.Printf("时角: %s\n", fta)
}
}

View File

@ -1,76 +0,0 @@
package astro
import (
"b612.me/astro/star"
"b612.me/astro/sun"
"b612.me/astro/tools"
"fmt"
"time"
)
func BasicSun(date time.Time, format uint8) {
fmt.Printf("时间: %s\n", date.Format("2006-01-02 15:04:05"))
ra, dec := sun.ApparentRaDec(date)
lo := sun.ApparentLo(date)
eo := sun.EclipticObliquity(date, true)
fmt.Println("太阳")
fmt.Println("------------------------")
switch format {
case 0:
fmt.Printf("视赤经: %f\n", ra)
fmt.Printf("视赤纬: %f\n", dec)
fmt.Printf("视黄经: %f\n", lo)
fmt.Printf("黄赤交角: %f\n", eo)
case 1:
fra := tools.Format(ra/15, 1)
fdec := tools.Format(dec, 0)
flo := tools.Format(lo, 0)
feo := tools.Format(eo, 0)
fmt.Printf("视赤经: %s\n", fra)
fmt.Printf("视赤纬: %s\n", fdec)
fmt.Printf("视黄经: %s\n", flo)
fmt.Printf("黄赤交角: %s\n", feo)
}
cst := star.Constellation(ra, dec, date)
fmt.Printf("星座: %s\n", cst)
}
func SunDetail(date time.Time, lon, lat, height float64, format uint8) {
rise, err := sun.RiseTime(date, lon, lat, height, true)
if err != nil {
fmt.Println(err)
}
set, err := sun.DownTime(date, lon, lat, height, true)
if err != nil {
fmt.Println(err)
}
morning, err := sun.MorningTwilight(date, lon, lat, -6)
if err != nil {
fmt.Println(err)
}
evening, err := sun.EveningTwilight(date, lon, lat, -6)
if err != nil {
fmt.Println(err)
}
fmt.Printf("晨朦影: %s\n", morning.Format("2006-01-02 15:04:05"))
fmt.Printf("升起: %s\n", rise.Format("2006-01-02 15:04:05"))
fmt.Printf("落下: %s\n", set.Format("2006-01-02 15:04:05"))
fmt.Printf("昏朦影: %s\n", evening.Format("2006-01-02 15:04:05"))
az := sun.Azimuth(date, lon, lat)
alt := sun.Zenith(date, lon, lat)
ta := sun.HourAngle(date, lon, lat)
switch format {
case 0:
fmt.Printf("方位角: %f\n", az)
fmt.Printf("高度角: %f\n", alt)
fmt.Printf("时角: %f\n", ta)
case 1:
faz := tools.Format(az, 0)
falt := tools.Format(alt, 0)
fta := tools.Format(ta/15, 1)
fmt.Printf("方位角: %s\n", faz)
fmt.Printf("高度角: %s\n", falt)
fmt.Printf("时角: %s\n", fta)
}
}

View File

@ -1,38 +0,0 @@
# Changelog
## [v0.2.8](https://b612.me/apps/b612/bed/compare/v0.2.7..v0.2.8) (2024-12-01)
* Refactor drawing command line and completion candidates.
* Fix jump back action not to crash when the buffer is edited.
## [v0.2.7](https://b612.me/apps/b612/bed/compare/v0.2.6..v0.2.7) (2024-10-20)
* Support environment variable expansion in the command line.
* Implement `:cd`, `:chdir`, `:pwd` commands to change the working directory.
* Improve command line completion for command name and environment variables.
* Recognize file name argument and bang for `:wq` command.
## [v0.2.6](https://b612.me/apps/b612/bed/compare/v0.2.5..v0.2.6) (2024-10-08)
* Support reading from standard input.
* Implement command line history.
## [v0.2.5](https://b612.me/apps/b612/bed/compare/v0.2.4..v0.2.5) (2024-05-03)
* Require Go 1.22.
## [v0.2.4](https://b612.me/apps/b612/bed/compare/v0.2.3..v0.2.4) (2023-09-30)
* Require Go 1.21.
## [v0.2.3](https://b612.me/apps/b612/bed/compare/v0.2.2..v0.2.3) (2022-12-25)
* Fix crash on window moving commands on the last window.
## [v0.2.2](https://b612.me/apps/b612/bed/compare/v0.2.1..v0.2.2) (2021-09-14)
* Add `:only` command to make the current window the only one.
* Reduce memory allocations on rendering.
* Release `arm64` artifacts.
## [v0.2.1](https://b612.me/apps/b612/bed/compare/v0.2.0..v0.2.1) (2020-12-29)
* Add `:{count}%` to go to the position by percentage in the file.
* Add `:{count}go[to]` command to go to the specific line.
## [v0.2.0](https://b612.me/apps/b612/bed/compare/v0.1.0..v0.2.0) (2020-04-10)
* Add `:cquit` command.
## [v0.1.0](https://b612.me/apps/b612/bed/compare/8239ec4..v0.1.0) (2020-01-25)
* Initial implementation.

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2017-2024 itchyny
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,64 +0,0 @@
BIN := bed
VERSION := $$(make -s show-version)
VERSION_PATH := cmd/$(BIN)
CURRENT_REVISION = $(shell git rev-parse --short HEAD)
BUILD_LDFLAGS = "-s -w -X main.revision=$(CURRENT_REVISION)"
GOBIN ?= $(shell go env GOPATH)/bin
.PHONY: all
all: build
.PHONY: build
build:
go build -ldflags=$(BUILD_LDFLAGS) -o $(BIN) ./cmd/$(BIN)
.PHONY: install
install:
go install -ldflags=$(BUILD_LDFLAGS) ./cmd/$(BIN)
.PHONY: show-version
show-version: $(GOBIN)/gobump
@gobump show -r "$(VERSION_PATH)"
$(GOBIN)/gobump:
@go install github.com/x-motemen/gobump/cmd/gobump@latest
.PHONY: cross
cross: $(GOBIN)/goxz CREDITS
goxz -n $(BIN) -pv=v$(VERSION) -build-ldflags=$(BUILD_LDFLAGS) ./cmd/$(BIN)
$(GOBIN)/goxz:
go install github.com/Songmu/goxz/cmd/goxz@latest
CREDITS: $(GOBIN)/gocredits go.sum
go mod tidy
gocredits -w .
$(GOBIN)/gocredits:
go install github.com/Songmu/gocredits/cmd/gocredits@latest
.PHONY: test
test: build
go test -v -race -timeout 30s ./...
.PHONY: lint
lint: $(GOBIN)/staticcheck
go vet ./...
staticcheck -checks all,-ST1000 ./...
$(GOBIN)/staticcheck:
go install honnef.co/go/tools/cmd/staticcheck@latest
.PHONY: clean
clean:
rm -rf $(BIN) goxz CREDITS
go clean
.PHONY: bump
bump: $(GOBIN)/gobump
test -z "$$(git status --porcelain || echo .)"
test "$$(git branch --show-current)" = "main"
@gobump up -w "$(VERSION_PATH)"
git commit -am "bump up version to $(VERSION)"
git tag "v$(VERSION)"
git push --atomic origin main tag "v$(VERSION)"

View File

@ -1,81 +0,0 @@
# bed
[![CI Status](https://b612.me/apps/b612/bed/actions/workflows/ci.yaml/badge.svg?branch=main)](https://b612.me/apps/b612/bed/actions?query=branch:main)
[![Go Report Card](https://goreportcard.com/badge/b612.me/apps/b612/bed)](https://goreportcard.com/report/b612.me/apps/b612/bed)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://b612.me/apps/b612/bed/blob/main/LICENSE)
[![release](https://img.shields.io/github/release/itchyny/bed/all.svg)](https://b612.me/apps/b612/bed/releases)
[![pkg.go.dev](https://pkg.go.dev/badge/b612.me/apps/b612/bed)](https://pkg.go.dev/b612.me/apps/b612/bed)
Binary editor written in Go
## Screenshot
![bed command screenshot](https://user-images.githubusercontent.com/375258/38499347-2f71306c-3c42-11e8-926e-1782b0bc73f3.png)
## Motivation
I wanted to create a binary editor with Vim-like user interface, which runs in terminals, fast, and is portable.
I have always been interested in various binary formats and I wanted to create my own editor to handle them.
I also wanted to learn how a binary editor can handle large files and allow users to edit them interactively.
While creating this binary editor, I leaned a lot about programming in Go language.
I spent a lot of time writing the core logic of buffer implementation of the editor.
It was a great learning experience for me and a lot of fun.
## Installation
### Homebrew
```sh
brew install bed
```
### Build from source
```bash
go install b612.me/apps/b612/bed/cmd/bed@latest
```
## Features
- Basic byte editing
- Large file support
- Command line interface
- Window splitting
- Partial writing
- Text searching
- Undo and redo
### Commands and keyboard shortcuts
This binary editor is influenced by the Vim editor.
- File operations
- `:edit`, `:enew`, `:new`, `:vnew`, `:only`
- Current working directory
- `:cd`, `:chdir`, `:pwd`
- Quit and save
- `:quit`, `ZQ`, `:qall`, `:write`,
`:wq`, `ZZ`, `:xit`, `:xall`, `:cquit`
- Window operations
- `:wincmd [nohjkltbpHJKL]`, `<C-w>[nohjkltbpHJKL]`
- Cursor motions
- `h`, `j`, `k`, `l`, `w`, `b`, `^`, `0`, `$`,
`<C-[fb]>`, `<C-[du]>`, `<C-[ey]>`, `<C-[np]>`,
`G`, `gg`, `:{count}`, `:{count}goto`, `:{count}%`,
`H`, `M`, `L`, `zt`, `zz`, `z.`, `zb`, `z-`,
`<TAB>` (toggle focus between hex and text views)
- Mode operations
- `i`, `I`, `a`, `A`, `v`, `r`, `R`, `<ESC>`
- Inspect and edit
- `gb` (binary), `gd` (decimal), `x` (delete), `X` (delete backward),
`d` (delete selection), `y` (copy selection), `p`, `P` (paste),
`<` (left shift), `>` (right shift), `<C-a>` (increment), `<C-x>` (decrement)
- Undo and redo
- `:undo`, `u`, `:redo`, `<C-r>`
- Search
- `/`, `?`, `n`, `N`, `<C-c>` (abort)
## Bug Tracker
Report bug at [Issues・itchyny/bed - GitHub](https://b612.me/apps/b612/bed/issues).
## Author
itchyny (<https://github.com/itchyny>)
## License
This software is released under the MIT License, see LICENSE.

View File

@ -1,502 +0,0 @@
package buffer
import (
"errors"
"io"
"math"
"slices"
"sync"
)
// Buffer represents a buffer.
type Buffer struct {
rrs []readerRange
index int64
mu *sync.Mutex
bytes []byte
offset int64
}
type readAtSeeker interface {
io.ReaderAt
io.Seeker
}
type readerRange struct {
r readAtSeeker
min int64
max int64
diff int64
}
// NewBuffer creates a new buffer.
func NewBuffer(r readAtSeeker) *Buffer {
return &Buffer{
rrs: []readerRange{{r: r, min: 0, max: math.MaxInt64, diff: 0}},
index: 0,
mu: new(sync.Mutex),
}
}
// Read reads bytes.
func (b *Buffer) Read(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.read(p)
}
func (b *Buffer) read(p []byte) (i int, err error) {
index := b.index
for _, rr := range b.rrs {
if b.index < rr.min {
break
}
if b.index >= rr.max {
continue
}
m := int(min(int64(len(p)-i), rr.max-b.index))
var k int
if k, err = rr.r.ReadAt(p[i:i+m], b.index+rr.diff); err != nil && k == 0 {
break
}
err = nil
b.index += int64(m)
i += k
}
if len(b.bytes) > 0 {
j, k := max(b.offset-index, 0), max(index-b.offset, 0)
if j < int64(len(p)) && k < int64(len(b.bytes)) {
if cnt := copy(p[j:], b.bytes[k:]); i < int(j)+cnt {
i = int(j) + cnt
}
}
}
return
}
// Seek sets the offset.
func (b *Buffer) Seek(offset int64, whence int) (int64, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.seek(offset, whence)
}
func (b *Buffer) seek(offset int64, whence int) (int64, error) {
var index int64
switch whence {
case io.SeekStart:
index = offset
case io.SeekCurrent:
index = b.index + offset
case io.SeekEnd:
var l int64
var err error
if l, err = b.len(); err != nil {
return 0, err
}
index = l + offset
default:
return 0, errors.New("buffer.Buffer.Seek: invalid whence")
}
if index < 0 {
return 0, errors.New("buffer.Buffer.Seek: negative position")
}
b.index = index
return index, nil
}
// Len returns the total size of the buffer.
func (b *Buffer) Len() (int64, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.len()
}
func (b *Buffer) len() (int64, error) {
rr := b.rrs[len(b.rrs)-1]
l, err := rr.r.Seek(0, io.SeekEnd)
if err != nil {
return 0, err
}
return max(l-rr.diff, b.offset+int64(len(b.bytes))), nil
}
// ReadAt reads bytes at the specific offset.
func (b *Buffer) ReadAt(p []byte, offset int64) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
if _, err := b.seek(offset, io.SeekStart); err != nil {
return 0, err
}
return b.read(p)
}
// EditedIndices returns the indices of edited regions.
func (b *Buffer) EditedIndices() []int64 {
b.mu.Lock()
defer b.mu.Unlock()
eis := make([]int64, 0, len(b.rrs))
for _, rr := range b.rrs {
switch rr.r.(type) {
case *bytesReader, constReader:
// constReader can be adjacent to another bytesReader or constReader.
if l := len(eis); l > 0 && eis[l-1] == rr.min {
eis[l-1] = rr.max
continue
}
eis = append(eis, rr.min, rr.max)
}
}
if len(b.bytes) > 0 {
eis = insertInterval(eis, b.offset, b.offset+int64(len(b.bytes)))
}
return eis
}
func insertInterval(xs []int64, start, end int64) []int64 {
i, fi := slices.BinarySearch(xs, start)
j, fj := slices.BinarySearch(xs, end)
if i%2 == 0 {
if i == j && !fi && !fj {
return slices.Insert(xs, i, start, end)
}
xs[i] = start
i++
}
if j%2 == 0 {
if fj {
j++
} else {
j--
xs[j] = end
}
}
return slices.Delete(xs, i, j)
}
// Clone the buffer.
func (b *Buffer) Clone() *Buffer {
b.mu.Lock()
defer b.mu.Unlock()
newBuf := new(Buffer)
newBuf.rrs = make([]readerRange, len(b.rrs))
for i, rr := range b.rrs {
newBuf.rrs[i] = readerRange{rr.r, rr.min, rr.max, rr.diff}
}
newBuf.index = b.index
newBuf.mu = new(sync.Mutex)
newBuf.bytes = slices.Clone(b.bytes)
newBuf.offset = b.offset
return newBuf
}
// Copy a part of the buffer.
func (b *Buffer) Copy(start, end int64) *Buffer {
b.mu.Lock()
defer b.mu.Unlock()
b.flush()
newBuf := new(Buffer)
rrs := make([]readerRange, 0, len(b.rrs)+1)
index := start
for _, rr := range b.rrs {
if index < rr.min || index >= end {
break
}
if index >= rr.max {
continue
}
size := min(end-index, rr.max-index)
rrs = append(rrs, readerRange{rr.r, index - start, index - start + size, rr.diff + start})
index += size
}
newBuf.rrs = append(rrs, readerRange{newBytesReader(nil), index - start, math.MaxInt64, -index + start})
newBuf.cleanup()
newBuf.index = 0
newBuf.mu = new(sync.Mutex)
return newBuf
}
// Cut a part of the buffer.
func (b *Buffer) Cut(start, end int64) {
b.mu.Lock()
defer b.mu.Unlock()
b.flush()
rrs := make([]readerRange, 0, len(b.rrs)+1)
var index, max int64
for _, rr := range b.rrs {
if start >= rr.max {
rrs = append(rrs, rr)
index = rr.max
continue
}
if end <= rr.min {
if rr.max == math.MaxInt64 {
max = math.MaxInt64
} else {
max = rr.max - rr.min + index
}
rrs = append(rrs, readerRange{rr.r, index, max, rr.diff - index + rr.min})
index = max
continue
}
if start >= rr.min {
max = start
rrs = append(rrs, readerRange{rr.r, index, max, rr.diff})
index = max
}
if end < rr.max {
if rr.max == math.MaxInt64 {
max = math.MaxInt64
} else {
max = rr.max - end + index
}
rrs = append(rrs, readerRange{rr.r, index, max, rr.diff + end - index})
index = max
}
}
if index != math.MaxInt64 {
rrs = append(rrs, readerRange{newBytesReader(nil), index, math.MaxInt64, -index})
}
b.rrs = rrs
b.index = 0
b.cleanup()
}
// Paste a buffer into a buffer.
func (b *Buffer) Paste(offset int64, c *Buffer) {
b.mu.Lock()
c.mu.Lock()
defer b.mu.Unlock()
defer c.mu.Unlock()
b.flush()
rrs := make([]readerRange, 0, len(b.rrs)+len(c.rrs)+1)
var index, max int64
for _, rr := range b.rrs {
if offset >= rr.max {
rrs = append(rrs, rr)
continue
}
if offset < rr.min {
if rr.max == math.MaxInt64 {
max = math.MaxInt64
} else {
max = rr.max - rr.min + index
}
rrs = append(rrs, readerRange{rr.r, index, max, rr.diff - index + rr.min})
index = max
continue
}
rrs = append(rrs, readerRange{rr.r, rr.min, offset, rr.diff})
index = offset
for _, rr := range c.rrs {
if rr.max == math.MaxInt64 {
l, _ := rr.r.Seek(0, io.SeekEnd)
max = l + index
} else {
max = rr.max - rr.min + index
}
rrs = append(rrs, readerRange{rr.r, index, max, rr.diff - index + rr.min})
index = max
}
if rr.max == math.MaxInt64 {
max = math.MaxInt64
} else {
max = rr.max - offset + index
}
rrs = append(rrs, readerRange{rr.r, index, max, rr.diff - index + offset})
index = max
}
b.rrs = rrs
b.cleanup()
}
// Insert inserts a byte at the specific position.
func (b *Buffer) Insert(offset int64, c byte) {
b.mu.Lock()
defer b.mu.Unlock()
b.flush()
for i, rr := range b.rrs {
if offset > rr.max {
continue
}
var r *bytesReader
var ok bool
if rr.max != math.MaxInt64 {
if r, ok = rr.r.(*bytesReader); ok {
r = r.clone()
r.insert(offset+rr.diff, c)
b.rrs[i], i = readerRange{r, rr.min, rr.max + 1, rr.diff}, i+1
}
}
if !ok {
b.rrs = append(b.rrs, readerRange{}, readerRange{})
copy(b.rrs[i+2:], b.rrs[i:])
b.rrs[i], i = readerRange{rr.r, rr.min, offset, rr.diff}, i+1
b.rrs[i], i = readerRange{newBytesReader([]byte{c}), offset, offset + 1, -offset}, i+1
b.rrs[i].min = offset
}
for ; i < len(b.rrs); i++ {
b.rrs[i].min++
if b.rrs[i].max != math.MaxInt64 {
b.rrs[i].max++
}
b.rrs[i].diff--
}
b.cleanup()
return
}
panic("buffer.Buffer.Insert: unreachable")
}
// Replace replaces a byte at the specific position.
// This method does not overwrite the reader ranges,
// but just append the byte to the temporary byte slice
// in order to cancel the replacement with backspace key.
func (b *Buffer) Replace(offset int64, c byte) {
b.mu.Lock()
defer b.mu.Unlock()
if b.offset+int64(len(b.bytes)) != offset {
b.flush()
}
if len(b.bytes) == 0 {
b.offset = offset
}
b.bytes = append(b.bytes, c)
}
// UndoReplace removes the last byte of the replacing byte slice.
func (b *Buffer) UndoReplace(offset int64) {
b.mu.Lock()
defer b.mu.Unlock()
if len(b.bytes) > 0 && b.offset+int64(len(b.bytes))-1 == offset {
b.bytes = b.bytes[:len(b.bytes)-1]
}
}
// ReplaceIn replaces bytes within a specific range.
func (b *Buffer) ReplaceIn(start, end int64, c byte) {
b.mu.Lock()
defer b.mu.Unlock()
rrs := make([]readerRange, 0, len(b.rrs)+1)
for _, rr := range b.rrs {
if rr.max <= start || end <= rr.min {
rrs = append(rrs, rr)
continue
}
if start > rr.min {
rrs = append(rrs, readerRange{rr.r, rr.min, start, rr.diff})
}
if start >= rr.min {
rrs = append(rrs, readerRange{constReader(c), start, end, -start})
}
if end < rr.max {
rrs = append(rrs, readerRange{rr.r, end, rr.max, rr.diff})
}
}
b.rrs = rrs
b.cleanup()
}
// Flush temporary bytes.
func (b *Buffer) Flush() {
b.mu.Lock()
defer b.mu.Unlock()
b.flush()
}
func (b *Buffer) flush() {
if len(b.bytes) == 0 {
return
}
rrs := make([]readerRange, 0, len(b.rrs)+1)
end := b.offset + int64(len(b.bytes))
for _, rr := range b.rrs {
if b.offset >= rr.max || end <= rr.min {
rrs = append(rrs, rr)
continue
}
if b.offset >= rr.min {
if rr.min < b.offset {
rrs = append(rrs, readerRange{rr.r, rr.min, b.offset, rr.diff})
}
rrs = append(rrs, readerRange{newBytesReader(b.bytes), b.offset, end, -b.offset})
}
if rr.max == math.MaxInt64 {
l, _ := rr.r.Seek(0, io.SeekEnd)
if l-rr.diff <= end {
rrs = append(rrs, readerRange{newBytesReader(nil), end, math.MaxInt64, -end})
continue
}
}
if end < rr.max {
rrs = append(rrs, readerRange{rr.r, end, rr.max, rr.diff})
}
}
b.rrs = rrs
b.offset = 0
b.bytes = nil
b.cleanup()
}
// Delete deletes a byte at the specific position.
func (b *Buffer) Delete(offset int64) {
b.mu.Lock()
defer b.mu.Unlock()
b.flush()
for i, rr := range b.rrs {
if offset >= rr.max {
continue
}
if r, ok := rr.r.(*bytesReader); ok {
r = r.clone()
r.delete(offset + rr.diff)
b.rrs[i] = readerRange{r, rr.min, rr.max - 1, rr.diff}
} else {
b.rrs = append(b.rrs, readerRange{})
copy(b.rrs[i+1:], b.rrs[i:])
b.rrs[i] = readerRange{rr.r, rr.min, offset, rr.diff}
b.rrs[i+1] = readerRange{rr.r, offset + 1, rr.max, rr.diff}
}
for i++; i < len(b.rrs); i++ {
b.rrs[i].min--
if b.rrs[i].max != math.MaxInt64 {
b.rrs[i].max--
}
b.rrs[i].diff++
}
b.cleanup()
return
}
panic("buffer.Buffer.Delete: unreachable")
}
func (b *Buffer) cleanup() {
for i := 0; i < len(b.rrs); i++ {
if rr := b.rrs[i]; rr.min == rr.max {
b.rrs = slices.Delete(b.rrs, i, i+1)
}
}
for i := len(b.rrs) - 1; i > 0; i-- {
rr1, rr2 := b.rrs[i-1], b.rrs[i]
switch r1 := rr1.r.(type) {
case constReader:
if r1 == rr2.r {
b.rrs[i-1].max = rr2.max
b.rrs = slices.Delete(b.rrs, i, i+1)
}
case *bytesReader:
if r2, ok := rr2.r.(*bytesReader); ok {
bs := make([]byte, int(rr1.max-rr1.min)+len(r2.bs)-int(rr2.min+rr2.diff))
copy(bs, r1.bs[rr1.min+rr1.diff:rr1.max+rr1.diff])
copy(bs[rr1.max-rr1.min:], r2.bs[rr2.min+rr2.diff:])
b.rrs[i-1] = readerRange{newBytesReader(bs), rr1.min, rr2.max, -rr1.min}
b.rrs = slices.Delete(b.rrs, i, i+1)
}
default:
if r1 == rr2.r && rr1.diff == rr2.diff && rr1.max == rr2.min {
b.rrs[i-1].max = rr2.max
b.rrs = slices.Delete(b.rrs, i, i+1)
}
}
}
}

View File

@ -1,712 +0,0 @@
package buffer
import (
"io"
"math"
"reflect"
"slices"
"strings"
"testing"
)
func TestBufferEmpty(t *testing.T) {
b := NewBuffer(strings.NewReader(""))
p := make([]byte, 10)
n, err := b.Read(p)
if err != io.EOF {
t.Errorf("err should be EOF but got: %v", err)
}
if n != 0 {
t.Errorf("n should be 0 but got: %d", n)
}
l, err := b.Len()
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if l != 0 {
t.Errorf("l should be 0 but got: %d", l)
}
}
func TestBuffer(t *testing.T) {
b := NewBuffer(strings.NewReader("0123456789abcdef"))
p := make([]byte, 8)
n, err := b.Read(p)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if n != 8 {
t.Errorf("n should be 8 but got: %d", n)
}
if expected := "01234567"; string(p) != expected {
t.Errorf("p should be %q but got: %s", expected, string(p))
}
l, err := b.Len()
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if l != 16 {
t.Errorf("l should be 16 but got: %d", l)
}
_, err = b.Seek(4, io.SeekStart)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
n, err = b.Read(p)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if n != 8 {
t.Errorf("n should be 8 but got: %d", n)
}
if expected := "456789ab"; string(p) != expected {
t.Errorf("p should be %q but got: %s", expected, string(p))
}
_, err = b.Seek(-4, io.SeekCurrent)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
n, err = b.Read(p)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if n != 8 {
t.Errorf("n should be 8 but got: %d", n)
}
if expected := "89abcdef"; string(p) != expected {
t.Errorf("p should be %q but got: %s", expected, string(p))
}
_, err = b.Seek(-4, io.SeekEnd)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
n, err = b.Read(p)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if n != 4 {
t.Errorf("n should be 4 but got: %d", n)
}
if expected := "cdefcdef"; string(p) != expected {
t.Errorf("p should be %q but got: %s", expected, string(p))
}
n, err = b.ReadAt(p, 7)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if n != 8 {
t.Errorf("n should be 8 but got: %d", n)
}
if expected := "789abcde"; string(p) != expected {
t.Errorf("p should be %q but got: %s", expected, string(p))
}
n, err = b.ReadAt(p, -1)
if err == nil {
t.Errorf("err should not be nil but got: %v", err)
}
if n != 0 {
t.Errorf("n should be 0 but got: %d", n)
}
}
func TestBufferClone(t *testing.T) {
b0 := NewBuffer(strings.NewReader("0123456789abcdef"))
b1 := b0.Clone()
bufferEqual := func(b0 *Buffer, b1 *Buffer) bool {
if b0.index != b1.index || len(b0.rrs) != len(b1.rrs) {
return false
}
for i := range len(b0.rrs) {
if b0.rrs[i].min != b1.rrs[i].min || b0.rrs[i].max != b1.rrs[i].max ||
b0.rrs[i].diff != b1.rrs[i].diff {
return false
}
switch r0 := b0.rrs[i].r.(type) {
case *bytesReader:
switch r1 := b1.rrs[i].r.(type) {
case *bytesReader:
if !reflect.DeepEqual(r0.bs, r1.bs) || r0.index != r1.index {
t.Logf("buffer differs: %+v, %+v", r0, r1)
return false
}
default:
t.Logf("buffer differs: %+v, %+v", r0, r1)
return false
}
case *strings.Reader:
switch r1 := b1.rrs[i].r.(type) {
case *strings.Reader:
if r0 != r1 {
t.Logf("buffer differs: %+v, %+v", r0, r1)
return false
}
default:
t.Logf("buffer differs: %+v, %+v", r0, r1)
return false
}
default:
t.Logf("buffer differs: %+v, %+v", b0.rrs[i].r, b1.rrs[i].r)
return false
}
}
return true
}
if !bufferEqual(b1, b0) {
t.Errorf("Buffer#Clone should be %+v but got %+v", b0, b1)
}
b1.Insert(4, 0x40)
if bufferEqual(b1, b0) {
t.Errorf("Buffer should not be equal: %+v, %+v", b0, b1)
}
b2 := b1.Clone()
if !bufferEqual(b2, b1) {
t.Errorf("Buffer#Clone should be %+v but got %+v", b1, b2)
}
b2.Replace(4, 0x40)
b2.Flush()
if !bufferEqual(b2, b1) {
t.Errorf("Buffer should be equal: %+v, %+v", b1, b2)
}
b2.Replace(5, 0x40)
b2.Flush()
if bufferEqual(b2, b1) {
t.Errorf("Buffer should not be equal: %+v, %+v", b1, b2)
}
}
func TestBufferCopy(t *testing.T) {
b := NewBuffer(strings.NewReader("0123456789abcdef"))
b.Replace(3, 0x41)
b.Replace(4, 0x42)
b.Replace(5, 0x43)
b.Replace(9, 0x43)
b.Replace(10, 0x44)
b.Replace(11, 0x45)
b.Replace(12, 0x46)
b.Replace(14, 0x47)
testCases := []struct {
start, end int64
expected string
}{
{0, 16, "012ABC678CDEFdGf"},
{0, 15, "012ABC678CDEFdG"},
{1, 12, "12ABC678CDE"},
{4, 14, "BC678CDEFd"},
{2, 10, "2ABC678C"},
{4, 10, "BC678C"},
{2, 7, "2ABC6"},
{5, 10, "C678C"},
{7, 11, "78CD"},
{8, 10, "8C"},
{14, 20, "Gf"},
{9, 9, ""},
{10, 8, ""},
}
for _, testCase := range testCases {
got := b.Copy(testCase.start, testCase.end)
p := make([]byte, 17)
_, _ = got.Read(p)
if !strings.HasPrefix(string(p), testCase.expected+"\x00") {
t.Errorf("Copy(%d, %d) should clone %q but got %q",
testCase.start, testCase.end, testCase.expected, string(p))
}
got.Insert(0, 0x48)
got.Insert(int64(len(testCase.expected)+1), 0x49)
p = make([]byte, 19)
_, _ = got.ReadAt(p, 0)
if !strings.HasPrefix(string(p), "H"+testCase.expected+"I\x00") {
t.Errorf("Copy(%d, %d) should clone %q but got %q",
testCase.start, testCase.end, testCase.expected, string(p))
}
}
}
func TestBufferCut(t *testing.T) {
b := NewBuffer(strings.NewReader("0123456789abcdef"))
b.Replace(3, 0x41)
b.Replace(4, 0x42)
b.Replace(5, 0x43)
b.Replace(9, 0x43)
b.Replace(10, 0x44)
b.Replace(11, 0x45)
b.Replace(12, 0x46)
b.Replace(14, 0x47)
testCases := []struct {
start, end int64
expected string
}{
{0, 0, "012ABC678CDEFdGf"},
{0, 4, "BC678CDEFdGf"},
{0, 7, "78CDEFdGf"},
{0, 10, "DEFdGf"},
{0, 16, ""},
{0, 20, ""},
{3, 4, "012BC678CDEFdGf"},
{3, 6, "012678CDEFdGf"},
{3, 11, "012EFdGf"},
{6, 10, "012ABCDEFdGf"},
{6, 14, "012ABCGf"},
{6, 15, "012ABCf"},
{6, 17, "012ABC"},
{8, 10, "012ABC67DEFdGf"},
{8, 10, "012ABC67DEFdGf"},
{10, 8, "012ABC678CDEFdGf"},
}
for _, testCase := range testCases {
got := b.Clone()
got.Cut(testCase.start, testCase.end)
p := make([]byte, 17)
_, _ = got.Read(p)
if !strings.HasPrefix(string(p), testCase.expected+"\x00") {
t.Errorf("Cut(%d, %d) should result into %q but got %q",
testCase.start, testCase.end, testCase.expected, string(p))
}
got.Insert(0, 0x48)
got.Insert(int64(len(testCase.expected)+1), 0x49)
p = make([]byte, 19)
_, _ = got.ReadAt(p, 0)
if !strings.HasPrefix(string(p), "H"+testCase.expected+"I\x00") {
t.Errorf("Cut(%d, %d) should result into %q but got %q",
testCase.start, testCase.end, testCase.expected, string(p))
}
}
}
func TestBufferPaste(t *testing.T) {
b := NewBuffer(strings.NewReader("0123456789abcdef"))
c := b.Copy(3, 13)
b.Paste(5, c)
p := make([]byte, 100)
_, _ = b.ReadAt(p, 0)
expected := "012343456789abc56789abcdef"
if !strings.HasPrefix(string(p), expected+"\x00") {
t.Errorf("p should be %q but got: %q", expected, string(p))
}
c.Replace(5, 0x41)
c.Insert(6, 0x42)
c.Insert(7, 0x43)
b.Paste(10, c)
p = make([]byte, 100)
_, _ = b.ReadAt(p, 0)
expected = "012343456734567ABC9abc89abc56789abcdef"
if !strings.HasPrefix(string(p), expected+"\x00") {
t.Errorf("p should be %q but got: %q", expected, string(p))
}
b.Cut(11, 14)
b.Paste(13, c)
b.Replace(13, 0x44)
p = make([]byte, 100)
_, _ = b.ReadAt(p, 0)
expected = "012343456737AD4567ABC9abcBC9abc89abc56789abcdef"
if !strings.HasPrefix(string(p), expected+"\x00") {
t.Errorf("p should be %q but got: %q", expected, string(p))
}
b.Insert(14, 0x45)
p = make([]byte, 100)
_, _ = b.ReadAt(p, 0)
expected = "012343456737ADE4567ABC9abcBC9abc89abc56789abcdef"
if !strings.HasPrefix(string(p), expected+"\x00") {
t.Errorf("p should be %q but got: %q", expected, string(p))
}
}
func TestBufferInsert(t *testing.T) {
b := NewBuffer(strings.NewReader("0123456789abcdef"))
tests := []struct {
index int64
b byte
offset int64
expected string
len int64
}{
{0, 0x39, 0, "90123456", 17},
{0, 0x38, 0, "89012345", 18},
{4, 0x37, 0, "89017234", 19},
{8, 0x30, 3, "17234056", 20},
{9, 0x31, 3, "17234015", 21},
{9, 0x32, 4, "72340215", 22},
{23, 0x39, 19, "def9\x00\x00\x00\x00", 23},
{23, 0x38, 19, "def89\x00\x00\x00", 24},
}
for _, test := range tests {
b.Insert(test.index, test.b)
p := make([]byte, 8)
_, err := b.Seek(test.offset, io.SeekStart)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
n, err := b.Read(p)
if err != nil && err != io.EOF {
t.Errorf("err should be nil or io.EOF but got: %v", err)
}
if expected := len(strings.TrimRight(test.expected, "\x00")); n != expected {
t.Errorf("n should be %d but got: %d", expected, n)
}
if string(p) != test.expected {
t.Errorf("p should be %s but got: %s", test.expected, string(p))
}
l, err := b.Len()
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if l != test.len {
t.Errorf("l should be %d but got: %d", test.len, l)
}
}
eis := b.EditedIndices()
if expected := []int64{0, 2, 4, 5, 8, 11, 23, 25}; !reflect.DeepEqual(eis, expected) {
t.Errorf("edited indices should be %v but got: %v", expected, eis)
}
if len(b.rrs) != 8 {
t.Errorf("len(b.rrs) should be 8 but got: %d", len(b.rrs))
}
}
func TestBufferReplace(t *testing.T) {
b := NewBuffer(strings.NewReader("0123456789abcdef"))
tests := []struct {
index int64
b byte
offset int64
expected string
len int64
}{
{0, 0x39, 0, "91234567", 16},
{0, 0x38, 0, "81234567", 16},
{1, 0x37, 0, "87234567", 16},
{5, 0x30, 0, "87234067", 16},
{4, 0x31, 0, "87231067", 16},
{3, 0x30, 0, "87201067", 16},
{2, 0x31, 0, "87101067", 16},
{15, 0x30, 8, "89abcde0", 16},
{16, 0x31, 9, "9abcde01", 17},
{2, 0x39, 0, "87901067", 17},
{17, 0x32, 10, "abcde012", 18},
}
for _, test := range tests {
b.Replace(test.index, test.b)
p := make([]byte, 8)
_, err := b.Seek(test.offset, io.SeekStart)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
n, err := b.Read(p)
if err != nil && err != io.EOF {
t.Errorf("err should be nil or io.EOF but got: %v", err)
}
if n != 8 {
t.Errorf("n should be 8 but got: %d", n)
}
if string(p) != test.expected {
t.Errorf("p should be %s but got: %s", test.expected, string(p))
}
l, err := b.Len()
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if l != test.len {
t.Errorf("l should be %d but got: %d", test.len, l)
}
}
eis := b.EditedIndices()
if expected := []int64{0, 6, 15, math.MaxInt64}; !reflect.DeepEqual(eis, expected) {
t.Errorf("edited indices should be %v but got: %v", expected, eis)
}
if len(b.rrs) != 3 {
t.Errorf("len(b.rrs) should be 3 but got: %d", len(b.rrs))
}
{
b.Replace(3, 0x39)
b.Replace(4, 0x38)
b.Replace(5, 0x37)
b.Replace(6, 0x36)
b.Replace(7, 0x35)
p := make([]byte, 8)
if _, err := b.ReadAt(p, 2); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "99876589"; string(p) != expected {
t.Errorf("p should be %s but got: %s", expected, string(p))
}
b.UndoReplace(7)
b.UndoReplace(6)
p = make([]byte, 8)
if _, err := b.ReadAt(p, 2); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "99876789"; string(p) != expected {
t.Errorf("p should be %s but got: %s", expected, string(p))
}
b.UndoReplace(5)
b.UndoReplace(4)
b.Flush()
b.UndoReplace(3)
b.UndoReplace(2)
p = make([]byte, 8)
if _, err := b.ReadAt(p, 2); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "99106789"; string(p) != expected {
t.Errorf("p should be %s but got: %s", expected, string(p))
}
eis := b.EditedIndices()
if expected := []int64{0, 6, 15, math.MaxInt64}; !reflect.DeepEqual(eis, expected) {
t.Errorf("edited indices should be %v but got: %v", expected, eis)
}
}
{
b := NewBuffer(strings.NewReader("0123456789abcdef"))
b.Replace(16, 0x30)
b.Replace(10, 0x30)
p := make([]byte, 8)
if _, err := b.ReadAt(p, 9); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "90bcdef0"; string(p) != expected {
t.Errorf("p should be %s but got: %s", expected, string(p))
}
l, _ := b.Len()
if expected := int64(17); l != expected {
t.Errorf("l should be %d but got: %d", expected, l)
}
eis := b.EditedIndices()
if expected := []int64{10, 11, 16, math.MaxInt64}; !reflect.DeepEqual(eis, expected) {
t.Errorf("edited indices should be %v but got: %v", expected, eis)
}
}
}
func TestBufferReplaceIn(t *testing.T) {
b := NewBuffer(strings.NewReader("0123456789abcdef"))
tests := []struct {
start int64
end int64
b byte
offset int64
expected string
len int64
}{
{1, 2, 0x39, 0, "09234567", 16},
{0, 6, 0x38, 0, "88888867", 16},
{1, 3, 0x37, 0, "87788867", 16},
{5, 7, 0x30, 0, "87788007", 16},
{2, 6, 0x31, 0, "87111107", 16},
{3, 4, 0x30, 0, "87101107", 16},
{14, 15, 0x30, 8, "89abcd0f", 16},
{15, 16, 0x30, 8, "89abcd00", 16},
{1, 5, 0x39, 0, "89999107", 16},
}
for _, test := range tests {
b.ReplaceIn(test.start, test.end, test.b)
p := make([]byte, 8)
_, err := b.Seek(test.offset, io.SeekStart)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
n, err := b.Read(p)
if err != nil && err != io.EOF {
t.Errorf("err should be nil or io.EOF but got: %v", err)
}
if n != 8 {
t.Errorf("n should be 8 but got: %d", n)
}
if string(p) != test.expected {
t.Errorf("p should be %s but got: %s", test.expected, string(p))
}
l, err := b.Len()
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if l != test.len {
t.Errorf("l should be %d but got: %d", test.len, l)
}
}
eis := b.EditedIndices()
if expected := []int64{0, 7, 14, 16}; !reflect.DeepEqual(eis, expected) {
t.Errorf("edited indices should be %v but got: %v", expected, eis)
}
if expected := 7; len(b.rrs) != expected {
t.Errorf("len(b.rrs) should be %d but got: %d", expected, len(b.rrs))
}
{
b := NewBuffer(strings.NewReader("0123456789abcdef"))
b.ReplaceIn(16, 17, 0x30)
b.ReplaceIn(10, 11, 0x30)
p := make([]byte, 8)
if _, err := b.ReadAt(p, 9); err != io.EOF {
t.Errorf("err should be io.EOF but got: %v", err)
}
if expected := "90bcdef0"; string(p) != expected {
t.Errorf("p should be %s but got: %s", expected, string(p))
}
l, _ := b.Len()
if expected := int64(16); l != expected {
t.Errorf("l should be %d but got: %d", expected, l)
}
eis := b.EditedIndices()
if expected := []int64{10, 11, 16, 17}; !reflect.DeepEqual(eis, expected) {
t.Errorf("edited indices should be %v but got: %v", expected, eis)
}
}
}
func TestBufferDelete(t *testing.T) {
b := NewBuffer(strings.NewReader("0123456789abcdef"))
tests := []struct {
index int64
b byte
offset int64
expected string
len int64
}{
{4, 0x00, 0, "01235678", 15},
{3, 0x00, 0, "01256789", 14},
{6, 0x00, 0, "0125679a", 13},
{0, 0x00, 0, "125679ab", 12},
{4, 0x39, 0, "1256979a", 13},
{5, 0x38, 0, "12569879", 14},
{3, 0x00, 0, "1259879a", 13},
{4, 0x00, 0, "125979ab", 12},
{3, 0x00, 0, "12579abc", 11},
{8, 0x39, 4, "9abc9def", 12},
{8, 0x38, 4, "9abc89de", 13},
{8, 0x00, 4, "9abc9def", 12},
{8, 0x00, 4, "9abcdef\x00", 11},
}
for _, test := range tests {
if test.b == 0x00 {
b.Delete(test.index)
} else {
b.Insert(test.index, test.b)
}
p := make([]byte, 8)
_, err := b.Seek(test.offset, io.SeekStart)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
n, err := b.Read(p)
if err != nil && err != io.EOF {
t.Errorf("err should be nil or io.EOF but got: %v", err)
}
if expected := len(strings.TrimRight(test.expected, "\x00")); n != expected {
t.Errorf("n should be %d but got: %d", expected, n)
}
if string(p) != test.expected {
t.Errorf("p should be %s but got: %s", test.expected, string(p))
}
l, err := b.Len()
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if l != test.len {
t.Errorf("l should be %d but got: %d", test.len, l)
}
}
eis := b.EditedIndices()
if expected := []int64{}; !reflect.DeepEqual(eis, expected) {
t.Errorf("edited indices should be %v but got: %v", expected, eis)
}
if len(b.rrs) != 4 {
t.Errorf("len(b.rrs) should be 4 but got: %d", len(b.rrs))
}
}
func TestInsertInterval(t *testing.T) {
tests := []struct {
intervals []int64
newInterval []int64
expected []int64
}{
{[]int64{}, []int64{10, 20}, []int64{10, 20}},
{[]int64{10, 20}, []int64{0, 5}, []int64{0, 5, 10, 20}},
{[]int64{10, 20}, []int64{5, 15}, []int64{5, 20}},
{[]int64{10, 20}, []int64{15, 17}, []int64{10, 20}},
{[]int64{10, 20}, []int64{15, 25}, []int64{10, 25}},
{[]int64{10, 20}, []int64{25, 30}, []int64{10, 20, 25, 30}},
{[]int64{10, 20, 30, 40}, []int64{0, 5}, []int64{0, 5, 10, 20, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{5, 10}, []int64{5, 20, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{5, 15}, []int64{5, 20, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{5, 20}, []int64{5, 20, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{5, 25}, []int64{5, 25, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{5, 30}, []int64{5, 40}},
{[]int64{10, 20, 30, 40}, []int64{5, 45}, []int64{5, 45}},
{[]int64{10, 20, 30, 40}, []int64{10, 20}, []int64{10, 20, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{10, 30}, []int64{10, 40}},
{[]int64{10, 20, 30, 40}, []int64{15, 45}, []int64{10, 45}},
{[]int64{10, 20, 30, 40}, []int64{15, 25}, []int64{10, 25, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{15, 30}, []int64{10, 40}},
{[]int64{10, 20, 30, 40}, []int64{15, 35}, []int64{10, 40}},
{[]int64{10, 20, 30, 40}, []int64{20, 25}, []int64{10, 25, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{20, 30}, []int64{10, 40}},
{[]int64{10, 20, 30, 40}, []int64{25, 30}, []int64{10, 20, 25, 40}},
{[]int64{10, 20, 30, 40}, []int64{30, 30}, []int64{10, 20, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{35, 37}, []int64{10, 20, 30, 40}},
{[]int64{10, 20, 30, 40}, []int64{40, 50}, []int64{10, 20, 30, 50}},
{[]int64{10, 20, 30, 40, 50, 60, 70, 80}, []int64{45, 47}, []int64{10, 20, 30, 40, 45, 47, 50, 60, 70, 80}},
{[]int64{10, 20, 30, 40, 50, 60, 70, 80}, []int64{35, 65}, []int64{10, 20, 30, 65, 70, 80}},
{[]int64{10, 20, 30, 40, 50, 60, 70, 80}, []int64{25, 55}, []int64{10, 20, 25, 60, 70, 80}},
{[]int64{10, 20, 30, 40, 50, 60, 70, 80}, []int64{75, 90}, []int64{10, 20, 30, 40, 50, 60, 70, 90}},
{[]int64{10, 20, 30, 40, 50, 60, 70, 80}, []int64{0, 100}, []int64{0, 100}},
}
for _, test := range tests {
got := insertInterval(slices.Clone(test.intervals), test.newInterval[0], test.newInterval[1])
if !reflect.DeepEqual(got, test.expected) {
t.Errorf("insertInterval(%+v, %d, %d) should be %+v but got: %+v",
test.intervals, test.newInterval[0], test.newInterval[1], test.expected, got)
}
}
}

View File

@ -1,66 +0,0 @@
package buffer
import (
"errors"
"io"
"slices"
)
type bytesReader struct {
bs []byte
index int64
}
func newBytesReader(bs []byte) *bytesReader {
return &bytesReader{bs: bs, index: 0}
}
// Read implements the io.Reader interface.
func (r *bytesReader) Read(b []byte) (n int, err error) {
if r.index >= int64(len(r.bs)) {
return 0, io.EOF
}
n = copy(b, r.bs[r.index:])
r.index += int64(n)
return
}
// Seek implements the io.Seeker interface.
func (r *bytesReader) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
r.index = offset
case io.SeekCurrent:
r.index += offset
case io.SeekEnd:
r.index = int64(len(r.bs)) + offset
}
return r.index, nil
}
// ReadAt implements the io.ReaderAt interface.
func (r *bytesReader) ReadAt(b []byte, offset int64) (n int, err error) {
if offset < 0 {
return 0, errors.New("buffer.bytesReader.ReadAt: negative offset")
}
if offset >= int64(len(r.bs)) {
return 0, io.EOF
}
n = copy(b, r.bs[offset:])
if n < len(b) {
err = io.EOF
}
return
}
func (r *bytesReader) insert(offset int64, b byte) {
r.bs = slices.Insert(r.bs, int(offset), b)
}
func (r *bytesReader) delete(offset int64) {
r.bs = slices.Delete(r.bs, int(offset), int(offset+1))
}
func (r *bytesReader) clone() *bytesReader {
return newBytesReader(slices.Clone(r.bs))
}

View File

@ -1,21 +0,0 @@
package buffer
type constReader byte
// Read implements the io.Reader interface.
func (r constReader) Read(b []byte) (int, error) {
for i := range b {
b[i] = byte(r)
}
return len(b), nil
}
// Seek implements the io.Seeker interface.
func (constReader) Seek(int64, int) (int64, error) {
return 0, nil
}
// ReadAt implements the io.ReaderAt interface.
func (r constReader) ReadAt(b []byte, _ int64) (int, error) {
return r.Read(b)
}

View File

@ -1,75 +0,0 @@
package bed
import (
"fmt"
"os"
"runtime"
"github.com/spf13/cobra"
"golang.org/x/term"
"b612.me/apps/b612/bed/cmdline"
"b612.me/apps/b612/bed/editor"
"b612.me/apps/b612/bed/tui"
"b612.me/apps/b612/bed/window"
)
const (
name = "bed"
version = "0.2.8"
revision = "HEAD"
)
var Cmd = &cobra.Command{
Use: "bed [文件路径]",
Short: "基于 Go 开发的二进制文件编辑器",
Long: `二进制文件编辑器 bed - 支持直接编辑二进制文件的命令行工具
支持功能
- 十六进制查看/编辑
- 文件差异对比
- 多窗口操作
- 快速跳转地址`,
Version: fmt.Sprintf("%s (修订版本: %s/%s)", version, revision, runtime.Version()),
SilenceUsage: true,
SilenceErrors: true,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runEditor(args)
},
}
func init() {
Cmd.SetVersionTemplate(`{{printf "%s 版本信息:" .Name}}{{.Version}}` + "\n")
Cmd.Flags().BoolP("version", "v", false, "显示版本信息")
}
func runEditor(args []string) error {
editor := editor.NewEditor(
tui.NewTui(),
window.NewManager(),
cmdline.NewCmdline(),
)
if err := editor.Init(); err != nil {
return fmt.Errorf("编辑器初始化失败: %w", err)
}
switch {
case len(args) > 0 && args[0] != "-": // 处理文件参数
if err := editor.Open(args[0]); err != nil {
return fmt.Errorf("无法打开文件: %w", err)
}
case term.IsTerminal(int(os.Stdin.Fd())): // 交互模式
if err := editor.OpenEmpty(); err != nil {
return fmt.Errorf("创建空白文档失败: %w", err)
}
default: // 从标准输入读取
if err := editor.Read(os.Stdin); err != nil {
return fmt.Errorf("读取输入流失败: %w", err)
}
}
defer editor.Close()
return editor.Run()
}

View File

@ -1,244 +0,0 @@
package cmdline
import (
"slices"
"sync"
"unicode"
"b612.me/apps/b612/bed/event"
)
// Cmdline implements editor.Cmdline
type Cmdline struct {
cmdline []rune
cursor int
completor *completor
typ rune
historyIndex int
history []string
histories map[bool][]string
eventCh chan<- event.Event
cmdlineCh <-chan event.Event
redrawCh chan<- struct{}
mu *sync.Mutex
}
// NewCmdline creates a new Cmdline.
func NewCmdline() *Cmdline {
return &Cmdline{
completor: newCompletor(&filesystem{}, &environment{}),
histories: map[bool][]string{false: {}, true: {}},
mu: new(sync.Mutex),
}
}
// Init initializes the Cmdline.
func (c *Cmdline) Init(eventCh chan<- event.Event, cmdlineCh <-chan event.Event, redrawCh chan<- struct{}) {
c.eventCh, c.cmdlineCh, c.redrawCh = eventCh, cmdlineCh, redrawCh
}
// Run the cmdline.
func (c *Cmdline) Run() {
for e := range c.cmdlineCh {
c.mu.Lock()
switch e.Type {
case event.StartCmdlineCommand:
c.start(':', e.Arg)
case event.StartCmdlineSearchForward:
c.start('/', "")
case event.StartCmdlineSearchBackward:
c.start('?', "")
case event.ExitCmdline:
c.clear()
case event.CursorUp:
c.cursorUp()
case event.CursorDown:
c.cursorDown()
case event.CursorLeft:
c.cursorLeft()
case event.CursorRight:
c.cursorRight()
case event.CursorHead:
c.cursorHead()
case event.CursorEnd:
c.cursorEnd()
case event.BackspaceCmdline:
c.backspace()
case event.DeleteCmdline:
c.deleteRune()
case event.DeleteWordCmdline:
c.deleteWord()
case event.ClearToHeadCmdline:
c.clearToHead()
case event.ClearCmdline:
c.clear()
case event.Rune:
c.insert(e.Rune)
case event.CompleteForwardCmdline:
c.complete(true)
c.redrawCh <- struct{}{}
c.mu.Unlock()
continue
case event.CompleteBackCmdline:
c.complete(false)
c.redrawCh <- struct{}{}
c.mu.Unlock()
continue
case event.ExecuteCmdline:
if c.execute() {
c.mu.Unlock()
continue
}
default:
c.mu.Unlock()
continue
}
c.completor.clear()
c.mu.Unlock()
c.redrawCh <- struct{}{}
}
}
func (c *Cmdline) cursorUp() {
if c.historyIndex--; c.historyIndex >= 0 {
c.cmdline = []rune(c.history[c.historyIndex])
c.cursor = len(c.cmdline)
} else {
c.clear()
c.historyIndex = -1
}
}
func (c *Cmdline) cursorDown() {
if c.historyIndex++; c.historyIndex < len(c.history) {
c.cmdline = []rune(c.history[c.historyIndex])
c.cursor = len(c.cmdline)
} else {
c.clear()
c.historyIndex = len(c.history)
}
}
func (c *Cmdline) cursorLeft() {
c.cursor = max(0, c.cursor-1)
}
func (c *Cmdline) cursorRight() {
c.cursor = min(len(c.cmdline), c.cursor+1)
}
func (c *Cmdline) cursorHead() {
c.cursor = 0
}
func (c *Cmdline) cursorEnd() {
c.cursor = len(c.cmdline)
}
func (c *Cmdline) backspace() {
if c.cursor > 0 {
c.cmdline = slices.Delete(c.cmdline, c.cursor-1, c.cursor)
c.cursor--
return
}
if len(c.cmdline) == 0 {
c.eventCh <- event.Event{Type: event.ExitCmdline}
}
}
func (c *Cmdline) deleteRune() {
if c.cursor < len(c.cmdline) {
c.cmdline = slices.Delete(c.cmdline, c.cursor, c.cursor+1)
}
}
func (c *Cmdline) deleteWord() {
i := c.cursor
for i > 0 && unicode.IsSpace(c.cmdline[i-1]) {
i--
}
if i > 0 {
isk := isKeyword(c.cmdline[i-1])
for i > 0 && isKeyword(c.cmdline[i-1]) == isk && !unicode.IsSpace(c.cmdline[i-1]) {
i--
}
}
c.cmdline = slices.Delete(c.cmdline, i, c.cursor)
c.cursor = i
}
func isKeyword(c rune) bool {
return unicode.IsDigit(c) || unicode.IsLetter(c) || c == '_'
}
func (c *Cmdline) start(typ rune, arg string) {
c.typ = typ
c.cmdline = []rune(arg)
c.cursor = len(c.cmdline)
c.history = c.histories[typ == ':']
c.historyIndex = len(c.history)
}
func (c *Cmdline) clear() {
c.cmdline = []rune{}
c.cursor = 0
}
func (c *Cmdline) clearToHead() {
c.cmdline = slices.Delete(c.cmdline, 0, c.cursor)
c.cursor = 0
}
func (c *Cmdline) insert(ch rune) {
if unicode.IsPrint(ch) {
c.cmdline = slices.Insert(c.cmdline, c.cursor, ch)
c.cursor++
}
}
func (c *Cmdline) complete(forward bool) {
c.cmdline = []rune(c.completor.complete(string(c.cmdline), forward))
c.cursor = len(c.cmdline)
}
func (c *Cmdline) execute() (finish bool) {
defer c.saveHistory()
switch c.typ {
case ':':
cmd, r, bang, _, _, arg, err := parse(string(c.cmdline))
if err != nil {
c.eventCh <- event.Event{Type: event.Error, Error: err}
} else if cmd.name != "" {
c.eventCh <- event.Event{Type: cmd.eventType, Range: r, CmdName: cmd.name, Bang: bang, Arg: arg}
finish = cmd.eventType == event.QuitAll || cmd.eventType == event.QuitErr
}
case '/':
c.eventCh <- event.Event{Type: event.ExecuteSearch, Arg: string(c.cmdline), Rune: '/'}
case '?':
c.eventCh <- event.Event{Type: event.ExecuteSearch, Arg: string(c.cmdline), Rune: '?'}
default:
panic("cmdline.Cmdline.execute: unreachable")
}
return
}
func (c *Cmdline) saveHistory() {
cmdline := string(c.cmdline)
if cmdline == "" {
return
}
for i, h := range c.history {
if h == cmdline {
c.history = slices.Delete(c.history, i, i+1)
break
}
}
c.histories[c.typ == ':'] = append(c.history, cmdline)
}
// Get returns the current state of cmdline.
func (c *Cmdline) Get() ([]rune, int, []string, int) {
c.mu.Lock()
defer c.mu.Unlock()
return c.cmdline, c.cursor, c.completor.results, c.completor.index
}

View File

@ -1,832 +0,0 @@
package cmdline
import (
"reflect"
"runtime"
"strings"
"testing"
"b612.me/apps/b612/bed/event"
)
func TestNewCmdline(t *testing.T) {
c := NewCmdline()
cmdline, cursor, _, _ := c.Get()
if len(cmdline) != 0 {
t.Errorf("cmdline should be empty but got %v", cmdline)
}
if cursor != 0 {
t.Errorf("cursor should be 0 but got %v", cursor)
}
}
func TestCmdlineRun(t *testing.T) {
c := NewCmdline()
eventCh, cmdlineCh, redrawCh := make(chan event.Event), make(chan event.Event), make(chan struct{})
c.Init(eventCh, cmdlineCh, redrawCh)
go c.Run()
events := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.Rune, Rune: 't'},
{Type: event.Rune, Rune: 'e'},
{Type: event.CursorLeft},
{Type: event.CursorRight},
{Type: event.CursorHead},
{Type: event.CursorEnd},
{Type: event.BackspaceCmdline},
{Type: event.DeleteCmdline},
{Type: event.DeleteWordCmdline},
{Type: event.ClearToHeadCmdline},
{Type: event.ClearCmdline},
{Type: event.Rune, Rune: 't'},
{Type: event.Rune, Rune: 'e'},
{Type: event.ExecuteCmdline},
{Type: event.StartCmdlineCommand},
{Type: event.ExecuteCmdline},
}
go func() {
for _, e := range events {
cmdlineCh <- e
}
}()
for range len(events) - 3 {
<-redrawCh
}
e := <-eventCh
if e.Type != event.Error {
t.Errorf("cmdline should emit Error event but got %v", e)
}
cmdline, cursor, _, _ := c.Get()
if expected := "te"; string(cmdline) != expected {
t.Errorf("cmdline should be %q got %q", expected, string(cmdline))
}
if cursor != 2 {
t.Errorf("cursor should be 2 but got %v", cursor)
}
for range 3 {
<-redrawCh
}
cmdline, _, _, _ = c.Get()
if expected := ""; string(cmdline) != expected {
t.Errorf("cmdline should be %q got %q", expected, string(cmdline))
}
}
func TestCmdlineCursorMotion(t *testing.T) {
c := NewCmdline()
for _, ch := range "abcde" {
c.insert(ch)
}
cmdline, cursor, _, _ := c.Get()
if expected := "abcde"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 5 {
t.Errorf("cursor should be 5 but got %v", cursor)
}
c.cursorLeft()
_, cursor, _, _ = c.Get()
if cursor != 4 {
t.Errorf("cursor should be 4 but got %v", cursor)
}
for range 10 {
c.cursorLeft()
}
_, cursor, _, _ = c.Get()
if cursor != 0 {
t.Errorf("cursor should be 0 but got %v", cursor)
}
c.cursorRight()
_, cursor, _, _ = c.Get()
if cursor != 1 {
t.Errorf("cursor should be 1 but got %v", cursor)
}
for range 10 {
c.cursorRight()
}
_, cursor, _, _ = c.Get()
if cursor != 5 {
t.Errorf("cursor should be 5 but got %v", cursor)
}
c.cursorHead()
_, cursor, _, _ = c.Get()
if cursor != 0 {
t.Errorf("cursor should be 0 but got %v", cursor)
}
c.cursorEnd()
_, cursor, _, _ = c.Get()
if cursor != 5 {
t.Errorf("cursor should be 5 but got %v", cursor)
}
}
func TestCmdlineCursorBackspaceDelete(t *testing.T) {
c := NewCmdline()
for _, ch := range "abcde" {
c.insert(ch)
}
cmdline, cursor, _, _ := c.Get()
if expected := "abcde"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 5 {
t.Errorf("cursor should be 5 but got %v", cursor)
}
c.cursorLeft()
c.backspace()
cmdline, cursor, _, _ = c.Get()
if expected := "abce"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 3 {
t.Errorf("cursor should be 3 but got %v", cursor)
}
c.deleteRune()
cmdline, cursor, _, _ = c.Get()
if expected := "abc"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 3 {
t.Errorf("cursor should be 3 but got %v", cursor)
}
c.deleteRune()
cmdline, cursor, _, _ = c.Get()
if expected := "abc"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 3 {
t.Errorf("cursor should be 3 but got %v", cursor)
}
c.cursorLeft()
c.cursorLeft()
c.backspace()
c.backspace()
cmdline, cursor, _, _ = c.Get()
if expected := "bc"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 0 {
t.Errorf("cursor should be 0 but got %v", cursor)
}
}
func TestCmdlineCursorDeleteWord(t *testing.T) {
c := NewCmdline()
for _, ch := range "abcde" {
c.insert(ch)
}
c.cursorLeft()
c.cursorLeft()
c.deleteWord()
cmdline, cursor, _, _ := c.Get()
if expected := "de"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 0 {
t.Errorf("cursor should be 0 but got %v", cursor)
}
for _, ch := range "x0z!123 " {
c.insert(ch)
}
c.cursorLeft()
c.deleteWord()
cmdline, cursor, _, _ = c.Get()
if expected := "x0z! de"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 4 {
t.Errorf("cursor should be 4 but got %v", cursor)
}
c.deleteWord()
cmdline, cursor, _, _ = c.Get()
if expected := "x0z de"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 3 {
t.Errorf("cursor should be 3 but got %v", cursor)
}
}
func TestCmdlineCursorClear(t *testing.T) {
c := NewCmdline()
for _, ch := range "abcde" {
c.insert(ch)
}
cmdline, cursor, _, _ := c.Get()
if expected := "abcde"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 5 {
t.Errorf("cursor should be 5 but got %v", cursor)
}
c.cursorLeft()
c.clear()
cmdline, cursor, _, _ = c.Get()
if expected := ""; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 0 {
t.Errorf("cursor should be 0 but got %v", cursor)
}
}
func TestCmdlineCursorClearToHead(t *testing.T) {
c := NewCmdline()
for _, ch := range "abcde" {
c.insert(ch)
}
cmdline, cursor, _, _ := c.Get()
if expected := "abcde"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 5 {
t.Errorf("cursor should be 5 but got %v", cursor)
}
c.cursorLeft()
c.cursorLeft()
c.clearToHead()
cmdline, cursor, _, _ = c.Get()
if expected := "de"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 0 {
t.Errorf("cursor should be 0 but got %v", cursor)
}
}
func TestCmdlineCursorInsert(t *testing.T) {
c := NewCmdline()
for _, ch := range "abcde" {
c.insert(ch)
}
c.cursorLeft()
c.cursorLeft()
c.backspace()
c.insert('x')
c.insert('y')
cmdline, cursor, _, _ := c.Get()
if expected := "abxyde"; string(cmdline) != expected {
t.Errorf("cmdline should be %q but got %q", expected, string(cmdline))
}
if cursor != 4 {
t.Errorf("cursor should be 4 but got %v", cursor)
}
}
func TestCmdlineQuit(t *testing.T) {
c := NewCmdline()
ch := make(chan event.Event, 1)
c.Init(ch, make(chan event.Event), make(chan struct{}))
for _, cmd := range []struct {
cmd string
name string
}{
{"exi", "exi[t]"},
{"quit", "q[uit]"},
{"q", "q[uit]"},
} {
c.clear()
c.cmdline = []rune(cmd.cmd)
c.typ = ':'
c.execute()
e := <-ch
if e.CmdName != cmd.name {
t.Errorf("cmdline should report command name %q but got %q", cmd.name, e.CmdName)
}
if e.Type != event.Quit {
t.Errorf("cmdline should emit quit event with %q", cmd.cmd)
}
if e.Bang {
t.Errorf("cmdline should emit quit event without bang")
}
}
}
func TestCmdlineForceQuit(t *testing.T) {
c := NewCmdline()
ch := make(chan event.Event, 1)
c.Init(ch, make(chan event.Event), make(chan struct{}))
for _, cmd := range []struct {
cmd string
name string
}{
{"exit!", "exi[t]"},
{"q!", "q[uit]"},
{"quit!", "q[uit]"},
} {
c.clear()
c.cmdline = []rune(cmd.cmd)
c.typ = ':'
c.execute()
e := <-ch
if e.CmdName != cmd.name {
t.Errorf("cmdline should report command name %q but got %q", cmd.name, e.CmdName)
}
if e.Type != event.Quit {
t.Errorf("cmdline should emit quit event with %q", cmd.cmd)
}
if !e.Bang {
t.Errorf("cmdline should emit quit event with bang")
}
}
}
func TestCmdlineExecuteQuitAll(t *testing.T) {
c := NewCmdline()
ch := make(chan event.Event, 1)
c.Init(ch, make(chan event.Event), make(chan struct{}))
for _, cmd := range []struct {
cmd string
name string
}{
{"qall", "qa[ll]"},
{"qa", "qa[ll]"},
} {
c.clear()
c.cmdline = []rune(cmd.cmd)
c.typ = ':'
c.execute()
e := <-ch
if e.CmdName != cmd.name {
t.Errorf("cmdline should report command name %q but got %q", cmd.name, e.CmdName)
}
if e.Type != event.QuitAll {
t.Errorf("cmdline should emit QuitAll event with %q", cmd.cmd)
}
}
}
func TestCmdlineExecuteQuitErr(t *testing.T) {
c := NewCmdline()
ch := make(chan event.Event, 1)
c.Init(ch, make(chan event.Event), make(chan struct{}))
for _, cmd := range []struct {
cmd string
name string
}{
{"cquit", "cq[uit]"},
{"cq", "cq[uit]"},
} {
c.clear()
c.cmdline = []rune(cmd.cmd)
c.typ = ':'
c.execute()
e := <-ch
if e.CmdName != cmd.name {
t.Errorf("cmdline should report command name %q but got %q", cmd.name, e.CmdName)
}
if e.Type != event.QuitErr {
t.Errorf("cmdline should emit QuitErr event with %q", cmd.cmd)
}
}
}
func TestCmdlineExecuteWrite(t *testing.T) {
c := NewCmdline()
ch := make(chan event.Event, 1)
c.Init(ch, make(chan event.Event), make(chan struct{}))
for _, cmd := range []struct {
cmd string
name string
}{
{"w", "w[rite]"},
{" : : write sample.txt", "w[rite]"},
{"'<,'>write sample.txt", "w[rite]"},
} {
c.clear()
c.cmdline = []rune(cmd.cmd)
c.typ = ':'
c.execute()
e := <-ch
if e.CmdName != cmd.name {
t.Errorf("cmdline should report command name %q but got %q", cmd.name, e.CmdName)
}
if e.Type != event.Write {
t.Errorf("cmdline should emit Write event with %q", cmd.cmd)
}
}
}
func TestCmdlineExecuteWriteQuit(t *testing.T) {
c := NewCmdline()
ch := make(chan event.Event, 1)
c.Init(ch, make(chan event.Event), make(chan struct{}))
for _, cmd := range []struct {
cmd string
name string
}{
{"wq", "wq"},
{"x", "x[it]"},
{"xit", "x[it]"},
{"xa", "xa[ll]"},
{"xall", "xa[ll]"},
} {
c.clear()
c.cmdline = []rune(cmd.cmd)
c.typ = ':'
c.execute()
e := <-ch
if e.CmdName != cmd.name {
t.Errorf("cmdline should report command name %q but got %q", cmd.name, e.CmdName)
}
if e.Type != event.WriteQuit {
t.Errorf("cmdline should emit WriteQuit event with %q", cmd.cmd)
}
}
}
func TestCmdlineExecuteGoto(t *testing.T) {
c := NewCmdline()
ch := make(chan event.Event, 1)
c.Init(ch, make(chan event.Event), make(chan struct{}))
for _, cmd := range []struct {
cmd string
pos event.Position
typ event.Type
}{
{" : : $ ", event.End{}, event.CursorGoto},
{" :123456789 ", event.Absolute{Offset: 123456789}, event.CursorGoto},
{" +16777216 ", event.Relative{Offset: 16777216}, event.CursorGoto},
{" -256 ", event.Relative{Offset: -256}, event.CursorGoto},
{" : 0x123456789abcdef ", event.Absolute{Offset: 0x123456789abcdef}, event.CursorGoto},
{" 0xfedcba ", event.Absolute{Offset: 0xfedcba}, event.CursorGoto},
{" +0x44ef ", event.Relative{Offset: 0x44ef}, event.CursorGoto},
{" -0xff ", event.Relative{Offset: -0xff}, event.CursorGoto},
{"10go", event.Absolute{Offset: 10}, event.CursorGoto},
{"+10 got", event.Relative{Offset: 10}, event.CursorGoto},
{"$-10 goto", event.End{Offset: -10}, event.CursorGoto},
{"10%", event.Absolute{Offset: 10}, event.CursorGoto},
{"+10%", event.Relative{Offset: 10}, event.CursorGoto},
{"$-10%", event.End{Offset: -10}, event.CursorGoto},
} {
c.clear()
c.cmdline = []rune(cmd.cmd)
c.typ = ':'
c.execute()
e := <-ch
expected := "goto"
if strings.HasSuffix(cmd.cmd, "%") {
expected = "%"
} else if strings.Contains(cmd.cmd, "go") {
expected = "go[to]"
}
if e.CmdName != expected {
t.Errorf("cmdline should report command name %q but got %q", expected, e.CmdName)
}
if !reflect.DeepEqual(e.Range.From, cmd.pos) {
t.Errorf("cmdline should report command with position %#v but got %#v", cmd.pos, e.Range.From)
}
if e.Type != cmd.typ {
t.Errorf("cmdline should emit %d but got %d with %q", cmd.typ, e.Type, cmd.cmd)
}
}
}
func TestCmdlineComplete(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
c := NewCmdline()
c.completor = newCompletor(&mockFilesystem{}, nil)
eventCh, cmdlineCh, redrawCh := make(chan event.Event), make(chan event.Event), make(chan struct{})
c.Init(eventCh, cmdlineCh, redrawCh)
waitCh := make(chan struct{})
go c.Run()
go func() {
cmdlineCh <- event.Event{Type: event.StartCmdlineCommand}
cmdlineCh <- event.Event{Type: event.Rune, Rune: 'e'}
cmdlineCh <- event.Event{Type: event.Rune, Rune: ' '}
cmdlineCh <- event.Event{Type: event.Rune, Rune: '/'}
cmdlineCh <- event.Event{Type: event.CompleteForwardCmdline}
<-waitCh
cmdlineCh <- event.Event{Type: event.CompleteForwardCmdline}
<-waitCh
cmdlineCh <- event.Event{Type: event.CompleteBackCmdline}
<-waitCh
cmdlineCh <- event.Event{Type: event.CursorEnd}
cmdlineCh <- event.Event{Type: event.CompleteForwardCmdline}
cmdlineCh <- event.Event{Type: event.CompleteForwardCmdline}
<-waitCh
cmdlineCh <- event.Event{Type: event.ExecuteCmdline}
}()
for range 5 {
<-redrawCh
}
cmdline, cursor, _, _ := c.Get()
if expected := "e /bin/"; string(cmdline) != expected {
t.Errorf("cmdline should be %q got %q", expected, string(cmdline))
}
if cursor != 7 {
t.Errorf("cursor should be 7 but got %v", cursor)
}
waitCh <- struct{}{}
<-redrawCh
cmdline, cursor, _, _ = c.Get()
if expected := "e /tmp/"; string(cmdline) != expected {
t.Errorf("cmdline should be %q got %q", expected, string(cmdline))
}
if cursor != 7 {
t.Errorf("cursor should be 7 but got %v", cursor)
}
waitCh <- struct{}{}
<-redrawCh
cmdline, cursor, _, _ = c.Get()
if expected := "e /bin/"; string(cmdline) != expected {
t.Errorf("cmdline should be %q got %q", expected, string(cmdline))
}
if cursor != 7 {
t.Errorf("cursor should be 7 but got %v", cursor)
}
waitCh <- struct{}{}
<-redrawCh
<-redrawCh
<-redrawCh
cmdline, cursor, _, _ = c.Get()
if expected := "e /bin/echo"; string(cmdline) != expected {
t.Errorf("cmdline should be %q got %q", expected, string(cmdline))
}
if cursor != 11 {
t.Errorf("cursor should be 11 but got %v", cursor)
}
waitCh <- struct{}{}
go func() { <-redrawCh }()
e := <-eventCh
cmdline, cursor, _, _ = c.Get()
if expected := "e /bin/echo"; string(cmdline) != expected {
t.Errorf("cmdline should be %q got %q", expected, string(cmdline))
}
if cursor != 11 {
t.Errorf("cursor should be 11 but got %v", cursor)
}
if e.Type != event.Edit {
t.Errorf("cmdline should emit Edit event but got %v", e)
}
if expected := "/bin/echo"; e.Arg != expected {
t.Errorf("cmdline should emit event with arg %q but got %v", expected, e)
}
}
func TestCmdlineSearch(t *testing.T) {
c := NewCmdline()
eventCh, cmdlineCh, redrawCh := make(chan event.Event), make(chan event.Event), make(chan struct{})
waitCh := make(chan struct{})
c.Init(eventCh, cmdlineCh, redrawCh)
defer func() {
close(eventCh)
close(cmdlineCh)
close(redrawCh)
}()
go c.Run()
events1 := []event.Event{
{Type: event.StartCmdlineSearchForward},
{Type: event.Rune, Rune: 't'},
{Type: event.Rune, Rune: 't'},
{Type: event.CursorLeft},
{Type: event.Rune, Rune: 'e'},
{Type: event.Rune, Rune: 's'},
{Type: event.ExecuteCmdline},
}
events2 := []event.Event{
{Type: event.StartCmdlineSearchBackward},
{Type: event.Rune, Rune: 'x'},
{Type: event.Rune, Rune: 'y'},
{Type: event.Rune, Rune: 'z'},
{Type: event.ExecuteCmdline},
}
go func() {
for _, e := range events1 {
cmdlineCh <- e
}
<-waitCh
for _, e := range events2 {
cmdlineCh <- e
}
}()
for range len(events1) - 1 {
<-redrawCh
}
e := <-eventCh
<-redrawCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "test"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
if e.Rune != '/' {
t.Errorf("cmdline should emit search event with Rune %q but got %q", '/', e.Rune)
}
waitCh <- struct{}{}
for range len(events2) - 1 {
<-redrawCh
}
e = <-eventCh
<-redrawCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "xyz"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
if e.Rune != '?' {
t.Errorf("cmdline should emit search event with Rune %q but got %q", '?', e.Rune)
}
}
func TestCmdlineHistory(t *testing.T) {
c := NewCmdline()
eventCh, cmdlineCh, redrawCh := make(chan event.Event), make(chan event.Event), make(chan struct{})
c.Init(eventCh, cmdlineCh, redrawCh)
go c.Run()
events0 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.Rune, Rune: 'n'},
{Type: event.Rune, Rune: 'e'},
{Type: event.Rune, Rune: 'w'},
{Type: event.ExecuteCmdline},
}
events1 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.Rune, Rune: 'v'},
{Type: event.Rune, Rune: 'n'},
{Type: event.Rune, Rune: 'e'},
{Type: event.Rune, Rune: 'w'},
{Type: event.ExecuteCmdline},
}
events2 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
events3 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.CursorUp},
{Type: event.CursorUp},
{Type: event.CursorUp},
{Type: event.CursorDown},
{Type: event.ExecuteCmdline},
}
events4 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
events5 := []event.Event{
{Type: event.StartCmdlineSearchForward},
{Type: event.Rune, Rune: 't'},
{Type: event.Rune, Rune: 'e'},
{Type: event.Rune, Rune: 's'},
{Type: event.Rune, Rune: 't'},
{Type: event.ExecuteCmdline},
}
events6 := []event.Event{
{Type: event.StartCmdlineSearchForward},
{Type: event.CursorUp},
{Type: event.CursorDown},
{Type: event.Rune, Rune: 'n'},
{Type: event.Rune, Rune: 'e'},
{Type: event.Rune, Rune: 'w'},
{Type: event.ExecuteCmdline},
}
events7 := []event.Event{
{Type: event.StartCmdlineSearchBackward},
{Type: event.CursorUp},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
events8 := []event.Event{
{Type: event.StartCmdlineCommand},
{Type: event.CursorUp},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
events9 := []event.Event{
{Type: event.StartCmdlineSearchForward},
{Type: event.CursorUp},
{Type: event.ExecuteCmdline},
}
go func() {
for _, events := range [][]event.Event{
events0, events1, events2, events3, events4,
events5, events6, events7, events8, events9,
} {
for _, e := range events {
cmdlineCh <- e
}
}
}()
for range len(events0) - 1 {
<-redrawCh
}
e := <-eventCh
if e.Type != event.New {
t.Errorf("cmdline should emit New event but got %v", e)
}
for range len(events1) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.Vnew {
t.Errorf("cmdline should emit Vnew event but got %v", e)
}
for range len(events2) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.Vnew {
t.Errorf("cmdline should emit Vnew event but got %v", e)
}
for range len(events3) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.New {
t.Errorf("cmdline should emit New event but got %v", e.Type)
}
for range len(events4) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.New {
t.Errorf("cmdline should emit New event but got %v", e.Type)
}
for range len(events5) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "test"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
for range len(events6) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "new"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
for range len(events7) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "test"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
for range len(events8) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.Vnew {
t.Errorf("cmdline should emit Vnew event but got %v", e.Type)
}
for range len(events9) {
<-redrawCh
}
e = <-eventCh
if e.Type != event.ExecuteSearch {
t.Errorf("cmdline should emit ExecuteSearch event but got %v", e)
}
if expected := "test"; e.Arg != expected {
t.Errorf("cmdline should emit search event with Arg %q but got %q", expected, e.Arg)
}
<-redrawCh
}

View File

@ -1,57 +0,0 @@
package cmdline
import "b612.me/apps/b612/bed/event"
type command struct {
name string
fullname string
eventType event.Type
rangeType rangeType
}
type rangeType int
const (
rangeEmpty rangeType = 1 << iota
rangeCount
rangeBoth
)
func (rt rangeType) allows(r *event.Range) bool {
switch {
case r == nil:
return rt&rangeEmpty != 0
case r.To == nil:
return rt&rangeCount != 0
default:
return rt&rangeBoth != 0
}
}
var commands = []command{
{"e[dit]", "edit", event.Edit, rangeEmpty},
{"ene[w]", "enew", event.Enew, rangeEmpty},
{"new", "new", event.New, rangeEmpty},
{"vne[w]", "vnew", event.Vnew, rangeEmpty},
{"on[ly]", "only", event.Only, rangeEmpty},
{"winc[md]", "wincmd", event.Wincmd, rangeEmpty},
{"go[to]", "goto", event.CursorGoto, rangeCount},
{"%", "%", event.CursorGoto, rangeCount},
{"u[ndo]", "undo", event.Undo, rangeEmpty},
{"red[o]", "redo", event.Redo, rangeEmpty},
{"pw[d]", "pwd", event.Pwd, rangeEmpty},
{"cd", "cd", event.Chdir, rangeEmpty},
{"chd[ir]", "chdir", event.Chdir, rangeEmpty},
{"exi[t]", "exit", event.Quit, rangeEmpty},
{"q[uit]", "quit", event.Quit, rangeEmpty},
{"qa[ll]", "qall", event.QuitAll, rangeEmpty},
{"quita[ll]", "quitall", event.QuitAll, rangeEmpty},
{"cq[uit]", "cquit", event.QuitErr, rangeEmpty},
{"w[rite]", "write", event.Write, rangeEmpty | rangeBoth},
{"wq", "wq", event.WriteQuit, rangeEmpty | rangeBoth},
{"x[it]", "xit", event.WriteQuit, rangeEmpty | rangeBoth},
{"xa[ll]", "xall", event.WriteQuit, rangeEmpty | rangeBoth},
}

View File

@ -1,266 +0,0 @@
package cmdline
import (
"os"
"path/filepath"
"slices"
"strings"
"unicode"
"unicode/utf8"
"b612.me/apps/b612/bed/event"
)
type completor struct {
fs fs
env env
command bool
target string
arg string
results []string
index int
}
func newCompletor(fs fs, env env) *completor {
return &completor{fs: fs, env: env}
}
func (c *completor) complete(cmdline string, forward bool) string {
cmd, r, _, name, prefix, arg, _ := parse(cmdline)
if name == "" || c.command ||
!hasSuffixFunc(prefix, unicode.IsSpace) && cmd.fullname != name {
cmdline = c.completeCommand(cmdline, name, prefix, r, forward)
if c.results != nil {
return cmdline
}
prefix = cmdline
}
switch cmd.eventType {
case event.Edit, event.New, event.Vnew, event.Write, event.WriteQuit:
return c.completeFilepath(cmdline, prefix, arg, forward, false)
case event.Chdir:
return c.completeFilepath(cmdline, prefix, arg, forward, true)
case event.Wincmd:
return c.completeWincmd(cmdline, prefix, arg, forward)
default:
return cmdline
}
}
func (c *completor) completeNext(prefix string, forward bool) string {
if len(c.results) == 0 {
return c.target
}
if forward {
c.index = (c.index+2)%(len(c.results)+1) - 1
} else {
c.index = (c.index+len(c.results)+1)%(len(c.results)+1) - 1
}
if c.index < 0 {
return c.target
}
if len(c.results) == 1 {
defer c.clear()
}
return prefix + c.arg + c.results[c.index]
}
func (c *completor) completeCommand(
cmdline, name, prefix string, r *event.Range, forward bool,
) string {
prefix = prefix[:len(prefix)-len(name)]
if c.results == nil {
c.command, c.target, c.index = true, cmdline, -1
c.arg, c.results = "", listCommandNames(name, r)
}
return c.completeNext(prefix, forward)
}
func listCommandNames(name string, r *event.Range) []string {
var targets []string
for _, cmd := range commands {
if strings.HasPrefix(cmd.fullname, name) && cmd.rangeType.allows(r) {
targets = append(targets, cmd.fullname)
}
}
slices.Sort(targets)
return targets
}
func (c *completor) completeFilepath(
cmdline, prefix, arg string, forward, dirOnly bool,
) string {
if !hasSuffixFunc(prefix, unicode.IsSpace) {
prefix += " "
}
if c.results == nil {
c.command, c.target, c.index = false, cmdline, -1
c.arg, c.results = c.listFileNames(arg, dirOnly)
}
return c.completeNext(prefix, forward)
}
const separator = string(filepath.Separator)
func (c *completor) listFileNames(arg string, dirOnly bool) (string, []string) {
var targets []string
path, simplify := c.expandPath(arg)
if strings.HasPrefix(arg, "$") && !strings.Contains(arg, separator) {
base := strings.ToLower(arg[1:])
for _, env := range c.env.List() {
name, value, ok := strings.Cut(env, "=")
if !ok {
continue
}
if !strings.HasPrefix(strings.ToLower(name), base) {
continue
}
if !filepath.IsAbs(value) {
continue
}
fi, err := c.fs.Stat(value)
if err != nil {
continue
}
if fi.IsDir() {
name += separator
} else if dirOnly {
continue
}
targets = append(targets, "$"+name)
}
slices.Sort(targets)
return "", targets
}
if arg != "" && !strings.HasSuffix(arg, separator) &&
(!strings.HasSuffix(arg, ".") || strings.HasSuffix(arg, "..")) {
if stat, err := c.fs.Stat(path); err == nil && stat.IsDir() {
return "", []string{arg + separator}
}
}
if strings.HasSuffix(arg, separator) || strings.HasSuffix(arg, separator+".") {
path += separator
}
dir, base := filepath.Dir(path), strings.ToLower(filepath.Base(path))
if arg == "" {
base = ""
} else if strings.HasSuffix(path, separator) {
if strings.HasSuffix(arg, separator+".") {
base = "."
} else {
base = ""
}
}
f, err := c.fs.Open(dir)
if err != nil {
return arg, nil
}
defer f.Close()
fileInfos, err := f.Readdir(1024)
if err != nil {
return arg, nil
}
for _, fileInfo := range fileInfos {
name := fileInfo.Name()
if !strings.HasPrefix(strings.ToLower(name), base) {
continue
}
isDir := fileInfo.IsDir()
if !isDir && fileInfo.Mode()&os.ModeSymlink != 0 {
fileInfo, err := c.fs.Stat(filepath.Join(dir, name))
if err != nil {
continue
}
isDir = fileInfo.IsDir()
}
if isDir {
name += separator
} else if dirOnly {
continue
}
targets = append(targets, name)
}
slices.SortFunc(targets, func(p, q string) int {
ps, pd := p[len(p)-1] == filepath.Separator, p[0] == '.'
qs, qd := q[len(q)-1] == filepath.Separator, q[0] == '.'
switch {
case ps && !qs:
return 1
case !ps && qs:
return -1
case pd && !qd:
return 1
case !pd && qd:
return -1
default:
return strings.Compare(p, q)
}
})
if simplify != nil {
arg = simplify(dir) + separator
} else if !strings.HasPrefix(arg, "."+separator) && dir == "." {
arg = ""
} else if arg = dir; !strings.HasSuffix(arg, separator) {
arg += separator
}
return arg, targets
}
func (c *completor) expandPath(path string) (string, func(string) string) {
switch {
case strings.HasPrefix(path, "~"):
if name, rest, _ := strings.Cut(path[1:], separator); name != "" {
user, err := c.fs.GetUser(name)
if err != nil {
return path, nil
}
return filepath.Join(user.HomeDir, rest), func(path string) string {
return filepath.Join("~"+user.Username, strings.TrimPrefix(path, user.HomeDir))
}
}
homedir, err := c.fs.UserHomeDir()
if err != nil {
return path, nil
}
return filepath.Join(homedir, path[1:]), func(path string) string {
return filepath.Join("~", strings.TrimPrefix(path, homedir))
}
case strings.HasPrefix(path, "$"):
name, rest, _ := strings.Cut(path[1:], separator)
value := strings.TrimRight(c.env.Get(name), separator)
if value == "" {
return path, nil
}
return filepath.Join(value, rest), func(path string) string {
return filepath.Join("$"+name, strings.TrimPrefix(path, value))
}
default:
return path, nil
}
}
func (c *completor) completeWincmd(
cmdline, prefix, arg string, forward bool,
) string {
if !hasSuffixFunc(prefix, unicode.IsSpace) {
prefix += " "
}
if c.results == nil {
if arg != "" {
return cmdline
}
c.command, c.target, c.arg, c.index = false, cmdline, "", -1
c.results = strings.Split("nohjkltbpHJKL", "")
}
return c.completeNext(prefix, forward)
}
func (c *completor) clear() {
c.command, c.target, c.arg = false, "", ""
c.results, c.index = nil, 0
}
func hasSuffixFunc(s string, f func(rune) bool) bool {
r, size := utf8.DecodeLastRuneInString(s)
return size > 0 && f(r)
}

View File

@ -1,566 +0,0 @@
package cmdline
import (
"path/filepath"
"runtime"
"slices"
"testing"
)
func TestCompletorCompleteCommand(t *testing.T) {
c := newCompletor(nil, nil)
cmdline := c.complete("", true)
if expected := "cd"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
if expected := "edit"; !slices.Contains(c.results, expected) {
t.Errorf("completion results should contain %q but got %v", expected, c.results)
}
if expected := "goto"; slices.Contains(c.results, expected) {
t.Errorf("completion results should not contain %q but got %v", expected, c.results)
}
if expected := "write"; !slices.Contains(c.results, expected) {
t.Errorf("completion results should contain %q but got %v", expected, c.results)
}
for range 3 {
cmdline = c.complete(cmdline, true)
}
if expected := "edit"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
for range 4 {
cmdline = c.complete(cmdline, false)
}
if expected := ""; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
for range 3 {
cmdline = c.complete(cmdline, false)
}
if expected := "write"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
c.clear()
cmdline = c.complete(": :\t", true)
if expected := ": :\tcd"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
c.clear()
cmdline = c.complete(": : cq", true)
if expected := ": : cquit"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
c.clear()
cmdline = c.complete("e", false)
if expected := "exit"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
cmdline = c.complete(cmdline, true)
if expected := "e"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
cmdline = c.complete(cmdline, true)
if expected := "edit"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
cmdline = c.complete(cmdline, false)
if expected := "e"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
c.clear()
cmdline = "p"
for _, expected := range []string{"pwd", "pwd"} {
cmdline = c.complete(cmdline, true)
if cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
}
c.clear()
cmdline = "10"
for _, command := range []string{"%", "goto", ""} {
cmdline = c.complete(cmdline, true)
if expected := "10" + command; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
}
c.clear()
cmdline = "10,20"
for _, command := range []string{"wq", "write", "xall", "xit", ""} {
cmdline = c.complete(cmdline, true)
if expected := "10,20" + command; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
}
c.clear()
cmdline = c.complete("not", true)
if expected := "not"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if len(c.results) != 0 {
t.Errorf("completion results should be empty but got %v", c.results)
}
}
func TestCompletorCompleteFilepath(t *testing.T) {
c := newCompletor(&mockFilesystem{}, nil)
cmdline := c.complete("new", true)
if expected := "new CHANGELOG.md"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "new"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if expected := "README.md"; !slices.Contains(c.results, expected) {
t.Errorf("completion results should contain %q but got %v", expected, c.results)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
for range 3 {
cmdline = c.complete(cmdline, true)
}
if expected := "new .gitignore"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "new"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 3 {
t.Errorf("completion index should be %d but got %d", 3, c.index)
}
for range 4 {
cmdline = c.complete(cmdline, true)
}
if expected := "new editor" + string(filepath.Separator); cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "new"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 7 {
t.Errorf("completion index should be %d but got %d", 7, c.index)
}
cmdline = c.complete(cmdline, true)
if expected := "new"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != -1 {
t.Errorf("completion index should be %d but got %d", -1, c.index)
}
cmdline = c.complete(cmdline, true)
if expected := "new CHANGELOG.md"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
cmdline = c.complete(cmdline, false)
if expected := "new"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != -1 {
t.Errorf("completion index should be %d but got %d", -1, c.index)
}
for range 3 {
cmdline = c.complete(cmdline, true)
}
if expected := "new README.md"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 2 {
t.Errorf("completion index should be %d but got %d", 2, c.index)
}
c.clear()
cmdline = c.complete("w change", true)
if expected := "w CHANGELOG.md"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := ""; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
c.clear()
cmdline = c.complete("wq .", true)
if expected := "wq .gitignore"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := ""; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
c.clear()
cmdline = c.complete("new not", true)
if expected := "new not"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "new not"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != -1 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
c.clear()
cmdline = c.complete("edit", true)
if expected := "edit CHANGELOG.md"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "edit"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
}
func TestCompletorCompleteFilepathLeadingDot(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
c := newCompletor(&mockFilesystem{}, nil)
cmdline := c.complete("edit .", true)
if expected := "edit .gitignore"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := ""; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
c.clear()
cmdline = c.complete("edit ./r", true)
if expected := "edit ./README.md"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := ""; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
c.clear()
cmdline = c.complete("cd ..", true)
if expected := "cd ../"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := ""; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
}
func TestCompletorCompleteFilepathKeepPrefix(t *testing.T) {
c := newCompletor(&mockFilesystem{}, nil)
cmdline := c.complete(" : : : new \tB", true)
if expected := " : : : new \tbuffer" + string(filepath.Separator); cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := " : : : new \tB"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
cmdline = c.complete(cmdline, true)
if expected := " : : : new \tbuild" + string(filepath.Separator); cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 1 {
t.Errorf("completion index should be %d but got %d", 1, c.index)
}
for range 2 {
cmdline = c.complete(cmdline, false)
}
if expected := " : : : new \tB"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != -1 {
t.Errorf("completion index should be %d but got %d", -1, c.index)
}
c.clear()
cmdline = c.complete(" : cd\u3000", true)
if expected := " : cd\u3000buffer" + string(filepath.Separator); cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
}
func TestCompletorCompleteFilepathHomedir(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
c := newCompletor(&mockFilesystem{}, nil)
cmdline := c.complete("vnew ~/", true)
if expected := "vnew ~/example.txt"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "vnew ~/"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
cmdline = c.complete(cmdline, true)
if expected := "vnew ~/.vimrc"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 1 {
t.Errorf("completion index should be %d but got %d", 1, c.index)
}
for range 3 {
cmdline = c.complete(cmdline, true)
}
if expected := "vnew ~/Library/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 4 {
t.Errorf("completion index should be %d but got %d", 4, c.index)
}
for range 2 {
cmdline = c.complete(cmdline, true)
}
if expected := "vnew ~/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != -1 {
t.Errorf("completion index should be %d but got %d", -1, c.index)
}
c.clear()
cmdline = c.complete("cd ~user/", true)
if expected := "cd ~user/Documents/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "cd ~user/"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
}
func TestCompletorCompleteFilepathHomedirDot(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
c := newCompletor(&mockFilesystem{}, nil)
cmdline := c.complete("vnew ~/.", false)
if expected := "vnew ~/.zshrc"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "vnew ~/."; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 1 {
t.Errorf("completion index should be %d but got %d", 1, c.index)
}
cmdline = c.complete(cmdline, true)
if expected := "vnew ~/."; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != -1 {
t.Errorf("completion index should be %d but got %d", -1, c.index)
}
}
func TestCompletorCompleteFilepathEnviron(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
c := newCompletor(&mockFilesystem{}, &mockEnvironment{})
cmdline := c.complete("e $h", true)
if expected := "e $HOME/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
c.clear()
cmdline = c.complete("e $HOME/", true)
if expected := "e $HOME/example.txt"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "e $HOME/"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
c.clear()
cmdline = c.complete("cd $h", true)
if expected := "cd $HOME/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
c.clear()
cmdline = c.complete("cd $HOME/", true)
if expected := "cd $HOME/Documents/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
}
func TestCompletorCompleteFilepathRoot(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
c := newCompletor(&mockFilesystem{}, nil)
cmdline := c.complete("e /", true)
if expected := "e /bin/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "e /"; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
cmdline = c.complete(cmdline, true)
if expected := "e /tmp/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 1 {
t.Errorf("completion index should be %d but got %d", 1, c.index)
}
cmdline = c.complete(cmdline, false)
c.clear()
cmdline = c.complete(cmdline, true)
if expected := "e /bin/cp"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
}
func TestCompletorCompleteFilepathChdir(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
c := newCompletor(&mockFilesystem{}, nil)
cmdline := c.complete("cd ", false)
if expected := "cd editor/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if expected := "cd "; c.target != expected {
t.Errorf("completion target should be %q but got %q", expected, c.target)
}
if c.index != 3 {
t.Errorf("completion index should be %d but got %d", 3, c.index)
}
c.clear()
cmdline = c.complete("cd ~/", false)
if expected := "cd ~/Pictures/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 2 {
t.Errorf("completion index should be %d but got %d", 2, c.index)
}
c.clear()
cmdline = c.complete("cd /", true)
if expected := "cd /bin/"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
}
func TestCompletorCompleteWincmd(t *testing.T) {
c := newCompletor(&mockFilesystem{}, nil)
cmdline := c.complete("winc", true)
if expected := "wincmd n"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
for range 7 {
cmdline = c.complete(cmdline, true)
}
if expected := "wincmd b"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 7 {
t.Errorf("completion index should be %d but got %d", 7, c.index)
}
for range 7 {
cmdline = c.complete(cmdline, true)
}
if expected := "wincmd n"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
cmdline = c.complete(cmdline, false)
if expected := "wincmd"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != -1 {
t.Errorf("completion index should be %d but got %d", -1, c.index)
}
c.clear()
cmdline = c.complete("winc j", true)
if expected := "winc j"; cmdline != expected {
t.Errorf("cmdline should be %q but got %q", expected, cmdline)
}
if c.index != 0 {
t.Errorf("completion index should be %d but got %d", 0, c.index)
}
}

View File

@ -1,18 +0,0 @@
package cmdline
import "os"
type env interface {
Get(string) string
List() []string
}
type environment struct{}
func (*environment) Get(key string) string {
return os.Getenv(key)
}
func (*environment) List() []string {
return os.Environ()
}

View File

@ -1,14 +0,0 @@
package cmdline
type mockEnvironment struct{}
func (*mockEnvironment) Get(key string) string {
if key == "HOME" {
return mockHomeDir
}
return ""
}
func (*mockEnvironment) List() []string {
return []string{"HOME=" + mockHomeDir}
}

View File

@ -1,36 +0,0 @@
package cmdline
import (
"os"
"os/user"
)
type fs interface {
Open(string) (file, error)
Stat(string) (os.FileInfo, error)
GetUser(string) (*user.User, error)
UserHomeDir() (string, error)
}
type file interface {
Close() error
Readdir(int) ([]os.FileInfo, error)
}
type filesystem struct{}
func (*filesystem) Open(path string) (file, error) {
return os.Open(path)
}
func (*filesystem) Stat(path string) (os.FileInfo, error) {
return os.Stat(path)
}
func (*filesystem) GetUser(name string) (*user.User, error) {
return user.Lookup(name)
}
func (*filesystem) UserHomeDir() (string, error) {
return os.UserHomeDir()
}

View File

@ -1,118 +0,0 @@
package cmdline
import (
"os"
"os/user"
"time"
)
const mockHomeDir = "/home/user"
type mockFilesystem struct{}
func (*mockFilesystem) Open(path string) (file, error) {
return &mockFile{path}, nil
}
func (*mockFilesystem) Stat(path string) (os.FileInfo, error) {
return &mockFileInfo{
name: path,
isDir: path == mockHomeDir || path == "..",
}, nil
}
func (*mockFilesystem) GetUser(name string) (*user.User, error) {
return &user.User{Username: name, HomeDir: mockHomeDir}, nil
}
func (*mockFilesystem) UserHomeDir() (string, error) {
return mockHomeDir, nil
}
type mockFile struct {
path string
}
func (*mockFile) Close() error {
return nil
}
func createFileInfoList(infos []*mockFileInfo) []os.FileInfo {
fileInfos := make([]os.FileInfo, len(infos))
for i, info := range infos {
fileInfos[i] = info
}
return fileInfos
}
func (f *mockFile) Readdir(_ int) ([]os.FileInfo, error) {
if f.path == "." {
return createFileInfoList([]*mockFileInfo{
{"CHANGELOG.md", false},
{"README.md", false},
{"Makefile", false},
{".gitignore", false},
{"editor", true},
{"cmdline", true},
{"buffer", true},
{"build", true},
}), nil
}
if f.path == mockHomeDir {
return createFileInfoList([]*mockFileInfo{
{"Documents", true},
{"Pictures", true},
{"Library", true},
{".vimrc", false},
{".zshrc", false},
{"example.txt", false},
}), nil
}
if f.path == "/" {
return createFileInfoList([]*mockFileInfo{
{"bin", true},
{"tmp", true},
{"var", true},
{"usr", true},
}), nil
}
if f.path == "/bin" {
return createFileInfoList([]*mockFileInfo{
{"cp", false},
{"echo", false},
{"rm", false},
{"ls", false},
{"kill", false},
}), nil
}
return nil, nil
}
type mockFileInfo struct {
name string
isDir bool
}
func (fi *mockFileInfo) Name() string {
return fi.name
}
func (fi *mockFileInfo) IsDir() bool {
return fi.isDir
}
func (*mockFileInfo) Size() int64 {
return 0
}
func (*mockFileInfo) Mode() os.FileMode {
return os.FileMode(0x1ed)
}
func (*mockFileInfo) ModTime() time.Time {
return time.Time{}
}
func (*mockFileInfo) Sys() any {
return nil
}

View File

@ -1,55 +0,0 @@
package cmdline
import (
"errors"
"strings"
"unicode"
"b612.me/apps/b612/bed/event"
)
func parse(src string) (cmd command, r *event.Range,
bang bool, name, prefix, arg string, err error) {
prefix, arg = cutPrefixFunc(src, func(r rune) bool {
return unicode.IsSpace(r) || r == ':'
})
if arg == "" {
return
}
r, arg = event.ParseRange(arg)
name, arg = cutPrefixFunc(arg, func(r rune) bool {
return !unicode.IsSpace(r)
})
name, bang = strings.CutSuffix(name, "!")
prefix = src[:len(src)-len(arg)]
if name == "" {
// To jump by byte offset, name should not be "go[to]".
cmd = command{name: "goto", eventType: event.CursorGoto}
return
}
for _, cmd = range commands {
if matchCommand(cmd.name, name) {
arg = strings.TrimLeftFunc(arg, unicode.IsSpace)
prefix = src[:len(src)-len(arg)]
return
}
}
cmd, err = command{}, errors.New("unknown command: "+name)
return
}
func cutPrefixFunc(src string, f func(rune) bool) (string, string) {
for i, r := range src {
if !f(r) {
return src[:i], src[i:]
}
}
return src, ""
}
func matchCommand(cmd, name string) bool {
prefix, rest, _ := strings.Cut(cmd, "[")
abbr, _, _ := strings.Cut(rest, "]")
return strings.HasPrefix(name, prefix) &&
strings.HasPrefix(abbr, name[len(prefix):])
}

View File

@ -1,10 +0,0 @@
package editor
import "b612.me/apps/b612/bed/event"
// Cmdline defines the required cmdline interface for the editor.
type Cmdline interface {
Init(chan<- event.Event, <-chan event.Event, chan<- struct{})
Run()
Get() ([]rune, int, []string, int)
}

View File

@ -1,345 +0,0 @@
package editor
import (
"errors"
"fmt"
"io"
"strconv"
"strings"
"sync"
"b612.me/apps/b612/bed/buffer"
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/mode"
"b612.me/apps/b612/bed/state"
)
// Editor is the main struct for this command.
type Editor struct {
ui UI
wm Manager
cmdline Cmdline
mode mode.Mode
prevMode mode.Mode
searchTarget string
searchMode rune
prevEventType event.Type
buffer *buffer.Buffer
err error
errtyp int
cmdEventCh chan event.Event
wmEventCh chan event.Event
uiEventCh chan event.Event
redrawCh chan struct{}
cmdlineCh chan event.Event
quitCh chan struct{}
mu *sync.Mutex
}
// NewEditor creates a new editor.
func NewEditor(ui UI, wm Manager, cmdline Cmdline) *Editor {
return &Editor{
ui: ui,
wm: wm,
cmdline: cmdline,
mode: mode.Normal,
prevMode: mode.Normal,
}
}
// Init initializes the editor.
func (e *Editor) Init() error {
e.cmdEventCh = make(chan event.Event)
e.wmEventCh = make(chan event.Event)
e.uiEventCh = make(chan event.Event)
e.redrawCh = make(chan struct{})
e.cmdlineCh = make(chan event.Event)
e.cmdline.Init(e.cmdEventCh, e.cmdlineCh, e.redrawCh)
e.quitCh = make(chan struct{})
e.wm.Init(e.wmEventCh, e.redrawCh)
e.mu = new(sync.Mutex)
return nil
}
func (e *Editor) listen() error {
var wg sync.WaitGroup
errCh := make(chan error, 1)
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-e.redrawCh:
_ = e.redraw()
case <-e.quitCh:
return
}
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case ev := <-e.wmEventCh:
if redraw, finish, err := e.emit(ev); redraw {
e.redrawCh <- struct{}{}
} else if finish {
close(e.quitCh)
errCh <- err
}
case <-e.quitCh:
return
}
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case ev := <-e.cmdEventCh:
if redraw, finish, err := e.emit(ev); redraw {
e.redrawCh <- struct{}{}
} else if finish {
close(e.quitCh)
errCh <- err
}
case ev := <-e.uiEventCh:
if redraw, finish, err := e.emit(ev); redraw {
e.redrawCh <- struct{}{}
} else if finish {
close(e.quitCh)
errCh <- err
}
case <-e.quitCh:
return
}
}
}()
wg.Wait()
select {
case err := <-errCh:
return err
default:
return nil
}
}
type quitErr struct {
code int
}
func (err *quitErr) Error() string {
return "exit with " + strconv.Itoa(err.code)
}
func (err *quitErr) ExitCode() int {
return err.code
}
func (e *Editor) emit(ev event.Event) (redraw, finish bool, err error) {
e.mu.Lock()
if ev.Type != event.Redraw {
e.prevEventType = ev.Type
}
switch ev.Type {
case event.QuitAll:
if ev.Arg != "" {
e.err, e.errtyp = errors.New("too many arguments for "+ev.CmdName), state.MessageError
redraw = true
} else {
finish = true
}
case event.QuitErr:
args := strings.Fields(ev.Arg)
if len(args) > 1 {
e.err, e.errtyp = errors.New("too many arguments for "+ev.CmdName), state.MessageError
redraw = true
} else if len(args) > 0 {
n, er := strconv.Atoi(args[0])
if er != nil {
e.err, e.errtyp = fmt.Errorf("invalid argument for %s: %w", ev.CmdName, er), state.MessageError
redraw = true
} else {
err = &quitErr{n}
finish = true
}
} else {
err = &quitErr{1}
finish = true
}
case event.Suspend:
e.mu.Unlock()
if err := suspend(e); err != nil {
e.mu.Lock()
e.err, e.errtyp = err, state.MessageError
e.mu.Unlock()
}
redraw = true
return
case event.Info:
e.err, e.errtyp = ev.Error, state.MessageInfo
redraw = true
case event.Error:
e.err, e.errtyp = ev.Error, state.MessageError
redraw = true
case event.Redraw:
width, height := e.ui.Size()
e.wm.Resize(width, height-1)
redraw = true
case event.Copied:
e.mode, e.prevMode = mode.Normal, e.mode
if ev.Buffer != nil {
e.buffer = ev.Buffer
if l, err := e.buffer.Len(); err != nil {
e.err, e.errtyp = err, state.MessageError
} else {
e.err, e.errtyp = fmt.Errorf("%[1]d (0x%[1]x) bytes %[2]s", l, ev.Arg), state.MessageInfo
}
}
redraw = true
case event.Pasted:
e.err, e.errtyp = fmt.Errorf("%[1]d (0x%[1]x) bytes pasted", ev.Count), state.MessageInfo
redraw = true
default:
switch ev.Type {
case event.StartInsert, event.StartInsertHead, event.StartAppend, event.StartAppendEnd:
e.mode, e.prevMode = mode.Insert, e.mode
case event.StartReplaceByte, event.StartReplace:
e.mode, e.prevMode = mode.Replace, e.mode
case event.ExitInsert:
e.mode, e.prevMode = mode.Normal, e.mode
case event.StartVisual:
e.mode, e.prevMode = mode.Visual, e.mode
case event.ExitVisual:
e.mode, e.prevMode = mode.Normal, e.mode
case event.StartCmdlineCommand:
if e.mode == mode.Visual {
ev.Arg = "'<,'>"
} else if ev.Count > 0 {
ev.Arg = ".,.+" + strconv.FormatInt(ev.Count-1, 10)
}
e.mode, e.prevMode = mode.Cmdline, e.mode
e.err = nil
case event.StartCmdlineSearchForward:
e.mode, e.prevMode = mode.Search, e.mode
e.err = nil
e.searchMode = '/'
case event.StartCmdlineSearchBackward:
e.mode, e.prevMode = mode.Search, e.mode
e.err = nil
e.searchMode = '?'
case event.ExitCmdline:
e.mode, e.prevMode = mode.Normal, e.mode
case event.ExecuteCmdline:
m := mode.Normal
if e.mode == mode.Search {
m = e.prevMode
}
e.mode, e.prevMode = m, e.mode
case event.ExecuteSearch:
e.searchTarget, e.searchMode = ev.Arg, ev.Rune
case event.NextSearch:
ev.Arg, ev.Rune, e.err = e.searchTarget, e.searchMode, nil
case event.PreviousSearch:
ev.Arg, ev.Rune, e.err = e.searchTarget, e.searchMode, nil
case event.Paste, event.PastePrev:
if e.buffer == nil {
e.mu.Unlock()
return
}
ev.Buffer = e.buffer
}
if e.mode == mode.Cmdline || e.mode == mode.Search ||
ev.Type == event.ExitCmdline || ev.Type == event.ExecuteCmdline {
e.mu.Unlock()
e.cmdlineCh <- ev
} else {
if event.ScrollUp <= ev.Type && ev.Type <= event.SwitchFocus {
e.prevMode, e.err = e.mode, nil
}
ev.Mode = e.mode
width, height := e.ui.Size()
e.wm.Resize(width, height-1)
e.mu.Unlock()
e.wm.Emit(ev)
}
return
}
e.mu.Unlock()
return
}
// Open opens a new file.
func (e *Editor) Open(name string) error {
return e.wm.Open(name)
}
// OpenEmpty creates a new window.
func (e *Editor) OpenEmpty() error {
return e.wm.Open("")
}
// Read [io.Reader] and creates a new window.
func (e *Editor) Read(r io.Reader) error {
return e.wm.Read(r)
}
// Run the editor.
func (e *Editor) Run() error {
if err := e.ui.Init(e.uiEventCh); err != nil {
return err
}
if err := e.redraw(); err != nil {
return err
}
go e.ui.Run(defaultKeyManagers())
go e.cmdline.Run()
return e.listen()
}
func (e *Editor) redraw() (err error) {
e.mu.Lock()
defer e.mu.Unlock()
var s state.State
var windowIndex int
s.WindowStates, s.Layout, windowIndex, err = e.wm.State()
if err != nil {
return err
}
if s.WindowStates[windowIndex] == nil {
return errors.New("index out of windows")
}
s.WindowStates[windowIndex].Mode = e.mode
s.Mode, s.PrevMode, s.Error, s.ErrorType = e.mode, e.prevMode, e.err, e.errtyp
if s.Mode != mode.Visual && s.PrevMode != mode.Visual {
for _, ws := range s.WindowStates {
ws.VisualStart = -1
}
}
s.Cmdline, s.CmdlineCursor, s.CompletionResults, s.CompletionIndex = e.cmdline.Get()
if e.mode == mode.Search || e.prevEventType == event.ExecuteSearch {
s.SearchMode = e.searchMode
} else if e.prevEventType == event.NextSearch {
s.SearchMode, s.Cmdline = e.searchMode, []rune(e.searchTarget)
} else if e.prevEventType == event.PreviousSearch {
if e.searchMode == '/' {
s.SearchMode, s.Cmdline = '?', []rune(e.searchTarget)
} else {
s.SearchMode, s.Cmdline = '/', []rune(e.searchTarget)
}
}
return e.ui.Redraw(s)
}
// Close terminates the editor.
func (e *Editor) Close() error {
close(e.cmdEventCh)
close(e.wmEventCh)
close(e.uiEventCh)
close(e.redrawCh)
close(e.cmdlineCh)
e.wm.Close()
return e.ui.Close()
}

View File

@ -1,883 +0,0 @@
package editor
import (
"fmt"
"os"
"reflect"
"runtime"
"strings"
"testing"
"b612.me/apps/b612/bed/cmdline"
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/key"
"b612.me/apps/b612/bed/mode"
"b612.me/apps/b612/bed/state"
"b612.me/apps/b612/bed/window"
)
type testUI struct {
eventCh chan<- event.Event
initCh chan struct{}
redrawCh chan struct{}
}
func newTestUI() *testUI {
return &testUI{
initCh: make(chan struct{}),
redrawCh: make(chan struct{}),
}
}
func (ui *testUI) Init(eventCh chan<- event.Event) error {
ui.eventCh = eventCh
go func() { defer close(ui.initCh); <-ui.redrawCh }()
return nil
}
func (*testUI) Run(map[mode.Mode]*key.Manager) {}
func (*testUI) Size() (int, int) { return 90, 20 }
func (ui *testUI) Redraw(state.State) error {
ui.redrawCh <- struct{}{}
return nil
}
func (*testUI) Close() error { return nil }
func (ui *testUI) Emit(e event.Event) {
<-ui.initCh
ui.eventCh <- e
switch e.Type {
case event.ExecuteCmdline, event.NextSearch, event.PreviousSearch:
<-ui.redrawCh
}
<-ui.redrawCh
}
func createTemp(dir, str string) (*os.File, error) {
f, err := os.CreateTemp(dir, "")
if err != nil {
return nil, err
}
if str != "" {
if _, err = f.WriteString(str); err != nil {
return nil, err
}
}
if err = f.Close(); err != nil {
return nil, err
}
if str == "" {
if err = os.Remove(f.Name()); err != nil {
return nil, err
}
}
return f, nil
}
func TestEditorOpenEmptyWriteQuit(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.OpenEmpty(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.Increment, Count: 13})
ui.Emit(event.Event{Type: event.Decrement, Count: 6})
ui.Emit(event.Event{Type: event.Write, Arg: f.Name()})
ui.Emit(event.Event{Type: event.Quit, Bang: true})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "1 (0x1) bytes written"; editor.err == nil ||
!strings.HasSuffix(editor.err.Error(), expected) {
t.Errorf("err should end with %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name())
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "\x07"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorOpenWriteQuit(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.StartInsert})
ui.Emit(event.Event{Type: event.Rune, Rune: '4'})
ui.Emit(event.Event{Type: event.Rune, Rune: '8'})
ui.Emit(event.Event{Type: event.Rune, Rune: '0'})
ui.Emit(event.Event{Type: event.Rune, Rune: '0'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'f'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'a'})
ui.Emit(event.Event{Type: event.ExitInsert})
ui.Emit(event.Event{Type: event.CursorLeft})
ui.Emit(event.Event{Type: event.Decrement})
ui.Emit(event.Event{Type: event.StartInsertHead})
ui.Emit(event.Event{Type: event.Rune, Rune: '1'})
ui.Emit(event.Event{Type: event.Rune, Rune: '2'})
ui.Emit(event.Event{Type: event.ExitInsert})
ui.Emit(event.Event{Type: event.CursorEnd})
ui.Emit(event.Event{Type: event.Delete})
ui.Emit(event.Event{Type: event.WriteQuit})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.err; err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name())
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "\x12\x48\xff"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorOpenQuitBang(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.OpenEmpty(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.StartInsert})
ui.Emit(event.Event{Type: event.Rune, Rune: '4'})
ui.Emit(event.Event{Type: event.Rune, Rune: '8'})
ui.Emit(event.Event{Type: event.ExitInsert})
ui.Emit(event.Event{Type: event.Quit})
ui.Emit(event.Event{Type: event.Quit, Bang: true})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err, expected := editor.err, "you have unsaved changes in [No Name], "+
"add ! to force :quit"; err == nil || !strings.HasSuffix(err.Error(), expected) {
t.Errorf("err should end with %q but got: %v", expected, err)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
}
func TestEditorOpenWriteQuitBang(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "ab")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.SwitchFocus})
ui.Emit(event.Event{Type: event.StartAppendEnd})
ui.Emit(event.Event{Type: event.Rune, Rune: 'c'})
ui.Emit(event.Event{Type: event.ExitInsert})
ui.Emit(event.Event{Type: event.WriteQuit, Arg: f.Name() + ".out", Bang: true})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name())
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "ab"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
bs, err = os.ReadFile(f.Name() + ".out")
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "abc"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorReadWriteQuit(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
r := strings.NewReader("Hello, world!")
if err := editor.Read(r); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.WriteQuit, Arg: f.Name()})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name())
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "Hello, world!"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorWritePartial(t *testing.T) {
str := "Hello, world! こんにちは、世界!"
f, err := createTemp(t.TempDir(), str)
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
for _, testCase := range []struct {
cmdRange string
count int
expected string
}{
{"", 41, str},
{"-10,$+10", 41, str},
{"10,25", 16, str[10:26]},
{".+3+3+3+5+5 , .+0xa-0x6", 16, str[4:20]},
{"$-20,.+28", 9, str[20:29]},
} {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
fout, err := createTemp(t.TempDir(), "")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func(name string) {
ui.Emit(event.Event{Type: event.StartCmdlineCommand})
for _, c := range testCase.cmdRange + "w " + name {
ui.Emit(event.Event{Type: event.Rune, Rune: c})
}
ui.Emit(event.Event{Type: event.ExecuteCmdline})
ui.Emit(event.Event{Type: event.Quit, Bang: true})
}(fout.Name())
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := fmt.Sprintf("%[1]d (0x%[1]x) bytes written", testCase.count); editor.err == nil ||
!strings.Contains(editor.err.Error(), expected) {
t.Errorf("err should be contain %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(fout.Name())
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if string(bs) != testCase.expected {
t.Errorf("file contents should be %q but got %q", testCase.expected, string(bs))
}
}
}
func TestEditorWriteVisualSelection(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "Hello, world!")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.CursorNext, Count: 4})
ui.Emit(event.Event{Type: event.StartVisual})
ui.Emit(event.Event{Type: event.CursorNext, Count: 5})
ui.Emit(event.Event{Type: event.StartCmdlineCommand})
ui.Emit(event.Event{Type: event.Rune, Rune: 'w'})
ui.Emit(event.Event{Type: event.Rune, Rune: ' '})
for _, ch := range f.Name() + ".out" {
ui.Emit(event.Event{Type: event.Rune, Rune: ch})
}
ui.Emit(event.Event{Type: event.ExecuteCmdline})
ui.Emit(event.Event{Type: event.Quit})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "6 (0x6) bytes written"; editor.err == nil ||
!strings.HasSuffix(editor.err.Error(), expected) {
t.Errorf("err should end with %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name() + ".out")
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "o, wor"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorWriteUndo(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "abc")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.DeleteByte})
ui.Emit(event.Event{Type: event.Write, Arg: f.Name() + ".out"})
ui.Emit(event.Event{Type: event.Undo})
ui.Emit(event.Event{Type: event.Quit})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "2 (0x2) bytes written"; editor.err == nil ||
!strings.HasSuffix(editor.err.Error(), expected) {
t.Errorf("err should end with %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name())
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "abc"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
bs, err = os.ReadFile(f.Name() + ".out")
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "bc"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorSearch(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "abcdefabcdefabcdef")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.StartCmdlineSearchForward})
ui.Emit(event.Event{Type: event.Rune, Rune: 'e'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'f'})
ui.Emit(event.Event{Type: event.ExecuteCmdline})
ui.Emit(event.Event{Type: event.Nop}) // wait for redraw
ui.Emit(event.Event{Type: event.DeleteByte})
ui.Emit(event.Event{Type: event.PreviousSearch})
ui.Emit(event.Event{Type: event.NextSearch})
ui.Emit(event.Event{Type: event.DeleteByte})
ui.Emit(event.Event{Type: event.StartCmdlineSearchBackward})
ui.Emit(event.Event{Type: event.Rune, Rune: 'b'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'c'})
ui.Emit(event.Event{Type: event.ExecuteCmdline})
ui.Emit(event.Event{Type: event.Nop}) // wait for redraw
ui.Emit(event.Event{Type: event.DeleteByte})
ui.Emit(event.Event{Type: event.PreviousSearch})
ui.Emit(event.Event{Type: event.DeleteByte})
ui.Emit(event.Event{Type: event.Write, Arg: f.Name() + ".out"})
ui.Emit(event.Event{Type: event.Quit, Bang: true})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "14 (0xe) bytes written"; editor.err == nil ||
!strings.HasSuffix(editor.err.Error(), expected) {
t.Errorf("err should end with %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name() + ".out")
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "abcdfacdfacdef"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorCmdlineCursorGoto(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "Hello, world!")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.StartCmdlineCommand})
ui.Emit(event.Event{Type: event.Rune, Rune: '6'})
ui.Emit(event.Event{Type: event.ExecuteCmdline})
ui.Emit(event.Event{Type: event.DeleteByte})
ui.Emit(event.Event{Type: event.Write, Arg: f.Name() + ".out1"})
ui.Emit(event.Event{Type: event.Undo})
ui.Emit(event.Event{Type: event.StartCmdlineCommand})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '0'})
ui.Emit(event.Event{Type: event.Rune, Rune: '%'})
ui.Emit(event.Event{Type: event.ExecuteCmdline})
ui.Emit(event.Event{Type: event.DeletePrevByte})
ui.Emit(event.Event{Type: event.Write, Arg: f.Name() + ".out2"})
ui.Emit(event.Event{Type: event.Quit, Bang: true})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name() + ".out1")
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "Hello,world!"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
bs, err = os.ReadFile(f.Name() + ".out2")
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "Hello, wrld!"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorCmdlineQuit(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.OpenEmpty(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.StartCmdlineCommand})
ui.Emit(event.Event{Type: event.Rune, Rune: 'q'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'u'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'i'})
ui.Emit(event.Event{Type: event.Rune, Rune: 't'})
ui.Emit(event.Event{Type: event.ExecuteCmdline})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.err; err != nil {
t.Errorf("err should be nil but got: %v", err)
}
}
func TestEditorCmdlineQuitAll(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.OpenEmpty(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.StartCmdlineCommand})
ui.Emit(event.Event{Type: event.Rune, Rune: 'q'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'a'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'l'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'l'})
ui.Emit(event.Event{Type: event.ExecuteCmdline})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.err; err != nil {
t.Errorf("err should be nil but got: %v", err)
}
}
func TestEditorCmdlineQuitErr(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.OpenEmpty(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.StartCmdlineCommand})
ui.Emit(event.Event{Type: event.Rune, Rune: 'c'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'q'})
ui.Emit(event.Event{Type: event.Rune, Rune: ' '})
ui.Emit(event.Event{Type: event.Rune, Rune: '4'})
ui.Emit(event.Event{Type: event.Rune, Rune: '2'})
ui.Emit(event.Event{Type: event.ExecuteCmdline})
}()
if err, expected := editor.Run(), (&quitErr{42}); !reflect.DeepEqual(expected, err) {
t.Errorf("err should be %v but got: %v", expected, err)
}
if err := editor.err; err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
}
func TestEditorReplace(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "Hello, world!")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.CursorNext, Count: 2})
ui.Emit(event.Event{Type: event.StartReplace})
ui.Emit(event.Event{Type: event.SwitchFocus})
ui.Emit(event.Event{Type: event.Rune, Rune: 'a'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'b'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'c'})
ui.Emit(event.Event{Type: event.CursorNext, Count: 2})
ui.Emit(event.Event{Type: event.Rune, Rune: 'd'})
ui.Emit(event.Event{Type: event.Rune, Rune: 'e'})
ui.Emit(event.Event{Type: event.ExitInsert})
ui.Emit(event.Event{Type: event.CursorLeft, Count: 5})
ui.Emit(event.Event{Type: event.StartReplaceByte})
ui.Emit(event.Event{Type: event.SwitchFocus})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '2'})
ui.Emit(event.Event{Type: event.CursorNext, Count: 2})
ui.Emit(event.Event{Type: event.StartReplace})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '2'})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '3'})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '4'})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '5'})
ui.Emit(event.Event{Type: event.Backspace})
ui.Emit(event.Event{Type: event.ExitInsert})
ui.Emit(event.Event{Type: event.CursorEnd})
ui.Emit(event.Event{Type: event.StartReplace})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '6'})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '7'})
ui.Emit(event.Event{Type: event.Rune, Rune: '8'})
ui.Emit(event.Event{Type: event.Backspace})
ui.Emit(event.Event{Type: event.ExitInsert})
ui.Emit(event.Event{Type: event.CursorHead})
ui.Emit(event.Event{Type: event.DeleteByte})
ui.Emit(event.Event{Type: event.Write, Arg: f.Name() + ".out"})
ui.Emit(event.Event{Type: event.Quit, Bang: true})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "13 (0xd) bytes written"; editor.err == nil ||
!strings.HasSuffix(editor.err.Error(), expected) {
t.Errorf("err should end with %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name() + ".out")
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "earcrsterldvw"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorCopyCutPaste(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "Hello, world!")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.CursorNext, Count: 2})
ui.Emit(event.Event{Type: event.StartVisual})
ui.Emit(event.Event{Type: event.CursorNext, Count: 5})
ui.Emit(event.Event{Type: event.Copy})
ui.Emit(event.Event{Type: event.CursorNext, Count: 3})
ui.Emit(event.Event{Type: event.Paste})
ui.Emit(event.Event{Type: event.CursorPrev, Count: 2})
ui.Emit(event.Event{Type: event.StartVisual})
ui.Emit(event.Event{Type: event.CursorPrev, Count: 5})
ui.Emit(event.Event{Type: event.Cut})
ui.Emit(event.Event{Type: event.CursorNext, Count: 5})
ui.Emit(event.Event{Type: event.PastePrev})
ui.Emit(event.Event{Type: event.Write, Arg: f.Name() + ".out"})
ui.Emit(event.Event{Type: event.Quit, Bang: true})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "19 (0x13) bytes written"; editor.err == nil ||
!strings.HasSuffix(editor.err.Error(), expected) {
t.Errorf("err should end with %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name() + ".out")
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "Hell w woo,llo,rld!"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorShowBinary(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "Hello, world!")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.ShowBinary})
ui.Emit(event.Event{Type: event.Quit})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "01001000"; editor.err == nil || editor.err.Error() != expected {
t.Errorf("err should be %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
}
func TestEditorShowDecimal(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "Hello, world!")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.ShowDecimal})
ui.Emit(event.Event{Type: event.Quit})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "72"; editor.err == nil || editor.err.Error() != expected {
t.Errorf("err should be %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
}
func TestEditorShift(t *testing.T) {
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
f, err := createTemp(t.TempDir(), "Hello, world!")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.ShiftLeft, Count: 1})
ui.Emit(event.Event{Type: event.CursorNext, Count: 7})
ui.Emit(event.Event{Type: event.ShiftRight, Count: 3})
ui.Emit(event.Event{Type: event.Write, Arg: f.Name() + ".out"})
ui.Emit(event.Event{Type: event.Quit, Bang: true})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "13 (0xd) bytes written"; editor.err == nil ||
!strings.HasSuffix(editor.err.Error(), expected) {
t.Errorf("err should end with %q but got: %v", expected, editor.err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(f.Name() + ".out")
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "\x90ello, \x0eorld!"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
func TestEditorChdir(t *testing.T) {
dir, err := os.Getwd()
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := editor.OpenEmpty(); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
ui.Emit(event.Event{Type: event.Pwd})
ui.Emit(event.Event{Type: event.Chdir, Arg: "../"})
ui.Emit(event.Event{Type: event.Chdir, Arg: "-"})
ui.Emit(event.Event{Type: event.Quit})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.err; err == nil || err.Error() != dir {
t.Errorf("err should be %q but got: %v", dir, err)
}
if editor.errtyp != state.MessageInfo {
t.Errorf("errtyp should be MessageInfo but got: %v", editor.errtyp)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
}

View File

@ -1,198 +0,0 @@
package editor
import (
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/key"
"b612.me/apps/b612/bed/mode"
)
func defaultKeyManagers() map[mode.Mode]*key.Manager {
kms := make(map[mode.Mode]*key.Manager)
km := defaultNormalAndVisual()
km.Register(event.Quit, "c-w", "q")
km.Register(event.Quit, "c-w", "c-q")
km.Register(event.Quit, "c-w", "c")
km.RegisterBang(event.Quit, "Z", "Q")
km.Register(event.WriteQuit, "Z", "Z")
km.Register(event.Suspend, "c-z")
km.Register(event.JumpTo, "\x1d")
km.Register(event.JumpBack, "c-t")
km.Register(event.DeleteByte, "x")
km.Register(event.DeleteByte, "delete")
km.Register(event.DeletePrevByte, "X")
km.Register(event.Increment, "c-a")
km.Register(event.Increment, "+")
km.Register(event.Decrement, "c-x")
km.Register(event.Decrement, "-")
km.Register(event.ShiftLeft, "<")
km.Register(event.ShiftRight, ">")
km.Register(event.ShowBinary, "g", "b")
km.Register(event.ShowDecimal, "g", "d")
km.Register(event.Paste, "p")
km.Register(event.PastePrev, "P")
km.Register(event.StartInsert, "i")
km.Register(event.StartInsertHead, "I")
km.Register(event.StartAppend, "a")
km.Register(event.StartAppendEnd, "A")
km.Register(event.StartReplace, "R")
km.Register(event.Undo, "u")
km.Register(event.Redo, "c-r")
km.Register(event.StartVisual, "v")
km.Register(event.New, "c-w", "n")
km.Register(event.New, "c-w", "c-n")
km.Register(event.Only, "c-w", "o")
km.Register(event.Only, "c-w", "c-o")
km.Register(event.Alternative, "\x1e")
km.Register(event.FocusWindowDown, "c-w", "down")
km.Register(event.FocusWindowDown, "c-w", "c-j")
km.Register(event.FocusWindowDown, "c-w", "j")
km.Register(event.FocusWindowUp, "c-w", "up")
km.Register(event.FocusWindowUp, "c-w", "c-k")
km.Register(event.FocusWindowUp, "c-w", "k")
km.Register(event.FocusWindowLeft, "c-w", "left")
km.Register(event.FocusWindowLeft, "c-w", "c-h")
km.Register(event.FocusWindowLeft, "c-w", "backspace")
km.Register(event.FocusWindowLeft, "c-w", "h")
km.Register(event.FocusWindowRight, "c-w", "right")
km.Register(event.FocusWindowRight, "c-w", "c-l")
km.Register(event.FocusWindowRight, "c-w", "l")
km.Register(event.FocusWindowTopLeft, "c-w", "t")
km.Register(event.FocusWindowTopLeft, "c-w", "c-t")
km.Register(event.FocusWindowBottomRight, "c-w", "b")
km.Register(event.FocusWindowBottomRight, "c-w", "c-b")
km.Register(event.FocusWindowPrevious, "c-w", "p")
km.Register(event.FocusWindowPrevious, "c-w", "c-p")
km.Register(event.MoveWindowTop, "c-w", "K")
km.Register(event.MoveWindowBottom, "c-w", "J")
km.Register(event.MoveWindowLeft, "c-w", "H")
km.Register(event.MoveWindowRight, "c-w", "L")
kms[mode.Normal] = km
km = key.NewManager(false)
km.Register(event.ExitInsert, "escape")
km.Register(event.ExitInsert, "c-c")
km.Register(event.CursorUp, "up")
km.Register(event.CursorDown, "down")
km.Register(event.CursorLeft, "left")
km.Register(event.CursorRight, "right")
km.Register(event.CursorUp, "c-p")
km.Register(event.CursorDown, "c-n")
km.Register(event.CursorPrev, "c-b")
km.Register(event.CursorNext, "c-f")
km.Register(event.PageUp, "pgup")
km.Register(event.PageDown, "pgdn")
km.Register(event.PageTop, "home")
km.Register(event.PageEnd, "end")
km.Register(event.Backspace, "backspace")
km.Register(event.Backspace, "backspace2")
km.Register(event.Delete, "delete")
km.Register(event.SwitchFocus, "tab")
km.Register(event.SwitchFocus, "backtab")
kms[mode.Insert] = km
kms[mode.Replace] = km
km = defaultNormalAndVisual()
km.Register(event.ExitVisual, "escape")
km.Register(event.ExitVisual, "c-c")
km.Register(event.ExitVisual, "v")
km.Register(event.SwitchVisualEnd, "o")
km.Register(event.SwitchVisualEnd, "O")
km.Register(event.Copy, "y")
km.Register(event.Cut, "x")
km.Register(event.Cut, "d")
km.Register(event.Cut, "delete")
kms[mode.Visual] = km
km = key.NewManager(false)
km.Register(event.CursorUp, "up")
km.Register(event.CursorDown, "down")
km.Register(event.CursorLeft, "left")
km.Register(event.CursorRight, "right")
km.Register(event.CursorUp, "c-p")
km.Register(event.CursorDown, "c-n")
km.Register(event.CursorLeft, "c-b")
km.Register(event.CursorRight, "c-f")
km.Register(event.CursorHead, "home")
km.Register(event.CursorHead, "c-a")
km.Register(event.CursorEnd, "end")
km.Register(event.CursorEnd, "c-e")
km.Register(event.BackspaceCmdline, "c-h")
km.Register(event.BackspaceCmdline, "backspace")
km.Register(event.BackspaceCmdline, "backspace2")
km.Register(event.DeleteCmdline, "delete")
km.Register(event.DeleteWordCmdline, "c-w")
km.Register(event.ClearToHeadCmdline, "c-u")
km.Register(event.ClearCmdline, "c-k")
km.Register(event.ExitCmdline, "escape")
km.Register(event.ExitCmdline, "c-c")
km.Register(event.CompleteForwardCmdline, "tab")
km.Register(event.CompleteBackCmdline, "backtab")
km.Register(event.ExecuteCmdline, "enter")
km.Register(event.ExecuteCmdline, "c-j")
km.Register(event.ExecuteCmdline, "c-m")
kms[mode.Cmdline] = km
kms[mode.Search] = km
return kms
}
func defaultNormalAndVisual() *key.Manager {
km := key.NewManager(true)
km.Register(event.CursorUp, "up")
km.Register(event.CursorDown, "down")
km.Register(event.CursorLeft, "left")
km.Register(event.CursorRight, "right")
km.Register(event.PageUp, "pgup")
km.Register(event.PageDown, "pgdn")
km.Register(event.PageTop, "home")
km.Register(event.PageEnd, "end")
km.Register(event.CursorUp, "k")
km.Register(event.CursorDown, "j")
km.Register(event.CursorLeft, "h")
km.Register(event.CursorRight, "l")
km.Register(event.CursorPrev, "b")
km.Register(event.CursorPrev, "backspace")
km.Register(event.CursorPrev, "backspace2")
km.Register(event.CursorNext, "w")
km.Register(event.CursorNext, " ")
km.Register(event.CursorHead, "0")
km.Register(event.CursorHead, "^")
km.Register(event.CursorEnd, "$")
km.Register(event.ScrollUp, "c-y")
km.Register(event.ScrollDown, "c-e")
km.Register(event.ScrollTop, "z", "t")
km.Register(event.ScrollTopHead, "z", "enter")
km.Register(event.ScrollMiddle, "z", "z")
km.Register(event.ScrollMiddleHead, "z", ".")
km.Register(event.ScrollBottom, "z", "b")
km.Register(event.ScrollBottomHead, "z", "-")
km.Register(event.WindowTop, "H")
km.Register(event.WindowMiddle, "M")
km.Register(event.WindowBottom, "L")
km.Register(event.PageUp, "c-b")
km.Register(event.PageDown, "c-f")
km.Register(event.PageUpHalf, "c-u")
km.Register(event.PageDownHalf, "c-d")
km.Register(event.PageTop, "g", "g")
km.Register(event.PageEnd, "G")
km.Register(event.SwitchFocus, "tab")
km.Register(event.SwitchFocus, "backtab")
km.Register(event.StartCmdlineSearchForward, "/")
km.Register(event.StartCmdlineSearchBackward, "?")
km.Register(event.NextSearch, "n")
km.Register(event.PreviousSearch, "N")
km.Register(event.AbortSearch, "c-c")
km.Register(event.StartCmdlineCommand, ":")
km.Register(event.StartReplaceByte, "r")
return km
}

View File

@ -1,21 +0,0 @@
package editor
import (
"io"
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/layout"
"b612.me/apps/b612/bed/state"
)
// Manager defines the required window manager interface for the editor.
type Manager interface {
Init(chan<- event.Event, chan<- struct{})
Open(string) error
Read(io.Reader) error
SetSize(int, int)
Resize(int, int)
Emit(event.Event)
State() (map[int]*state.WindowState, layout.Layout, int, error)
Close()
}

View File

@ -1,23 +0,0 @@
//go:build linux
package editor
import "syscall"
func suspend(e *Editor) error {
if err := e.ui.Close(); err != nil {
return err
}
pid, tid := syscall.Getpid(), syscall.Gettid()
if err := syscall.Tgkill(pid, tid, syscall.SIGSTOP); err != nil {
return err
}
if err := e.ui.Init(e.uiEventCh); err != nil {
return err
}
if err := e.redraw(); err != nil {
return err
}
go e.ui.Run(defaultKeyManagers())
return nil
}

View File

@ -1,23 +0,0 @@
//go:build !windows && !linux
package editor
import "syscall"
func suspend(e *Editor) error {
if err := e.ui.Close(); err != nil {
return err
}
pid := syscall.Getpid()
if err := syscall.Kill(pid, syscall.SIGSTOP); err != nil {
return err
}
if err := e.ui.Init(e.uiEventCh); err != nil {
return err
}
if err := e.redraw(); err != nil {
return err
}
go e.ui.Run(defaultKeyManagers())
return nil
}

View File

@ -1,7 +0,0 @@
//go:build windows
package editor
func suspend(_ *Editor) error {
return nil
}

View File

@ -1,17 +0,0 @@
package editor
import (
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/key"
"b612.me/apps/b612/bed/mode"
"b612.me/apps/b612/bed/state"
)
// UI defines the required user interface for the editor.
type UI interface {
Init(chan<- event.Event) error
Run(map[mode.Mode]*key.Manager)
Size() (int, int)
Redraw(state.State) error
Close() error
}

View File

@ -1,141 +0,0 @@
package event
import (
"b612.me/apps/b612/bed/buffer"
"b612.me/apps/b612/bed/mode"
)
// Event represents the event emitted by UI.
type Event struct {
Type Type
Range *Range
Count int64
Rune rune
CmdName string
Bang bool
Arg string
Error error
Mode mode.Mode
Buffer *buffer.Buffer
}
// Type ...
type Type int
// Event types
const (
Nop Type = iota
Redraw
CursorUp
CursorDown
CursorLeft
CursorRight
CursorPrev
CursorNext
CursorHead
CursorEnd
CursorGoto
ScrollUp
ScrollDown
ScrollTop
ScrollTopHead
ScrollMiddle
ScrollMiddleHead
ScrollBottom
ScrollBottomHead
PageUp
PageDown
PageUpHalf
PageDownHalf
PageTop
PageEnd
WindowTop
WindowMiddle
WindowBottom
JumpTo
JumpBack
DeleteByte
DeletePrevByte
Increment
Decrement
ShiftLeft
ShiftRight
SwitchFocus
ShowBinary
ShowDecimal
StartInsert
StartInsertHead
StartAppend
StartAppendEnd
StartReplaceByte
StartReplace
ExitInsert
Backspace
Delete
Rune
Undo
Redo
StartVisual
SwitchVisualEnd
ExitVisual
Copy
Cut
Copied
Paste
PastePrev
Pasted
StartCmdlineCommand
StartCmdlineSearchForward
StartCmdlineSearchBackward
BackspaceCmdline
DeleteCmdline
DeleteWordCmdline
ClearToHeadCmdline
ClearCmdline
ExitCmdline
CompleteForwardCmdline
CompleteBackCmdline
ExecuteCmdline
ExecuteSearch
NextSearch
PreviousSearch
AbortSearch
Edit
Enew
New
Vnew
Only
Alternative
Wincmd
FocusWindowUp
FocusWindowDown
FocusWindowLeft
FocusWindowRight
FocusWindowTopLeft
FocusWindowBottomRight
FocusWindowPrevious
MoveWindowTop
MoveWindowBottom
MoveWindowLeft
MoveWindowRight
Pwd
Chdir
Suspend
Quit
QuitAll
QuitErr
Write
WriteQuit
Info
Error
)

View File

@ -1,96 +0,0 @@
package event
import (
"strings"
"unicode"
)
// ParseRange parses a Range.
func ParseRange(src string) (*Range, string) {
var from, to Position
from, src = parsePosition(src)
if from == nil {
return nil, src
}
var ok bool
if src, ok = strings.CutPrefix(src, ","); !ok {
return &Range{From: from}, src
}
to, src = parsePosition(src)
return &Range{From: from, To: to}, src
}
func parsePosition(src string) (Position, string) {
var pos Position
var offset int64
src = strings.TrimLeftFunc(src, unicode.IsSpace)
if src == "" {
return nil, src
}
switch src[0] {
case '.':
src = src[1:]
fallthrough
case '-', '+':
pos = Relative{}
case '$':
pos = End{}
src = src[1:]
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
offset, src = parseNum(src)
pos = Absolute{offset}
case '\'':
if len(src) == 1 {
return nil, src
}
switch src[1] {
case '<':
pos = VisualStart{}
case '>':
pos = VisualEnd{}
default:
return nil, src
}
src = src[2:]
default:
return nil, src
}
for src != "" {
src = strings.TrimLeftFunc(src, unicode.IsSpace)
if src == "" {
break
}
sign := int64(1)
switch src[0] {
case '-':
sign = -1
fallthrough
case '+':
offset, src = parseNum(src[1:])
pos = pos.add(sign * offset)
default:
return pos, src
}
}
return pos, src
}
func parseNum(src string) (int64, string) {
offset, radix, ishex := int64(0), int64(10), false
if src, ishex = strings.CutPrefix(src, "0x"); ishex {
radix = 16
}
for src != "" {
c := src[0]
switch {
case '0' <= c && c <= '9':
offset = offset*radix + int64(c-'0')
case ('A' <= c && c <= 'F' || 'a' <= c && c <= 'f') && ishex:
offset = offset*radix + int64(c|('a'-'A')-'a'+10)
default:
return offset, src
}
src = src[1:]
}
return offset, src
}

View File

@ -1,42 +0,0 @@
package event
import (
"reflect"
"testing"
)
func TestParseRange(t *testing.T) {
testCases := []struct {
target string
expected *Range
rest string
}{
{"", nil, ""},
{"e", nil, "e"},
{" ", nil, ""},
{"$", &Range{End{}, nil}, ""},
{" $-72 , $-36 ", &Range{End{-72}, End{-36}}, ""},
{"32", &Range{Absolute{32}, nil}, ""},
{"+32", &Range{Relative{32}, nil}, ""},
{"-32", &Range{Relative{-32}, nil}, ""},
{"1024,4096", &Range{Absolute{1024}, Absolute{4096}}, ""},
{"1+2+3+4+5+6+7+8+9,0xa+0xb+0xc+0xD+0xE+0xF", &Range{Absolute{45}, Absolute{75}}, ""},
{"10d", &Range{Absolute{10}, nil}, "d"},
{"0x12G", &Range{Absolute{0x12}, nil}, "G"},
{"0x10fag", &Range{Absolute{0x10fa}, nil}, "g"},
{".-100,.+100", &Range{Relative{-100}, Relative{100}}, ""},
{"'", nil, "'"},
{"' ", nil, "' "},
{"'<", &Range{VisualStart{}, nil}, ""},
{"'>", &Range{VisualEnd{}, nil}, ""},
{" '< , '> write", &Range{VisualStart{}, VisualEnd{}}, "write"},
{" '<+0x10 , '>-10w", &Range{VisualStart{0x10}, VisualEnd{-10}}, "w"},
}
for _, testCase := range testCases {
got, rest := ParseRange(testCase.target)
if !reflect.DeepEqual(got, testCase.expected) || rest != testCase.rest {
t.Errorf("ParseRange(%q) should return\n\t%#v, %q\nbut got\n\t%#v, %q",
testCase.target, testCase.expected, testCase.rest, got, rest)
}
}
}

View File

@ -1,45 +0,0 @@
package event
// Range of event
type Range struct {
From Position
To Position
}
// Position ...
type Position interface{ add(int64) Position }
// Absolute is the absolute position of the buffer.
type Absolute struct{ Offset int64 }
func (p Absolute) add(offset int64) Position {
return Absolute{p.Offset + offset}
}
// Relative is the relative position of the buffer.
type Relative struct{ Offset int64 }
func (p Relative) add(offset int64) Position {
return Relative{p.Offset + offset}
}
// End is the end of the buffer.
type End struct{ Offset int64 }
func (p End) add(offset int64) Position {
return End{p.Offset + offset}
}
// VisualStart is the start position of visual selection.
type VisualStart struct{ Offset int64 }
func (p VisualStart) add(offset int64) Position {
return VisualStart{p.Offset + offset}
}
// VisualEnd is the end position of visual selection.
type VisualEnd struct{ Offset int64 }
func (p VisualEnd) add(offset int64) Position {
return VisualEnd{p.Offset + offset}
}

View File

@ -1,56 +0,0 @@
package history
import "b612.me/apps/b612/bed/buffer"
// History manages the buffer history.
type History struct {
entries []*historyEntry
index int
}
type historyEntry struct {
buffer *buffer.Buffer
offset int64
cursor int64
tick uint64
}
// NewHistory creates a new history manager.
func NewHistory() *History {
return &History{index: -1}
}
// Push a new buffer to the history.
func (h *History) Push(buffer *buffer.Buffer, offset, cursor int64, tick uint64) {
newEntry := &historyEntry{buffer.Clone(), offset, cursor, tick}
if len(h.entries)-1 > h.index {
h.index++
h.entries[h.index] = newEntry
h.entries = h.entries[:h.index+1]
} else {
h.entries = append(h.entries, newEntry)
h.index++
}
}
// Undo the history.
func (h *History) Undo() (*buffer.Buffer, int, int64, int64, uint64) {
if h.index < 0 {
return nil, h.index, 0, 0, 0
}
if h.index > 0 {
h.index--
}
e := h.entries[h.index]
return e.buffer.Clone(), h.index, e.offset, e.cursor, e.tick
}
// Redo the history.
func (h *History) Redo() (*buffer.Buffer, int64, int64, uint64) {
if h.index == len(h.entries)-1 || h.index < 0 {
return nil, 0, 0, 0
}
h.index++
e := h.entries[h.index]
return e.buffer.Clone(), e.offset, e.cursor, e.tick
}

View File

@ -1,99 +0,0 @@
package history
import (
"strings"
"testing"
"b612.me/apps/b612/bed/buffer"
)
func TestHistoryUndo(t *testing.T) {
history := NewHistory()
b, index, offset, cursor, tick := history.Undo()
if b != nil {
t.Errorf("history.Undo should return nil buffer but got %v", b)
}
if index != -1 {
t.Errorf("history.Undo should return index -1 but got %d", index)
}
if offset != 0 {
t.Errorf("history.Undo should return offset 0 but got %d", offset)
}
if cursor != 0 {
t.Errorf("history.Undo should return cursor 0 but got %d", cursor)
}
if tick != 0 {
t.Errorf("history.Undo should return tick 0 but got %d", tick)
}
buffer1 := buffer.NewBuffer(strings.NewReader("test1"))
history.Push(buffer1, 2, 1, 1)
buffer2 := buffer.NewBuffer(strings.NewReader("test2"))
history.Push(buffer2, 3, 2, 2)
buf := make([]byte, 8)
b, index, offset, cursor, tick = history.Undo()
if b == nil {
t.Fatalf("history.Undo should return buffer but got nil")
}
_, err := b.Read(buf)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "test1\x00\x00\x00"; string(buf) != expected {
t.Errorf("buf should be %q but got %q", expected, string(buf))
}
if index != 0 {
t.Errorf("history.Undo should return index 0 but got %d", index)
}
if offset != 2 {
t.Errorf("history.Undo should return offset 2 but got %d", offset)
}
if cursor != 1 {
t.Errorf("history.Undo should return cursor 1 but got %d", cursor)
}
if tick != 1 {
t.Errorf("history.Undo should return tick 1 but got %d", tick)
}
buf = make([]byte, 8)
b, offset, cursor, tick = history.Redo()
if b == nil {
t.Fatalf("history.Redo should return buffer but got nil")
}
_, err = b.Read(buf)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "test2\x00\x00\x00"; string(buf) != expected {
t.Errorf("buf should be %q but got %q", expected, string(buf))
}
if offset != 3 {
t.Errorf("history.Redo should return offset 3 but got %d", offset)
}
if cursor != 2 {
t.Errorf("history.Redo should return cursor 2 but got %d", cursor)
}
if tick != 2 {
t.Errorf("history.Redo should return cursor 2 but got %d", tick)
}
history.Undo()
buffer3 := buffer.NewBuffer(strings.NewReader("test2"))
history.Push(buffer3, 3, 2, 3)
b, offset, cursor, tick = history.Redo()
if b != nil {
t.Errorf("history.Redo should return nil buffer but got %v", b)
}
if offset != 0 {
t.Errorf("history.Redo should return offset 0 but got %d", offset)
}
if cursor != 0 {
t.Errorf("history.Redo should return cursor 0 but got %d", cursor)
}
if tick != 0 {
t.Errorf("history.Redo should return tick 0 but got %d", tick)
}
}

View File

@ -1,91 +0,0 @@
package key
import (
"strconv"
"b612.me/apps/b612/bed/event"
)
// Key represents one keyboard stroke.
type Key string
type keyEvent struct {
keys []Key
event event.Type
bang bool
}
const (
keysEq = iota
keysPending
keysNeq
)
func (ke keyEvent) cmp(ks []Key) int {
if len(ke.keys) < len(ks) {
return keysNeq
}
for i, k := range ke.keys {
if i >= len(ks) {
return keysPending
}
if k != ks[i] {
return keysNeq
}
}
return keysEq
}
// Manager holds the key mappings and current key sequence.
type Manager struct {
keys []Key
events []keyEvent
count bool
}
// NewManager creates a new Manager.
func NewManager(count bool) *Manager {
return &Manager{count: count}
}
// Register adds a new key mapping.
func (km *Manager) Register(eventType event.Type, keys ...Key) {
km.events = append(km.events, keyEvent{keys, eventType, false})
}
// RegisterBang adds a new key mapping with bang.
func (km *Manager) RegisterBang(eventType event.Type, keys ...Key) {
km.events = append(km.events, keyEvent{keys, eventType, true})
}
// Press checks the new key down event.
func (km *Manager) Press(k Key) event.Event {
km.keys = append(km.keys, k)
for i := range len(km.keys) {
keys := km.keys[i:]
var count int64
if km.count {
numStr := ""
for j, k := range keys {
if len(k) == 1 && ('1' <= k[0] && k[0] <= '9' || k[0] == '0' && j > 0) {
numStr += string(k)
} else {
break
}
}
keys = keys[len(numStr):]
count, _ = strconv.ParseInt(numStr, 10, 64)
}
for _, ke := range km.events {
switch ke.cmp(keys) {
case keysPending:
return event.Event{Type: event.Nop}
case keysEq:
km.keys = nil
return event.Event{Type: ke.event, Count: count, Bang: ke.bang}
}
}
}
km.keys = nil
return event.Event{Type: event.Nop}
}

View File

@ -1,71 +0,0 @@
package key
import (
"testing"
"b612.me/apps/b612/bed/event"
)
func TestKeyManagerPress(t *testing.T) {
km := NewManager(true)
km.Register(event.CursorUp, "k")
e := km.Press("k")
if e.Type != event.CursorUp {
t.Errorf("pressing k should emit event.CursorUp but got: %d", e.Type)
}
e = km.Press("j")
if e.Type != event.Nop {
t.Errorf("pressing j should be nop but got: %d", e.Type)
}
}
func TestKeyManagerPressMulti(t *testing.T) {
km := NewManager(true)
km.Register(event.CursorUp, "k", "k", "j")
km.Register(event.CursorDown, "k", "j", "j")
km.Register(event.CursorDown, "j", "k", "k")
e := km.Press("k")
if e.Type != event.Nop {
t.Errorf("pressing k should be nop but got: %d", e.Type)
}
e = km.Press("k")
if e.Type != event.Nop {
t.Errorf("pressing k twice should be nop but got: %d", e.Type)
}
e = km.Press("k")
if e.Type != event.Nop {
t.Errorf("pressing k three times should be nop but got: %d", e.Type)
}
e = km.Press("j")
if e.Type != event.CursorUp {
t.Errorf("pressing kkj should emit event.CursorUp but got: %d", e.Type)
}
}
func TestKeyManagerPressCount(t *testing.T) {
km := NewManager(true)
km.Register(event.CursorUp, "k", "j")
e := km.Press("k")
if e.Type != event.Nop {
t.Errorf("pressing k should be nop but got: %d", e.Type)
}
e = km.Press("3")
if e.Type != event.Nop {
t.Errorf("pressing 3 should be nop but got: %d", e.Type)
}
e = km.Press("7")
if e.Type != event.Nop {
t.Errorf("pressing 7 should be nop but got: %d", e.Type)
}
e = km.Press("k")
if e.Type != event.Nop {
t.Errorf("pressing k should be nop but got: %d", e.Type)
}
e = km.Press("j")
if e.Type != event.CursorUp {
t.Errorf("pressing 37kj should emit event.CursorUp but got: %d", e.Type)
}
if e.Count != 37 {
t.Errorf("pressing 37kj should emit event.CursorUp with count 37 but got: %d", e.Count)
}
}

View File

@ -1,500 +0,0 @@
package layout
// Layout represents the window layout.
type Layout interface {
isLayout()
Collect() map[int]Window
Replace(int) Layout
Resize(int, int, int, int) Layout
LeftMargin() int
TopMargin() int
Width() int
Height() int
SplitTop(int) Layout
SplitBottom(int) Layout
SplitLeft(int) Layout
SplitRight(int) Layout
Count() (int, int)
Activate(int) Layout
ActivateFirst() Layout
ActiveWindow() Window
Lookup(func(Window) bool) Window
Close() Layout
}
// Window holds the window index and it is active or not.
type Window struct {
Index int
Active bool
left int
top int
width int
height int
}
// NewLayout creates a new Layout from a window index.
func NewLayout(index int) Layout {
return Window{Index: index, Active: true}
}
func (Window) isLayout() {}
// Collect returns all the Window.
func (l Window) Collect() map[int]Window {
return map[int]Window{l.Index: l}
}
// Replace the active window with new window index.
func (l Window) Replace(index int) Layout {
if l.Active {
// revive:disable-next-line:modifies-value-receiver
l.Index = index
}
return l
}
// Resize recalculates the position.
func (l Window) Resize(left, top, width, height int) Layout {
// revive:disable-next-line:modifies-value-receiver
l.left, l.top, l.width, l.height = left, top, width, height
return l
}
// LeftMargin returns the left margin.
func (l Window) LeftMargin() int {
return l.left
}
// TopMargin returns the top margin.
func (l Window) TopMargin() int {
return l.top
}
// Width returns the width.
func (l Window) Width() int {
return l.width
}
// Height returns the height.
func (l Window) Height() int {
return l.height
}
// SplitTop splits the layout and opens a new window to the top.
func (l Window) SplitTop(index int) Layout {
if !l.Active {
return l
}
return Horizontal{
Top: Window{Index: index, Active: true},
Bottom: Window{Index: l.Index, Active: false},
}
}
// SplitBottom splits the layout and opens a new window to the bottom.
func (l Window) SplitBottom(index int) Layout {
if !l.Active {
return l
}
return Horizontal{
Top: Window{Index: l.Index, Active: false},
Bottom: Window{Index: index, Active: true},
}
}
// SplitLeft splits the layout and opens a new window to the left.
func (l Window) SplitLeft(index int) Layout {
if !l.Active {
return l
}
return Vertical{
Left: Window{Index: index, Active: true},
Right: Window{Index: l.Index, Active: false},
}
}
// SplitRight splits the layout and opens a new window to the right.
func (l Window) SplitRight(index int) Layout {
if !l.Active {
return l
}
return Vertical{
Left: Window{Index: l.Index, Active: false},
Right: Window{Index: index, Active: true},
}
}
// Count returns the width and height counts.
func (Window) Count() (int, int) {
return 1, 1
}
// Activate the specific window layout.
func (l Window) Activate(i int) Layout {
// revive:disable-next-line:modifies-value-receiver
l.Active = l.Index == i
return l
}
// ActivateFirst the first layout.
func (l Window) ActivateFirst() Layout {
// revive:disable-next-line:modifies-value-receiver
l.Active = true
return l
}
// ActiveWindow returns the active window.
func (l Window) ActiveWindow() Window {
if l.Active {
return l
}
return Window{Index: -1}
}
// Lookup search for the window meets the condition.
func (l Window) Lookup(cond func(Window) bool) Window {
if cond(l) {
return l
}
return Window{Index: -1}
}
// Close the active layout.
func (l Window) Close() Layout {
if l.Active {
panic("Active Window should not be closed")
}
return l
}
// Horizontal holds two layout horizontally.
type Horizontal struct {
Top Layout
Bottom Layout
left int
top int
width int
height int
}
func (Horizontal) isLayout() {}
// Collect returns all the Window.
func (l Horizontal) Collect() map[int]Window {
m := l.Top.Collect()
for i, l := range l.Bottom.Collect() {
m[i] = l
}
return m
}
// Replace the active window with new window index.
func (l Horizontal) Replace(index int) Layout {
return Horizontal{
Top: l.Top.Replace(index),
Bottom: l.Bottom.Replace(index),
left: l.left,
top: l.top,
width: l.width,
height: l.height,
}
}
// Resize recalculates the position.
func (l Horizontal) Resize(left, top, width, height int) Layout {
_, h1 := l.Top.Count()
_, h2 := l.Bottom.Count()
topHeight := height * h1 / (h1 + h2)
return Horizontal{
Top: l.Top.Resize(left, top, width, topHeight),
Bottom: l.Bottom.Resize(left, top+topHeight, width, height-topHeight),
left: left,
top: top,
width: width,
height: height,
}
}
// LeftMargin returns the left margin.
func (l Horizontal) LeftMargin() int {
return l.left
}
// TopMargin returns the top margin.
func (l Horizontal) TopMargin() int {
return l.top
}
// Width returns the width.
func (l Horizontal) Width() int {
return l.width
}
// Height returns the height.
func (l Horizontal) Height() int {
return l.height
}
// SplitTop splits the layout and opens a new window to the top.
func (l Horizontal) SplitTop(index int) Layout {
return Horizontal{
Top: l.Top.SplitTop(index),
Bottom: l.Bottom.SplitTop(index),
}
}
// SplitBottom splits the layout and opens a new window to the bottom.
func (l Horizontal) SplitBottom(index int) Layout {
return Horizontal{
Top: l.Top.SplitBottom(index),
Bottom: l.Bottom.SplitBottom(index),
}
}
// SplitLeft splits the layout and opens a new window to the left.
func (l Horizontal) SplitLeft(index int) Layout {
return Horizontal{
Top: l.Top.SplitLeft(index),
Bottom: l.Bottom.SplitLeft(index),
}
}
// SplitRight splits the layout and opens a new window to the right.
func (l Horizontal) SplitRight(index int) Layout {
return Horizontal{
Top: l.Top.SplitRight(index),
Bottom: l.Bottom.SplitRight(index),
}
}
// Count returns the width and height counts.
func (l Horizontal) Count() (int, int) {
w1, h1 := l.Top.Count()
w2, h2 := l.Bottom.Count()
return max(w1, w2), h1 + h2
}
// Activate the specific window layout.
func (l Horizontal) Activate(i int) Layout {
return Horizontal{
Top: l.Top.Activate(i),
Bottom: l.Bottom.Activate(i),
left: l.left,
top: l.top,
width: l.width,
height: l.height,
}
}
// ActivateFirst the first layout.
func (l Horizontal) ActivateFirst() Layout {
return Horizontal{
Top: l.Top.ActivateFirst(),
Bottom: l.Bottom,
left: l.left,
top: l.top,
width: l.width,
height: l.height,
}
}
// ActiveWindow returns the active window.
func (l Horizontal) ActiveWindow() Window {
if layout := l.Top.ActiveWindow(); layout.Index >= 0 {
return layout
}
return l.Bottom.ActiveWindow()
}
// Lookup search for the window meets the condition.
func (l Horizontal) Lookup(cond func(Window) bool) Window {
if layout := l.Top.Lookup(cond); layout.Index >= 0 {
return layout
}
return l.Bottom.Lookup(cond)
}
// Close the active layout.
func (l Horizontal) Close() Layout {
if m, ok := l.Top.(Window); ok {
if m.Active {
return l.Bottom.ActivateFirst()
}
}
if m, ok := l.Bottom.(Window); ok {
if m.Active {
return l.Top.ActivateFirst()
}
}
return Horizontal{
Top: l.Top.Close(),
Bottom: l.Bottom.Close(),
}
}
// Vertical holds two layout vertically.
type Vertical struct {
Left Layout
Right Layout
left int
top int
width int
height int
}
func (Vertical) isLayout() {}
// Collect returns all the Window.
func (l Vertical) Collect() map[int]Window {
m := l.Left.Collect()
for i, l := range l.Right.Collect() {
m[i] = l
}
return m
}
// Replace the active window with new window index.
func (l Vertical) Replace(index int) Layout {
return Vertical{
Left: l.Left.Replace(index),
Right: l.Right.Replace(index),
left: l.left,
top: l.top,
width: l.width,
height: l.height,
}
}
// Resize recalculates the position.
func (l Vertical) Resize(left, top, width, height int) Layout {
w1, _ := l.Left.Count()
w2, _ := l.Right.Count()
leftWidth := width * w1 / (w1 + w2)
return Vertical{
Left: l.Left.Resize(left, top, leftWidth, height),
Right: l.Right.Resize(
min(left+leftWidth+1, left+width), top,
max(width-leftWidth-1, 0), height),
left: left,
top: top,
width: width,
height: height,
}
}
// LeftMargin returns the left margin.
func (l Vertical) LeftMargin() int {
return l.left
}
// TopMargin returns the top margin.
func (l Vertical) TopMargin() int {
return l.top
}
// Width returns the width.
func (l Vertical) Width() int {
return l.width
}
// Height returns the height.
func (l Vertical) Height() int {
return l.height
}
// SplitTop splits the layout and opens a new window to the top.
func (l Vertical) SplitTop(index int) Layout {
return Vertical{
Left: l.Left.SplitTop(index),
Right: l.Right.SplitTop(index),
}
}
// SplitBottom splits the layout and opens a new window to the bottom.
func (l Vertical) SplitBottom(index int) Layout {
return Vertical{
Left: l.Left.SplitBottom(index),
Right: l.Right.SplitBottom(index),
}
}
// SplitLeft splits the layout and opens a new window to the left.
func (l Vertical) SplitLeft(index int) Layout {
return Vertical{
Left: l.Left.SplitLeft(index),
Right: l.Right.SplitLeft(index),
}
}
// SplitRight splits the layout and opens a new window to the right.
func (l Vertical) SplitRight(index int) Layout {
return Vertical{
Left: l.Left.SplitRight(index),
Right: l.Right.SplitRight(index),
}
}
// Count returns the width and height counts.
func (l Vertical) Count() (int, int) {
w1, h1 := l.Left.Count()
w2, h2 := l.Right.Count()
return w1 + w2, max(h1, h2)
}
// Activate the specific window layout.
func (l Vertical) Activate(i int) Layout {
return Vertical{
Left: l.Left.Activate(i),
Right: l.Right.Activate(i),
left: l.left,
top: l.top,
width: l.width,
height: l.height,
}
}
// ActivateFirst the first layout.
func (l Vertical) ActivateFirst() Layout {
return Vertical{
Left: l.Left.ActivateFirst(),
Right: l.Right,
left: l.left,
top: l.top,
width: l.width,
height: l.height,
}
}
// ActiveWindow returns the active window.
func (l Vertical) ActiveWindow() Window {
if layout := l.Left.ActiveWindow(); layout.Index >= 0 {
return layout
}
return l.Right.ActiveWindow()
}
// Lookup search for the window meets the condition.
func (l Vertical) Lookup(cond func(Window) bool) Window {
if layout := l.Left.Lookup(cond); layout.Index >= 0 {
return layout
}
return l.Right.Lookup(cond)
}
// Close the active layout.
func (l Vertical) Close() Layout {
if m, ok := l.Left.(Window); ok {
if m.Active {
return l.Right.ActivateFirst()
}
}
if m, ok := l.Right.(Window); ok {
if m.Active {
return l.Left.ActivateFirst()
}
}
return Vertical{
Left: l.Left.Close(),
Right: l.Right.Close(),
}
}

View File

@ -1,307 +0,0 @@
package layout
import (
"reflect"
"testing"
)
func TestLayout(t *testing.T) {
layout := NewLayout(0)
layout = layout.SplitTop(1)
layout = layout.SplitLeft(2)
layout = layout.SplitBottom(3)
layout = layout.SplitRight(4)
var expected Layout
expected = Horizontal{
Top: Vertical{
Left: Horizontal{
Top: Window{Index: 2, Active: false},
Bottom: Vertical{
Left: Window{Index: 3, Active: false},
Right: Window{Index: 4, Active: true},
},
},
Right: Window{Index: 1, Active: false},
},
Bottom: Window{Index: 0, Active: false},
}
if !reflect.DeepEqual(layout, expected) {
t.Errorf("layout should be %#v but got %#v", expected, layout)
}
w, h := layout.Count()
if w != 3 {
t.Errorf("layout width be %d but got %d", 3, w)
}
if h != 3 {
t.Errorf("layout height be %d but got %d", 3, h)
}
layout = layout.Resize(0, 0, 15, 15)
expected = Horizontal{
Top: Vertical{
Left: Horizontal{
Top: Window{Index: 2, Active: false, left: 0, top: 0, width: 10, height: 5},
Bottom: Vertical{
Left: Window{Index: 3, Active: false, left: 0, top: 5, width: 5, height: 5},
Right: Window{Index: 4, Active: true, left: 6, top: 5, width: 4, height: 5},
left: 0,
top: 5,
width: 10,
height: 5,
},
left: 0,
top: 0,
width: 10,
height: 10,
},
Right: Window{Index: 1, Active: false, left: 11, top: 0, width: 4, height: 10},
left: 0,
top: 0,
width: 15,
height: 10,
},
Bottom: Window{Index: 0, Active: false, left: 0, top: 10, width: 15, height: 5},
left: 0,
top: 0,
width: 15,
height: 15,
}
if !reflect.DeepEqual(layout, expected) {
t.Errorf("layout should be %#v but got %#v", expected, layout)
}
expectedWindow := Window{Index: 1, Active: false, left: 11, top: 0, width: 4, height: 10}
got := layout.Lookup(func(l Window) bool { return l.Index == 1 })
if !reflect.DeepEqual(got, expectedWindow) {
t.Errorf("Lookup(Index == 1) should be %+v but got %+v", expectedWindow, got)
}
if got.LeftMargin() != 11 {
t.Errorf("LeftMargin() should be %d but got %d", 11, got.LeftMargin())
}
if got.TopMargin() != 0 {
t.Errorf("TopMargin() should be %d but got %d", 0, got.TopMargin())
}
if got.Width() != 4 {
t.Errorf("Width() should be %d but got %d", 4, got.Width())
}
if got.Height() != 10 {
t.Errorf("Height() should be %d but got %d", 10, got.Height())
}
expectedWindow = Window{Index: 3, Active: false, left: 0, top: 5, width: 5, height: 5}
got = layout.Lookup(func(l Window) bool { return l.Index == 3 })
if !reflect.DeepEqual(got, expectedWindow) {
t.Errorf("Lookup(Index == 3) should be %+v but got %+v", expectedWindow, got)
}
if got.LeftMargin() != 0 {
t.Errorf("LeftMargin() should be %d but got %d", 0, got.LeftMargin())
}
if got.TopMargin() != 5 {
t.Errorf("TopMargin() should be %d but got %d", 5, got.TopMargin())
}
if got.Width() != 5 {
t.Errorf("Width() should be %d but got %d", 5, got.Width())
}
if got.Height() != 5 {
t.Errorf("Height() should be %d but got %d", 5, got.Height())
}
expectedWindow = Window{Index: -1}
got = layout.Lookup(func(l Window) bool { return l.Index == 5 })
if !reflect.DeepEqual(got, expectedWindow) {
t.Errorf("Lookup(Index == 5) should be %+v but got %+v", expectedWindow, got)
}
expectedWindow = Window{Index: 4, Active: true, left: 6, top: 5, width: 4, height: 5}
got = layout.ActiveWindow()
if !reflect.DeepEqual(got, expectedWindow) {
t.Errorf("ActiveWindow() should be %+v but got %+v", expectedWindow, got)
}
expectedMap := map[int]Window{
0: {Index: 0, Active: false, left: 0, top: 10, width: 15, height: 5},
1: {Index: 1, Active: false, left: 11, top: 0, width: 4, height: 10},
2: {Index: 2, Active: false, left: 0, top: 0, width: 10, height: 5},
3: {Index: 3, Active: false, left: 0, top: 5, width: 5, height: 5},
4: {Index: 4, Active: true, left: 6, top: 5, width: 4, height: 5},
}
if !reflect.DeepEqual(layout.Collect(), expectedMap) {
t.Errorf("Collect should be %+v but got %+v", expectedMap, layout.Collect())
}
layout = layout.Close().Resize(0, 0, 15, 15)
expected = Horizontal{
Top: Vertical{
Left: Horizontal{
Top: Window{Index: 2, Active: false, left: 0, top: 0, width: 7, height: 5},
Bottom: Window{Index: 3, Active: true, left: 0, top: 5, width: 7, height: 5},
left: 0,
top: 0,
width: 7,
height: 10,
},
Right: Window{Index: 1, Active: false, left: 8, top: 0, width: 7, height: 10},
left: 0,
top: 0,
width: 15,
height: 10,
},
Bottom: Window{Index: 0, Active: false, left: 0, top: 10, width: 15, height: 5},
left: 0,
top: 0,
width: 15,
height: 15,
}
if !reflect.DeepEqual(layout, expected) {
t.Errorf("layout should be %#v but got %#v", expected, layout)
}
if layout.LeftMargin() != 0 {
t.Errorf("LeftMargin() should be %d but layout %d", 0, layout.LeftMargin())
}
if layout.TopMargin() != 0 {
t.Errorf("TopMargin() should be %d but layout %d", 0, layout.TopMargin())
}
if layout.Width() != 15 {
t.Errorf("Width() should be %d but layout %d", 15, layout.Width())
}
if layout.Height() != 15 {
t.Errorf("Height() should be %d but layout %d", 15, layout.Height())
}
expectedMap = map[int]Window{
0: {Index: 0, Active: false, left: 0, top: 10, width: 15, height: 5},
1: {Index: 1, Active: false, left: 8, top: 0, width: 7, height: 10},
2: {Index: 2, Active: false, left: 0, top: 0, width: 7, height: 5},
3: {Index: 3, Active: true, left: 0, top: 5, width: 7, height: 5},
}
if !reflect.DeepEqual(layout.Collect(), expectedMap) {
t.Errorf("Collect should be %+v but got %+v", expectedMap, layout.Collect())
}
w, h = layout.Count()
if w != 2 {
t.Errorf("layout width be %d but got %d", 3, w)
}
if h != 3 {
t.Errorf("layout height be %d but got %d", 3, h)
}
layout = layout.Replace(5)
expected = Horizontal{
Top: Vertical{
Left: Horizontal{
Top: Window{Index: 2, Active: false, left: 0, top: 0, width: 7, height: 5},
Bottom: Window{Index: 5, Active: true, left: 0, top: 5, width: 7, height: 5},
left: 0,
top: 0,
width: 7,
height: 10,
},
Right: Window{Index: 1, Active: false, left: 8, top: 0, width: 7, height: 10},
left: 0,
top: 0,
width: 15,
height: 10,
},
Bottom: Window{Index: 0, Active: false, left: 0, top: 10, width: 15, height: 5},
left: 0,
top: 0,
width: 15,
height: 15,
}
if !reflect.DeepEqual(layout, expected) {
t.Errorf("layout should be %#v but got %#v", expected, layout)
}
layout = layout.Activate(1)
expected = Horizontal{
Top: Vertical{
Left: Horizontal{
Top: Window{Index: 2, Active: false, left: 0, top: 0, width: 7, height: 5},
Bottom: Window{Index: 5, Active: false, left: 0, top: 5, width: 7, height: 5},
left: 0,
top: 0,
width: 7,
height: 10,
},
Right: Window{Index: 1, Active: true, left: 8, top: 0, width: 7, height: 10},
left: 0,
top: 0,
width: 15,
height: 10,
},
Bottom: Window{Index: 0, Active: false, left: 0, top: 10, width: 15, height: 5},
left: 0,
top: 0,
width: 15,
height: 15,
}
if !reflect.DeepEqual(layout, expected) {
t.Errorf("layout should be %#v but got %#v", expected, layout)
}
layout = Vertical{
Left: Window{Index: 6, Active: false},
Right: layout,
}.SplitLeft(7).SplitTop(8).Resize(0, 0, 15, 10)
expected = Vertical{
Left: Window{Index: 6, Active: false, left: 0, top: 0, width: 3, height: 10},
Right: Horizontal{
Top: Vertical{
Left: Horizontal{
Top: Window{Index: 2, Active: false, left: 4, top: 0, width: 3, height: 3},
Bottom: Window{Index: 5, Active: false, left: 4, top: 3, width: 3, height: 3},
left: 4, top: 0, width: 3, height: 6,
},
Right: Vertical{
Left: Horizontal{
Top: Window{Index: 8, Active: true, left: 8, top: 0, width: 3, height: 3},
Bottom: Window{Index: 7, Active: false, left: 8, top: 3, width: 3, height: 3},
left: 8, top: 0, width: 3, height: 6,
},
Right: Window{Index: 1, Active: false, left: 12, top: 0, width: 3, height: 6},
left: 8, top: 0, width: 7, height: 6,
},
left: 4, top: 0, width: 11, height: 6,
},
Bottom: Window{Index: 0, Active: false, left: 4, top: 6, width: 11, height: 4},
left: 4, top: 0, width: 11, height: 10,
},
left: 0, top: 0, width: 15, height: 10,
}
if !reflect.DeepEqual(layout, expected) {
t.Errorf("layout should be %#v but got %#v", expected, layout)
}
if layout.LeftMargin() != 0 {
t.Errorf("LeftMargin() should be %d but layout %d", 0, layout.LeftMargin())
}
if layout.TopMargin() != 0 {
t.Errorf("TopMargin() should be %d but layout %d", 0, layout.TopMargin())
}
if layout.Width() != 15 {
t.Errorf("Width() should be %d but layout %d", 15, layout.Width())
}
if layout.Height() != 10 {
t.Errorf("Height() should be %d but layout %d", 10, layout.Height())
}
}

View File

@ -1,14 +0,0 @@
package mode
// Mode ...
type Mode int
// Modes
const (
Normal Mode = iota
Insert
Replace
Visual
Cmdline
Search
)

View File

@ -1,155 +0,0 @@
package searcher
import (
"errors"
"unicode/utf8"
)
func patternToTarget(pattern string) ([]byte, error) {
if len(pattern) > 3 && pattern[0] == '0' {
switch pattern[1] {
case 'x', 'X':
return decodeHexLiteral(pattern)
case 'b', 'B':
return decodeBinLiteral(pattern)
}
}
return unescapePattern(pattern), nil
}
func decodeHexLiteral(pattern string) ([]byte, error) {
bs := make([]byte, 0, len(pattern)/2+1)
var c byte
var lower bool
for i := 2; i < len(pattern); i++ {
if !isHex(pattern[i]) {
return nil, errors.New("invalid hex pattern: " + pattern)
}
c = c<<4 | hexToDigit(pattern[i])
if lower {
bs = append(bs, c)
c = 0
}
lower = !lower
}
if lower {
bs = append(bs, c<<4)
}
return bs, nil
}
func decodeBinLiteral(pattern string) ([]byte, error) {
bs := make([]byte, 0, len(pattern)/16+1)
var c byte
var bits int
for i := 2; i < len(pattern); i++ {
if !isBin(pattern[i]) {
return nil, errors.New("invalid bin pattern: " + pattern)
}
c = c<<1 | hexToDigit(pattern[i])
bits++
if bits == 8 {
bits = 0
bs = append(bs, c)
c = 0
}
}
if bits > 0 {
bs = append(bs, c<<uint(8-bits))
}
return bs, nil
}
func unescapePattern(pattern string) []byte {
var escape bool
var buf [4]byte
bs := make([]byte, 0, len(pattern))
for i := 0; i < len(pattern); i++ {
b := pattern[i]
if escape {
switch b {
case '0':
bs = append(bs, 0)
case 'a':
bs = append(bs, '\a')
case 'b':
bs = append(bs, '\b')
case 'f':
bs = append(bs, '\f')
case 'n':
bs = append(bs, '\n')
case 'r':
bs = append(bs, '\r')
case 't':
bs = append(bs, '\t')
case 'v':
bs = append(bs, '\v')
case 'x', 'u', 'U':
var n int
switch b {
case 'x':
n = 2
case 'u':
n = 4
case 'U':
n = 8
}
appended := true
var c rune
if i+n < len(pattern) {
for k := 1; k <= n; k++ {
if !isHex(pattern[i+k]) {
appended = false
break
}
c = c<<4 | rune(hexToDigit(pattern[i+k]))
}
if appended {
if b == 'x' {
bs = append(bs, byte(c))
} else {
n := utf8.EncodeRune(buf[:], c)
bs = append(bs, buf[:n]...)
}
i += n
}
}
if !appended {
bs = append(bs, b)
}
default:
bs = append(bs, b)
}
escape = false
} else if b == '\\' {
escape = true
} else {
bs = append(bs, b)
}
}
if escape {
bs = append(bs, '\\')
}
return bs
}
func isHex(b byte) bool {
return '0' <= b && b <= '9' || 'A' <= b && b <= 'F' || 'a' <= b && b <= 'f'
}
func hexToDigit(b byte) byte {
switch {
case '0' <= b && b <= '9':
return b - '0'
case 'A' <= b && b <= 'F':
return b - 'A' + 10
case 'a' <= b && b <= 'f':
return b - 'a' + 10
default:
return 0
}
}
func isBin(b byte) bool {
return b == '0' || b == '1'
}

View File

@ -1,143 +0,0 @@
package searcher
import (
"bytes"
"errors"
"io"
"sync"
"time"
)
const loadSize = 1024 * 1024
// Searcher represents a searcher.
type Searcher struct {
r io.ReaderAt
bytes []byte
loopCh chan struct{}
cursor int64
pattern string
mu *sync.Mutex
}
// NewSearcher creates a new searcher.
func NewSearcher(r io.ReaderAt) *Searcher {
return &Searcher{r: r, mu: new(sync.Mutex)}
}
type errNotFound string
func (err errNotFound) Error() string {
return "pattern not found: " + string(err)
}
// Search the pattern.
func (s *Searcher) Search(cursor int64, pattern string, forward bool) <-chan any {
s.mu.Lock()
defer s.mu.Unlock()
if s.bytes == nil {
s.bytes = make([]byte, loadSize)
}
s.cursor, s.pattern = cursor, pattern
ch := make(chan any)
if forward {
s.loop(s.forward, ch)
} else {
s.loop(s.backward, ch)
}
return ch
}
func (s *Searcher) forward() (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
target, err := patternToTarget(s.pattern)
if err != nil {
return -1, err
}
base := s.cursor + 1
n, err := s.r.ReadAt(s.bytes, base)
if err != nil && err != io.EOF {
return -1, err
}
if n == 0 {
return -1, errNotFound(s.pattern)
}
if err == io.EOF {
s.cursor += int64(n)
} else {
s.cursor += int64(n - len(target) + 1)
}
i := bytes.Index(s.bytes[:n], target)
if i >= 0 {
return base + int64(i), nil
}
return -1, nil
}
func (s *Searcher) backward() (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
target, err := patternToTarget(s.pattern)
if err != nil {
return -1, err
}
base := max(0, s.cursor-int64(loadSize))
size := int(min(int64(loadSize), s.cursor))
n, err := s.r.ReadAt(s.bytes[:size], base)
if err != nil && err != io.EOF {
return -1, err
}
if n == 0 {
return -1, errNotFound(s.pattern)
}
if s.cursor == int64(n) {
s.cursor = 0
} else {
s.cursor = base + int64(len(target)-1)
}
i := bytes.LastIndex(s.bytes[:n], target)
if i >= 0 {
return base + int64(i), nil
}
return -1, nil
}
func (s *Searcher) loop(f func() (int64, error), ch chan<- any) {
if s.loopCh != nil {
close(s.loopCh)
}
loopCh := make(chan struct{})
s.loopCh = loopCh
go func() {
defer close(ch)
for {
select {
case <-loopCh:
return
case <-time.After(10 * time.Millisecond):
idx, err := f()
if err != nil {
ch <- err
return
}
if idx >= 0 {
ch <- idx
return
}
}
}
}()
}
// Abort the searching.
func (s *Searcher) Abort() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.loopCh != nil {
close(s.loopCh)
s.loopCh = nil
return errors.New("search is aborted")
}
return nil
}

View File

@ -1,181 +0,0 @@
package searcher
import (
"strings"
"testing"
)
func TestSearcher(t *testing.T) {
testCases := []struct {
name string
str string
cursor int64
pattern string
forward bool
expected int64
err error
}{
{
name: "search forward",
str: "abcde",
cursor: 0,
pattern: "cd",
forward: true,
expected: 2,
},
{
name: "search forward but not found",
str: "abcde",
cursor: 2,
pattern: "cd",
forward: true,
err: errNotFound("cd"),
},
{
name: "search backward",
str: "abcde",
cursor: 4,
pattern: "bc",
forward: false,
expected: 1,
},
{
name: "search backward but not found",
str: "abcde",
cursor: 0,
pattern: "ba",
forward: true,
err: errNotFound("ba"),
},
{
name: "search large target forward",
str: strings.Repeat(" ", 10*1024*1024+100) + "abcde",
cursor: 102,
pattern: "bcd",
forward: true,
expected: 10*1024*1024 + 101,
},
{
name: "search large target forward but not found",
str: strings.Repeat(" ", 10*1024*1024+100) + "abcde",
cursor: 102,
pattern: "cba",
forward: true,
err: errNotFound("cba"),
},
{
name: "search large target backward",
str: "abcde" + strings.Repeat(" ", 10*1024*1024),
cursor: 10*1024*1024 + 2,
pattern: "bcd",
forward: false,
expected: 1,
},
{
name: "search large target backward but not found",
str: "abcde" + strings.Repeat(" ", 10*1024*1024),
cursor: 10*1024*1024 + 2,
pattern: "cba",
forward: false,
err: errNotFound("cba"),
},
{
name: "search hex",
str: "\x13\x24\x35\x46\x57\x68",
cursor: 0,
pattern: `\x35\x46\x57`,
forward: true,
expected: 2,
},
{
name: "search nul",
str: "\x06\x07\x08\x00\x09\x10\x11",
cursor: 0,
pattern: `\0`,
forward: true,
expected: 3,
},
{
name: "search bell and bs",
str: "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x09\x0a",
cursor: 0,
pattern: `\a\b\v`,
forward: true,
expected: 7,
},
{
name: "search tab",
str: "\x06\x07\x08\x09\x10\x11",
cursor: 0,
pattern: `\t`,
forward: true,
expected: 3,
},
{
name: "search escape character",
str: `ab\cd\\e`,
cursor: 0,
pattern: `\\\`,
forward: true,
expected: 5,
},
{
name: "search unicode",
str: "\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf",
cursor: 0,
pattern: `\u3061\u306F`,
forward: true,
expected: 9,
},
{
name: "search unicode in supplementary multilingual plane",
str: "\U0001F604\U0001F606\U0001F60E\U0001F60D\U0001F642",
cursor: 0,
pattern: `\U0001F60E\U0001F60D`,
forward: true,
expected: 8,
},
{
name: "search hex literal",
str: "\x16\x27\x38\x49\x50\x61",
cursor: 0,
pattern: `0x38495`,
forward: true,
expected: 2,
},
{
name: "search bin literal",
str: "\x16\x27\x38\x48\x50\x61",
cursor: 0,
pattern: `0b0011100001001`,
forward: true,
expected: 2,
},
{
name: "search text starting with 0",
str: "432101234",
cursor: 0,
pattern: `0123`,
forward: true,
expected: 4,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
s := NewSearcher(strings.NewReader(testCase.str))
ch := s.Search(testCase.cursor, testCase.pattern, testCase.forward)
switch x := (<-ch).(type) {
case error:
if testCase.err == nil {
t.Error(x)
} else if x != testCase.err {
t.Errorf("Error should be %v but got %v", testCase.err, x)
}
case int64:
if x != testCase.expected {
t.Errorf("Search result should be %d but got %d", testCase.expected, x)
}
}
})
}
}

View File

@ -1,45 +0,0 @@
package state
import (
"b612.me/apps/b612/bed/layout"
"b612.me/apps/b612/bed/mode"
)
// State holds the state of the editor to display the user interface.
type State struct {
Mode mode.Mode
PrevMode mode.Mode
WindowStates map[int]*WindowState
Layout layout.Layout
Cmdline []rune
CmdlineCursor int
CompletionResults []string
CompletionIndex int
SearchMode rune
Error error
ErrorType int
}
// WindowState holds the state of one window.
type WindowState struct {
Name string
Modified bool
Width int
Offset int64
Cursor int64
Bytes []byte
Size int
Length int64
Mode mode.Mode
Pending bool
PendingByte byte
VisualStart int64
EditedIndices []int64
FocusText bool
}
// Message types
const (
MessageInfo = iota
MessageError
)

View File

@ -1,71 +0,0 @@
package tui
import (
"github.com/gdamore/tcell"
"b612.me/apps/b612/bed/key"
)
func eventToKey(event *tcell.EventKey) key.Key {
if key, ok := keyMap[event.Key()]; ok {
return key
}
return key.Key(event.Rune())
}
var keyMap = map[tcell.Key]key.Key{
tcell.KeyF1: key.Key("f1"),
tcell.KeyF2: key.Key("f2"),
tcell.KeyF3: key.Key("f3"),
tcell.KeyF4: key.Key("f4"),
tcell.KeyF5: key.Key("f5"),
tcell.KeyF6: key.Key("f6"),
tcell.KeyF7: key.Key("f7"),
tcell.KeyF8: key.Key("f8"),
tcell.KeyF9: key.Key("f9"),
tcell.KeyF10: key.Key("f10"),
tcell.KeyF11: key.Key("f11"),
tcell.KeyF12: key.Key("f12"),
tcell.KeyInsert: key.Key("insert"),
tcell.KeyDelete: key.Key("delete"),
tcell.KeyHome: key.Key("home"),
tcell.KeyEnd: key.Key("end"),
tcell.KeyPgUp: key.Key("pgup"),
tcell.KeyPgDn: key.Key("pgdn"),
tcell.KeyUp: key.Key("up"),
tcell.KeyDown: key.Key("down"),
tcell.KeyLeft: key.Key("left"),
tcell.KeyRight: key.Key("right"),
tcell.KeyCtrlA: key.Key("c-a"),
tcell.KeyCtrlB: key.Key("c-b"),
tcell.KeyCtrlC: key.Key("c-c"),
tcell.KeyCtrlD: key.Key("c-d"),
tcell.KeyCtrlE: key.Key("c-e"),
tcell.KeyCtrlF: key.Key("c-f"),
tcell.KeyCtrlG: key.Key("c-g"),
tcell.KeyBackspace: key.Key("backspace"),
tcell.KeyTab: key.Key("tab"),
tcell.KeyBacktab: key.Key("backtab"),
tcell.KeyCtrlJ: key.Key("c-j"),
tcell.KeyCtrlK: key.Key("c-k"),
tcell.KeyCtrlL: key.Key("c-l"),
tcell.KeyEnter: key.Key("enter"),
tcell.KeyCtrlN: key.Key("c-n"),
tcell.KeyCtrlO: key.Key("c-o"),
tcell.KeyCtrlP: key.Key("c-p"),
tcell.KeyCtrlQ: key.Key("c-q"),
tcell.KeyCtrlR: key.Key("c-r"),
tcell.KeyCtrlS: key.Key("c-s"),
tcell.KeyCtrlT: key.Key("c-t"),
tcell.KeyCtrlU: key.Key("c-u"),
tcell.KeyCtrlV: key.Key("c-v"),
tcell.KeyCtrlW: key.Key("c-w"),
tcell.KeyCtrlX: key.Key("c-x"),
tcell.KeyCtrlY: key.Key("c-y"),
tcell.KeyCtrlZ: key.Key("c-z"),
tcell.KeyEsc: key.Key("escape"),
tcell.KeyBackspace2: key.Key("backspace2"),
}

View File

@ -1,20 +0,0 @@
package tui
import "b612.me/apps/b612/bed/layout"
type region struct {
left, top, height, width int
}
func fromLayout(l layout.Layout) region {
return region{
left: l.LeftMargin(),
top: l.TopMargin(),
height: l.Height(),
width: l.Width(),
}
}
func (r region) valid() bool {
return 0 <= r.left && 0 <= r.top && 0 < r.height && 0 < r.width
}

View File

@ -1,63 +0,0 @@
package tui
import (
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
)
type textDrawer struct {
top, left, offset int
region region
screen tcell.Screen
}
func (d *textDrawer) setString(str string, style tcell.Style) {
top := d.region.top + d.top
left := d.region.left + d.left + d.offset
right := d.region.left + d.region.width
for _, c := range str {
w := runewidth.RuneWidth(c)
if left+w > right {
break
}
if left+w == right && c != ' ' {
if int(style)&int(tcell.AttrReverse) != 0 {
d.screen.SetContent(left, top, ' ', nil, style)
}
break
}
d.screen.SetContent(left, top, c, nil, style)
left += w
}
}
func (d *textDrawer) setByte(b byte, style tcell.Style) {
top := d.region.top + d.top
left := d.region.left + d.left + d.offset
d.screen.SetContent(left, top, rune(b), nil, style)
}
func (d *textDrawer) setTop(top int) *textDrawer {
d.top = top
return d
}
func (d *textDrawer) addTop(diff int) *textDrawer {
d.top += diff
return d
}
func (d *textDrawer) setLeft(left int) *textDrawer {
d.left = left
return d
}
func (d *textDrawer) addLeft(diff int) *textDrawer {
d.left += diff
return d
}
func (d *textDrawer) setOffset(offset int) *textDrawer {
d.offset = offset
return d
}

View File

@ -1,196 +0,0 @@
package tui
import (
"bytes"
"strings"
"sync"
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/key"
"b612.me/apps/b612/bed/layout"
"b612.me/apps/b612/bed/mode"
"b612.me/apps/b612/bed/state"
)
// Tui implements UI
type Tui struct {
eventCh chan<- event.Event
mode mode.Mode
screen tcell.Screen
waitCh chan struct{}
mu *sync.Mutex
}
// NewTui creates a new Tui.
func NewTui() *Tui {
return &Tui{mu: new(sync.Mutex)}
}
// Init initializes the Tui.
func (ui *Tui) Init(eventCh chan<- event.Event) (err error) {
ui.mu.Lock()
defer ui.mu.Unlock()
ui.eventCh = eventCh
ui.mode = mode.Normal
if ui.screen, err = tcell.NewScreen(); err != nil {
return
}
ui.waitCh = make(chan struct{})
return ui.screen.Init()
}
// Run the Tui.
func (ui *Tui) Run(kms map[mode.Mode]*key.Manager) {
for {
e := ui.screen.PollEvent()
switch ev := e.(type) {
case *tcell.EventKey:
var e event.Event
if km, ok := kms[ui.getMode()]; ok {
e = km.Press(eventToKey(ev))
}
if e.Type != event.Nop {
ui.eventCh <- e
} else {
ui.eventCh <- event.Event{Type: event.Rune, Rune: ev.Rune()}
}
case *tcell.EventResize:
if ui.eventCh != nil {
ui.eventCh <- event.Event{Type: event.Redraw}
}
case nil:
close(ui.waitCh)
return
}
}
}
func (ui *Tui) getMode() mode.Mode {
ui.mu.Lock()
defer ui.mu.Unlock()
return ui.mode
}
// Size returns the size for the screen.
func (ui *Tui) Size() (int, int) {
return ui.screen.Size()
}
// Redraw redraws the state.
func (ui *Tui) Redraw(s state.State) error {
ui.mu.Lock()
defer ui.mu.Unlock()
ui.mode = s.Mode
ui.screen.Clear()
ui.drawWindows(s.WindowStates, s.Layout)
ui.drawCmdline(s)
ui.screen.Show()
return nil
}
func (ui *Tui) setLine(line, offset int, str string, style tcell.Style) {
for _, c := range str {
ui.screen.SetContent(offset, line, c, nil, style)
offset += runewidth.RuneWidth(c)
}
}
func (ui *Tui) drawWindows(windowStates map[int]*state.WindowState, l layout.Layout) {
switch l := l.(type) {
case layout.Window:
r := fromLayout(l)
if ws, ok := windowStates[l.Index]; ok && r.valid() {
ui.newTuiWindow(r).drawWindow(ws,
l.Active && ui.mode != mode.Cmdline && ui.mode != mode.Search)
}
case layout.Horizontal:
ui.drawWindows(windowStates, l.Top)
ui.drawWindows(windowStates, l.Bottom)
case layout.Vertical:
ui.drawWindows(windowStates, l.Left)
ui.drawWindows(windowStates, l.Right)
ui.drawVerticalSplit(fromLayout(l.Left))
}
}
func (ui *Tui) newTuiWindow(region region) *tuiWindow {
return &tuiWindow{region: region, screen: ui.screen}
}
func (ui *Tui) drawVerticalSplit(region region) {
for i := range region.height {
ui.setLine(region.top+i, region.left+region.width, "|", tcell.StyleDefault.Reverse(true))
}
}
func (ui *Tui) drawCmdline(s state.State) {
var cmdline string
style := tcell.StyleDefault
width, height := ui.Size()
switch {
case s.Error != nil:
cmdline = s.Error.Error()
if s.ErrorType == state.MessageInfo {
style = style.Foreground(tcell.ColorYellow)
} else {
style = style.Foreground(tcell.ColorRed)
}
case s.Mode == mode.Cmdline:
if len(s.CompletionResults) > 0 {
ui.drawCompletionResults(s.CompletionResults, s.CompletionIndex, width, height)
}
ui.screen.ShowCursor(1+runewidth.StringWidth(string(s.Cmdline[:s.CmdlineCursor])), height-1)
fallthrough
case s.PrevMode == mode.Cmdline && len(s.Cmdline) > 0:
cmdline = ":" + string(s.Cmdline)
case s.Mode == mode.Search:
ui.screen.ShowCursor(1+runewidth.StringWidth(string(s.Cmdline[:s.CmdlineCursor])), height-1)
fallthrough
case s.SearchMode != '\x00':
cmdline = string(s.SearchMode) + string(s.Cmdline)
default:
return
}
ui.setLine(height-1, 0, cmdline, style)
}
func (ui *Tui) drawCompletionResults(results []string, index, width, height int) {
var line bytes.Buffer
var left, right int
for i, result := range results {
size := runewidth.StringWidth(result) + 2
if i <= index {
left, right = right, right+size
if right > width {
line.Reset()
left, right = 0, size
}
} else if right < width {
right += size
} else {
break
}
line.WriteString(" ")
line.WriteString(result)
line.WriteString(" ")
}
line.WriteString(strings.Repeat(" ", max(width-right, 0)))
ui.setLine(height-2, 0, line.String(), tcell.StyleDefault.Reverse(true))
if index >= 0 {
ui.setLine(height-2, left, " "+results[index]+" ",
tcell.StyleDefault.Foreground(tcell.ColorGrey).Reverse(true))
}
}
// Close terminates the Tui.
func (ui *Tui) Close() error {
ui.mu.Lock()
defer ui.mu.Unlock()
ui.eventCh = nil
ui.screen.Fini()
<-ui.waitCh
return nil
}

View File

@ -1,415 +0,0 @@
package tui
import (
"errors"
"strings"
"testing"
"github.com/gdamore/tcell"
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/key"
"b612.me/apps/b612/bed/layout"
"b612.me/apps/b612/bed/mode"
"b612.me/apps/b612/bed/state"
)
func (ui *Tui) initForTest(eventCh chan<- event.Event, screen tcell.SimulationScreen) (err error) {
ui.eventCh = eventCh
ui.mode = mode.Normal
ui.screen = screen
ui.waitCh = make(chan struct{})
return ui.screen.Init()
}
func mockKeyManager() map[mode.Mode]*key.Manager {
kms := make(map[mode.Mode]*key.Manager)
km := key.NewManager(true)
km.Register(event.Quit, "Z", "Q")
km.Register(event.CursorDown, "j")
kms[mode.Normal] = km
return kms
}
func getContents(screen tcell.SimulationScreen) string {
width, _ := screen.Size()
cells, _, _ := screen.GetContents()
var runes []rune
for i, cell := range cells {
runes = append(runes, cell.Runes...)
if (i+1)%width == 0 {
runes = append(runes, '\n')
}
}
return string(runes)
}
func shouldContain(t *testing.T, screen tcell.SimulationScreen, expected []string) {
got := getContents(screen)
for _, str := range expected {
if !strings.Contains(got, str) {
t.Errorf("screen should contain %q but got\n%v", str, got)
}
}
}
func TestTuiRun(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(90, 20)
go ui.Run(mockKeyManager())
screen.InjectKey(tcell.KeyRune, 'Z', tcell.ModNone)
screen.InjectKey(tcell.KeyRune, 'Q', tcell.ModNone)
e := <-eventCh
if e.Type != event.Rune {
t.Errorf("pressing Z should emit event.Rune but got: %+v", e)
}
e = <-eventCh
if e.Type != event.Quit {
t.Errorf("pressing ZQ should emit event.Quit but got: %+v", e)
}
screen.InjectKey(tcell.KeyRune, '7', tcell.ModNone)
screen.InjectKey(tcell.KeyRune, '0', tcell.ModNone)
screen.InjectKey(tcell.KeyRune, '9', tcell.ModNone)
screen.InjectKey(tcell.KeyRune, 'j', tcell.ModNone)
e = <-eventCh
e = <-eventCh
e = <-eventCh
e = <-eventCh
if e.Type != event.CursorDown {
t.Errorf("pressing 709j should emit event.CursorDown but got: %+v", e)
}
if e.Count != 709 {
t.Errorf("pressing 709j should emit event with count %d but got: %+v", 709, e)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiEmpty(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(90, 20)
width, height := screen.Size()
go ui.Run(mockKeyManager())
s := state.State{
WindowStates: map[int]*state.WindowState{
0: {
Name: "",
Modified: false,
Width: 16,
Offset: 0,
Cursor: 0,
Bytes: []byte(strings.Repeat("\x00", 16*(height-1))),
Size: 16 * (height - 1),
Length: 0,
Mode: mode.Normal,
},
},
Layout: layout.NewLayout(0).Resize(0, 0, width, height-1),
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ",
" 000000 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" 000010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" 000020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" 000100 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" [No name] : 0x00 : '\\x00' 0/0 : 0x000000/0x000000 : 0.00%",
})
x, y, visible := screen.GetCursor()
if x != 10 || y != 1 {
t.Errorf("cursor position should be (%d, %d) but got (%d, %d)", 10, 1, x, y)
}
if visible != true {
t.Errorf("cursor should be visible but got %v", visible)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiScrollBar(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(90, 20)
width, height := screen.Size()
go ui.Run(mockKeyManager())
s := state.State{
WindowStates: map[int]*state.WindowState{
0: {
Name: "",
Modified: true,
Width: 16,
Offset: 0,
Cursor: 0,
Bytes: []byte(strings.Repeat("a", 16*(height-1))),
Size: 16 * (height - 1),
Length: int64(16 * (height - 1) * 3),
Mode: mode.Normal,
},
},
Layout: layout.NewLayout(0).Resize(0, 0, width, height-1),
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ",
" 000000 | 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa # ",
" 000050 | 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa # ",
" 000060 | 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa | ",
" 000100 | 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa | ",
" [No name] : + : 0x61 : 'a' 0/912 : 0x000000/0x000390 : 0.00%",
})
x, y, visible := screen.GetCursor()
if x != 10 || y != 1 {
t.Errorf("cursor position should be (%d, %d) but got (%d, %d)", 10, 1, x, y)
}
if visible != true {
t.Errorf("cursor should be visible but got %v", visible)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiHorizontalSplit(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(110, 20)
width, height := screen.Size()
go ui.Run(mockKeyManager())
s := state.State{
WindowStates: map[int]*state.WindowState{
0: {
Name: "test0",
Modified: false,
Width: 16,
Offset: 0,
Cursor: 0,
Bytes: []byte("Test window 0." + strings.Repeat("\x00", 110*10)),
Size: 110 * 10,
Length: 600,
Mode: mode.Normal,
},
1: {
Name: "test1",
Modified: false,
Width: 16,
Offset: 0,
Cursor: 0,
Bytes: []byte("Test window 1." + strings.Repeat(" ", 110*10)),
Size: 110 * 10,
Length: 800,
Mode: mode.Normal,
},
},
Layout: layout.NewLayout(0).SplitBottom(1).Resize(0, 0, width, height-1),
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" 000000 | 54 65 73 74 20 77 69 6e 64 6f 77 20 30 2e 00 00 | Test window 0... #",
" 000010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" test0 : 0x54 : 'T' 0/600 : 0x000000/0x000258 : 0.00%",
" | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ",
" 000000 | 54 65 73 74 20 77 69 6e 64 6f 77 20 31 2e 20 20 | Test window 1. #",
" 000010 | 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | #",
" test1 : 0x54 : 'T' 0/800 : 0x000000/0x000320 : 0.00%",
})
x, y, visible := screen.GetCursor()
if x != 10 || y != 10 {
t.Errorf("cursor position should be (%d, %d) but got (%d, %d)", 10, 10, x, y)
}
if visible != true {
t.Errorf("cursor should be visible but got %v", visible)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiVerticalSplit(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(110, 20)
width, height := screen.Size()
go ui.Run(mockKeyManager())
s := state.State{
WindowStates: map[int]*state.WindowState{
0: {
Name: "test0",
Modified: false,
Width: 8,
Offset: 0,
Cursor: 0,
Bytes: []byte("Test window 0." + strings.Repeat("\x00", 55*19)),
Size: 55 * 19,
Length: 600,
Mode: mode.Normal,
},
1: {
Name: "test1",
Modified: false,
Width: 8,
Offset: 0,
Cursor: 0,
Bytes: []byte("Test window 1." + strings.Repeat(" ", 54*19)),
Size: 54 * 19,
Length: 800,
Mode: mode.Normal,
},
},
Layout: layout.NewLayout(0).SplitRight(1).Resize(0, 0, width, height-1),
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" | 0 1 2 3 4 5 6 7 | | | 0 1 2 3 4 5 6 7 |",
" 000000 | 54 65 73 74 20 77 69 6e | Test win # | 000000 | 54 65 73 74 20 77 69 6e | Test win #",
" 000008 | 64 6f 77 20 30 2e 00 00 | dow 0... # | 000008 | 64 6f 77 20 31 2e 20 20 | dow 1. #",
" 000010 | 00 00 00 00 00 00 00 00 | ........ # | 000010 | 20 20 20 20 20 20 20 20 | #",
" test0 : 0x54 : 'T' 0/600 : 0x000000/0x000258 : 0.00% | test1 : 0x54 : 'T' 0/800 : 0x000000/0x000320 : 0.00",
})
x, y, visible := screen.GetCursor()
if x != 66 || y != 1 {
t.Errorf("cursor position should be (%d, %d) but got (%d, %d)", 66, 1, x, y)
}
if visible != true {
t.Errorf("cursor should be visible but got %v", visible)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiCmdline(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(20, 15)
getCmdline := func() string {
cells, _, _ := screen.GetContents()
var runes []rune
for _, cell := range cells[20*14:] {
runes = append(runes, cell.Runes...)
}
return string(runes)
}
go ui.Run(mockKeyManager())
s := state.State{
Mode: mode.Cmdline,
Cmdline: []rune("vnew test"),
CmdlineCursor: 9,
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
got, expected := getCmdline(), ":vnew test "
if !strings.HasPrefix(got, expected) {
t.Errorf("cmdline should start with %q but got %q", expected, got)
}
s = state.State{
Mode: mode.Normal,
Error: errors.New("error"),
Cmdline: []rune("vnew test"),
CmdlineCursor: 9,
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
got, expected = getCmdline(), "error "
if !strings.HasPrefix(got, expected) {
t.Errorf("cmdline should start with %q but got %q", expected, got)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiCmdlineCompletionCandidates(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(20, 15)
go ui.Run(mockKeyManager())
s := state.State{
Mode: mode.Cmdline,
Cmdline: []rune("new test2"),
CmdlineCursor: 9,
CompletionResults: []string{"test1", "test2", "test3", "test9/", "/bin/ls"},
CompletionIndex: 1,
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" test1 test2 test3",
":new test2",
})
s.CompletionIndex += 2
s.Cmdline = []rune("new test9/")
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" test3 test9/ /bin",
":new test9/",
})
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}

View File

@ -1,225 +0,0 @@
package tui
import (
"cmp"
"fmt"
"github.com/gdamore/tcell"
"b612.me/apps/b612/bed/mode"
"b612.me/apps/b612/bed/state"
)
type tuiWindow struct {
region region
screen tcell.Screen
}
func (ui *tuiWindow) getTextDrawer() *textDrawer {
return &textDrawer{region: ui.region, screen: ui.screen}
}
func (ui *tuiWindow) setCursor(line, offset int) {
ui.screen.ShowCursor(ui.region.left+offset, ui.region.top+line)
}
func offsetStyleWidth(s *state.WindowState) int {
threshold := int64(0xfffff)
for i := range 10 {
if s.Length <= threshold {
return 6 + i
}
threshold = (threshold << 4) | 0x0f
}
return 16
}
func (ui *tuiWindow) drawWindow(s *state.WindowState, active bool) {
height, width := ui.region.height-2, s.Width
cursorPos := int(s.Cursor - s.Offset)
cursorLine := cursorPos / width
offsetStyleWidth := offsetStyleWidth(s)
eis := s.EditedIndices
for 0 < len(eis) && eis[1] <= s.Offset {
eis = eis[2:]
}
editedColor := tcell.ColorLightSeaGreen
d := ui.getTextDrawer()
var k int
for i := range height {
d.addTop(1).setLeft(0).setOffset(0)
d.setString(
fmt.Sprintf(" %0*x", offsetStyleWidth, s.Offset+int64(i*width)),
tcell.StyleDefault.Bold(i == cursorLine),
)
d.setLeft(offsetStyleWidth + 3)
for j := range width {
b, style := byte(0), tcell.StyleDefault
if s.Pending && i*width+j == cursorPos {
b, style = s.PendingByte, tcell.StyleDefault.Foreground(editedColor)
if s.Mode != mode.Replace {
k--
}
} else if k >= s.Size {
if k == cursorPos {
d.setOffset(3*j+1).setByte(' ', tcell.StyleDefault.Underline(!active || s.FocusText))
d.setOffset(3*width+j+3).setByte(' ', tcell.StyleDefault.Underline(!active || !s.FocusText))
}
k++
continue
} else {
b = s.Bytes[k]
pos := int64(k) + s.Offset
if 0 < len(eis) && eis[0] <= pos && pos < eis[1] {
style = tcell.StyleDefault.Foreground(editedColor)
} else if 0 < len(eis) && eis[1] <= pos {
eis = eis[2:]
}
if s.VisualStart >= 0 && s.Cursor < s.Length &&
(s.VisualStart <= pos && pos <= s.Cursor ||
s.Cursor <= pos && pos <= s.VisualStart) {
style = style.Underline(true)
}
}
style1, style2 := style, style
if i*width+j == cursorPos {
style1 = style1.Reverse(active && !s.FocusText).Bold(
!active || s.FocusText).Underline(!active || s.FocusText)
style2 = style2.Reverse(active && s.FocusText).Bold(
!active || !s.FocusText).Underline(!active || !s.FocusText)
}
d.setOffset(3*j+1).setByte(hex[b>>4], style1)
d.setOffset(3*j+2).setByte(hex[b&0x0f], style1)
d.setOffset(3*width+j+3).setByte(prettyByte(b), style2)
k++
}
d.setOffset(-2).setByte(' ', tcell.StyleDefault)
d.setOffset(-1).setByte('|', tcell.StyleDefault)
d.setOffset(0).setByte(' ', tcell.StyleDefault)
d.addLeft(3*width).setByte(' ', tcell.StyleDefault)
d.setOffset(1).setByte('|', tcell.StyleDefault)
d.setOffset(2).setByte(' ', tcell.StyleDefault)
}
i := int(s.Cursor % int64(width))
if active {
if s.FocusText {
ui.setCursor(cursorLine+1, 3*width+i+6+offsetStyleWidth)
} else if s.Pending {
ui.setCursor(cursorLine+1, 3*i+5+offsetStyleWidth)
} else {
ui.setCursor(cursorLine+1, 3*i+4+offsetStyleWidth)
}
}
ui.drawHeader(s, offsetStyleWidth)
ui.drawScrollBar(s, height, 4*width+7+offsetStyleWidth)
ui.drawFooter(s, offsetStyleWidth)
}
const hex = "0123456789abcdef"
func (ui *tuiWindow) drawHeader(s *state.WindowState, offsetStyleWidth int) {
style := tcell.StyleDefault.Underline(true)
d := ui.getTextDrawer().setLeft(-1)
cursor := int(s.Cursor % int64(s.Width))
for range offsetStyleWidth + 2 {
d.addLeft(1).setByte(' ', style)
}
d.addLeft(1).setByte('|', style)
for i := range s.Width {
d.addLeft(1).setByte(' ', style)
d.addLeft(1).setByte(" 123456789abcdef"[i>>4], style.Bold(cursor == i))
d.addLeft(1).setByte(hex[i&0x0f], style.Bold(cursor == i))
}
d.addLeft(1).setByte(' ', style)
d.addLeft(1).setByte('|', style)
for range s.Width + 3 {
d.addLeft(1).setByte(' ', style)
}
}
func (ui *tuiWindow) drawScrollBar(s *state.WindowState, height, left int) {
stateSize := s.Size
if s.Cursor+1 == s.Length && s.Cursor == s.Offset+int64(s.Size) {
stateSize++
}
total := int64((stateSize + s.Width - 1) / s.Width)
length := max((s.Length+int64(s.Width)-1)/int64(s.Width), 1)
size := max(total*total/length, 1)
pad := (total*total + length - length*size - 1) / max(total-size+1, 1)
top := (s.Offset / int64(s.Width) * total) / (length - pad)
d := ui.getTextDrawer().setLeft(left)
for i := range height {
var b byte
if int(top) <= i && i < int(top+size) {
b = '#'
} else {
b = '|'
}
d.addTop(1).setByte(b, tcell.StyleDefault)
}
}
func (ui *tuiWindow) drawFooter(s *state.WindowState, offsetStyleWidth int) {
var modified string
if s.Modified {
modified = " : +"
}
b := s.Bytes[int(s.Cursor-s.Offset)]
left := fmt.Sprintf(" %s%s%s : 0x%02x : '%s'",
prettyMode(s.Mode), cmp.Or(s.Name, "[No name]"), modified, b, prettyRune(b))
right := fmt.Sprintf("%[1]d/%[2]d : 0x%0[3]*[1]x/0x%0[3]*[2]x : %.2[4]f%% ",
s.Cursor, s.Length, offsetStyleWidth, float64(s.Cursor*100)/float64(max(s.Length, 1)))
line := fmt.Sprintf("%s %*s", left, max(ui.region.width-len(left)-2, 0), right)
ui.getTextDrawer().setTop(ui.region.height-1).setString(line, tcell.StyleDefault.Reverse(true))
}
func prettyByte(b byte) byte {
switch {
case 0x20 <= b && b < 0x7f:
return b
default:
return 0x2e
}
}
func prettyRune(b byte) string {
switch b {
case 0x07:
return "\\a"
case 0x08:
return "\\b"
case 0x09:
return "\\t"
case 0x0a:
return "\\n"
case 0x0b:
return "\\v"
case 0x0c:
return "\\f"
case 0x0d:
return "\\r"
case 0x27:
return "\\'"
default:
if b < 0x20 {
return fmt.Sprintf("\\x%02x", b)
} else if b < 0x7f {
return string(rune(b))
} else {
return fmt.Sprintf("\\u%04x", b)
}
}
}
func prettyMode(m mode.Mode) string {
switch m {
case mode.Insert:
return "[INSERT] "
case mode.Replace:
return "[REPLACE] "
case mode.Visual:
return "[VISUAL] "
default:
return ""
}
}

View File

@ -1,701 +0,0 @@
package window
import (
"bytes"
"errors"
"fmt"
"io"
"math/bits"
"math/rand"
"os"
"os/exec"
"os/signal"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/layout"
"b612.me/apps/b612/bed/state"
)
// Manager manages the windows and files.
type Manager struct {
width int
height int
windows []*window
layout layout.Layout
mu *sync.Mutex
windowIndex int
prevWindowIndex int
prevDir string
files map[string]file
eventCh chan<- event.Event
redrawCh chan<- struct{}
}
type file struct {
path string
file *os.File
perm os.FileMode
}
// NewManager creates a new Manager.
func NewManager() *Manager {
return &Manager{}
}
// Init initializes the Manager.
func (m *Manager) Init(eventCh chan<- event.Event, redrawCh chan<- struct{}) {
m.eventCh, m.redrawCh = eventCh, redrawCh
m.mu, m.files = new(sync.Mutex), make(map[string]file)
}
// Open a new window.
func (m *Manager) Open(name string) error {
m.mu.Lock()
defer m.mu.Unlock()
window, err := m.open(name)
if err != nil {
return err
}
return m.init(window)
}
// Read opens a new window from [io.Reader].
func (m *Manager) Read(r io.Reader) error {
m.mu.Lock()
defer m.mu.Unlock()
return m.read(r)
}
func (m *Manager) init(window *window) error {
m.addWindow(window)
m.layout = layout.NewLayout(m.windowIndex).Resize(0, 0, m.width, m.height)
return nil
}
func (m *Manager) addWindow(window *window) {
for i, w := range m.windows {
if w == window {
m.windowIndex, m.prevWindowIndex = i, m.windowIndex
return
}
}
m.windows = append(m.windows, window)
m.windowIndex, m.prevWindowIndex = len(m.windows)-1, m.windowIndex
}
func (m *Manager) open(name string) (*window, error) {
if name == "" {
window, err := newWindow(bytes.NewReader(nil), "", "", m.eventCh, m.redrawCh)
if err != nil {
return nil, err
}
return window, nil
}
if name == "#" {
return m.windows[m.prevWindowIndex], nil
}
if strings.HasPrefix(name, "#") {
index, err := strconv.Atoi(name[1:])
if err != nil || index <= 0 || len(m.windows) < index {
return nil, errors.New("invalid window index: " + name)
}
return m.windows[index-1], nil
}
name, err := expandBacktick(name)
if err != nil {
return nil, err
}
path, err := expandPath(name)
if err != nil {
return nil, err
}
r, err := m.openFile(path, name)
if err != nil {
return nil, err
}
return newWindow(r, path, filepath.Base(path), m.eventCh, m.redrawCh)
}
func (m *Manager) openFile(path, name string) (readAtSeeker, error) {
fi, err := os.Stat(path)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
return bytes.NewReader(nil), nil
} else if fi.IsDir() {
return nil, errors.New(name + " is a directory")
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
m.addFile(path, f, fi)
return f, nil
}
func expandBacktick(name string) (string, error) {
if len(name) <= 2 || name[0] != '`' || name[len(name)-1] != '`' {
return name, nil
}
name = strings.TrimSpace(name[1 : len(name)-1])
xs := strings.Fields(name)
if len(xs) < 1 {
return name, nil
}
out, err := exec.Command(xs[0], xs[1:]...).Output()
if err != nil {
return name, err
}
return strings.TrimSpace(string(out)), nil
}
func expandPath(path string) (string, error) {
switch {
case strings.HasPrefix(path, "~"):
if name, rest, _ := strings.Cut(path[1:], string(filepath.Separator)); name != "" {
user, err := user.Lookup(name)
if err != nil {
return path, nil
}
return filepath.Join(user.HomeDir, rest), nil
}
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(homeDir, path[1:]), nil
case strings.HasPrefix(path, "$"):
name, rest, _ := strings.Cut(path[1:], string(filepath.Separator))
value := os.Getenv(name)
if value == "" {
return path, nil
}
return filepath.Join(value, rest), nil
default:
return filepath.Abs(path)
}
}
func (m *Manager) read(r io.Reader) error {
bs, err := func() ([]byte, error) {
r, stop := newReader(r)
defer stop()
return io.ReadAll(r)
}()
if err != nil {
return err
}
window, err := newWindow(bytes.NewReader(bs), "", "", m.eventCh, m.redrawCh)
if err != nil {
return err
}
return m.init(window)
}
type reader struct {
io.Reader
abort chan os.Signal
}
func newReader(r io.Reader) (*reader, func()) {
done := make(chan struct{})
abort := make(chan os.Signal, 1)
signal.Notify(abort, os.Interrupt)
go func() {
select {
case <-time.After(time.Second):
fmt.Fprint(os.Stderr, "Reading stdin took more than 1 second, press <C-c> to abort...")
case <-done:
}
}()
return &reader{r, abort}, func() {
signal.Stop(abort)
close(abort)
close(done)
}
}
func (r *reader) Read(p []byte) (int, error) {
select {
case <-r.abort:
return 0, io.EOF
default:
}
return r.Reader.Read(p)
}
// SetSize sets the size of the screen.
func (m *Manager) SetSize(width, height int) {
m.width, m.height = width, height
}
// Resize sets the size of the screen.
func (m *Manager) Resize(width, height int) {
if m.width != width || m.height != height {
m.mu.Lock()
defer m.mu.Unlock()
m.width, m.height = width, height
m.layout = m.layout.Resize(0, 0, width, height)
}
}
// Emit an event to the current window.
func (m *Manager) Emit(e event.Event) {
switch e.Type {
case event.Edit:
if err := m.edit(e); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.Enew:
if err := m.enew(e); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.New:
if err := m.newWindow(e, false); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.Vnew:
if err := m.newWindow(e, true); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.Only:
if err := m.only(e); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.Alternative:
m.alternative(e)
m.eventCh <- event.Event{Type: event.Redraw}
case event.Wincmd:
if e.Arg == "" {
m.eventCh <- event.Event{Type: event.Error,
Error: errors.New("an argument is required for " + e.CmdName)}
} else if err := m.wincmd(e.Arg); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.FocusWindowDown:
if err := m.wincmd("j"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.FocusWindowUp:
if err := m.wincmd("k"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.FocusWindowLeft:
if err := m.wincmd("h"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.FocusWindowRight:
if err := m.wincmd("l"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.FocusWindowTopLeft:
if err := m.wincmd("t"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.FocusWindowBottomRight:
if err := m.wincmd("b"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.FocusWindowPrevious:
if err := m.wincmd("p"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.MoveWindowTop:
if err := m.wincmd("K"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.MoveWindowBottom:
if err := m.wincmd("J"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.MoveWindowLeft:
if err := m.wincmd("H"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.MoveWindowRight:
if err := m.wincmd("L"); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Redraw}
}
case event.Pwd:
if e.Arg != "" {
m.eventCh <- event.Event{Type: event.Error, Error: errors.New("too many arguments for " + e.CmdName)}
break
}
fallthrough
case event.Chdir:
if dir, err := m.chdir(e); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Info, Error: errors.New(dir)}
}
case event.Quit:
if err := m.quit(e); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
}
case event.Write:
if name, n, err := m.write(e); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else {
m.eventCh <- event.Event{Type: event.Info,
Error: fmt.Errorf("%s: %[2]d (0x%[2]x) bytes written", name, n)}
}
case event.WriteQuit:
if _, _, err := m.write(e); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
} else if err := m.quit(event.Event{Bang: e.Bang}); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
}
default:
m.windows[m.windowIndex].emit(e)
}
}
func (m *Manager) edit(e event.Event) error {
m.mu.Lock()
defer m.mu.Unlock()
name := e.Arg
if name == "" {
name = m.windows[m.windowIndex].path
}
window, err := m.open(name)
if err != nil {
return err
}
m.addWindow(window)
m.layout = m.layout.Replace(m.windowIndex)
return nil
}
func (m *Manager) enew(e event.Event) error {
if e.Arg != "" {
return errors.New("too many arguments for " + e.CmdName)
}
m.mu.Lock()
defer m.mu.Unlock()
window, err := m.open("")
if err != nil {
return err
}
m.addWindow(window)
m.layout = m.layout.Replace(m.windowIndex)
return nil
}
func (m *Manager) newWindow(e event.Event, vertical bool) error {
m.mu.Lock()
defer m.mu.Unlock()
window, err := m.open(e.Arg)
if err != nil {
return err
}
m.addWindow(window)
if vertical {
m.layout = m.layout.SplitLeft(m.windowIndex).Resize(0, 0, m.width, m.height)
} else {
m.layout = m.layout.SplitTop(m.windowIndex).Resize(0, 0, m.width, m.height)
}
return nil
}
func (m *Manager) only(e event.Event) error {
if e.Arg != "" {
return errors.New("too many arguments for " + e.CmdName)
}
m.mu.Lock()
defer m.mu.Unlock()
if !e.Bang {
for windowIndex, w := range m.layout.Collect() {
if window := m.windows[windowIndex]; !w.Active && window.changedTick != window.savedChangedTick {
return errors.New("you have unsaved changes in " + window.getName() + ", add ! to force :only")
}
}
}
m.layout = layout.NewLayout(m.windowIndex).Resize(0, 0, m.width, m.height)
return nil
}
func (m *Manager) alternative(e event.Event) {
m.mu.Lock()
defer m.mu.Unlock()
if e.Count == 0 {
m.windowIndex, m.prevWindowIndex = m.prevWindowIndex, m.windowIndex
} else if 0 < e.Count && e.Count <= int64(len(m.windows)) {
m.windowIndex, m.prevWindowIndex = int(e.Count)-1, m.windowIndex
}
m.layout = m.layout.Replace(m.windowIndex)
}
func (m *Manager) wincmd(arg string) error {
switch arg {
case "n":
return m.newWindow(event.Event{}, false)
case "o":
return m.only(event.Event{})
case "l":
m.focus(func(x, y layout.Window) bool {
return x.LeftMargin()+x.Width()+1 == y.LeftMargin() &&
y.TopMargin() <= x.TopMargin() &&
x.TopMargin() < y.TopMargin()+y.Height()
})
case "h":
m.focus(func(x, y layout.Window) bool {
return y.LeftMargin()+y.Width()+1 == x.LeftMargin() &&
y.TopMargin() <= x.TopMargin() &&
x.TopMargin() < y.TopMargin()+y.Height()
})
case "k":
m.focus(func(x, y layout.Window) bool {
return y.TopMargin()+y.Height() == x.TopMargin() &&
y.LeftMargin() <= x.LeftMargin() &&
x.LeftMargin() < y.LeftMargin()+y.Width()
})
case "j":
m.focus(func(x, y layout.Window) bool {
return x.TopMargin()+x.Height() == y.TopMargin() &&
y.LeftMargin() <= x.LeftMargin() &&
x.LeftMargin() < y.LeftMargin()+y.Width()
})
case "t":
m.focus(func(_, y layout.Window) bool {
return y.LeftMargin() == 0 && y.TopMargin() == 0
})
case "b":
m.focus(func(_, y layout.Window) bool {
return m.layout.LeftMargin()+m.layout.Width() == y.LeftMargin()+y.Width() &&
m.layout.TopMargin()+m.layout.Height() == y.TopMargin()+y.Height()
})
case "p":
m.focus(func(_, y layout.Window) bool {
return y.Index == m.prevWindowIndex
})
case "K":
m.move(func(x layout.Window, y layout.Layout) layout.Layout {
return layout.Horizontal{Top: x, Bottom: y}
})
case "J":
m.move(func(x layout.Window, y layout.Layout) layout.Layout {
return layout.Horizontal{Top: y, Bottom: x}
})
case "H":
m.move(func(x layout.Window, y layout.Layout) layout.Layout {
return layout.Vertical{Left: x, Right: y}
})
case "L":
m.move(func(x layout.Window, y layout.Layout) layout.Layout {
return layout.Vertical{Left: y, Right: x}
})
default:
return errors.New("Invalid argument for wincmd: " + arg)
}
return nil
}
func (m *Manager) focus(search func(layout.Window, layout.Window) bool) {
m.mu.Lock()
defer m.mu.Unlock()
activeWindow := m.layout.ActiveWindow()
newWindow := m.layout.Lookup(func(l layout.Window) bool {
return search(activeWindow, l)
})
if newWindow.Index >= 0 {
m.windowIndex, m.prevWindowIndex = newWindow.Index, m.windowIndex
m.layout = m.layout.Activate(m.windowIndex)
}
}
func (m *Manager) move(modifier func(layout.Window, layout.Layout) layout.Layout) {
m.mu.Lock()
defer m.mu.Unlock()
w, h := m.layout.Count()
if w != 1 || h != 1 {
activeWindow := m.layout.ActiveWindow()
m.layout = modifier(activeWindow, m.layout.Close()).Activate(
activeWindow.Index).Resize(0, 0, m.width, m.height)
}
}
func (m *Manager) chdir(e event.Event) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
if e.Arg == "-" && m.prevDir == "" {
return "", errors.New("no previous working directory")
}
dir, err := os.Getwd()
if err != nil {
return "", err
}
if e.Arg == "" {
return dir, nil
}
if e.Arg != "-" {
dir, m.prevDir = e.Arg, dir
} else {
dir, m.prevDir = m.prevDir, dir
}
if dir, err = expandPath(dir); err != nil {
return "", err
}
if err = os.Chdir(dir); err != nil {
return "", err
}
return os.Getwd()
}
func (m *Manager) quit(e event.Event) error {
if e.Arg != "" {
return errors.New("too many arguments for " + e.CmdName)
}
m.mu.Lock()
defer m.mu.Unlock()
window := m.windows[m.windowIndex]
if window.changedTick != window.savedChangedTick && !e.Bang {
return errors.New("you have unsaved changes in " + window.getName() + ", add ! to force :quit")
}
w, h := m.layout.Count()
if w == 1 && h == 1 {
m.eventCh <- event.Event{Type: event.QuitAll}
} else {
m.layout = m.layout.Close().Resize(0, 0, m.width, m.height)
m.windowIndex, m.prevWindowIndex = m.layout.ActiveWindow().Index, m.windowIndex
m.eventCh <- event.Event{Type: event.Redraw}
}
return nil
}
func (m *Manager) write(e event.Event) (string, int64, error) {
if e.Range != nil && e.Arg == "" {
return "", 0, errors.New("cannot overwrite partially with " + e.CmdName)
}
m.mu.Lock()
defer m.mu.Unlock()
window := m.windows[m.windowIndex]
var path string
name := e.Arg
if name == "" {
if window.name == "" {
return "", 0, errors.New("no file name")
}
path, name = window.path, window.name
} else {
var err error
path, err = expandPath(name)
if err != nil {
return "", 0, err
}
}
if runtime.GOOS == "windows" && m.opened(path) {
return "", 0, errors.New("cannot overwrite the original file on Windows")
}
if window.path == "" && window.name == "" {
window.setPathName(path, filepath.Base(path))
}
tmpf, err := os.OpenFile(
path+"-"+strconv.FormatUint(rand.Uint64(), 36),
os.O_RDWR|os.O_CREATE|os.O_EXCL, m.filePerm(path),
) //#nosec G404
if err != nil {
return "", 0, err
}
defer os.Remove(tmpf.Name())
n, err := window.writeTo(e.Range, tmpf)
if err != nil {
_ = tmpf.Close()
return "", 0, err
}
if err = tmpf.Close(); err != nil {
return "", 0, err
}
if window.path == path {
window.savedChangedTick = window.changedTick
}
return name, n, os.Rename(tmpf.Name(), path)
}
func (m *Manager) addFile(path string, f *os.File, fi os.FileInfo) {
m.files[path] = file{path: path, file: f, perm: fi.Mode().Perm()}
}
func (m *Manager) opened(path string) bool {
_, ok := m.files[path]
return ok
}
func (m *Manager) filePerm(path string) os.FileMode {
if f, ok := m.files[path]; ok {
return f.perm
}
return os.FileMode(0o644)
}
// State returns the state of the windows.
func (m *Manager) State() (map[int]*state.WindowState, layout.Layout, int, error) {
m.mu.Lock()
defer m.mu.Unlock()
layouts := m.layout.Collect()
states := make(map[int]*state.WindowState, len(m.windows))
for i, window := range m.windows {
if l, ok := layouts[i]; ok {
var err error
if states[i], err = window.state(
hexWindowWidth(l.Width()), max(l.Height()-2, 1),
); err != nil {
return nil, m.layout, 0, err
}
}
}
return states, m.layout, m.windowIndex, nil
}
func hexWindowWidth(width int) int {
width = min(max((width-18)/4, 4), 256)
return width & (0b11 << (bits.Len(uint(width)) - 2))
}
// Close the Manager.
func (m *Manager) Close() {
for _, f := range m.files {
_ = f.file.Close()
}
}

View File

@ -1,708 +0,0 @@
package window
import (
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"b612.me/apps/b612/bed/buffer"
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/layout"
"b612.me/apps/b612/bed/mode"
)
func createTemp(dir, contents string) (*os.File, error) {
f, err := os.CreateTemp(dir, "")
if err != nil {
return nil, err
}
if _, err = f.WriteString(contents); err != nil {
return nil, err
}
if err = f.Close(); err != nil {
return nil, err
}
return f, nil
}
func TestManagerOpenEmpty(t *testing.T) {
wm := NewManager()
eventCh, redrawCh, waitCh := make(chan event.Event), make(chan struct{}), make(chan struct{})
wm.Init(eventCh, redrawCh)
go func() {
defer func() {
close(eventCh)
close(redrawCh)
close(waitCh)
}()
ev := <-eventCh
if ev.Type != event.Error {
t.Errorf("event type should be %d but got: %d", event.Error, ev.Type)
}
if expected := "no file name"; ev.Error.Error() != expected {
t.Errorf("err should be %q but got: %v", expected, ev.Error)
}
}()
wm.SetSize(110, 20)
if err := wm.Open(""); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
windowStates, _, windowIndex, err := wm.State()
if expected := 0; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
ws, ok := windowStates[windowIndex]
if !ok {
t.Fatalf("windowStates should contain %d but got: %v", windowIndex, windowStates)
}
if expected := ""; ws.Name != expected {
t.Errorf("name should be %q but got %q", expected, ws.Name)
}
if ws.Width != 16 {
t.Errorf("width should be %d but got %d", 16, ws.Width)
}
if ws.Size != 0 {
t.Errorf("size should be %d but got %d", 0, ws.Size)
}
if ws.Length != int64(0) {
t.Errorf("Length should be %d but got %d", int64(0), ws.Length)
}
if expected := "\x00"; !strings.HasPrefix(string(ws.Bytes), expected) {
t.Errorf("Bytes should start with %q but got %q", expected, string(ws.Bytes))
}
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
wm.Emit(event.Event{Type: event.Write})
<-waitCh
wm.Close()
}
func TestManagerOpenStates(t *testing.T) {
wm := NewManager()
wm.Init(nil, nil)
wm.SetSize(110, 20)
str := "Hello, world! こんにちは、世界!"
f, err := createTemp(t.TempDir(), str)
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := wm.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
windowStates, _, windowIndex, err := wm.State()
if expected := 0; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
ws, ok := windowStates[windowIndex]
if !ok {
t.Fatalf("windowStates should contain %d but got: %v", windowIndex, windowStates)
}
if expected := filepath.Base(f.Name()); ws.Name != expected {
t.Errorf("name should be %q but got %q", expected, ws.Name)
}
if ws.Width != 16 {
t.Errorf("width should be %d but got %d", 16, ws.Width)
}
if ws.Size != 41 {
t.Errorf("size should be %d but got %d", 41, ws.Size)
}
if ws.Length != int64(41) {
t.Errorf("Length should be %d but got %d", int64(41), ws.Length)
}
if !strings.HasPrefix(string(ws.Bytes), str) {
t.Errorf("Bytes should start with %q but got %q", str, string(ws.Bytes))
}
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
wm.Close()
}
func TestManagerOpenNonExistsWrite(t *testing.T) {
wm := NewManager()
eventCh, redrawCh, waitCh := make(chan event.Event), make(chan struct{}), make(chan struct{})
wm.Init(eventCh, redrawCh)
go func() {
defer func() {
close(eventCh)
close(redrawCh)
close(waitCh)
}()
for range 16 {
<-redrawCh
}
if ev := <-eventCh; ev.Type != event.QuitAll {
t.Errorf("event type should be %d but got: %d", event.QuitAll, ev.Type)
}
}()
wm.SetSize(110, 20)
fname := filepath.Join(t.TempDir(), "test")
if err := wm.Open(fname); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
_, _, _, _ = wm.State()
str := "Hello, world!"
wm.Emit(event.Event{Type: event.StartInsert})
wm.Emit(event.Event{Type: event.SwitchFocus})
for _, c := range str {
wm.Emit(event.Event{Type: event.Rune, Rune: c, Mode: mode.Insert})
}
wm.Emit(event.Event{Type: event.ExitInsert})
wm.Emit(event.Event{Type: event.WriteQuit})
windowStates, _, windowIndex, err := wm.State()
if expected := 0; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
ws, ok := windowStates[windowIndex]
if !ok {
t.Fatalf("windowStates should contain %d but got: %v", windowIndex, windowStates)
}
if expected := filepath.Base(fname); ws.Name != expected {
t.Errorf("name should be %q but got %q", expected, ws.Name)
}
if ws.Width != 16 {
t.Errorf("width should be %d but got %d", 16, ws.Width)
}
if ws.Size != 13 {
t.Errorf("size should be %d but got %d", 13, ws.Size)
}
if ws.Length != int64(13) {
t.Errorf("Length should be %d but got %d", int64(13), ws.Length)
}
if !strings.HasPrefix(string(ws.Bytes), str) {
t.Errorf("Bytes should start with %q but got %q", str, string(ws.Bytes))
}
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, err := os.ReadFile(fname)
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if string(bs) != str {
t.Errorf("file contents should be %q but got %q", str, string(bs))
}
<-waitCh
wm.Close()
}
func TestManagerOpenExpandBacktick(t *testing.T) {
wm := NewManager()
wm.Init(nil, nil)
wm.SetSize(110, 20)
cmd, name := "`which ls`", "ls"
if runtime.GOOS == "windows" {
cmd, name = "`where ping`", "PING.EXE"
}
if err := wm.Open(cmd); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
windowStates, _, windowIndex, err := wm.State()
ws, ok := windowStates[windowIndex]
if !ok {
t.Fatalf("windowStates should contain %d but got: %v", windowIndex, windowStates)
}
if ws.Name != name {
t.Errorf("name should be %q but got %q", name, ws.Name)
}
if ws.Width != 16 {
t.Errorf("width should be %d but got %d", 16, ws.Width)
}
if ws.Size == 0 {
t.Errorf("size should not be %d but got %d", 0, ws.Size)
}
if ws.Length == 0 {
t.Errorf("length should not be %d but got %d", 0, ws.Length)
}
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
wm.Close()
}
func TestManagerOpenExpandHomedir(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
wm := NewManager()
wm.Init(nil, nil)
wm.SetSize(110, 20)
str := "Hello, world!"
f, err := createTemp(t.TempDir(), str)
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
home := os.Getenv("HOME")
t.Cleanup(func() {
if err := os.Setenv("HOME", home); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
})
if err := os.Setenv("HOME", filepath.Dir(f.Name())); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
for i, prefix := range []string{"~/", "$HOME/"} {
if err := wm.Open(prefix + filepath.Base(f.Name())); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
windowStates, _, windowIndex, err := wm.State()
if windowIndex != i {
t.Errorf("windowIndex should be %d but got %d", i, windowIndex)
}
ws, ok := windowStates[windowIndex]
if !ok {
t.Fatalf("windowStates should contain %d but got: %v", windowIndex, windowStates)
}
if expected := filepath.Base(f.Name()); ws.Name != expected {
t.Errorf("name should be %q but got %q", expected, ws.Name)
}
if !strings.HasPrefix(string(ws.Bytes), str) {
t.Errorf("Bytes should start with %q but got %q", str, string(ws.Bytes))
}
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
}
wm.Close()
}
func TestManagerOpenChdirWrite(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip on Windows")
}
wm := NewManager()
eventCh, redrawCh, waitCh := make(chan event.Event), make(chan struct{}), make(chan struct{})
wm.Init(eventCh, redrawCh)
f, err := createTemp(t.TempDir(), "Hello")
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
go func() {
defer func() {
close(eventCh)
close(redrawCh)
close(waitCh)
}()
ev := <-eventCh
if ev.Type != event.Info {
t.Errorf("event type should be %d but got: %d", event.Info, ev.Type)
}
dir, err := filepath.EvalSymlinks(filepath.Dir(f.Name()))
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := dir; ev.Error.Error() != expected {
t.Errorf("err should be %q but got: %v", expected, ev.Error)
}
ev = <-eventCh
if ev.Type != event.Info {
t.Errorf("event type should be %d but got: %d", event.Info, ev.Type)
}
if expected := filepath.Dir(dir); ev.Error.Error() != expected {
t.Errorf("err should be %q but got: %v", expected, ev.Error)
}
for range 11 {
<-redrawCh
}
ev = <-eventCh
if ev.Type != event.Info {
t.Errorf("event type should be %d but got: %d", event.Info, ev.Type)
}
if expected := "13 (0xd) bytes written"; !strings.HasSuffix(ev.Error.Error(), expected) {
t.Errorf("err should be %q but got: %v", expected, ev.Error)
}
}()
wm.SetSize(110, 20)
wm.Emit(event.Event{Type: event.Chdir, Arg: filepath.Dir(f.Name())})
if err := wm.Open(filepath.Base(f.Name())); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
_, _, windowIndex, _ := wm.State()
if expected := 0; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
wm.Emit(event.Event{Type: event.Chdir, Arg: "../"})
wm.Emit(event.Event{Type: event.StartAppendEnd})
wm.Emit(event.Event{Type: event.SwitchFocus})
for _, c := range ", world!" {
wm.Emit(event.Event{Type: event.Rune, Rune: c, Mode: mode.Insert})
}
wm.Emit(event.Event{Type: event.ExitInsert})
wm.Emit(event.Event{Type: event.Write})
bs, err := os.ReadFile(f.Name())
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if expected := "Hello, world!"; string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
<-waitCh
wm.Close()
}
func TestManagerOpenDirectory(t *testing.T) {
wm := NewManager()
wm.Init(nil, nil)
wm.SetSize(110, 20)
dir := t.TempDir()
if err := wm.Open(dir); err != nil {
if expected := dir + " is a directory"; err.Error() != expected {
t.Errorf("err should be %q but got: %v", expected, err)
}
} else {
t.Errorf("err should not be nil but got: %v", err)
}
wm.Close()
}
func TestManagerRead(t *testing.T) {
wm := NewManager()
wm.Init(nil, nil)
wm.SetSize(110, 20)
r := strings.NewReader("Hello, world!")
if err := wm.Read(r); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
windowStates, _, windowIndex, err := wm.State()
if expected := 0; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
ws, ok := windowStates[windowIndex]
if !ok {
t.Fatalf("windowStates should contain %d but got: %v", windowIndex, windowStates)
}
if ws.Name != "" {
t.Errorf("name should be %q but got %q", "", ws.Name)
}
if ws.Width != 16 {
t.Errorf("width should be %d but got %d", 16, ws.Width)
}
if ws.Size != 13 {
t.Errorf("size should be %d but got %d", 13, ws.Size)
}
if ws.Length != int64(13) {
t.Errorf("Length should be %d but got %d", int64(13), ws.Length)
}
if err != nil {
t.Errorf("err should be nil but got: %v", err)
}
wm.Close()
}
func TestManagerOnly(t *testing.T) {
wm := NewManager()
eventCh, redrawCh, waitCh := make(chan event.Event), make(chan struct{}), make(chan struct{})
wm.Init(eventCh, redrawCh)
go func() {
defer func() {
close(eventCh)
close(redrawCh)
close(waitCh)
}()
for range 4 {
<-eventCh
}
}()
wm.SetSize(110, 20)
if err := wm.Open(""); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
wm.Emit(event.Event{Type: event.Vnew})
wm.Emit(event.Event{Type: event.Vnew})
wm.Emit(event.Event{Type: event.FocusWindowRight})
wm.Resize(110, 20)
_, got, _, _ := wm.State()
expected := layout.NewLayout(0).SplitLeft(1).SplitLeft(2).
Activate(1).Resize(0, 0, 110, 20)
if !reflect.DeepEqual(got, expected) {
t.Errorf("layout should be %#v but got %#v", expected, got)
}
wm.Emit(event.Event{Type: event.Only})
wm.Resize(110, 20)
_, got, _, _ = wm.State()
expected = layout.NewLayout(1).Resize(0, 0, 110, 20)
if !reflect.DeepEqual(got, expected) {
t.Errorf("layout should be %#v but got %#v", expected, got)
}
<-waitCh
wm.Close()
}
func TestManagerAlternative(t *testing.T) {
wm := NewManager()
eventCh, redrawCh, waitCh := make(chan event.Event), make(chan struct{}), make(chan struct{})
wm.Init(eventCh, redrawCh)
go func() {
defer func() {
close(eventCh)
close(redrawCh)
close(waitCh)
}()
for range 9 {
<-eventCh
}
}()
wm.SetSize(110, 20)
if err := os.Chdir(os.TempDir()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := wm.Open("bed-test-manager-alternative-1"); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
if err := wm.Open("bed-test-manager-alternative-2"); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
wm.Emit(event.Event{Type: event.Alternative})
_, _, windowIndex, _ := wm.State()
if expected := 0; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
if err := wm.Open("bed-test-manager-alternative-3"); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
_, _, windowIndex, _ = wm.State()
if expected := 2; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
wm.Emit(event.Event{Type: event.Alternative})
_, _, windowIndex, _ = wm.State()
if expected := 0; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
wm.Emit(event.Event{Type: event.Alternative})
_, _, windowIndex, _ = wm.State()
if expected := 2; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
if err := wm.Open("bed-test-manager-alternative-4"); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
_, _, windowIndex, _ = wm.State()
if expected := 3; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
wm.Emit(event.Event{Type: event.Alternative, Count: 2})
_, _, windowIndex, _ = wm.State()
if expected := 1; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
wm.Emit(event.Event{Type: event.Alternative, Count: 4})
_, _, windowIndex, _ = wm.State()
if expected := 3; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
wm.Emit(event.Event{Type: event.Alternative})
_, _, windowIndex, _ = wm.State()
if expected := 1; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
wm.Emit(event.Event{Type: event.Edit, Arg: "#2"})
_, _, windowIndex, _ = wm.State()
if expected := 1; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
wm.Emit(event.Event{Type: event.Edit, Arg: "#4"})
_, _, windowIndex, _ = wm.State()
if expected := 3; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
wm.Emit(event.Event{Type: event.Edit, Arg: "#"})
_, _, windowIndex, _ = wm.State()
if expected := 1; windowIndex != expected {
t.Errorf("windowIndex should be %d but got %d", expected, windowIndex)
}
<-waitCh
wm.Close()
}
func TestManagerWincmd(t *testing.T) {
wm := NewManager()
eventCh, redrawCh, waitCh := make(chan event.Event), make(chan struct{}), make(chan struct{})
wm.Init(eventCh, redrawCh)
go func() {
defer func() {
close(eventCh)
close(redrawCh)
close(waitCh)
}()
for range 17 {
<-eventCh
}
}()
wm.SetSize(110, 20)
if err := wm.Open(""); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
wm.Emit(event.Event{Type: event.Wincmd, Arg: "n"})
wm.Emit(event.Event{Type: event.Wincmd, Arg: "n"})
wm.Emit(event.Event{Type: event.Wincmd, Arg: "n"})
wm.Emit(event.Event{Type: event.MoveWindowLeft})
wm.Emit(event.Event{Type: event.FocusWindowRight})
wm.Emit(event.Event{Type: event.FocusWindowBottomRight})
wm.Emit(event.Event{Type: event.MoveWindowRight})
wm.Emit(event.Event{Type: event.FocusWindowLeft})
wm.Emit(event.Event{Type: event.MoveWindowTop})
wm.Resize(110, 20)
_, got, _, _ := wm.State()
expected := layout.NewLayout(2).SplitBottom(0).SplitLeft(1).
SplitLeft(3).Activate(2).Resize(0, 0, 110, 20)
if !reflect.DeepEqual(got, expected) {
t.Errorf("layout should be %#v but got %#v", expected, got)
}
wm.Emit(event.Event{Type: event.FocusWindowDown})
wm.Emit(event.Event{Type: event.FocusWindowRight})
wm.Emit(event.Event{Type: event.Quit})
_, got, _, _ = wm.State()
expected = layout.NewLayout(2).SplitBottom(0).SplitLeft(3).Resize(0, 0, 110, 20)
if !reflect.DeepEqual(got, expected) {
t.Errorf("layout should be %#v but got %#v", expected, got)
}
wm.Emit(event.Event{Type: event.Wincmd, Arg: "o"})
_, got, _, _ = wm.State()
expected = layout.NewLayout(3).Resize(0, 0, 110, 20)
if !reflect.DeepEqual(got, expected) {
t.Errorf("layout should be %#v but got %#v", expected, got)
}
wm.Emit(event.Event{Type: event.MoveWindowLeft})
wm.Emit(event.Event{Type: event.MoveWindowRight})
wm.Emit(event.Event{Type: event.MoveWindowTop})
wm.Emit(event.Event{Type: event.MoveWindowBottom})
wm.Resize(110, 20)
_, got, _, _ = wm.State()
if !reflect.DeepEqual(got, expected) {
t.Errorf("layout should be %#v but got %#v", expected, got)
}
<-waitCh
wm.Close()
}
func TestManagerCopyCutPaste(t *testing.T) {
wm := NewManager()
eventCh, redrawCh, waitCh := make(chan event.Event), make(chan struct{}), make(chan struct{})
wm.Init(eventCh, redrawCh)
str := "Hello, world!"
f, err := createTemp(t.TempDir(), str)
if err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
wm.SetSize(110, 20)
if err := wm.Open(f.Name()); err != nil {
t.Fatalf("err should be nil but got: %v", err)
}
_, _, _, _ = wm.State()
go func() {
defer func() {
close(eventCh)
close(redrawCh)
close(waitCh)
}()
<-redrawCh
<-redrawCh
<-redrawCh
waitCh <- struct{}{}
ev := <-eventCh
if ev.Type != event.Copied {
t.Errorf("event type should be %d but got: %d", event.Copied, ev.Type)
}
if ev.Buffer == nil {
t.Errorf("Buffer should not be nil but got: %#v", ev)
}
if expected := "yanked"; ev.Arg != expected {
t.Errorf("Arg should be %q but got: %q", expected, ev.Arg)
}
p := make([]byte, 20)
_, _ = ev.Buffer.ReadAt(p, 0)
if !strings.HasPrefix(string(p), "lo, worl") {
t.Errorf("buffer string should be %q but got: %q", "", string(p))
}
waitCh <- struct{}{}
<-redrawCh
<-redrawCh
waitCh <- struct{}{}
ev = <-eventCh
if ev.Type != event.Copied {
t.Errorf("event type should be %d but got: %d", event.Copied, ev.Type)
}
if ev.Buffer == nil {
t.Errorf("Buffer should not be nil but got: %#v", ev)
}
if expected := "deleted"; ev.Arg != expected {
t.Errorf("Arg should be %q but got: %q", expected, ev.Arg)
}
p = make([]byte, 20)
_, _ = ev.Buffer.ReadAt(p, 0)
if !strings.HasPrefix(string(p), "lo, wo") {
t.Errorf("buffer string should be %q but got: %q", "", string(p))
}
windowStates, _, windowIndex, _ := wm.State()
ws, ok := windowStates[windowIndex]
if !ok {
t.Errorf("windowStates should contain %d but got: %v", windowIndex, windowStates)
return
}
if ws.Length != int64(7) {
t.Errorf("Length should be %d but got %d", int64(7), ws.Length)
}
if expected := "Helrld!"; !strings.HasPrefix(string(ws.Bytes), expected) {
t.Errorf("Bytes should start with %q but got %q", expected, string(ws.Bytes))
}
waitCh <- struct{}{}
<-redrawCh
waitCh <- struct{}{}
ev = <-eventCh
if ev.Type != event.Pasted {
t.Errorf("event type should be %d but got: %d", event.Pasted, ev.Type)
}
if ev.Count != 18 {
t.Errorf("Count should be %d but got: %d", 18, ev.Count)
}
windowStates, _, _, _ = wm.State()
ws = windowStates[0]
if ws.Length != int64(25) {
t.Errorf("Length should be %d but got %d", int64(25), ws.Length)
}
if expected := "Hefoobarfoobarfoobarlrld!"; !strings.HasPrefix(string(ws.Bytes), expected) {
t.Errorf("Bytes should start with %q but got %q", expected, string(ws.Bytes))
}
}()
wm.Emit(event.Event{Type: event.CursorNext, Mode: mode.Normal, Count: 3})
wm.Emit(event.Event{Type: event.StartVisual})
wm.Emit(event.Event{Type: event.CursorNext, Mode: mode.Visual, Count: 7})
<-waitCh
wm.Emit(event.Event{Type: event.Copy})
<-waitCh
wm.Emit(event.Event{Type: event.StartVisual})
wm.Emit(event.Event{Type: event.CursorNext, Mode: mode.Visual, Count: 5})
<-waitCh
wm.Emit(event.Event{Type: event.Cut})
<-waitCh
wm.Emit(event.Event{Type: event.CursorPrev, Mode: mode.Normal, Count: 2})
<-waitCh
wm.Emit(event.Event{Type: event.Paste, Buffer: buffer.NewBuffer(strings.NewReader("foobar")), Count: 3})
<-waitCh
wm.Close()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,16 +16,14 @@ go build -o .\bin\b612_x86_64 -ldflags "-w -s" .
upx -9 .\bin\b612_x86_64 upx -9 .\bin\b612_x86_64
set GOARCH=386 set GOARCH=386
go build -o .\bin\b612_x86 -ldflags "-w -s" . go build -o .\bin\b612_x86 -ldflags "-w -s" .
upx -9 .\bin\b612_x86 #upx -9 .\bin\b612_x86
set GOARCH=arm64 set GOARCH=arm64
go build -o .\bin\b612_aarch64 -ldflags "-w -s" . go build -o .\bin\b612_aarch64 -ldflags "-w -s" .
upx -9 .\bin\b612_aarch64 upx -9 .\bin\b612_aarch64
set GOARCH=mips set GOARCH=mips
go build -o .\bin\b612_mips -ldflags "-w -s" . go build -o .\bin\b612_mips -ldflags "-w -s" .
upx -9 .\bin\b612_mips
set GOARCH=mipsle set GOARCH=mipsle
go build -o .\bin\b612_mipsle -ldflags "-w -s" . go build -o .\bin\b612_mipsle -ldflags "-w -s" .
upx -9 .\bin\b612_mipsle
set GOARCH=mips64 set GOARCH=mips64
go build -o .\bin\b612_mips64 -ldflags "-w -s" . go build -o .\bin\b612_mips64 -ldflags "-w -s" .
set GOARCH=mips64le set GOARCH=mips64le

View File

@ -50,15 +50,3 @@ func LoadCA(caKeyPath, caCertPath, KeyPwd string) (crypto.PrivateKey, *x509.Cert
} }
return caKey, cert, nil return caKey, cert, nil
} }
func LoadPriv(caKeyPath, KeyPwd string) (crypto.PrivateKey, error) {
caKeyBytes, err := os.ReadFile(caKeyPath)
if err != nil {
return nil, err
}
caKey, err := starcrypto.DecodePrivateKey(caKeyBytes, KeyPwd)
if err != nil {
return nil, err
}
return caKey, nil
}

View File

@ -15,7 +15,6 @@ import (
"fmt" "fmt"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"os" "os"
"reflect"
"software.sslmate.com/src/go-pkcs12" "software.sslmate.com/src/go-pkcs12"
"strings" "strings"
) )
@ -105,9 +104,9 @@ func ParseCert(data []byte, pwd string) {
switch n := priv.(type) { switch n := priv.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
starlog.Green("这是一个RSA私钥\n") starlog.Green("这是一个RSA私钥\n")
starlog.Green("钥位数:%d\n", n.Size()) starlog.Green("钥位数:%d\n", n.Size())
starlog.Green("钥长度:%d\n", n.N.BitLen()) starlog.Green("钥长度:%d\n", n.N.BitLen())
starlog.Green("钥指数:%d\n", n.E) starlog.Green("钥指数:%d\n", n.E)
starlog.Green("私钥系数:%d\n", n.D) starlog.Green("私钥系数:%d\n", n.D)
starlog.Green("私钥质数p%d\n", n.Primes[0]) starlog.Green("私钥质数p%d\n", n.Primes[0])
starlog.Green("私钥质数q%d\n", n.Primes[1]) starlog.Green("私钥质数q%d\n", n.Primes[1])
@ -116,8 +115,8 @@ func ParseCert(data []byte, pwd string) {
starlog.Green("私钥系数qInv%d\n", n.Precomputed.Qinv) starlog.Green("私钥系数qInv%d\n", n.Precomputed.Qinv)
case *ecdsa.PrivateKey: case *ecdsa.PrivateKey:
starlog.Green("这是一个ECDSA私钥\n") starlog.Green("这是一个ECDSA私钥\n")
starlog.Green("钥位数:%d\n", n.Curve.Params().BitSize) starlog.Green("钥位数:%d\n", n.Curve.Params().BitSize)
starlog.Green("钥曲线:%s\n", n.Curve.Params().Name) starlog.Green("钥曲线:%s\n", n.Curve.Params().Name)
starlog.Green("私钥长度:%d\n", n.Params().BitSize) starlog.Green("私钥长度:%d\n", n.Params().BitSize)
starlog.Green("私钥系数:%d\n", n.D) starlog.Green("私钥系数:%d\n", n.D)
starlog.Green("私钥公钥X%d\n", n.PublicKey.X) starlog.Green("私钥公钥X%d\n", n.PublicKey.X)
@ -126,7 +125,7 @@ func ParseCert(data []byte, pwd string) {
starlog.Green("这是一个DSA私钥\n") starlog.Green("这是一个DSA私钥\n")
starlog.Green("私钥系数:%d\n", n.X) starlog.Green("私钥系数:%d\n", n.X)
starlog.Green("私钥公钥Y%d\n", n.Y) starlog.Green("私钥公钥Y%d\n", n.Y)
case *ed25519.PrivateKey, ed25519.PrivateKey: case *ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n") starlog.Green("这是一个ED25519私钥\n")
case *ecdh.PrivateKey: case *ecdh.PrivateKey:
starlog.Green("这是一个ECDH私钥\n") starlog.Green("这是一个ECDH私钥\n")
@ -209,7 +208,7 @@ func ParseCert(data []byte, pwd string) {
starlog.Green("公钥公钥Y%d\n", n.Y) starlog.Green("公钥公钥Y%d\n", n.Y)
case *ecdh.PublicKey: case *ecdh.PublicKey:
starlog.Green("公钥算法为ECDH\n") starlog.Green("公钥算法为ECDH\n")
case *ed25519.PublicKey, ed25519.PublicKey: case *ed25519.PublicKey:
starlog.Green("公钥算法为ED25519\n") starlog.Green("公钥算法为ED25519\n")
default: default:
starlog.Green("未知公钥类型\n") starlog.Green("未知公钥类型\n")
@ -237,9 +236,9 @@ func ParseCert(data []byte, pwd string) {
switch n := priv.(type) { switch n := priv.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
starlog.Green("这是一个RSA私钥\n") starlog.Green("这是一个RSA私钥\n")
starlog.Green("钥位数:%d\n", n.Size()) starlog.Green("钥位数:%d\n", n.Size())
starlog.Green("钥长度:%d\n", n.N.BitLen()) starlog.Green("钥长度:%d\n", n.N.BitLen())
starlog.Green("钥指数:%d\n", n.E) starlog.Green("钥指数:%d\n", n.E)
starlog.Green("私钥系数:%d\n", n.D) starlog.Green("私钥系数:%d\n", n.D)
starlog.Green("私钥质数p%d\n", n.Primes[0]) starlog.Green("私钥质数p%d\n", n.Primes[0])
starlog.Green("私钥质数q%d\n", n.Primes[1]) starlog.Green("私钥质数q%d\n", n.Primes[1])
@ -258,18 +257,12 @@ func ParseCert(data []byte, pwd string) {
starlog.Green("这是一个DSA私钥\n") starlog.Green("这是一个DSA私钥\n")
starlog.Green("私钥系数:%d\n", n.X) starlog.Green("私钥系数:%d\n", n.X)
starlog.Green("私钥公钥Y%d\n", n.Y) starlog.Green("私钥公钥Y%d\n", n.Y)
case ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n")
sshPub, _ := starcrypto.EncodeSSHPublicKey(n.Public())
starlog.Green("公钥:%s\n", string(sshPub))
case *ed25519.PrivateKey: case *ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n") starlog.Green("这是一个ED25519私钥\n")
sshPub, _ := starcrypto.EncodeSSHPublicKey(n.Public()) case *ecdh.PrivateKey:
starlog.Green("公钥:%s\n", string(sshPub))
case ecdh.PrivateKey:
starlog.Green("这是一个ECDH私钥\n") starlog.Green("这是一个ECDH私钥\n")
default: default:
starlog.Infof("不支持的私钥类型:%v\n", reflect.TypeOf(n)) starlog.Green("未知私钥类型\n")
} }
continue continue
@ -378,9 +371,9 @@ func ParseCert(data []byte, pwd string) {
switch n := priv.(type) { switch n := priv.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
starlog.Green("这是一个RSA私钥\n") starlog.Green("这是一个RSA私钥\n")
starlog.Green("钥位数:%d\n", n.Size()) starlog.Green("钥位数:%d\n", n.Size())
starlog.Green("钥长度:%d\n", n.N.BitLen()) starlog.Green("钥长度:%d\n", n.N.BitLen())
starlog.Green("钥指数:%d\n", n.E) starlog.Green("钥指数:%d\n", n.E)
starlog.Green("私钥系数:%d\n", n.D) starlog.Green("私钥系数:%d\n", n.D)
starlog.Green("私钥质数p%d\n", n.Primes[0]) starlog.Green("私钥质数p%d\n", n.Primes[0])
starlog.Green("私钥质数q%d\n", n.Primes[1]) starlog.Green("私钥质数q%d\n", n.Primes[1])
@ -399,18 +392,12 @@ func ParseCert(data []byte, pwd string) {
starlog.Green("这是一个DSA私钥\n") starlog.Green("这是一个DSA私钥\n")
starlog.Green("私钥系数:%d\n", n.X) starlog.Green("私钥系数:%d\n", n.X)
starlog.Green("私钥公钥Y%d\n", n.Y) starlog.Green("私钥公钥Y%d\n", n.Y)
case ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n")
sshPub, _ := starcrypto.EncodeSSHPublicKey(n.Public())
starlog.Green("公钥:%s\n", string(sshPub))
case *ed25519.PrivateKey: case *ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n") starlog.Green("这是一个ED25519私钥\n")
sshPub, _ := starcrypto.EncodeSSHPublicKey(n.Public()) case *ecdh.PrivateKey:
starlog.Green("公钥:%s\n", string(sshPub))
case ecdh.PrivateKey:
starlog.Green("这是一个ECDH私钥\n") starlog.Green("这是一个ECDH私钥\n")
default: default:
starlog.Infof("不支持的私钥类型:%v\n", reflect.TypeOf(n)) starlog.Green("未知私钥类型\n")
} }
continue continue
case "OPENSSH PUBLIC KEY": case "OPENSSH PUBLIC KEY":
@ -423,7 +410,7 @@ func ParseCert(data []byte, pwd string) {
starlog.Green("公钥算法:%s\n", pub.Type()) starlog.Green("公钥算法:%s\n", pub.Type())
continue continue
default: default:
starlog.Infof("不支持的证书文件类型:%v\n", reflect.TypeOf(block)) starlog.Infof("未知证书文件类型\n")
} }
} }
} }
@ -559,9 +546,9 @@ func GetCert(data []byte, pwd string) ([]any, []x509.Certificate, error) {
starlog.Green("私钥位数:%d\n", n.Curve.Params().BitSize) starlog.Green("私钥位数:%d\n", n.Curve.Params().BitSize)
case *dsa.PrivateKey: case *dsa.PrivateKey:
starlog.Green("这是一个DSA私钥\n") starlog.Green("这是一个DSA私钥\n")
case ed25519.PrivateKey, *ed25519.PrivateKey: case *ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n") starlog.Green("这是一个ED25519私钥\n")
case ecdh.PrivateKey, *ecdh.PrivateKey: case *ecdh.PrivateKey:
starlog.Green("这是一个ECDH私钥\n") starlog.Green("这是一个ECDH私钥\n")
default: default:
starlog.Green("未知私钥类型\n") starlog.Green("未知私钥类型\n")
@ -640,15 +627,15 @@ func GetCert(data []byte, pwd string) ([]any, []x509.Certificate, error) {
switch n := priv.(type) { switch n := priv.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
starlog.Green("这是一个RSA私钥\n") starlog.Green("这是一个RSA私钥\n")
starlog.Green("钥位数:%d\n", n.Size()) starlog.Green("钥位数:%d\n", n.Size())
case *ecdsa.PrivateKey: case *ecdsa.PrivateKey:
starlog.Green("这是一个ECDSA私钥\n") starlog.Green("这是一个ECDSA私钥\n")
starlog.Green("私钥位数:%d\n", n.Curve.Params().BitSize) starlog.Green("私钥位数:%d\n", n.Curve.Params().BitSize)
case *dsa.PrivateKey: case *dsa.PrivateKey:
starlog.Green("这是一个DSA私钥\n") starlog.Green("这是一个DSA私钥\n")
case ed25519.PrivateKey, *ed25519.PrivateKey: case *ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n") starlog.Green("这是一个ED25519私钥\n")
case ecdh.PrivateKey: case *ecdh.PrivateKey:
starlog.Green("这是一个ECDH私钥\n") starlog.Green("这是一个ECDH私钥\n")
default: default:
starlog.Green("未知私钥类型\n") starlog.Green("未知私钥类型\n")
@ -760,8 +747,8 @@ func GetCert(data []byte, pwd string) ([]any, []x509.Certificate, error) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
common = append(common, n) common = append(common, n)
starlog.Green("这是一个RSA私钥\n") starlog.Green("这是一个RSA私钥\n")
starlog.Green("钥位数:%d\n", n.Size()) starlog.Green("钥位数:%d\n", n.Size())
starlog.Green("钥长度:%d\n", n.N.BitLen()) starlog.Green("钥长度:%d\n", n.N.BitLen())
case *ecdsa.PrivateKey: case *ecdsa.PrivateKey:
common = append(common, n) common = append(common, n)
starlog.Green("这是一个ECDSA私钥\n") starlog.Green("这是一个ECDSA私钥\n")
@ -769,10 +756,10 @@ func GetCert(data []byte, pwd string) ([]any, []x509.Certificate, error) {
case *dsa.PrivateKey: case *dsa.PrivateKey:
common = append(common, n) common = append(common, n)
starlog.Green("这是一个DSA私钥\n") starlog.Green("这是一个DSA私钥\n")
case ed25519.PrivateKey, *ed25519.PrivateKey: case *ed25519.PrivateKey:
common = append(common, n) common = append(common, n)
starlog.Green("这是一个ED25519私钥\n") starlog.Green("这是一个ED25519私钥\n")
case ecdh.PrivateKey: case *ecdh.PrivateKey:
common = append(common, n) common = append(common, n)
starlog.Green("这是一个ECDH私钥\n") starlog.Green("这是一个ECDH私钥\n")
default: default:
@ -790,42 +777,28 @@ func Pkcs8(data []byte, pwd, newPwd string, originName string, outpath string) e
if err != nil { if err != nil {
return err return err
} }
fmt.Println(len(keys))
for _, v := range keys { for _, v := range keys {
if v == nil { if v == nil {
continue continue
} }
switch n := v.(type) { switch n := v.(type) {
case *ecdsa.PrivateKey, *rsa.PrivateKey, *dsa.PrivateKey, ed25519.PrivateKey, *ed25519.PrivateKey, ecdh.PrivateKey: case *ecdsa.PrivateKey, *rsa.PrivateKey, *dsa.PrivateKey, *ed25519.PrivateKey, *ecdh.PrivateKey:
var key interface{} = n data, err = x509.MarshalPKCS8PrivateKey(n)
if reflect.TypeOf(n) == reflect.TypeOf(&ed25519.PrivateKey{}) {
fmt.Println("1")
key = *(n.(*ed25519.PrivateKey))
}
fmt.Println("2")
data, err = x509.MarshalPKCS8PrivateKey(key)
if err != nil { if err != nil {
return err return err
} }
fmt.Println("3")
var block *pem.Block var block *pem.Block
if newPwd != "" { if newPwd != "" {
block, err = x509.EncryptPEMBlock(rand.Reader, "PRIVATE KEY", data, []byte(newPwd), x509.PEMCipherAES256) block, err = x509.EncryptPEMBlock(rand.Reader, "PRIVATE KEY", data, []byte(newPwd), x509.PEMCipherAES256)
if err != nil {
return err
}
} else { } else {
block = &pem.Block{Type: "PRIVATE KEY", Bytes: data} block = &pem.Block{Type: "PRIVATE KEY", Bytes: data}
} }
fmt.Println("4")
err = os.WriteFile(outpath+"/"+originName+".pkcs8", pem.EncodeToMemory(block), 0644) err = os.WriteFile(outpath+"/"+originName+".pkcs8", pem.EncodeToMemory(block), 0644)
if err != nil { if err != nil {
fmt.Println("5")
return err return err
} else { } else {
starlog.Green("已将私钥保存到%s\n", outpath+"/"+originName+".pkcs8") starlog.Green("已将私钥保存到%s\n", outpath+"/"+originName+".pkcs8")
} }
fmt.Println("6")
case *ecdsa.PublicKey, *rsa.PublicKey, *dsa.PublicKey, *ed25519.PublicKey, *ecdh.PublicKey: case *ecdsa.PublicKey, *rsa.PublicKey, *dsa.PublicKey, *ed25519.PublicKey, *ecdh.PublicKey:
data, err = x509.MarshalPKIXPublicKey(n) data, err = x509.MarshalPKIXPublicKey(n)
if err != nil { if err != nil {
@ -837,8 +810,6 @@ func Pkcs8(data []byte, pwd, newPwd string, originName string, outpath string) e
} else { } else {
starlog.Green("已将公钥保存到%s\n", outpath+"/"+originName+".pub.pkcs8") starlog.Green("已将公钥保存到%s\n", outpath+"/"+originName+".pub.pkcs8")
} }
default:
return fmt.Errorf("未知的密钥类型:%v", reflect.TypeOf(n))
} }
} }
return nil return nil
@ -894,11 +865,8 @@ func Pkcs12(keys []any, certs []x509.Certificate, enPwd string, originName strin
continue continue
} }
switch n := v.(type) { switch n := v.(type) {
case *ecdsa.PrivateKey, *rsa.PrivateKey, *dsa.PrivateKey, ed25519.PrivateKey, ecdh.PrivateKey, *ed25519.PrivateKey, *ecdh.PrivateKey: case *ecdsa.PrivateKey, *rsa.PrivateKey, *dsa.PrivateKey, *ed25519.PrivateKey, *ecdh.PrivateKey:
priv = n priv = n
if reflect.TypeOf(n) == reflect.TypeOf(&ed25519.PrivateKey{}) {
priv = *(n.(*ed25519.PrivateKey))
}
break break
} }
} }
@ -959,7 +927,7 @@ func Tran(data []byte, pwd string, originName string, outpath string) error {
} else { } else {
starlog.Green("已将公钥保存到%s\n", fmt.Sprintf("%s/%s_%v.tran.pub", outpath, originName, idx)) starlog.Green("已将公钥保存到%s\n", fmt.Sprintf("%s/%s_%v.tran.pub", outpath, originName, idx))
} }
case *dsa.PrivateKey, ed25519.PrivateKey, ecdh.PrivateKey: case *dsa.PrivateKey, *ed25519.PrivateKey, *ecdh.PrivateKey:
data, err = x509.MarshalPKCS8PrivateKey(n) data, err = x509.MarshalPKCS8PrivateKey(n)
if err != nil { if err != nil {
return err return err
@ -970,7 +938,7 @@ func Tran(data []byte, pwd string, originName string, outpath string) error {
} else { } else {
starlog.Green("已将私钥保存到%s\n", outpath+"/"+originName+".tran.key") starlog.Green("已将私钥保存到%s\n", outpath+"/"+originName+".tran.key")
} }
case *dsa.PublicKey, ed25519.PublicKey, ecdh.PublicKey: case *dsa.PublicKey, *ed25519.PublicKey, *ecdh.PublicKey:
data, err = x509.MarshalPKIXPublicKey(n) data, err = x509.MarshalPKIXPublicKey(n)
if err != nil { if err != nil {
return err return err
@ -998,15 +966,11 @@ func Openssh(data []byte, pwd, newPwd string, originName string, outpath string)
} }
var block *pem.Block var block *pem.Block
switch n := v.(type) { switch n := v.(type) {
case *ecdsa.PrivateKey, *rsa.PrivateKey, *dsa.PrivateKey, ed25519.PrivateKey, ecdh.PrivateKey, *ed25519.PrivateKey, *ecdh.PrivateKey: case *ecdsa.PrivateKey, *rsa.PrivateKey, *dsa.PrivateKey, *ed25519.PrivateKey, *ecdh.PrivateKey:
var key interface{} = n if newPwd != "" {
if reflect.TypeOf(n) == reflect.TypeOf(&ed25519.PrivateKey{}) { block, err = ssh.MarshalPrivateKey(n, "")
key = *(n.(*ed25519.PrivateKey))
}
if newPwd == "" {
block, err = ssh.MarshalPrivateKey(key, "")
} else { } else {
block, err = ssh.MarshalPrivateKeyWithPassphrase(key, "", []byte(newPwd)) block, err = ssh.MarshalPrivateKeyWithPassphrase(n, "", []byte(newPwd))
} }
if err != nil { if err != nil {
return err return err
@ -1017,7 +981,7 @@ func Openssh(data []byte, pwd, newPwd string, originName string, outpath string)
} else { } else {
starlog.Green("已将私钥保存到%s\n", outpath+"/"+originName+".openssh") starlog.Green("已将私钥保存到%s\n", outpath+"/"+originName+".openssh")
} }
case *ecdsa.PublicKey, *rsa.PublicKey, *dsa.PublicKey, ed25519.PublicKey, ecdh.PublicKey: case *ecdsa.PublicKey, *rsa.PublicKey, *dsa.PublicKey, *ed25519.PublicKey, *ecdh.PublicKey:
sk, err := ssh.NewPublicKey(n) sk, err := ssh.NewPublicKey(n)
if err != nil { if err != nil {
return err return err

View File

@ -1,14 +1,12 @@
package cert package cert
import ( import (
"b612.me/apps/b612/utils" "b612.me/starcrypto"
"b612.me/stario" "b612.me/stario"
"b612.me/starlog" "b612.me/starlog"
"crypto"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"math/big"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -27,11 +25,10 @@ var maxPathLen int
var caKey string var caKey string
var caCert string var caCert string
var csr string var csr string
var pubKey string
var caKeyPwd string var caKeyPwd string
var passwd string var passwd string
var enPasswd string var enPasswd string
var keyUsage int
var extKeyUsage []int
var Cmd = &cobra.Command{ var Cmd = &cobra.Command{
Use: "cert", Use: "cert",
@ -67,13 +64,24 @@ var CmdCsr = &cobra.Command{
if dnsName == nil { if dnsName == nil {
dnsName = stario.MessageBox("请输入dns名称用逗号分割", "").MustSliceString(",") dnsName = stario.MessageBox("请输入dns名称用逗号分割", "").MustSliceString(",")
} }
if startStr == "" {
startStr = stario.MessageBox("请输入开始时间:", "").MustString()
} }
key, err := LoadPriv(caKey, caKeyPwd) if endStr == "" {
endStr = stario.MessageBox("请输入结束时间:", "").MustString()
}
}
start, err = time.Parse(time.RFC3339, startStr)
if err != nil { if err != nil {
starlog.Errorln("加载Key错误", err) starlog.Errorln("开始时间格式错误,格式:2006-01-02T15:04:05Z07:00", err)
os.Exit(1) os.Exit(1)
} }
csr := outputCsr(GenerateCsr(country, province, city, org, orgUnit, name, dnsName), key) end, err = time.Parse(time.RFC3339, endStr)
if err != nil {
starlog.Errorln("结束时间格式错误,格式:2006-01-02T15:04:05Z07:00", err)
os.Exit(1)
}
csr := outputCsr(GenerateCsr(country, province, city, org, orgUnit, name, dnsName, start, end, isCa, maxPathLenZero, maxPathLen))
err = os.WriteFile(savefolder+"/"+name+".csr", csr, 0644) err = os.WriteFile(savefolder+"/"+name+".csr", csr, 0644)
if err != nil { if err != nil {
starlog.Errorln("保存csr文件错误", err) starlog.Errorln("保存csr文件错误", err)
@ -100,93 +108,31 @@ var CmdGen = &cobra.Command{
starlog.Errorln("证书请求不能为空") starlog.Errorln("证书请求不能为空")
os.Exit(1) os.Exit(1)
} }
var caKeyRaw crypto.PrivateKey if pubKey == "" {
var caCertRaw *x509.Certificate starlog.Errorln("证书公钥不能为空")
var err error os.Exit(1)
if !isCa { }
caKeyRaw, caCertRaw, err = LoadCA(caKey, caCert, caKeyPwd) caKeyRaw, caCertRaw, err := LoadCA(caKey, caCert, caKeyPwd)
if err != nil { if err != nil {
starlog.Errorln("加载CA错误", err) starlog.Errorln("加载CA错误", err)
os.Exit(1) os.Exit(1)
} }
} else {
caKeyRaw, err = LoadPriv(caKey, caKeyPwd)
if err != nil {
starlog.Errorln("加载CA错误", err)
os.Exit(1)
}
}
csrRaw, err := LoadCsr(csr) csrRaw, err := LoadCsr(csr)
if err != nil { if err != nil {
starlog.Errorln("加载证书请求错误", err) starlog.Errorln("加载证书请求错误", err)
os.Exit(1) os.Exit(1)
} }
start, err = time.Parse(time.RFC3339, startStr) pubKeyByte, err := os.ReadFile(pubKey)
if err != nil { if err != nil {
starlog.Errorln("开始时间格式错误,格式:2006-01-02T15:04:05Z07:00", err) starlog.Errorln("加载公钥错误", err)
os.Exit(1) os.Exit(1)
} }
end, err = time.Parse(time.RFC3339, endStr) pubKeyRaw, err := starcrypto.DecodePublicKey(pubKeyByte)
if err != nil { if err != nil {
starlog.Errorln("结束时间格式错误,格式:2006-01-02T15:04:05Z07:00", err) starlog.Errorln("解析公钥错误", err)
os.Exit(1) os.Exit(1)
} }
pubKeyRaw := csrRaw.PublicKey cert, err := MakeCert(caKeyRaw, caCertRaw, csrRaw, pubKeyRaw)
certReq := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: csrRaw.Subject,
IsCA: isCa,
NotBefore: start,
NotAfter: end,
MaxPathLen: maxPathLen,
MaxPathLenZero: maxPathLenZero,
DNSNames: csrRaw.DNSNames,
IPAddresses: csrRaw.IPAddresses,
}
if !isCa {
if keyUsage == 0 {
certReq.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
}
if len(extKeyUsage) == 0 {
certReq.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
}
} else {
if len(extKeyUsage) == 0 {
certReq.ExtKeyUsage = []x509.ExtKeyUsage{
x509.ExtKeyUsageAny,
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageCodeSigning,
x509.ExtKeyUsageEmailProtection,
x509.ExtKeyUsageIPSECEndSystem,
x509.ExtKeyUsageIPSECTunnel,
x509.ExtKeyUsageIPSECUser,
x509.ExtKeyUsageTimeStamping,
x509.ExtKeyUsageOCSPSigning,
x509.ExtKeyUsageMicrosoftServerGatedCrypto,
x509.ExtKeyUsageNetscapeServerGatedCrypto,
x509.ExtKeyUsageMicrosoftCommercialCodeSigning,
x509.ExtKeyUsageMicrosoftKernelCodeSigning,
}
}
if keyUsage == 0 {
certReq.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature
}
}
if keyUsage != 0 {
certReq.KeyUsage = x509.KeyUsage(keyUsage)
}
if len(extKeyUsage) > 0 {
certReq.ExtKeyUsage = make([]x509.ExtKeyUsage, len(extKeyUsage))
for i, v := range extKeyUsage {
certReq.ExtKeyUsage[i] = x509.ExtKeyUsage(v)
}
}
certReq.Subject.SerialNumber = fmt.Sprint(time.Now().UnixNano())
if isCa {
caCertRaw = certReq
}
cert, err := MakeCert(caKeyRaw, caCertRaw, certReq, pubKeyRaw)
if err != nil { if err != nil {
starlog.Errorln("生成证书错误", err) starlog.Errorln("生成证书错误", err)
os.Exit(1) os.Exit(1)
@ -201,91 +147,6 @@ var CmdGen = &cobra.Command{
}, },
} }
var CmdFastGen = &cobra.Command{
Use: "fastgen",
Short: "快速生成证书",
Long: "快速生成证书",
Run: func(cmd *cobra.Command, args []string) {
if promptMode {
if fastgen.Country == "" {
fastgen.Country = stario.MessageBox("请输入国家:", "").MustString()
}
if fastgen.Province == "" {
fastgen.Province = stario.MessageBox("请输入省份:", "").MustString()
}
if fastgen.City == "" {
fastgen.City = stario.MessageBox("请输入城市:", "").MustString()
}
if fastgen.Organization == "" {
fastgen.Organization = stario.MessageBox("请输入组织:", "").MustString()
}
if fastgen.OrganizationUnit == "" {
fastgen.OrganizationUnit = stario.MessageBox("请输入组织单位:", "").MustString()
}
if fastgen.CommonName == "" {
fastgen.CommonName = stario.MessageBox("请输入通用名称:", "").MustString()
}
if fastgen.Dns == nil {
fastgen.Dns = stario.MessageBox("请输入dns名称用逗号分割", "").MustSliceString(",")
}
if fastgen.Type == "" {
fastgen.Type = stario.MessageBox("请输入证书类型(RSA/ECDSA)", "RSA").MustString()
}
if fastgen.Bits <= 0 {
fastgen.Bits = stario.MessageBox("请输入证书位数:", "2048").MustInt()
}
if startStr == "" {
startStr = stario.MessageBox("请输入证书开始时间,格式:2006-01-02T15:04:05Z07:00", time.Now().Format(time.RFC3339)).MustString()
}
if endStr == "" {
endStr = stario.MessageBox("请输入证书结束时间,格式:2006-01-02T15:04:05Z07:00", time.Now().AddDate(1, 0, 0).Format(time.RFC3339)).MustString()
}
}
var err error
fastgen.StartDate, err = time.Parse(time.RFC3339, startStr)
if err != nil {
starlog.Errorln("开始时间格式错误,格式:2006-01-02T15:04:05Z07:00", err)
os.Exit(1)
}
fastgen.EndDate, err = time.Parse(time.RFC3339, endStr)
if err != nil {
starlog.Errorln("结束时间格式错误,格式:2006-01-02T15:04:05Z07:00", err)
os.Exit(1)
}
if caCert != "" && caKey != "" {
fastgen.CAPriv, fastgen.CA, err = LoadCA(caKey, caCert, caKeyPwd)
if err != nil {
starlog.Errorln("加载CA错误", err)
os.Exit(1)
}
}
if fastgen.CAPriv == nil {
fastgen.CA, fastgen.CAPriv = utils.ToolCert("")
}
byteCrt, byteKey, err := utils.GenerateCert(fastgen)
if err != nil {
starlog.Errorln("生成证书错误", err)
os.Exit(1)
}
name := fastgen.CommonName
if name == "" {
name = "cert"
}
err = os.WriteFile(filepath.Join(savefolder, name+".crt"), byteCrt, 0644)
if err != nil {
starlog.Errorln("保存证书错误", err)
os.Exit(1)
}
starlog.Infoln("保存证书成功", filepath.Join(savefolder, name+".crt"))
err = os.WriteFile(filepath.Join(savefolder, name+".key"), byteKey, 0644)
if err != nil {
starlog.Errorln("保存私钥错误", err)
os.Exit(1)
}
starlog.Infoln("保存私钥成功", filepath.Join(savefolder, name+".key"))
},
}
var CmdParse = &cobra.Command{ var CmdParse = &cobra.Command{
Use: "parse", Use: "parse",
Short: "解析证书", Short: "解析证书",
@ -307,8 +168,6 @@ var CmdParse = &cobra.Command{
}, },
} }
var fastgen utils.GenerateCertParams
func init() { func init() {
Cmd.AddCommand(CmdCsr) Cmd.AddCommand(CmdCsr)
CmdCsr.Flags().BoolVarP(&promptMode, "prompt", "P", false, "是否交互模式") CmdCsr.Flags().BoolVarP(&promptMode, "prompt", "P", false, "是否交互模式")
@ -319,26 +178,19 @@ func init() {
CmdCsr.Flags().StringVarP(&orgUnit, "orgUnit", "u", "", "组织单位") CmdCsr.Flags().StringVarP(&orgUnit, "orgUnit", "u", "", "组织单位")
CmdCsr.Flags().StringVarP(&name, "name", "n", "", "通用名称") CmdCsr.Flags().StringVarP(&name, "name", "n", "", "通用名称")
CmdCsr.Flags().StringSliceVarP(&dnsName, "dnsName", "d", nil, "dns名称") CmdCsr.Flags().StringSliceVarP(&dnsName, "dnsName", "d", nil, "dns名称")
CmdCsr.Flags().StringVarP(&startStr, "start", "S", time.Now().Format(time.RFC3339), "开始时间,格式:2006-01-02T15:04:05Z07:00")
CmdCsr.Flags().StringVarP(&endStr, "end", "E", time.Now().AddDate(1, 0, 0).Format(time.RFC3339), "结束时间,格式:2006-01-02T15:04:05Z07:00")
CmdCsr.Flags().StringVarP(&savefolder, "savefolder", "s", "./", "保存文件夹") CmdCsr.Flags().StringVarP(&savefolder, "savefolder", "s", "./", "保存文件夹")
CmdCsr.Flags().StringVarP(&caKey, "secret-key", "k", "", "加密私钥") CmdCsr.Flags().BoolVarP(&isCa, "isCa", "A", false, "是否是CA")
CmdCsr.Flags().StringVarP(&caKeyPwd, "secret-key-passwd", "K", "", "加密私钥的密码") CmdCsr.Flags().BoolVarP(&maxPathLenZero, "maxPathLenZero", "z", false, "允许最大路径长度为0")
//CmdCsr.Flags().BoolVarP(&isCa, "isCa", "A", false, "是否是CA") CmdCsr.Flags().IntVarP(&maxPathLen, "maxPathLen", "m", 0, "最大路径长度")
//CmdCsr.Flags().StringVarP(&startStr, "start", "S", time.Now().Format(time.RFC3339), "开始时间,格式:2006-01-02T15:04:05Z07:00")
//CmdCsr.Flags().StringVarP(&endStr, "end", "E", time.Now().AddDate(1, 0, 0).Format(time.RFC3339), "结束时间,格式:2006-01-02T15:04:05Z07:00")
//CmdCsr.Flags().BoolVarP(&maxPathLenZero, "maxPathLenZero", "z", false, "允许最大路径长度为0")
//CmdCsr.Flags().IntVarP(&maxPathLen, "maxPathLen", "m", 0, "最大路径长度")
CmdGen.Flags().IntVarP(&keyUsage, "keyUsage", "u", 0, "证书使用类型默认数字00表示数字签名和密钥加密1表示证书签名2表示CRL签名4表示密钥协商8表示数据加密")
CmdGen.Flags().IntSliceVarP(&extKeyUsage, "extKeyUsage", "e", []int{0, 1}, "扩展证书使用类型默认数字0和10表示服务器认证1表示客户端认证2表示代码签名3表示电子邮件保护4表示IPSEC终端系统5表示IPSEC隧道6表示IPSEC用户7表示时间戳8表示OCSP签名9表示Microsoft服务器网关加密10表示Netscape服务器网关加密11表示Microsoft商业代码签名12表示Microsoft内核代码签名")
CmdGen.Flags().StringVarP(&caKey, "caKey", "k", "", "CA私钥") CmdGen.Flags().StringVarP(&caKey, "caKey", "k", "", "CA私钥")
CmdGen.Flags().StringVarP(&caCert, "caCert", "C", "", "CA证书") CmdGen.Flags().StringVarP(&caCert, "caCert", "C", "", "CA证书")
CmdGen.Flags().StringVarP(&csr, "csr", "r", "", "证书请求") CmdGen.Flags().StringVarP(&csr, "csr", "r", "", "证书请求")
CmdGen.Flags().StringVarP(&pubKey, "pubKey", "P", "", "证书公钥")
CmdGen.Flags().StringVarP(&savefolder, "savefolder", "s", "./", "保存文件夹") CmdGen.Flags().StringVarP(&savefolder, "savefolder", "s", "./", "保存文件夹")
CmdGen.Flags().StringVarP(&caKeyPwd, "caKeyPwd", "p", "", "CA私钥密码") CmdGen.Flags().StringVarP(&caKeyPwd, "caKeyPwd", "p", "", "CA私钥密码")
CmdGen.Flags().BoolVarP(&isCa, "isCa", "A", false, "是否是CA")
CmdGen.Flags().StringVarP(&startStr, "start", "S", time.Now().Format(time.RFC3339), "开始时间,格式:2006-01-02T15:04:05Z07:00")
CmdGen.Flags().StringVarP(&endStr, "end", "E", time.Now().AddDate(1, 0, 0).Format(time.RFC3339), "结束时间,格式:2006-01-02T15:04:05Z07:00")
CmdGen.Flags().BoolVarP(&maxPathLenZero, "maxPathLenZero", "z", false, "允许最大路径长度为0")
CmdGen.Flags().IntVarP(&maxPathLen, "maxPathLen", "m", 0, "最大路径长度")
Cmd.AddCommand(CmdGen) Cmd.AddCommand(CmdGen)
CmdParse.Flags().StringVarP(&passwd, "passwd", "p", "", "pfx解密密码") CmdParse.Flags().StringVarP(&passwd, "passwd", "p", "", "pfx解密密码")
@ -367,28 +219,6 @@ func init() {
CmdOpenssh.Flags().StringVarP(&enPasswd, "en-passwd", "P", "", "加密密码") CmdOpenssh.Flags().StringVarP(&enPasswd, "en-passwd", "P", "", "加密密码")
Cmd.AddCommand(CmdOpenssh) Cmd.AddCommand(CmdOpenssh)
CmdFastGen.Flags().BoolVarP(&promptMode, "prompt", "P", false, "是否交互模式")
CmdFastGen.Flags().StringVarP(&fastgen.Country, "country", "c", "", "国家")
CmdFastGen.Flags().StringVarP(&fastgen.Province, "province", "p", "", "省份")
CmdFastGen.Flags().StringVar(&fastgen.City, "city", "", "城市")
CmdFastGen.Flags().StringVarP(&fastgen.Organization, "org", "o", "", "组织")
CmdFastGen.Flags().StringVarP(&fastgen.OrganizationUnit, "orgUnit", "u", "", "组织单位")
CmdFastGen.Flags().StringVarP(&fastgen.CommonName, "name", "n", "", "通用名称")
CmdFastGen.Flags().StringSliceVarP(&fastgen.Dns, "dnsName", "d", nil, "dns名称")
CmdFastGen.Flags().StringVarP(&savefolder, "savefolder", "s", "./", "保存文件夹")
CmdFastGen.Flags().IntVarP(&fastgen.KeyUsage, "keyUsage", "U", 0, "证书使用类型默认数字00表示数字签名和密钥加密1表示证书签名2表示CRL签名4表示密钥协商8表示数据加密")
CmdFastGen.Flags().IntSliceVarP(&fastgen.ExtendedKeyUsage, "extKeyUsage", "e", []int{0, 1}, "扩展证书使用类型默认数字0和10表示服务器认证1表示客户端认证2表示代码签名3表示电子邮件保护4表示IPSEC终端系统5表示IPSEC隧道6表示IPSEC用户7表示时间戳8表示OCSP签名9表示Microsoft服务器网关加密10表示Netscape服务器网关加密11表示Microsoft商业代码签名12表示Microsoft内核代码签名")
CmdFastGen.Flags().BoolVarP(&fastgen.IsCA, "isCa", "A", false, "是否是CA")
CmdFastGen.Flags().StringVarP(&startStr, "start", "S", time.Now().Format(time.RFC3339), "开始时间,格式:2006-01-02T15:04:05Z07:00")
CmdFastGen.Flags().StringVarP(&endStr, "end", "E", time.Now().AddDate(1, 0, 0).Format(time.RFC3339), "结束时间,格式:2006-01-02T15:04:05Z07:00")
CmdFastGen.Flags().BoolVarP(&fastgen.MaxPathLengthZero, "maxPathLenZero", "z", false, "允许最大路径长度为0")
CmdFastGen.Flags().IntVarP(&fastgen.MaxPathLength, "maxPathLen", "m", 0, "最大路径长度")
CmdFastGen.Flags().StringVarP(&caKey, "caKey", "K", "", "CA私钥可以留空")
CmdFastGen.Flags().StringVarP(&caCert, "caCert", "C", "", "CA证书可以留空")
CmdFastGen.Flags().StringVar(&caKeyPwd, "caKeyPwd", "", "CA私钥密码")
CmdFastGen.Flags().StringVarP(&fastgen.Type, "type", "t", "RSA", "证书类型支持RSA和ECDSA")
CmdFastGen.Flags().IntVarP(&fastgen.Bits, "bits", "b", 2048, "证书位数默认2048")
Cmd.AddCommand(CmdFastGen)
} }
var CmdPkcs8 = &cobra.Command{ var CmdPkcs8 = &cobra.Command{

View File

@ -1,16 +1,17 @@
package cert package cert
import ( import (
"crypto/rand"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"errors" "errors"
"math/big"
"net" "net"
"os" "os"
"time"
) )
func GenerateCsr(country, province, city, org, orgUnit, name string, dnsName []string) *x509.CertificateRequest { func GenerateCsr(country, province, city, org, orgUnit, name string, dnsName []string, start, end time.Time, isCa bool, maxPathLenZero bool, maxPathLen int) *x509.Certificate {
var trueDNS []string var trueDNS []string
var trueIp []net.IP var trueIp []net.IP
for _, v := range dnsName { for _, v := range dnsName {
@ -21,17 +22,15 @@ func GenerateCsr(country, province, city, org, orgUnit, name string, dnsName []s
} }
trueIp = append(trueIp, ip) trueIp = append(trueIp, ip)
} }
/*
ku := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment ku := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
eku := x509.ExtKeyUsageServerAuth eku := x509.ExtKeyUsageServerAuth
if isCa { if isCa {
ku = x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature ku = x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature
eku = x509.ExtKeyUsageAny eku = x509.ExtKeyUsageAny
} }
*/ return &x509.Certificate{
return &x509.CertificateRequest{
Version: 3, Version: 3,
//SerialNumber: big.NewInt(time.Now().Unix()), SerialNumber: big.NewInt(time.Now().Unix()),
Subject: pkix.Name{ Subject: pkix.Name{
Country: s2s(country), Country: s2s(country),
Province: s2s(province), Province: s2s(province),
@ -42,25 +41,21 @@ func GenerateCsr(country, province, city, org, orgUnit, name string, dnsName []s
}, },
DNSNames: trueDNS, DNSNames: trueDNS,
IPAddresses: trueIp, IPAddresses: trueIp,
//NotBefore: start, NotBefore: start,
//NotAfter: end, NotAfter: end,
//BasicConstraintsValid: true, BasicConstraintsValid: true,
//IsCA: isCa, IsCA: isCa,
//MaxPathLen: maxPathLen, MaxPathLen: maxPathLen,
//MaxPathLenZero: maxPathLenZero, MaxPathLenZero: maxPathLenZero,
//KeyUsage: ku, KeyUsage: ku,
//ExtKeyUsage: []x509.ExtKeyUsage{eku}, ExtKeyUsage: []x509.ExtKeyUsage{eku},
} }
} }
func outputCsr(csr *x509.CertificateRequest, priv interface{}) []byte { func outputCsr(csr *x509.Certificate) []byte {
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csr, priv)
if err != nil {
return nil
}
return pem.EncodeToMemory(&pem.Block{ return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST", Type: "CERTIFICATE REQUEST",
Bytes: csrBytes, Bytes: csr.Raw,
}) })
} }
@ -71,7 +66,7 @@ func s2s(str string) []string {
return []string{str} return []string{str}
} }
func LoadCsr(csrPath string) (*x509.CertificateRequest, error) { func LoadCsr(csrPath string) (*x509.Certificate, error) {
csrBytes, err := os.ReadFile(csrPath) csrBytes, err := os.ReadFile(csrPath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -80,7 +75,7 @@ func LoadCsr(csrPath string) (*x509.CertificateRequest, error) {
if block == nil || block.Type != "CERTIFICATE REQUEST" { if block == nil || block.Type != "CERTIFICATE REQUEST" {
return nil, errors.New("Failed to decode PEM block containing the certificate") return nil, errors.New("Failed to decode PEM block containing the certificate")
} }
cert, err := x509.ParseCertificateRequest(block.Bytes) cert, err := x509.ParseCertificate(block.Bytes)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -151,7 +151,6 @@ func (c *DoHClient) Exchange(req *dns.Msg, address string) (r *dns.Msg, rtt time
// No need to use hreq.URL.Query() // No need to use hreq.URL.Query()
hreq, _ := http.NewRequest("GET", address+"?dns="+string(b64), nil) hreq, _ := http.NewRequest("GET", address+"?dns="+string(b64), nil)
hreq.Header.Set("User-Agent", "B612 DoH Client")
hreq.Header.Add("Accept", DoHMediaType) hreq.Header.Add("Accept", DoHMediaType)
resp, err := c.cli.Do(hreq) resp, err := c.cli.Do(hreq)
if err != nil { if err != nil {

View File

@ -1,204 +0,0 @@
package filedate
import (
"b612.me/staros"
"fmt"
"github.com/spf13/cobra"
"os"
"path/filepath"
"regexp"
"strconv"
"time"
)
var aTime, mTime, cTime, allTime string // "2006-01-02 15:04:05" format
var fileRegexp string // 正则表达式匹配文件名
var recursive bool // 是否递归子目录
var dateFromName bool // 是否从文件名中提取日期
var dateFromNameReg string // 从文件名提取日期的正则表达式
var dateFromNameReplace string // 从文件名提取日期正则替代
var dateFromNameFormat string
var isTimestamp bool // 文件名是否是时间戳
var onlyModifyAccess bool // 只修改访问时间
var onlyModifyCreate bool // 只修改创建时间
var onlyModifyModify bool // 只修改修改时间
var excludeFolder bool // 是否排除目录本身
var Cmd = &cobra.Command{
Use: "filedate",
Short: "修改文件的创建/修改/访问时间",
Long: `修改文件的创建/修改/访问时间
可以通过 --access, --modify, --create, --all 来设置不同的时间
例如
b612 filedate file.txt --access "2023-10-01 12:00:00" --modify "2023-10-01 12:00:00"
b612 filedate dir/ --all "2023-10-01 12:00:00" -r
也可以通过 --regexp 来匹配文件名或者通过 --date-from-name 来从文件名中提取日期
比如文件名为IMG_20160711_161625.jpg,可以通过如下命令来设置时间
b612 filedate -D -y '$1$2' -x 20060102150405 -z '.*?(\d+)_(\d+).+' .\IMG_20160711_161625.jpg
`,
Run: run,
}
func init() {
Cmd.Flags().StringVarP(&aTime, "access", "a", "", "设置访问时间,格式: 2006-01-02 15:04:05")
Cmd.Flags().StringVarP(&mTime, "modify", "m", "", "设置修改时间,格式: 2006-01-02 15:04:05")
Cmd.Flags().StringVarP(&cTime, "create", "c", "", "设置创建时间,格式: 2006-01-02 15:04:05")
Cmd.Flags().StringVarP(&allTime, "all", "l", "", "设置所有时间,格式: 2006-01-02 15:04:05")
Cmd.Flags().StringVarP(&fileRegexp, "regexp", "R", "", "正则表达式匹配文件名")
Cmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "递归子目录")
Cmd.Flags().BoolVarP(&dateFromName, "date-from-name", "D", false, "从文件名中提取日期")
Cmd.Flags().StringVarP(&dateFromNameReg, "date-from-name-regex", "z", "", "从文件名提取日期的正则表达式")
Cmd.Flags().StringVarP(&dateFromNameReplace, "date-from-name-replace", "y", "", "从文件名提取日期的替换表达式")
Cmd.Flags().StringVarP(&dateFromNameFormat, "date-from-name-format", "x", "2006-01-02 15:04:05", "从文件名提取日期的格式")
Cmd.Flags().BoolVarP(&isTimestamp, "timestamp", "t", false, "文件名是否是时间戳")
Cmd.Flags().BoolVarP(&onlyModifyAccess, "only-modify-access", "A", false, "只修改访问时间")
Cmd.Flags().BoolVarP(&onlyModifyCreate, "only-modify-create", "C", false, "只修改创建时间")
Cmd.Flags().BoolVarP(&onlyModifyModify, "only-modify-modify", "M", false, "只修改修改时间")
Cmd.Flags().BoolVarP(&excludeFolder, "exclude-folder", "e", false, "排除目录本身,不修改目录的时间")
}
func run(cmd *cobra.Command, args []string) {
var aDate, mDate, cDate, allDate time.Time
var err error
if aTime != "" && (onlyModifyAccess || !(onlyModifyCreate || onlyModifyModify)) {
aDate, err = time.ParseInLocation("2006-01-02 15:04:05", aTime, time.Local)
if err != nil {
cmd.PrintErrf("解析访问时间失败: %v\n", err)
return
}
}
if mTime != "" && (onlyModifyCreate || !(onlyModifyAccess || onlyModifyModify)) {
mDate, err = time.ParseInLocation("2006-01-02 15:04:05", mTime, time.Local)
if err != nil {
cmd.PrintErrf("解析修改时间失败: %v\n", err)
return
}
}
if cTime != "" && (onlyModifyCreate || !(onlyModifyAccess || onlyModifyModify)) {
cDate, err = time.ParseInLocation("2006-01-02 15:04:05", cTime, time.Local)
if err != nil {
cmd.PrintErrf("解析创建时间失败: %v\n", err)
return
}
}
if allTime != "" {
allDate, err = time.ParseInLocation("2006-01-02 15:04:05", allTime, time.Local)
if err != nil {
cmd.PrintErrf("解析所有时间失败: %v\n", err)
return
}
if mDate.IsZero() && (onlyModifyCreate || !(onlyModifyAccess || onlyModifyModify)) {
mDate = allDate
}
if cDate.IsZero() && (onlyModifyCreate || !(onlyModifyAccess || onlyModifyModify)) {
cDate = allDate
}
if aDate.IsZero() && (onlyModifyAccess || !(onlyModifyCreate || onlyModifyModify)) {
aDate = allDate
}
}
var reg, namereg *regexp.Regexp
if dateFromName {
if dateFromNameReg == "" {
cmd.PrintErrf("未指定从文件名提取日期的正则表达式\n")
return
}
if dateFromNameReplace == "" {
cmd.PrintErrf("未指定从文件名提取日期的替换表达式\n")
return
}
if dateFromNameFormat == "" {
cmd.PrintErrf("未指定从文件名提取日期的格式\n")
return
}
namereg, err = regexp.Compile(dateFromNameReg)
if err != nil {
cmd.PrintErrf("解析正则表达式失败: %v\n", err)
return
}
}
if fileRegexp != "" {
reg, err = regexp.Compile(fileRegexp)
if err != nil {
cmd.PrintErrf("解析正则表达式失败: %v\n", err)
return
}
}
for _, path := range args {
SetFileTimes(path, reg, namereg, cDate, aDate, mDate)
}
}
func SetFileTimes(path string, reg, namereg *regexp.Regexp, ctime, atime, mtime time.Time) error {
setFile := func(path string, info os.FileInfo) error {
var err error
if reg != nil && !reg.MatchString(info.Name()) {
return nil // 跳过不匹配的文件
}
if dateFromName && namereg != nil {
date := namereg.ReplaceAllString(filepath.Base(path), dateFromNameReplace)
if date == "" {
fmt.Printf("从文件名提取日期失败: %s\n", path)
return nil // 跳过无法提取日期的文件
}
var realDate time.Time
if !isTimestamp {
realDate, err = time.ParseInLocation(dateFromNameFormat, date, time.Local)
if err != nil {
fmt.Printf("解析文件名日期失败: %s, 错误: %v\n", path, err)
return nil // 跳过无法解析日期的文件
}
} else {
tmp, err := strconv.ParseInt(date, 10, 64)
if err != nil {
fmt.Printf("解析时间戳失败: %s, 错误: %v\n", path, err)
return nil // 跳过无法解析时间戳的文件
}
realDate = time.Unix(tmp, 0)
}
if onlyModifyCreate || !(onlyModifyAccess || onlyModifyModify) {
mtime = realDate
}
if onlyModifyCreate || !(onlyModifyAccess || onlyModifyModify) {
ctime = realDate
}
if onlyModifyAccess || !(onlyModifyCreate || onlyModifyModify) {
atime = realDate
}
}
if err := SetFileTime(path, ctime, atime, mtime); err != nil {
return fmt.Errorf("设置文件时间失败: %s, 错误: %v", path, err)
}
fmt.Printf("已设置文件时间: %s\n", path)
return nil
}
if !staros.Exists(path) {
return fmt.Errorf("文件不存在: %s", path)
}
if staros.IsFile(path) {
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("获取文件信息失败: %s, 错误: %v", path, err)
}
return setFile(path, info)
}
return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if staros.IsFile(path) {
return setFile(path, info)
}
if staros.IsFolder(path) {
if recursive {
err = SetFileTimes(path, reg, namereg, ctime, atime, mtime)
if err != nil {
return fmt.Errorf("递归设置文件时间失败: %s, 错误: %v", path, err)
}
}
if excludeFolder {
return nil // 如果排除目录本身,则不处理目录的时间
}
return setFile(path, info) // 处理目录本身
}
return nil
})
}

View File

@ -1,38 +0,0 @@
//go:build darwin
package filedate
import (
"fmt"
"os"
"time"
)
func SetFileTime(path string, ctime, atime, mtime time.Time) error {
var err error
originalAtime := atime
originalMtime := mtime
if atime.IsZero() || mtime.IsZero() {
var fi os.FileInfo
fi, err = os.Stat(path)
if err != nil {
return fmt.Errorf("failed to get file info: %w", err)
}
// 获取原始时间
if atime.IsZero() {
originalAtime = fi.ModTime() // macOS 没有访问时间,使用修改时间代替
}
if mtime.IsZero() {
originalMtime = fi.ModTime()
}
}
err = os.Chtimes(path, originalAtime, originalMtime)
if err != nil {
return fmt.Errorf("Chtimes failed: %w", err)
}
return nil
}

View File

@ -1,32 +0,0 @@
//go:build !windows && !darwin
package filedate
import (
"fmt"
"golang.org/x/sys/unix"
"time"
)
func SetFileTime(path string, ctime, atime, mtime time.Time) error {
var ts [2]unix.Timespec
if atime.IsZero() {
ts[0].Nsec = unix.UTIME_OMIT
} else {
ts[0] = unix.NsecToTimespec(atime.UnixNano())
}
if mtime.IsZero() {
ts[1].Nsec = unix.UTIME_OMIT
} else {
ts[1] = unix.NsecToTimespec(mtime.UnixNano())
}
// 使用 AT_FDCWD 表示相对当前工作目录
err := unix.UtimesNanoAt(unix.AT_FDCWD, path, ts[:], unix.AT_SYMLINK_NOFOLLOW)
if err != nil {
return fmt.Errorf("utimensat failed: %w", err)
}
return nil
}

View File

@ -1,54 +0,0 @@
//go:build windows
package filedate
import (
"fmt"
"golang.org/x/sys/windows"
"time"
)
func SetFileTime(path string, ctime, atime, mtime time.Time) error {
path16, err := windows.UTF16PtrFromString(path)
if err != nil {
return err
}
handle, err := windows.CreateFile(
path16,
windows.FILE_WRITE_ATTRIBUTES,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
nil,
windows.OPEN_EXISTING,
windows.FILE_FLAG_BACKUP_SEMANTICS,
0,
)
if err != nil {
return fmt.Errorf("CreateFile failed: %w", err)
}
defer windows.CloseHandle(handle)
var (
ctimePtr *windows.Filetime
atimePtr *windows.Filetime
mtimePtr *windows.Filetime
)
if !ctime.IsZero() {
ctimeFt := windows.NsecToFiletime(ctime.UnixNano())
ctimePtr = &ctimeFt
}
if !atime.IsZero() {
atimeFt := windows.NsecToFiletime(atime.UnixNano())
atimePtr = &atimeFt
}
if !mtime.IsZero() {
mtimeFt := windows.NsecToFiletime(mtime.UnixNano())
mtimePtr = &mtimeFt
}
if err := windows.SetFileTime(handle, ctimePtr, atimePtr, mtimePtr); err != nil {
return fmt.Errorf("SetFileTime failed: %w", err)
}
return nil
}

View File

@ -1,208 +0,0 @@
package ftp
import (
"errors"
"fmt"
"goftp.io/server/v2"
"io"
"os"
"path/filepath"
"strings"
)
type Driver struct {
RootPath string
}
// NewDriver implements Driver
func NewDriver(rootPath string) (server.Driver, error) {
var err error
rootPath, err = filepath.Abs(rootPath)
if err != nil {
return nil, err
}
if fi, err := os.Lstat(rootPath); err == nil {
if fi.Mode()&os.ModeSymlink != 0 {
realPath, err := filepath.EvalSymlinks(rootPath)
if err != nil {
return nil, err
}
rootPath = realPath
fmt.Println("Real path:", rootPath)
}
}
return &Driver{rootPath}, nil
}
func (driver *Driver) realPath(path string) string {
paths := strings.Split(path, "/")
realPath, _ := filepath.Abs(filepath.Join(append([]string{driver.RootPath}, paths...)...))
if strings.HasPrefix(realPath, driver.RootPath) {
return realPath
}
return filepath.Join(driver.RootPath)
}
// Stat implements Driver
func (driver *Driver) Stat(ctx *server.Context, path string) (os.FileInfo, error) {
basepath := driver.realPath(path)
rPath, err := filepath.Abs(basepath)
if err != nil {
return nil, err
}
return os.Lstat(rPath)
}
// ListDir implements Driver
func (driver *Driver) ListDir(ctx *server.Context, path string, callback func(os.FileInfo) error) error {
basepath := driver.realPath(path)
return filepath.Walk(basepath, func(f string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rPath, _ := filepath.Rel(basepath, f)
if rPath == info.Name() {
err = callback(info)
if err != nil {
return err
}
if info.IsDir() {
return filepath.SkipDir
}
}
return nil
})
}
// DeleteDir implements Driver
func (driver *Driver) DeleteDir(ctx *server.Context, path string) error {
rPath := driver.realPath(path)
f, err := os.Lstat(rPath)
if err != nil {
return err
}
if f.IsDir() {
return os.RemoveAll(rPath)
}
return errors.New("Not a directory")
}
// DeleteFile implements Driver
func (driver *Driver) DeleteFile(ctx *server.Context, path string) error {
rPath := driver.realPath(path)
f, err := os.Lstat(rPath)
if err != nil {
return err
}
if !f.IsDir() {
return os.Remove(rPath)
}
return errors.New("Not a file")
}
// Rename implements Driver
func (driver *Driver) Rename(ctx *server.Context, fromPath string, toPath string) error {
oldPath := driver.realPath(fromPath)
newPath := driver.realPath(toPath)
return os.Rename(oldPath, newPath)
}
// MakeDir implements Driver
func (driver *Driver) MakeDir(ctx *server.Context, path string) error {
rPath := driver.realPath(path)
return os.MkdirAll(rPath, os.ModePerm)
}
// GetFile implements Driver
func (driver *Driver) GetFile(ctx *server.Context, path string, offset int64) (int64, io.ReadCloser, error) {
rPath := driver.realPath(path)
f, err := os.Open(rPath)
if err != nil {
return 0, nil, err
}
defer func() {
if err != nil && f != nil {
f.Close()
}
}()
info, err := f.Stat()
if err != nil {
return 0, nil, err
}
_, err = f.Seek(offset, io.SeekStart)
if err != nil {
return 0, nil, err
}
return info.Size() - offset, f, nil
}
// PutFile implements Driver
func (driver *Driver) PutFile(ctx *server.Context, destPath string, data io.Reader, offset int64) (int64, error) {
rPath := driver.realPath(destPath)
var isExist bool
f, err := os.Lstat(rPath)
if err == nil {
isExist = true
if f.IsDir() {
return 0, errors.New("A dir has the same name")
}
} else {
if os.IsNotExist(err) {
isExist = false
} else {
return 0, errors.New(fmt.Sprintln("Put File error:", err))
}
}
if offset > -1 && !isExist {
offset = -1
}
if offset == -1 {
if isExist {
err = os.Remove(rPath)
if err != nil {
return 0, err
}
}
f, err := os.Create(rPath)
if err != nil {
return 0, err
}
defer f.Close()
bytes, err := io.Copy(f, data)
if err != nil {
return 0, err
}
return bytes, nil
}
of, err := os.OpenFile(rPath, os.O_APPEND|os.O_RDWR, 0660)
if err != nil {
return 0, err
}
defer of.Close()
info, err := of.Stat()
if err != nil {
return 0, err
}
if offset > info.Size() {
return 0, fmt.Errorf("Offset %d is beyond file size %d", offset, info.Size())
}
_, err = of.Seek(offset, os.SEEK_END)
if err != nil {
return 0, err
}
bytes, err := io.Copy(of, data)
if err != nil {
return 0, err
}
return bytes, nil
}

View File

@ -2,34 +2,16 @@ package ftp
import ( import (
"log" "log"
"net"
"os/user"
"path/filepath" "path/filepath"
"strings"
filedriver "github.com/goftp/file-driver"
"github.com/goftp/server"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"goftp.io/server/v2"
) )
var ports int var ports int
var authuser, authpwd string var username, pwd string
var path, ip, publicIp string var path, ip string
var username, groupname, passiveports string
var cert, key string
var forceTls bool
type Auth struct {
}
func (a Auth) CheckPasswd(ctx *server.Context, user string, name string) (bool, error) {
if authuser == "" && authpwd == "" {
return true, nil
}
if authuser == user && authpwd == name {
return true, nil
}
return false, nil
}
// ftpCmd represents the ftp command // ftpCmd represents the ftp command
var Cmd = &cobra.Command{ var Cmd = &cobra.Command{
@ -37,56 +19,21 @@ var Cmd = &cobra.Command{
Short: `FTP文件服务器`, Short: `FTP文件服务器`,
Long: `FTP文件服务器`, Long: `FTP文件服务器`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if username == "" || groupname == "" {
u, err := user.Current()
if err == nil {
username = u.Username
groupname = u.Username
} else {
username = "root"
groupname = "root"
}
}
if publicIp == "" {
if tmp, err := net.Dial("udp", "139.199.163.65:443"); err == nil {
publicIp = strings.Split(tmp.LocalAddr().String(), ":")[0]
tmp.Close()
}
}
path, _ = filepath.Abs(path) path, _ = filepath.Abs(path)
driver, err := NewDriver(path) factory := &filedriver.FileDriverFactory{
if err != nil { RootPath: path,
log.Fatal("Error starting server:", err) Perm: server.NewSimplePerm("user", "group"),
} }
opts := &server.Options{ opts := &server.ServerOpts{
Auth: Auth{}, Factory: factory,
Driver: driver,
Perm: server.NewSimplePerm(username, groupname),
Name: "B612 FTP Server",
Hostname: ip,
PublicIP: publicIp,
PassivePorts: passiveports,
Port: ports, Port: ports,
WelcomeMessage: "B612 FTP Server", Hostname: ip,
Logger: nil, Auth: &server.SimpleAuth{Name: username, Password: pwd},
RateLimit: 0,
TLS: key != "" && cert != "",
ForceTLS: forceTls,
CertFile: cert,
KeyFile: key,
} }
log.Printf("Starting ftp server on %v:%v", opts.Hostname, opts.Port) log.Printf("Starting ftp server on %v:%v", opts.Hostname, opts.Port)
log.Printf("AuthUser %v, Password %v", authuser, authpwd) log.Printf("Username %v, Password %v", username, pwd)
log.Printf("Public IP %v", publicIp) server := server.NewServer(opts)
log.Printf("Path %v", path) err := server.ListenAndServe()
log.Printf("Passive Ports %v", passiveports)
log.Printf("TLS %v", opts.TLS)
log.Printf("Username %v, Groupname %v", username, groupname)
server, err := server.NewServer(opts)
if err != nil {
log.Fatal("Error starting server:", err)
}
err = server.ListenAndServe()
if err != nil { if err != nil {
log.Fatal("Error starting server:", err) log.Fatal("Error starting server:", err)
} }
@ -94,16 +41,9 @@ var Cmd = &cobra.Command{
} }
func init() { func init() {
Cmd.Flags().StringVarP(&publicIp, "public-ip", "I", "", "公网IP")
Cmd.Flags().StringVarP(&username, "username", "U", "", "FTP用户名")
Cmd.Flags().StringVarP(&groupname, "groupname", "G", "", "FTP组名")
Cmd.Flags().StringVarP(&passiveports, "passiveports", "P", "50000-60000", "被动模式端口范围格式50000-60000")
Cmd.Flags().IntVarP(&ports, "port", "p", 21, "监听端口") Cmd.Flags().IntVarP(&ports, "port", "p", 21, "监听端口")
Cmd.Flags().StringVarP(&ip, "ip", "i", "0.0.0.0", "监听地址") Cmd.Flags().StringVarP(&ip, "ip", "i", "0.0.0.0", "监听地址")
Cmd.Flags().StringVarP(&authuser, "auth-user", "u", "", "用户名,默认为任意") Cmd.Flags().StringVarP(&username, "user", "u", "1", "用户名默认为1")
Cmd.Flags().StringVarP(&authpwd, "auth-passwd", "k", "", "密码,默认为任意") Cmd.Flags().StringVarP(&pwd, "pwd", "k", "1", "密码默认为1")
Cmd.Flags().StringVarP(&path, "folder", "f", "./", "本地文件地址") Cmd.Flags().StringVarP(&path, "folder", "f", "./", "本地文件地址")
Cmd.Flags().BoolVarP(&forceTls, "force-tls", "t", false, "强制使用TLS加密传输")
Cmd.Flags().StringVarP(&cert, "cert", "C", "", "TLS证书文件")
Cmd.Flags().StringVarP(&key, "key", "K", "", "TLS密钥文件")
} }

6
gdu/.gitignore vendored
View File

@ -1,6 +0,0 @@
/.vscode
/.idea
/coverage.txt
/dist
/test_dir
/vendor

View File

@ -1,122 +0,0 @@
linters-settings:
errcheck:
check-blank: true
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unreachable-code
- name: redefines-builtin-id
# While we agree with this rule, right now it would break too many
# projects. So, we disable it by default.
- name: unused-parameter
disabled: true
gocyclo:
min-complexity: 25
dupl:
threshold: 100
goconst:
min-len: 3
min-occurrences: 3
lll:
line-length: 160
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- whyNoLint
funlen:
lines: 500
statements: 50
govet:
enable:
- shadow
linters:
disable-all: true
enable:
- bodyclose
- dogsled
- errcheck
- errorlint
- exhaustive
- exportloopref
- funlen
- goconst
- gocritic
- gocyclo
- gofmt
- goimports
- revive
- gosimple
- govet
- ineffassign
- lll
- nakedret
- staticcheck
- typecheck
- unparam
- unused
- whitespace
issues:
exclude:
# We allow error shadowing
- 'declaration of "err" shadows declaration at'
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gocyclo
- errcheck
- gosec
- funlen
- gocritic
- gochecknoglobals # Globals in test files are tolerated.
- goconst # Repeated consts in test files are tolerated.
# This rule is buggy and breaks on our `///Block` lines. Disable for now.
- linters:
- gocritic
text: "commentFormatting: put a space"
# This rule incorrectly flags nil references after assert.Assert(t, x != nil)
- path: _test\.go
text: "SA5011"
linters:
- staticcheck
- linters:
- lll
source: "^//go:generate "
- linters:
- lll
- gocritic
path: \.resolvers\.go
source: '^func \(r \*[a-zA-Z]+Resolvers\) '
output:
formats:
- format: colored-line-number
sort-results: true

View File

@ -1 +0,0 @@
golang 1.23.3

View File

@ -1,15 +0,0 @@
FROM docker.io/library/golang:1.23 as builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make build-static
FROM scratch
COPY --from=builder /app/dist/gdu /opt/gdu
ENTRYPOINT ["/opt/gdu"]

View File

@ -1,142 +0,0 @@
# Installation
[Arch Linux](https://archlinux.org/packages/extra/x86_64/gdu/):
pacman -S gdu
[Debian](https://packages.debian.org/bullseye/gdu):
apt install gdu
[Ubuntu](https://launchpad.net/~daniel-milde/+archive/ubuntu/gdu)
add-apt-repository ppa:daniel-milde/gdu
apt-get update
apt-get install gdu
[NixOS](https://search.nixos.org/packages?channel=unstable&show=gdu&query=gdu):
nix-env -iA nixos.gdu
[Homebrew](https://formulae.brew.sh/formula/gdu):
brew install -f gdu
# gdu will be installed as `gdu-go` to avoid conflicts with coreutils
gdu-go
[Snap](https://snapcraft.io/gdu-disk-usage-analyzer):
snap install gdu-disk-usage-analyzer
snap connect gdu-disk-usage-analyzer:mount-observe :mount-observe
snap connect gdu-disk-usage-analyzer:system-backup :system-backup
snap alias gdu-disk-usage-analyzer.gdu gdu
[Binenv](https://github.com/devops-works/binenv)
binenv install gdu
[Go](https://pkg.go.dev/github.com/dundee/gdu):
go install b612.me/apps/b612/gdu/cmd/gdu@latest
[Winget](https://github.com/microsoft/winget-pkgs/tree/master/manifests/d/dundee/gdu) (for Windows users):
winget install gdu
You can either run it as `gdu_windows_amd64.exe` or
* add an alias with `Doskey`.
* add `alias gdu="gdu_windows_amd64.exe"` to your `~/.bashrc` file if using Git Bash to run it as `gdu`.
You might need to restart your terminal.
[Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/gdu.json):
scoop install gdu
[X-cmd](https://www.x-cmd.com/start/)
x env use gdu
## [COPR builds](https://copr.fedorainfracloud.org/coprs/faramirza/gdu/)
COPR Builds exist for the the following Linux Distros.
[How to enable a CORP Repo](https://docs.pagure.org/copr.copr/how_to_enable_repo.html)
Amazon Linux 2023:
```
[copr:copr.fedorainfracloud.org:faramirza:gdu]
name=Copr repo for gdu owned by faramirza
baseurl=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/amazonlinux-2023-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
gpgkey=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/pubkey.gpg
repo_gpgcheck=0
enabled=1
enabled_metadata=1
```
EPEL 7:
```
[copr:copr.fedorainfracloud.org:faramirza:gdu]
name=Copr repo for gdu owned by faramirza
baseurl=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/epel-7-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
gpgkey=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/pubkey.gpg
repo_gpgcheck=0
enabled=1
enabled_metadata=1
```
EPEL 8:
```
[copr:copr.fedorainfracloud.org:faramirza:gdu]
name=Copr repo for gdu owned by faramirza
baseurl=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/epel-8-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
gpgkey=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/pubkey.gpg
repo_gpgcheck=0
enabled=1
enabled_metadata=1
```
EPEL 9:
```
[copr:copr.fedorainfracloud.org:faramirza:gdu]
name=Copr repo for gdu owned by faramirza
baseurl=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/epel-9-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
gpgkey=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/pubkey.gpg
repo_gpgcheck=0
enabled=1
enabled_metadata=1
```
Fedora 38:
```
[copr:copr.fedorainfracloud.org:faramirza:gdu]
name=Copr repo for gdu owned by faramirza
baseurl=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/fedora-$releasever-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
gpgkey=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/pubkey.gpg
repo_gpgcheck=0
enabled=1
enabled_metadata=1
```
Fedora 39:
```
[copr:copr.fedorainfracloud.org:faramirza:gdu]
name=Copr repo for gdu owned by faramirza
baseurl=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/fedora-$releasever-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
gpgkey=https://download.copr.fedorainfracloud.org/results/faramirza/gdu/pubkey.gpg
repo_gpgcheck=0
enabled=1
enabled_metadata=1
```

View File

@ -1,8 +0,0 @@
Copyright 2020-2021 Daniel Milde <daniel@milde.cz>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,159 +0,0 @@
NAME := gdu
MAJOR_VER := v5
PACKAGE := github.com/dundee/$(NAME)/$(MAJOR_VER)
CMD_GDU := cmd/gdu
VERSION := $(shell git describe --tags 2>/dev/null)
NAMEVER := $(NAME)-$(subst v,,$(VERSION))
DATE := $(shell date +'%Y-%m-%d')
GOFLAGS ?= -buildmode=pie -trimpath -mod=readonly -modcacherw -pgo=default.pgo
GOFLAGS_STATIC ?= -trimpath -mod=readonly -modcacherw -pgo=default.pgo
LDFLAGS := -s -w -extldflags '-static' \
-X '$(PACKAGE)/build.Version=$(VERSION)' \
-X '$(PACKAGE)/build.User=$(shell id -u -n)' \
-X '$(PACKAGE)/build.Time=$(shell LC_ALL=en_US.UTF-8 date)'
TAR := tar
ifeq ($(shell uname -s),Darwin)
TAR := gtar # brew install gnu-tar
endif
all: clean tarball build-all build-docker man clean-uncompressed-dist shasums
run:
go run $(PACKAGE)/$(CMD_GDU)
vendor: go.mod go.sum
go mod vendor
tarball: vendor
-mkdir dist
$(TAR) czf dist/$(NAMEVER).tgz --transform "s,^,$(NAMEVER)/," --exclude dist --exclude test_dir --exclude coverage.txt *
build:
@echo "Version: " $(VERSION)
mkdir -p dist
GOFLAGS="$(GOFLAGS)" CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o dist/$(NAME) $(PACKAGE)/$(CMD_GDU)
build-static:
@echo "Version: " $(VERSION)
mkdir -p dist
GOFLAGS="$(GOFLAGS_STATIC)" CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o dist/$(NAME) $(PACKAGE)/$(CMD_GDU)
build-docker:
@echo "Version: " $(VERSION)
docker build . --tag ghcr.io/dundee/gdu:$(VERSION)
build-all:
@echo "Version: " $(VERSION)
-mkdir dist
-CGO_ENABLED=0 gox \
-os="darwin" \
-arch="amd64 arm64" \
-output="dist/gdu_{{.OS}}_{{.Arch}}" \
-ldflags="$(LDFLAGS)" \
$(PACKAGE)/$(CMD_GDU)
-CGO_ENABLED=0 gox \
-os="windows" \
-arch="amd64" \
-output="dist/gdu_{{.OS}}_{{.Arch}}" \
-ldflags="$(LDFLAGS)" \
$(PACKAGE)/$(CMD_GDU)
-CGO_ENABLED=0 gox \
-os="linux freebsd netbsd openbsd" \
-output="dist/gdu_{{.OS}}_{{.Arch}}" \
-ldflags="$(LDFLAGS)" \
$(PACKAGE)/$(CMD_GDU)
GOFLAGS="$(GOFLAGS)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o dist/gdu_linux_amd64-x $(PACKAGE)/$(CMD_GDU)
GOFLAGS="$(GOFLAGS_STATIC)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o dist/gdu_linux_amd64_static $(PACKAGE)/$(CMD_GDU)
CGO_ENABLED=0 GOOS=linux GOARM=5 GOARCH=arm go build -ldflags="$(LDFLAGS)" -o dist/gdu_linux_armv5l $(PACKAGE)/$(CMD_GDU)
CGO_ENABLED=0 GOOS=linux GOARM=6 GOARCH=arm go build -ldflags="$(LDFLAGS)" -o dist/gdu_linux_armv6l $(PACKAGE)/$(CMD_GDU)
CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -ldflags="$(LDFLAGS)" -o dist/gdu_linux_armv7l $(PACKAGE)/$(CMD_GDU)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o dist/gdu_linux_arm64 $(PACKAGE)/$(CMD_GDU)
CGO_ENABLED=0 GOOS=android GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o dist/gdu_android_arm64 $(PACKAGE)/$(CMD_GDU)
cd dist; for file in gdu_linux_* gdu_darwin_* gdu_netbsd_* gdu_openbsd_* gdu_freebsd_* gdu_android_*; do tar czf $$file.tgz $$file; done
cd dist; for file in gdu_windows_*; do zip $$file.zip $$file; done
gdu.1: gdu.1.md
sed 's/{{date}}/$(DATE)/g' gdu.1.md > gdu.1.date.md
pandoc gdu.1.date.md -s -t man > gdu.1
rm -f gdu.1.date.md
man: gdu.1
cp gdu.1 dist
cd dist; tar czf gdu.1.tgz gdu.1
show-man:
sed 's/{{date}}/$(DATE)/g' gdu.1.md > gdu.1.date.md
pandoc gdu.1.date.md -s -t man | man -l -
test:
gotestsum
coverage:
gotestsum -- -race -coverprofile=coverage.txt -covermode=atomic ./...
coverage-html: coverage
go tool cover -html=coverage.txt
gobench:
go test -bench=. $(PACKAGE)/pkg/analyze
heap-profile:
go tool pprof -web http://localhost:6060/debug/pprof/heap
pgo:
wget -O cpu.pprof http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof -proto cpu.pprof default.pgo > merged.pprof
mv merged.pprof default.pgo
trace:
wget -O trace.out http://localhost:6060/debug/pprof/trace?seconds=30
gotraceui ./trace.out
benchmark:
sudo cpupower frequency-set -g performance
hyperfine --export-markdown=bench-cold.md \
--prepare 'sync; echo 3 | sudo tee /proc/sys/vm/drop_caches' \
--ignore-failure \
'dua ~' 'duc index ~' 'ncdu -0 -o /dev/null ~' \
'diskus ~' 'du -hs ~' 'dust -d0 ~' 'pdu ~' \
'gdu -npc ~' 'gdu -gnpc ~' 'gdu -npc --use-storage ~'
hyperfine --export-markdown=bench-warm.md \
--warmup 5 \
--ignore-failure \
'dua ~' 'duc index ~' 'ncdu -0 -o /dev/null ~' \
'diskus ~' 'du -hs ~' 'dust -d0 ~' 'pdu ~' \
'gdu -npc ~' 'gdu -gnpc ~' 'gdu -npc --use-storage ~'
sudo cpupower frequency-set -g schedutil
lint:
golangci-lint run -c .golangci.yml
clean:
go mod tidy
-rm coverage.txt
-rm -r test_dir
-rm -r vendor
-rm -r dist
clean-uncompressed-dist:
find dist -type f -not -name '*.tgz' -not -name '*.zip' -delete
shasums:
cd dist; sha256sum * > sha256sums.txt
cd dist; gpg --sign --armor --detach-sign sha256sums.txt
release:
gh release create -t "gdu $(VERSION)" $(VERSION) ./dist/*
install-dev-dependencies:
go install gotest.tools/gotestsum@latest
go install github.com/mitchellh/gox@latest
go install honnef.co/go/gotraceui/cmd/gotraceui@master
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
.PHONY: run build build-static build-all test gobench benchmark coverage coverage-html clean clean-uncompressed-dist man show-man release

View File

@ -1,308 +0,0 @@
# go DiskUsage()
<img src="./gdu.png" alt="Gdu " width="200" align="right">
[![Codecov](https://codecov.io/gh/dundee/gdu/branch/master/graph/badge.svg)](https://codecov.io/gh/dundee/gdu)
[![Go Report Card](https://goreportcard.com/badge/github.com/dundee/gdu)](https://goreportcard.com/report/github.com/dundee/gdu)
[![Maintainability](https://api.codeclimate.com/v1/badges/30d793274607f599e658/maintainability)](https://codeclimate.com/github/dundee/gdu/maintainability)
[![CodeScene Code Health](https://codescene.io/projects/13129/status-badges/code-health)](https://codescene.io/projects/13129)
Pretty fast disk usage analyzer written in Go.
Gdu is intended primarily for SSD disks where it can fully utilize parallel processing.
However HDDs work as well, but the performance gain is not so huge.
[![asciicast](https://asciinema.org/a/382738.svg)](https://asciinema.org/a/382738)
<a href="https://repology.org/project/gdu/versions">
<img src="https://repology.org/badge/vertical-allrepos/gdu.svg" alt="Packaging status" align="right">
</a>
## Installation
Head for the [releases page](https://github.com/dundee/gdu/releases) and download the binary for your system.
Using curl:
curl -L https://github.com/dundee/gdu/releases/latest/download/gdu_linux_amd64.tgz | tar xz
chmod +x gdu_linux_amd64
mv gdu_linux_amd64 /usr/bin/gdu
See the [installation page](./INSTALL.md) for other ways how to install Gdu to your system.
Or you can use Gdu directly via Docker:
docker run --rm --init --interactive --tty --privileged --volume /:/mnt/root ghcr.io/dundee/gdu /mnt/root
## Usage
```
gdu [flags] [directory_to_scan]
Flags:
--config-file string Read config from file (default is $HOME/.gdu.yaml)
-g, --const-gc Enable memory garbage collection during analysis with constant level set by GOGC
--enable-profiling Enable collection of profiling data and provide it on http://localhost:6060/debug/pprof/
-L, --follow-symlinks Follow symlinks for files, i.e. show the size of the file to which symlink points to (symlinks to directories are not followed)
-h, --help help for gdu
-i, --ignore-dirs strings Paths to ignore (separated by comma). Can be absolute or relative to current directory (default [/proc,/dev,/sys,/run])
-I, --ignore-dirs-pattern strings Path patterns to ignore (separated by comma)
-X, --ignore-from string Read path patterns to ignore from file
-f, --input-file string Import analysis from JSON file
-l, --log-file string Path to a logfile (default "/dev/null")
-m, --max-cores int Set max cores that Gdu will use. 12 cores available (default 12)
-c, --no-color Do not use colorized output
-x, --no-cross Do not cross filesystem boundaries
--no-delete Do not allow deletions
-H, --no-hidden Ignore hidden directories (beginning with dot)
--no-mouse Do not use mouse
--no-prefix Show sizes as raw numbers without any prefixes (SI or binary) in non-interactive mode
-p, --no-progress Do not show progress in non-interactive mode
-u, --no-unicode Do not use Unicode symbols (for size bar)
-n, --non-interactive Do not run in interactive mode
-o, --output-file string Export all info into file as JSON
-r, --read-from-storage Read analysis data from persistent key-value storage
--sequential Use sequential scanning (intended for rotating HDDs)
-a, --show-apparent-size Show apparent size
-d, --show-disks Show all mounted disks
-C, --show-item-count Show number of items in directory
-M, --show-mtime Show latest mtime of items in directory
-B, --show-relative-size Show relative size
--si Show sizes with decimal SI prefixes (kB, MB, GB) instead of binary prefixes (KiB, MiB, GiB)
--storage-path string Path to persistent key-value storage directory (default "/tmp/badger")
-s, --summarize Show only a total in non-interactive mode
-t, --top int Show only top X largest files in non-interactive mode
--use-storage Use persistent key-value storage for analysis data (experimental)
-v, --version Print version
--write-config Write current configuration to file (default is $HOME/.gdu.yaml)
Basic list of actions in interactive mode (show help modal for more):
↑ or k Move cursor up
↓ or j Move cursor down
→ or Enter or l Go to highlighted directory
← or h Go to parent directory
d Delete the selected file or directory
e Empty the selected directory
n Sort by name
s Sort by size
c Show number of items in directory
? Show help modal
```
## Examples
gdu # analyze current dir
gdu -a # show apparent size instead of disk usage
gdu --no-delete # prevent write operations
gdu <some_dir_to_analyze> # analyze given dir
gdu -d # show all mounted disks
gdu -l ./gdu.log <some_dir> # write errors to log file
gdu -i /sys,/proc / # ignore some paths
gdu -I '.*[abc]+' # ignore paths by regular pattern
gdu -X ignore_file / # ignore paths by regular patterns from file
gdu -c / # use only white/gray/black colors
gdu -n / # only print stats, do not start interactive mode
gdu -np / # do not show progress, useful when using its output in a script
gdu -nps /some/dir # show only total usage for given dir
gdu -nt 10 / # show top 10 largest files
gdu / > file # write stats to file, do not start interactive mode
gdu -o- / | gzip -c >report.json.gz # write all info to JSON file for later analysis
zcat report.json.gz | gdu -f- # read analysis from file
GOGC=10 gdu -g --use-storage / # use persistent key-value storage for saving analysis data
gdu -r / # read saved analysis data from persistent key-value storage
## Modes
Gdu has three modes: interactive (default), non-interactive and export.
Non-interactive mode is started automatically when TTY is not detected (using [go-isatty](https://github.com/mattn/go-isatty)), for example if the output is being piped to a file, or it can be started explicitly by using a flag.
Export mode (flag `-o`) outputs all usage data as JSON, which can be later opened using the `-f` flag.
Hard links are counted only once.
## File flags
Files and directories may be prefixed by a one-character
flag with following meaning:
* `!` An error occurred while reading this directory.
* `.` An error occurred while reading a subdirectory, size may be not correct.
* `@` File is symlink or socket.
* `H` Same file was already counted (hard link).
* `e` Directory is empty.
## Configuration file
Gdu can read (and write) YAML configuration file.
`$HOME/.config/gdu/gdu.yaml` and `$HOME/.gdu.yaml` are checked for the presence of the config file by default.
See the [full list of all configuration options](configuration).
### Examples
* To configure gdu to permanently run in gray-scale color mode:
```
echo "no-color: true" >> ~/.gdu.yaml
```
* To set default sorting in configuration file:
```
sorting:
by: name // size, name, itemCount, mtime
order: desc
```
* To configure gdu to set CWD variable when browsing directories:
```
echo "change-cwd: true" >> ~/.gdu.yaml
```
* To save the current configuration
```
gdu --write-config
```
## Styling
There are wide options for how terminals can be colored.
Some gdu primitives (like basic text) adapt to different color schemas, but the selected/highlighted row does not.
If the default look is not sufficient, it can be changed in configuration file, e.g.:
```
style:
selected-row:
text-color: black
background-color: "#ff0000"
```
## Deletion in background and in parallel (experimental)
Gdu can delete items in the background, thus not blocking the UI for additional work.
To enable:
```
echo "delete-in-background: true" >> ~/.gdu.yaml
```
Directory items can be also deleted in parallel, which might increase the speed of deletion.
To enable:
```
echo "delete-in-parallel: true" >> ~/.gdu.yaml
```
## Memory usage
### Automatic balancing
Gdu tries to balance performance and memory usage.
When less memory is used by gdu than the total free memory of the host,
then Garbage Collection is disabled during the analysis phase completely to gain maximum speed.
Otherwise GC is enabled.
The more memory is used and the less memory is free, the more often will the GC happen.
### Manual memory usage control
If you want manual control over Garbage Collection, you can use `--const-gc` / `-g` flag.
It will run Garbage Collection during the analysis phase with constant level of aggressiveness.
As a result, the analysis will be about 25% slower and will consume about 30% less memory.
To change the level, you can set the `GOGC` environment variable to specify how often the garbage collection will happen.
Lower value (than 100) means GC will run more often. Higher means less often. Negative number will stop GC.
Example running gdu with constant GC, but not so aggressive as default:
```
GOGC=200 gdu -g /
```
## Saving analysis data to persistent key-value storage (experimental)
Gdu can store the analysis data to persistent key-value storage instead of just memory.
Gdu will run much slower (approx 10x) but it should use much less memory (when using small GOGC as well).
Gdu can also reopen with the saved data.
Currently only BadgerDB is supported as the key-value storage (embedded).
```
GOGC=10 gdu -g --use-storage / # saves analysis data to key-value storage
gdu -r / # reads just saved data, does not run analysis again
```
## Running tests
make install-dev-dependencies
make test
## Profiling
Gdu can collect profiling data when the `--enable-profiling` flag is set.
The data are provided via embedded http server on URL `http://localhost:6060/debug/pprof/`.
You can then use e.g. `go tool pprof -web http://localhost:6060/debug/pprof/heap`
to open the heap profile as SVG image in your web browser.
## Benchmarks
Benchmarks were performed on 50G directory (100k directories, 400k files) on 500 GB SSD using [hyperfine](https://github.com/sharkdp/hyperfine).
See `benchmark` target in [Makefile](Makefile) for more info.
### Cold cache
Filesystem cache was cleared using `sync; echo 3 | sudo tee /proc/sys/vm/drop_caches`.
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `diskus ~` | 3.126 ± 0.020 | 3.087 | 3.155 | 1.00 |
| `gdu -npc ~` | 3.132 ± 0.019 | 3.111 | 3.173 | 1.00 ± 0.01 |
| `gdu -gnpc ~` | 3.136 ± 0.012 | 3.112 | 3.155 | 1.00 ± 0.01 |
| `pdu ~` | 3.657 ± 0.013 | 3.641 | 3.677 | 1.17 ± 0.01 |
| `dust -d0 ~` | 3.933 ± 0.144 | 3.849 | 4.213 | 1.26 ± 0.05 |
| `dua ~` | 3.994 ± 0.073 | 3.827 | 4.134 | 1.28 ± 0.02 |
| `gdu -npc --use-storage ~` | 12.812 ± 0.078 | 12.644 | 12.912 | 4.10 ± 0.04 |
| `du -hs ~` | 14.120 ± 0.213 | 13.969 | 14.703 | 4.52 ± 0.07 |
| `duc index ~` | 14.567 ± 0.080 | 14.385 | 14.657 | 4.66 ± 0.04 |
| `ncdu -0 -o /dev/null ~` | 14.963 ± 0.254 | 14.759 | 15.637 | 4.79 ± 0.09 |
### Warm cache
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `pdu ~` | 226.6 ± 3.7 | 219.6 | 231.2 | 1.00 |
| `diskus ~` | 227.7 ± 5.2 | 221.6 | 239.9 | 1.00 ± 0.03 |
| `dust -d0 ~` | 400.1 ± 7.1 | 386.7 | 409.4 | 1.77 ± 0.04 |
| `dua ~` | 444.9 ± 2.4 | 442.4 | 448.9 | 1.96 ± 0.03 |
| `gdu -npc ~` | 451.3 ± 3.8 | 445.9 | 458.5 | 1.99 ± 0.04 |
| `gdu -gnpc ~` | 516.1 ± 6.7 | 503.1 | 527.5 | 2.28 ± 0.05 |
| `du -hs ~` | 905.0 ± 3.9 | 901.2 | 913.4 | 3.99 ± 0.07 |
| `duc index ~` | 1053.0 ± 5.1 | 1046.2 | 1064.1 | 4.65 ± 0.08 |
| `ncdu -0 -o /dev/null ~` | 1653.9 ± 5.7 | 1645.9 | 1663.0 | 7.30 ± 0.12 |
| `gdu -npc --use-storage ~` | 9754.9 ± 688.7 | 8403.8 | 10427.4 | 43.04 ± 3.12 |
## Alternatives
* [ncdu](https://dev.yorhel.nl/ncdu) - NCurses based tool written in pure `C` (LTS) or `zig` (Stable)
* [godu](https://github.com/viktomas/godu) - Analyzer with a carousel like user interface
* [dua](https://github.com/Byron/dua-cli) - Tool written in `Rust` with interface similar to gdu (and ncdu)
* [diskus](https://github.com/sharkdp/diskus) - Very simple but very fast tool written in `Rust`
* [duc](https://duc.zevv.nl/) - Collection of tools with many possibilities for inspecting and visualising disk usage
* [dust](https://github.com/bootandy/dust) - Tool written in `Rust` showing tree like structures of disk usage
* [pdu](https://github.com/KSXGitHub/parallel-disk-usage) - Tool written in `Rust` showing tree like structures of disk usage
## Notes
[HDD icon created by Nikita Golubev - Flaticon](https://www.flaticon.com/free-icons/hdd)

View File

@ -1,16 +0,0 @@
package build
import "b612.me/apps/b612/version"
// Version stores the current version of the app
var Version = version.Version
// Time of the build
var Time string
// User who built it
var User string
// RootPathPrefix stores path to be prepended to given absolute path
// e.g. /var/lib/snapd/hostfs for snap
var RootPathPrefix = ""

View File

@ -1,473 +0,0 @@
package app
import (
"fmt"
"io"
"io/fs"
"net/http"
"net/http/pprof"
"os"
"path/filepath"
"runtime"
"strings"
log "github.com/sirupsen/logrus"
"b612.me/apps/b612/gdu/build"
"b612.me/apps/b612/gdu/internal/common"
"b612.me/apps/b612/gdu/pkg/analyze"
"b612.me/apps/b612/gdu/pkg/device"
gfs "b612.me/apps/b612/gdu/pkg/fs"
"b612.me/apps/b612/gdu/report"
"b612.me/apps/b612/gdu/stdout"
"b612.me/apps/b612/gdu/tui"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
// UI is common interface for both terminal UI and text output
type UI interface {
ListDevices(getter device.DevicesInfoGetter) error
AnalyzePath(path string, parentDir gfs.Item) error
ReadAnalysis(input io.Reader) error
ReadFromStorage(storagePath, path string) error
SetIgnoreDirPaths(paths []string)
SetIgnoreDirPatterns(paths []string) error
SetIgnoreFromFile(ignoreFile string) error
SetIgnoreHidden(value bool)
SetFollowSymlinks(value bool)
SetShowAnnexedSize(value bool)
SetAnalyzer(analyzer common.Analyzer)
StartUILoop() error
}
// Flags define flags accepted by Run
type Flags struct {
CfgFile string `yaml:"-"`
LogFile string `yaml:"log-file"`
InputFile string `yaml:"input-file"`
OutputFile string `yaml:"output-file"`
IgnoreDirs []string `yaml:"ignore-dirs"`
IgnoreDirPatterns []string `yaml:"ignore-dir-patterns"`
IgnoreFromFile string `yaml:"ignore-from-file"`
MaxCores int `yaml:"max-cores"`
SequentialScanning bool `yaml:"sequential-scanning"`
ShowDisks bool `yaml:"-"`
ShowApparentSize bool `yaml:"show-apparent-size"`
ShowRelativeSize bool `yaml:"show-relative-size"`
ShowAnnexedSize bool `yaml:"show-annexed-size"`
ShowVersion bool `yaml:"-"`
ShowItemCount bool `yaml:"show-item-count"`
ShowMTime bool `yaml:"show-mtime"`
NoColor bool `yaml:"no-color"`
NoMouse bool `yaml:"no-mouse"`
NonInteractive bool `yaml:"non-interactive"`
NoProgress bool `yaml:"no-progress"`
NoUnicode bool `yaml:"no-unicode"`
NoCross bool `yaml:"no-cross"`
NoHidden bool `yaml:"no-hidden"`
NoDelete bool `yaml:"no-delete"`
FollowSymlinks bool `yaml:"follow-symlinks"`
Profiling bool `yaml:"profiling"`
ConstGC bool `yaml:"const-gc"`
UseStorage bool `yaml:"use-storage"`
StoragePath string `yaml:"storage-path"`
ReadFromStorage bool `yaml:"read-from-storage"`
Summarize bool `yaml:"summarize"`
Top int `yaml:"top"`
UseSIPrefix bool `yaml:"use-si-prefix"`
NoPrefix bool `yaml:"no-prefix"`
WriteConfig bool `yaml:"-"`
ChangeCwd bool `yaml:"change-cwd"`
DeleteInBackground bool `yaml:"delete-in-background"`
DeleteInParallel bool `yaml:"delete-in-parallel"`
Style Style `yaml:"style"`
Sorting Sorting `yaml:"sorting"`
}
// Style define style config
type Style struct {
SelectedRow ColorStyle `yaml:"selected-row"`
ProgressModal ProgressModalOpts `yaml:"progress-modal"`
UseOldSizeBar bool `yaml:"use-old-size-bar"`
Footer FooterColorStyle `yaml:"footer"`
Header HeaderColorStyle `yaml:"header"`
ResultRow ResultRowColorStyle `yaml:"result-row"`
}
// ProgressModalOpts defines options for progress modal
type ProgressModalOpts struct {
CurrentItemNameMaxLen int `yaml:"current-item-path-max-len"`
}
// ColorStyle defines styling of some item
type ColorStyle struct {
TextColor string `yaml:"text-color"`
BackgroundColor string `yaml:"background-color"`
}
// FooterColorStyle defines styling of footer
type FooterColorStyle struct {
TextColor string `yaml:"text-color"`
BackgroundColor string `yaml:"background-color"`
NumberColor string `yaml:"number-color"`
}
// HeaderColorStyle defines styling of header
type HeaderColorStyle struct {
TextColor string `yaml:"text-color"`
BackgroundColor string `yaml:"background-color"`
Hidden bool `yaml:"hidden"`
}
// ResultRowColorStyle defines styling of result row
type ResultRowColorStyle struct {
NumberColor string `yaml:"number-color"`
DirectoryColor string `yaml:"directory-color"`
}
// Sorting defines default sorting of items
type Sorting struct {
By string `yaml:"by"`
Order string `yaml:"order"`
}
// App defines the main application
type App struct {
Args []string
Flags *Flags
Istty bool
Writer io.Writer
TermApp common.TermApplication
Screen tcell.Screen
Getter device.DevicesInfoGetter
PathChecker func(string) (fs.FileInfo, error)
}
func init() {
http.DefaultServeMux = http.NewServeMux()
}
// Run starts gdu main logic
func (a *App) Run() error {
var ui UI
if a.Flags.ShowVersion {
fmt.Fprintln(a.Writer, "Version:\t", build.Version)
fmt.Fprintln(a.Writer, "Built time:\t", build.Time)
fmt.Fprintln(a.Writer, "Built user:\t", build.User)
return nil
}
log.Printf("Runtime flags: %+v", *a.Flags)
if a.Flags.NoPrefix && a.Flags.UseSIPrefix {
return fmt.Errorf("--no-prefix and --si cannot be used at once")
}
path := a.getPath()
path, err := filepath.Abs(path)
if err != nil {
return err
}
ui, err = a.createUI()
if err != nil {
return err
}
if a.Flags.UseStorage {
ui.SetAnalyzer(analyze.CreateStoredAnalyzer(a.Flags.StoragePath))
}
if a.Flags.SequentialScanning {
ui.SetAnalyzer(analyze.CreateSeqAnalyzer())
}
if a.Flags.FollowSymlinks {
ui.SetFollowSymlinks(true)
}
if a.Flags.ShowAnnexedSize {
ui.SetShowAnnexedSize(true)
}
if err := a.setNoCross(path); err != nil {
return err
}
ui.SetIgnoreDirPaths(a.Flags.IgnoreDirs)
if len(a.Flags.IgnoreDirPatterns) > 0 {
if err := ui.SetIgnoreDirPatterns(a.Flags.IgnoreDirPatterns); err != nil {
return err
}
}
if a.Flags.IgnoreFromFile != "" {
if err := ui.SetIgnoreFromFile(a.Flags.IgnoreFromFile); err != nil {
return err
}
}
if a.Flags.NoHidden {
ui.SetIgnoreHidden(true)
}
a.setMaxProcs()
if err := a.runAction(ui, path); err != nil {
return err
}
return ui.StartUILoop()
}
func (a *App) getPath() string {
if len(a.Args) == 1 {
return a.Args[0]
}
return "."
}
func (a *App) setMaxProcs() {
if a.Flags.MaxCores < 1 || a.Flags.MaxCores > runtime.NumCPU() {
return
}
runtime.GOMAXPROCS(a.Flags.MaxCores)
// runtime.GOMAXPROCS(n) with n < 1 doesn't change current setting so we use it to check current value
log.Printf("Max cores set to %d", runtime.GOMAXPROCS(0))
}
func (a *App) createUI() (UI, error) {
var ui UI
switch {
case a.Flags.OutputFile != "":
var output io.Writer
var err error
if a.Flags.OutputFile == "-" {
output = os.Stdout
} else {
output, err = os.OpenFile(a.Flags.OutputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return nil, fmt.Errorf("opening output file: %w", err)
}
}
ui = report.CreateExportUI(
a.Writer,
output,
!a.Flags.NoColor && a.Istty,
!a.Flags.NoProgress && a.Istty,
a.Flags.ConstGC,
a.Flags.UseSIPrefix,
)
case a.Flags.NonInteractive || !a.Istty:
stdoutUI := stdout.CreateStdoutUI(
a.Writer,
!a.Flags.NoColor && a.Istty,
!a.Flags.NoProgress && a.Istty,
a.Flags.ShowApparentSize,
a.Flags.ShowRelativeSize,
a.Flags.Summarize,
a.Flags.ConstGC,
a.Flags.UseSIPrefix,
a.Flags.NoPrefix,
a.Flags.Top,
)
if a.Flags.NoUnicode {
stdoutUI.UseOldProgressRunes()
}
ui = stdoutUI
default:
opts := a.getOptions()
ui = tui.CreateUI(
a.TermApp,
a.Screen,
os.Stdout,
!a.Flags.NoColor,
a.Flags.ShowApparentSize,
a.Flags.ShowRelativeSize,
a.Flags.ConstGC,
a.Flags.UseSIPrefix,
opts...,
)
if !a.Flags.NoColor {
tview.Styles.TitleColor = tcell.NewRGBColor(27, 161, 227)
} else {
tview.Styles.ContrastBackgroundColor = tcell.NewRGBColor(150, 150, 150)
}
tview.Styles.BorderColor = tcell.ColorDefault
}
return ui, nil
}
func (a *App) getOptions() []tui.Option {
var opts []tui.Option
if a.Flags.Style.SelectedRow.TextColor != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetSelectedTextColor(tcell.GetColor(a.Flags.Style.SelectedRow.TextColor))
})
}
if a.Flags.Style.SelectedRow.BackgroundColor != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetSelectedBackgroundColor(tcell.GetColor(a.Flags.Style.SelectedRow.BackgroundColor))
})
}
if a.Flags.Style.Footer.TextColor != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetFooterTextColor(a.Flags.Style.Footer.TextColor)
})
}
if a.Flags.Style.Footer.BackgroundColor != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetFooterBackgroundColor(a.Flags.Style.Footer.BackgroundColor)
})
}
if a.Flags.Style.Footer.NumberColor != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetFooterNumberColor(a.Flags.Style.Footer.NumberColor)
})
}
if a.Flags.Style.Header.TextColor != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetHeaderTextColor(a.Flags.Style.Header.TextColor)
})
}
if a.Flags.Style.Header.BackgroundColor != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetHeaderBackgroundColor(a.Flags.Style.Header.BackgroundColor)
})
}
if a.Flags.Style.Header.Hidden {
opts = append(opts, func(ui *tui.UI) {
ui.SetHeaderHidden()
})
}
if a.Flags.Style.ResultRow.NumberColor != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetResultRowNumberColor(a.Flags.Style.ResultRow.NumberColor)
})
}
if a.Flags.Style.ResultRow.DirectoryColor != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetResultRowDirectoryColor(a.Flags.Style.ResultRow.DirectoryColor)
})
}
if a.Flags.Style.ProgressModal.CurrentItemNameMaxLen > 0 {
opts = append(opts, func(ui *tui.UI) {
ui.SetCurrentItemNameMaxLen(a.Flags.Style.ProgressModal.CurrentItemNameMaxLen)
})
}
if a.Flags.Style.UseOldSizeBar || a.Flags.NoUnicode {
opts = append(opts, func(ui *tui.UI) {
ui.UseOldSizeBar()
})
}
if a.Flags.Sorting.Order != "" || a.Flags.Sorting.By != "" {
opts = append(opts, func(ui *tui.UI) {
ui.SetDefaultSorting(a.Flags.Sorting.By, a.Flags.Sorting.Order)
})
}
if a.Flags.ChangeCwd {
opts = append(opts, func(ui *tui.UI) {
ui.SetChangeCwdFn(os.Chdir)
})
}
if a.Flags.ShowItemCount {
opts = append(opts, func(ui *tui.UI) {
ui.SetShowItemCount()
})
}
if a.Flags.ShowMTime {
opts = append(opts, func(ui *tui.UI) {
ui.SetShowMTime()
})
}
if a.Flags.NoDelete {
opts = append(opts, func(ui *tui.UI) {
ui.SetNoDelete()
})
}
if a.Flags.DeleteInBackground {
opts = append(opts, func(ui *tui.UI) {
ui.SetDeleteInBackground()
})
}
if a.Flags.DeleteInParallel {
opts = append(opts, func(ui *tui.UI) {
ui.SetDeleteInParallel()
})
}
return opts
}
func (a *App) setNoCross(path string) error {
if a.Flags.NoCross {
mounts, err := a.Getter.GetMounts()
if err != nil {
return fmt.Errorf("loading mount points: %w", err)
}
paths := device.GetNestedMountpointsPaths(path, mounts)
log.Printf("Ignoring mount points: %s", strings.Join(paths, ", "))
a.Flags.IgnoreDirs = append(a.Flags.IgnoreDirs, paths...)
}
return nil
}
func (a *App) runAction(ui UI, path string) error {
if a.Flags.Profiling {
go func() {
http.HandleFunc("/debug/pprof/", pprof.Index)
http.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
http.HandleFunc("/debug/pprof/profile", pprof.Profile)
http.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
http.HandleFunc("/debug/pprof/trace", pprof.Trace)
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
}
switch {
case a.Flags.ShowDisks:
if err := ui.ListDevices(a.Getter); err != nil {
return fmt.Errorf("loading mount points: %w", err)
}
case a.Flags.InputFile != "":
var input io.Reader
var err error
if a.Flags.InputFile == "-" {
input = os.Stdin
} else {
input, err = os.OpenFile(a.Flags.InputFile, os.O_RDONLY, 0o600)
if err != nil {
return fmt.Errorf("opening input file: %w", err)
}
}
if err := ui.ReadAnalysis(input); err != nil {
return fmt.Errorf("reading analysis: %w", err)
}
case a.Flags.ReadFromStorage:
ui.SetAnalyzer(analyze.CreateStoredAnalyzer(a.Flags.StoragePath))
if err := ui.ReadFromStorage(a.Flags.StoragePath, path); err != nil {
return fmt.Errorf("reading from storage (%s): %w", a.Flags.StoragePath, err)
}
default:
if build.RootPathPrefix != "" {
path = build.RootPathPrefix + path
}
_, err := a.PathChecker(path)
if err != nil {
return err
}
log.Printf("Analyzing path: %s", path)
if err := ui.AnalyzePath(path, nil); err != nil {
return fmt.Errorf("scanning dir: %w", err)
}
}
return nil
}

View File

@ -1,123 +0,0 @@
//go:build linux
// +build linux
package app
import (
"os"
"testing"
"b612.me/apps/b612/gdu/internal/testdev"
"b612.me/apps/b612/gdu/internal/testdir"
"b612.me/apps/b612/gdu/pkg/device"
"github.com/stretchr/testify/assert"
)
func TestNoCrossWithErr(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", NoCross: true},
[]string{"test_dir"},
false,
device.LinuxDevicesInfoGetter{MountsPath: "/xxxyyy"},
)
assert.Equal(t, "loading mount points: open /xxxyyy: no such file or directory", err.Error())
assert.Empty(t, out)
}
func TestListDevicesWithErr(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
_, err := runApp(
&Flags{LogFile: "/dev/null", ShowDisks: true},
[]string{},
false,
device.LinuxDevicesInfoGetter{MountsPath: "/xxxyyy"},
)
assert.Equal(t, "loading mount points: open /xxxyyy: no such file or directory", err.Error())
}
func TestOutputFileError(t *testing.T) {
out, err := runApp(
&Flags{LogFile: "/dev/null", OutputFile: "/xyzxyz"},
[]string{},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Contains(t, err.Error(), "permission denied")
}
func TestUseStorage(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
const storagePath = "/tmp/badger-test"
defer func() {
err := os.RemoveAll(storagePath)
if err != nil {
panic(err)
}
}()
out, err := runApp(
&Flags{LogFile: "/dev/null", UseStorage: true, StoragePath: storagePath},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestReadFromStorage(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
storagePath := "/tmp/badger-test4"
defer func() {
err := os.RemoveAll(storagePath)
if err != nil {
panic(err)
}
}()
out, err := runApp(
&Flags{LogFile: "/dev/null", UseStorage: true, StoragePath: storagePath},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
out, err = runApp(
&Flags{LogFile: "/dev/null", ReadFromStorage: true, StoragePath: storagePath},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestReadFromStorageWithErr(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
_, err := runApp(
&Flags{LogFile: "/dev/null", ReadFromStorage: true, StoragePath: "/tmp/badger-xxx"},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.ErrorContains(t, err, "Key not found")
}

View File

@ -1,566 +0,0 @@
package app
import (
"bytes"
"os"
"runtime"
"strings"
"testing"
log "github.com/sirupsen/logrus"
"b612.me/apps/b612/gdu/internal/testapp"
"b612.me/apps/b612/gdu/internal/testdev"
"b612.me/apps/b612/gdu/internal/testdir"
"b612.me/apps/b612/gdu/pkg/device"
"github.com/stretchr/testify/assert"
)
func init() {
log.SetLevel(log.WarnLevel)
}
func TestVersion(t *testing.T) {
out, err := runApp(
&Flags{ShowVersion: true},
[]string{},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "Version:\t development")
assert.Nil(t, err)
}
func TestAnalyzePath(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null"},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestSequentialScanning(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", SequentialScanning: true},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestFollowSymlinks(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", FollowSymlinks: true},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestShowAnnexedSize(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", ShowAnnexedSize: true},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestAnalyzePathProfiling(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", Profiling: true},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestAnalyzePathWithIgnoring(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{
LogFile: "/dev/null",
IgnoreDirPatterns: []string{"/[abc]+"},
NoHidden: true,
},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestAnalyzePathWithIgnoringPatternError(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{
LogFile: "/dev/null",
IgnoreDirPatterns: []string{"[[["},
NoHidden: true,
},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Equal(t, out, "")
assert.NotNil(t, err)
}
func TestAnalyzePathWithIgnoringFromNotExistingFile(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{
LogFile: "/dev/null",
IgnoreFromFile: "file",
NoHidden: true,
},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Equal(t, out, "")
assert.NotNil(t, err)
}
func TestAnalyzePathWithGui(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null"},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestAnalyzePathWithGuiNoColor(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", NoColor: true},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestGuiShowMTimeAndItemCount(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", ShowItemCount: true, ShowMTime: true},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestGuiNoDelete(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", NoDelete: true},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestGuiDeleteInParallel(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", DeleteInParallel: true},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestAnalyzePathWithGuiBackgroundDeletion(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", DeleteInBackground: true},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestAnalyzePathWithDefaultSorting(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{
LogFile: "/dev/null",
Sorting: Sorting{
By: "name",
Order: "asc",
},
},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestAnalyzePathWithStyle(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{
LogFile: "/dev/null",
Style: Style{
SelectedRow: ColorStyle{
TextColor: "black",
BackgroundColor: "red",
},
ProgressModal: ProgressModalOpts{
CurrentItemNameMaxLen: 10,
},
Footer: FooterColorStyle{
TextColor: "black",
BackgroundColor: "red",
NumberColor: "white",
},
Header: HeaderColorStyle{
TextColor: "black",
BackgroundColor: "red",
Hidden: true,
},
ResultRow: ResultRowColorStyle{
NumberColor: "orange",
DirectoryColor: "blue",
},
UseOldSizeBar: true,
},
},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestAnalyzePathNoUnicode(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{
LogFile: "/dev/null",
NoUnicode: true,
},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestAnalyzePathWithExport(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
defer func() {
os.Remove("output.json")
}()
out, err := runApp(
&Flags{LogFile: "/dev/null", OutputFile: "output.json"},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.NotEmpty(t, out)
assert.Nil(t, err)
}
func TestAnalyzePathWithChdir(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{
LogFile: "/dev/null",
ChangeCwd: true,
},
[]string{"test_dir"},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestReadAnalysisFromFile(t *testing.T) {
out, err := runApp(
&Flags{LogFile: "/dev/null", InputFile: "../../../internal/testdata/test.json"},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.NotEmpty(t, out)
assert.Contains(t, out, "main.go")
assert.Nil(t, err)
}
func TestReadWrongAnalysisFromFile(t *testing.T) {
out, err := runApp(
&Flags{LogFile: "/dev/null", InputFile: "../../../internal/testdata/wrong.json"},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Contains(t, err.Error(), "Array of maps not found")
}
func TestWrongCombinationOfPrefixes(t *testing.T) {
out, err := runApp(
&Flags{NoPrefix: true, UseSIPrefix: true},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Contains(t, err.Error(), "cannot be used at once")
}
func TestReadWrongAnalysisFromNotExistingFile(t *testing.T) {
out, err := runApp(
&Flags{LogFile: "/dev/null", InputFile: "xxx.json"},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Empty(t, out)
assert.Contains(t, err.Error(), "no such file or directory")
}
func TestAnalyzePathWithErr(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
buff := bytes.NewBufferString("")
app := App{
Flags: &Flags{LogFile: "/dev/null"},
Args: []string{"xxx"},
Istty: false,
Writer: buff,
TermApp: testapp.CreateMockedApp(false),
Getter: testdev.DevicesInfoGetterMock{},
PathChecker: os.Stat,
}
err := app.Run()
assert.Equal(t, "", strings.TrimSpace(buff.String()))
assert.Contains(t, err.Error(), "no such file or directory")
}
func TestNoCross(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", NoCross: true},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "nested")
assert.Nil(t, err)
}
func TestListDevices(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", ShowDisks: true},
[]string{},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Contains(t, out, "Device")
assert.Nil(t, err)
}
func TestListDevicesToFile(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
defer func() {
os.Remove("output.json")
}()
out, err := runApp(
&Flags{LogFile: "/dev/null", ShowDisks: true, OutputFile: "output.json"},
[]string{},
false,
testdev.DevicesInfoGetterMock{},
)
assert.Equal(t, "", out)
assert.Contains(t, err.Error(), "not supported")
}
func TestListDevicesWithGui(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
out, err := runApp(
&Flags{LogFile: "/dev/null", ShowDisks: true},
[]string{},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Nil(t, err)
assert.Empty(t, out)
}
func TestMaxCores(t *testing.T) {
_, err := runApp(
&Flags{LogFile: "/dev/null", MaxCores: 1},
[]string{},
true,
testdev.DevicesInfoGetterMock{},
)
assert.Equal(t, 1, runtime.GOMAXPROCS(0))
assert.Nil(t, err)
}
func TestMaxCoresHighEdge(t *testing.T) {
if runtime.NumCPU() < 2 {
t.Skip("Skipping on a single core CPU")
}
out, err := runApp(
&Flags{LogFile: "/dev/null", MaxCores: runtime.NumCPU() + 1},
[]string{},
true,
testdev.DevicesInfoGetterMock{},
)
assert.NotEqual(t, runtime.NumCPU(), runtime.GOMAXPROCS(0))
assert.Empty(t, out)
assert.Nil(t, err)
}
func TestMaxCoresLowEdge(t *testing.T) {
if runtime.NumCPU() < 2 {
t.Skip("Skipping on a single core CPU")
}
out, err := runApp(
&Flags{LogFile: "/dev/null", MaxCores: -100},
[]string{},
true,
testdev.DevicesInfoGetterMock{},
)
assert.NotEqual(t, runtime.NumCPU(), runtime.GOMAXPROCS(0))
assert.Empty(t, out)
assert.Nil(t, err)
}
// nolint: unparam // Why: it's used in linux tests
func runApp(flags *Flags, args []string, istty bool, getter device.DevicesInfoGetter) (string, error) {
buff := bytes.NewBufferString("")
app := App{
Flags: flags,
Args: args,
Istty: istty,
Writer: buff,
TermApp: testapp.CreateMockedApp(false),
Getter: getter,
PathChecker: testdir.MockedPathChecker,
}
err := app.Run()
return strings.TrimSpace(buff.String()), err
}

View File

@ -1,245 +0,0 @@
package gdu
import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-isatty"
"github.com/rivo/tview"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"b612.me/apps/b612/gdu/cmd/gdu/app"
"b612.me/apps/b612/gdu/pkg/device"
)
var (
af *app.Flags
configErr error
)
var Cmd = &cobra.Command{
Use: "gdu [directory_to_scan]",
Short: "一款使用 Go 语言编写的快速磁盘空间分析工具。",
Long: `一款使用 Go 语言编写的快速磁盘空间分析工具
Gdu 主要针对 SSD 固态硬盘设计能够充分利用并行处理优势虽然也支持机械硬盘HDD使用但性能提升效果不如前者显著
`,
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
RunE: runE,
}
func init() {
af = &app.Flags{}
flags := Cmd.Flags()
flags.StringVar(&af.CfgFile, "config-file", "", "从配置文件读取(默认为 $HOME/.gdu.yaml")
flags.StringVarP(&af.LogFile, "log-file", "l", "/dev/null", "日志文件路径")
flags.StringVarP(&af.OutputFile, "output-file", "o", "", "将所有信息导出为JSON文件")
flags.StringVarP(&af.InputFile, "input-file", "f", "", "从JSON文件导入分析数据")
flags.IntVarP(&af.MaxCores, "max-cores", "m", runtime.NumCPU(), fmt.Sprintf("设置Gdu使用的最大核心数。当前可用%d个核心", runtime.NumCPU()))
flags.BoolVar(&af.SequentialScanning, "sequential", false, "使用顺序扫描适用于机械硬盘HDD")
flags.BoolVarP(&af.ShowVersion, "version", "v", false, "打印版本信息")
flags.StringSliceVarP(&af.IgnoreDirs, "ignore-dirs", "i", []string{"/proc", "/dev", "/sys", "/run"},
"需要忽略的路径(逗号分隔),可为绝对路径或相对于当前目录的路径")
flags.StringSliceVarP(&af.IgnoreDirPatterns, "ignore-dirs-pattern", "I", []string{},
"需要忽略的路径模式(逗号分隔)")
flags.StringVarP(&af.IgnoreFromFile, "ignore-from", "X", "",
"从文件中读取需要忽略的路径模式")
flags.BoolVarP(&af.NoHidden, "no-hidden", "H", false, "忽略隐藏目录(以点号开头的目录)")
flags.BoolVarP(
&af.FollowSymlinks, "follow-symlinks", "L", false,
"跟踪文件的符号链接,显示链接指向文件的大小(不跟踪目录符号链接)",
)
flags.BoolVarP(
&af.ShowAnnexedSize, "show-annexed-size", "A", false,
"对git-annex文件显示表观大小当文件未本地存储时实际磁盘占用为零",
)
flags.BoolVarP(&af.NoCross, "no-cross", "x", false, "不跨越文件系统边界")
flags.BoolVarP(&af.ConstGC, "const-gc", "g", false, "启用恒定级别的内存垃圾回收由GOGC参数控制")
flags.BoolVar(&af.Profiling, "enable-profiling", false, "启用性能分析数据收集(访问地址 http://localhost:6060/debug/pprof/")
flags.BoolVar(&af.UseStorage, "use-storage", false, "使用持久化键值存储分析数据(实验性功能)")
flags.StringVar(&af.StoragePath, "storage-path", "/tmp/badger", "持久化键值存储目录路径")
flags.BoolVarP(&af.ReadFromStorage, "read-from-storage", "r", false, "从持久化键值存储读取分析数据")
flags.BoolVarP(&af.ShowDisks, "show-disks", "d", false, "显示所有已挂载磁盘")
flags.BoolVarP(&af.ShowApparentSize, "show-apparent-size", "a", false, "显示表观大小")
flags.BoolVarP(&af.ShowRelativeSize, "show-relative-size", "B", false, "显示相对大小")
flags.BoolVarP(&af.NoColor, "no-color", "c", false, "禁用彩色输出")
flags.BoolVarP(&af.ShowItemCount, "show-item-count", "C", false, "显示目录内项目数量")
flags.BoolVarP(&af.ShowMTime, "show-mtime", "M", false, "显示目录内项目最新修改时间")
flags.BoolVarP(&af.NonInteractive, "non-interactive", "n", false, "使用非交互模式")
flags.BoolVarP(&af.NoProgress, "no-progress", "p", false, "非交互模式下不显示进度条")
flags.BoolVarP(&af.NoUnicode, "no-unicode", "u", false, "禁用Unicode符号用于大小进度条")
flags.BoolVarP(&af.Summarize, "summarize", "s", false, "非交互模式下仅显示统计总数")
flags.IntVarP(&af.Top, "top", "t", 0, "非交互模式下仅显示前X个最大文件")
flags.BoolVar(&af.UseSIPrefix, "si", false, "使用十进制SI单位kB/MB/GB而非二进制单位KiB/MiB/GiB")
flags.BoolVar(&af.NoPrefix, "no-prefix", false, "非交互模式下显示原始数值(无单位前缀)")
flags.BoolVar(&af.NoMouse, "no-mouse", false, "禁用鼠标支持")
flags.BoolVar(&af.NoDelete, "no-delete", false, "禁止删除操作")
flags.BoolVar(&af.WriteConfig, "write-config", false, "将当前配置写入文件(默认为 $HOME/.gdu.yaml")
initConfig()
setDefaults()
}
func initConfig() {
setConfigFilePath()
data, err := os.ReadFile(af.CfgFile)
if err != nil {
configErr = err
return // config file does not exist, return
}
configErr = yaml.Unmarshal(data, &af)
}
func setDefaults() {
if af.Style.Footer.BackgroundColor == "" {
af.Style.Footer.BackgroundColor = "#2479D0"
}
if af.Style.Footer.TextColor == "" {
af.Style.Footer.TextColor = "#000000"
}
if af.Style.Footer.NumberColor == "" {
af.Style.Footer.NumberColor = "#FFFFFF"
}
if af.Style.Header.BackgroundColor == "" {
af.Style.Header.BackgroundColor = "#2479D0"
}
if af.Style.Header.TextColor == "" {
af.Style.Header.TextColor = "#000000"
}
if af.Style.ResultRow.NumberColor == "" {
af.Style.ResultRow.NumberColor = "#e67100"
}
if af.Style.ResultRow.DirectoryColor == "" {
af.Style.ResultRow.DirectoryColor = "#3498db"
}
}
func setConfigFilePath() {
command := strings.Join(os.Args, " ")
if strings.Contains(command, "--config-file") {
re := regexp.MustCompile("--config-file[= ]([^ ]+)")
parts := re.FindStringSubmatch(command)
if len(parts) > 1 {
af.CfgFile = parts[1]
return
}
}
setDefaultConfigFilePath()
}
func setDefaultConfigFilePath() {
home, err := os.UserHomeDir()
if err != nil {
configErr = err
return
}
path := filepath.Join(home, ".config", "gdu", "gdu.yaml")
if _, err := os.Stat(path); err == nil {
af.CfgFile = path
return
}
af.CfgFile = filepath.Join(home, ".gdu.yaml")
}
func runE(command *cobra.Command, args []string) error {
var (
termApp *tview.Application
screen tcell.Screen
err error
)
if af.WriteConfig {
data, err := yaml.Marshal(af)
if err != nil {
return fmt.Errorf("Error marshaling config file: %w", err)
}
if af.CfgFile == "" {
setDefaultConfigFilePath()
}
err = os.WriteFile(af.CfgFile, data, 0o600)
if err != nil {
return fmt.Errorf("Error writing config file %s: %w", af.CfgFile, err)
}
}
if runtime.GOOS == "windows" && af.LogFile == "/dev/null" {
af.LogFile = "nul"
}
var f *os.File
if af.LogFile == "-" {
f = os.Stdout
} else {
f, err = os.OpenFile(af.LogFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return fmt.Errorf("opening log file: %w", err)
}
defer func() {
cerr := f.Close()
if cerr != nil {
panic(cerr)
}
}()
}
log.SetOutput(f)
if configErr != nil {
log.Printf("Error reading config file: %s", configErr.Error())
}
istty := isatty.IsTerminal(os.Stdout.Fd())
// we are not able to analyze disk usage on Windows and Plan9
if runtime.GOOS == "windows" || runtime.GOOS == "plan9" {
af.ShowApparentSize = true
}
if !af.ShowVersion && !af.NonInteractive && istty && af.OutputFile == "" {
screen, err = tcell.NewScreen()
if err != nil {
return fmt.Errorf("Error creating screen: %w", err)
}
defer screen.Clear()
defer screen.Fini()
termApp = tview.NewApplication()
termApp.SetScreen(screen)
if !af.NoMouse {
termApp.EnableMouse(true)
}
}
a := app.App{
Flags: af,
Args: args,
Istty: istty,
Writer: os.Stdout,
TermApp: termApp,
Screen: screen,
Getter: device.Getter,
PathChecker: os.Stat,
}
return a.Run()
}
func main() {
if err := Cmd.Execute(); err != nil {
os.Exit(1)
}
}

View File

@ -1,10 +0,0 @@
coverage:
status:
project:
default:
target: auto
threshold: 2%
informational: true
patch:
default:
informational: true

View File

@ -1,195 +0,0 @@
# YAML file configuration options
Gdu provides an additional set of configuration options to the usual command line options.
You can get the full list of all possible options by running:
```
gdu --write-config
```
This will create file `$HOME/.gdu.yaml` with all the options set to default values.
Let's go through them one by one:
#### `log-file`
Path to a logfile (default "/dev/null")
#### `input-file`
Import analysis from JSON file
#### `output-file`
Export all info into file as JSON
#### `ignore-dirs`
Paths to ignore (separated by comma). Can be absolute (like `/proc`) or relative to the current working directory (like `node_modules`). Default values are [/proc,/dev,/sys,/run].
#### `ignore-dir-patterns`
Path patterns to ignore (separated by comma). Patterns can be absolute or relative to the current working directory.
#### `ignore-from-file`
Read path patterns to ignore from file. Patterns can be absolute or relative to the current working directory.
#### `max-cores`
Set max cores that Gdu will use.
#### `sequential-scanning`
Use sequential scanning (intended for rotating HDDs)
#### `show-apparent-size`
Show apparent size
#### `show-relative-size`
Show relative size
#### `show-item-count`
Show number of items in directory
#### `no-color`
Do not use colorized output
#### `no-mouse`
Do not use mouse
#### `non-interactive`
Do not run in interactive mode
#### `no-progress`
Do not show progress in non-interactive mode
#### `no-cross`
Do not cross filesystem boundaries
#### `no-hidden`
Ignore hidden directories (beginning with dot)
#### `no-delete`
Do not allow deletions
#### `follow-symlinks`
Follow symlinks for files, i.e. show the size of the file to which symlink points to (symlinks to directories are not followed)
#### `profiling`
Enable collection of profiling data and provide it on http://localhost:6060/debug/pprof/
#### `const-gc`
Enable memory garbage collection during analysis with constant level set by GOGC
#### `use-storage`
Use persistent key-value storage for analysis data (experimental)
#### `storage-path`
Path to persistent key-value storage directory (default is /tmp/badger)
#### `read-from-storage`
Read analysis data from persistent key-value storage
#### `summarize`
Show only a total in non-interactive mode
#### `use-si-prefix`
Show sizes with decimal SI prefixes (kB, MB, GB) instead of binary prefixes (KiB, MiB, GiB)
#### `no-prefix`
Show sizes as raw numbers without any prefixes (SI or binary) in non-interactive mode
#### `change-cwd`
Set CWD variable when browsing directories
#### `delete-in-background`
Delete items in the background, not blocking the UI from work
#### `delete-in-parallel`
Delete items in parallel, which might increase the speed of deletion
#### `style.selected-row.text-color`
Color of text for the selected row
#### `style.selected-row.background-color`
Background color for the selected row
#### `style.progress-modal.current-item-path-max-len`
Maximum length of file path for the current item in progress bar.
When the length is reached, the path is shortened with "/.../".
#### `style.use-old-size-bar`
Show size bar without Unicode symbols.
#### `style.footer.text-color`
Color of text for footer bar
#### `style.footer.background-color`
Background color for footer bar
#### `style.footer.number-color`
Color of numbers displayed in the footer
#### `style.header.text-color`
Color of text for header bar
#### `style.header.background-color`
Background color for header bar
#### `style.header.hidden`
Hide the header bar
#### `style.result-row.number-color`
Color of numbers in result rows
#### `style.result-row.directory-color`
Color of directory names in result rows
#### `sorting.by`
Sort items. Possible values:
* name - name of the item
* size - usage or apparent size
* itemCount - number of items in the folder tree
* mtime - modification time
#### `sorting.order`
Set sorting order. Possible values:
* asc - ascending order
* desc - descending order

Binary file not shown.

View File

@ -1,13 +0,0 @@
# Release process
1. update usage in README.md and gdu.1.md
1. `make show-man`
1. `make man`
1. commit the changes
1. tag new version with `-sa`
1. `make`
1. `git push --tags`
1. `git push`
1. `make release`
1. update `gdu.spec`
1. Release snapcraft, AUR, ...

123
gdu/gdu.1
View File

@ -1,123 +0,0 @@
.\" Automatically generated by Pandoc 3.1.11.1
.\"
.TH "gdu" "1" "2024\-12\-30" "" ""
.SH NAME
gdu \- Pretty fast disk usage analyzer written in Go
.SH SYNOPSIS
\f[B]gdu [flags] [directory_to_scan]\f[R]
.SH DESCRIPTION
Pretty fast disk usage analyzer written in Go.
.PP
Gdu is intended primarily for SSD disks where it can fully utilize
parallel processing.
However HDDs work as well, but the performance gain is not so huge.
.SH OPTIONS
\f[B]\-h\f[R], \f[B]\-\-help\f[R][=false] help for gdu
.PP
\f[B]\-i\f[R], \f[B]\-\-ignore\-dirs\f[R]=[/proc,/dev,/sys,/run]
Absolute paths to ignore (separated by comma)
.PP
\f[B]\-I\f[R], \f[B]\-\-ignore\-dirs\-pattern\f[R] Absolute path
patterns to ignore (separated by comma)
.PP
\f[B]\-X\f[R], \f[B]\-\-ignore\-from\f[R] Read absolute path patterns to
ignore from file
.PP
\f[B]\-l\f[R], \f[B]\-\-log\-file\f[R]=\[dq]/dev/null\[dq] Path to a
logfile
.PP
\f[B]\-m\f[R], \f[B]\-\-max\-cores\f[R] Set max cores that Gdu will use.
.PP
\f[B]\-c\f[R], \f[B]\-\-no\-color\f[R][=false] Do not use colorized
output
.PP
\f[B]\-x\f[R], \f[B]\-\-no\-cross\f[R][=false] Do not cross filesystem
boundaries
.PP
\f[B]\-H\f[R], \f[B]\-\-no\-hidden\f[R][=false] Ignore hidden
directories (beginning with dot)
.PP
\f[B]\-L\f[R], \f[B]\-\-follow\-symlinks\f[R][=false] Follow symlinks
for files, i.e.\ show the size of the file to which symlink points to
(symlinks to directories are not followed)
.PP
\f[B]\-n\f[R], \f[B]\-\-non\-interactive\f[R][=false] Do not run in
interactive mode
.PP
\f[B]\-p\f[R], \f[B]\-\-no\-progress\f[R][=false] Do not show progress
in non\-interactive mode
.PP
\f[B]\-u\f[R], \f[B]\-\-no\-unicode\f[R][=false] Do not use Unicode
symbols (for size bar)
.PP
\f[B]\-s\f[R], \f[B]\-\-summarize\f[R][=false] Show only a total in
non\-interactive mode
.PP
\f[B]\-t\f[R], \f[B]\-\-top\f[R][=0] Show only top X largest files in
non\-interactive mode
.PP
\f[B]\-d\f[R], \f[B]\-\-show\-disks\f[R][=false] Show all mounted disks
.PP
\f[B]\-a\f[R], \f[B]\-\-show\-apparent\-size\f[R][=false] Show apparent
size
.PP
\f[B]\-C\f[R], \f[B]\-\-show\-item\-count\f[R][=false] Show number of
items in directory
.PP
\f[B]\-M\f[R], \f[B]\-\-show\-mtime\f[R][=false] Show latest mtime of
items in directory
.PP
\f[B]\-\-si\f[R][=false] Show sizes with decimal SI prefixes (kB, MB,
GB) instead of binary prefixes (KiB, MiB, GiB)
.PP
\f[B]\-\-no\-prefix\f[R][=false] Show sizes as raw numbers without any
prefixes (SI or binary) in non\-interactive mode
.PP
\f[B]\-\-no\-mouse\f[R][=false] Do not use mouse
.PP
\f[B]\-\-no\-delete\f[R][=false] Do not allow deletions
.PP
\f[B]\-f\f[R], \f[B]\-\-input\-file\f[R] Import analysis from JSON file.
If the file is \[dq]\-\[dq], read from standard input.
.PP
\f[B]\-o\f[R], \f[B]\-\-output\-file\f[R] Export all info into file as
JSON.
If the file is \[dq]\-\[dq], write to standard output.
.PP
\f[B]\-\-config\-file\f[R]=\[dq]$HOME/.gdu.yaml\[dq] Read config from
file
.PP
\f[B]\-\-write\-config\f[R][=false] Write current configuration to file
(default is $HOME/.gdu.yaml)
.PP
\f[B]\-g\f[R], \f[B]\-\-const\-gc\f[R][=false] Enable memory garbage
collection during analysis with constant level set by GOGC
.PP
\f[B]\-\-enable\-profiling\f[R][=false] Enable collection of profiling
data and provide it on http://localhost:6060/debug/pprof/
.PP
\f[B]\-\-use\-storage\f[R][=false] Use persistent key\-value storage for
analysis data (experimental)
.PP
\f[B]\-r\f[R], \f[B]\-\-read\-from\-storage\f[R][=false] Read analysis
data from persistent key\-value storage
.PP
\f[B]\-v\f[R], \f[B]\-\-version\f[R][=false] Print version
.SH FILE FLAGS
Files and directories may be prefixed by a one\-character flag with
following meaning:
.TP
\f[B]!\f[R]
An error occurred while reading this directory.
.TP
\f[B].\f[R]
An error occurred while reading a subdirectory, size may be not correct.
.TP
\f[B]\[at]\f[R]
File is symlink or socket.
.TP
\f[B]H\f[R]
Same file was already counted (hard link).
.TP
\f[B]e\f[R]
Directory is empty.

View File

@ -1,120 +0,0 @@
---
date: {{date}}
section: 1
title: gdu
---
# NAME
gdu - Pretty fast disk usage analyzer written in Go
# SYNOPSIS
**gdu \[flags\] \[directory_to_scan\]**
# DESCRIPTION
Pretty fast disk usage analyzer written in Go.
Gdu is intended primarily for SSD disks where it can fully utilize
parallel processing. However HDDs work as well, but the performance gain
is not so huge.
# OPTIONS
**-h**, **\--help**\[=false\] help for gdu
**-i**, **\--ignore-dirs**=\[/proc,/dev,/sys,/run\]
Paths to ignore (separated by comma).
Supports both absolute and relative paths.
**-I**, **\--ignore-dirs-pattern**
Path patterns to ignore (separated by comma).
Supports both absolute and relative path patterns.
**-X**, **\--ignore-from**
Read path patterns to ignore from file.
Supports both absolute and relative path patterns.
**-l**, **\--log-file**=\"/dev/null\" Path to a logfile
**-m**, **\--max-cores** Set max cores that Gdu will use.
**-c**, **\--no-color**\[=false\] Do not use colorized output
**-x**, **\--no-cross**\[=false\] Do not cross filesystem boundaries
**-H**, **\--no-hidden**\[=false\] Ignore hidden directories (beginning with dot)
**-L**, **\--follow-symlinks**\[=false\] Follow symlinks for files, i.e. show the
size of the file to which symlink points to (symlinks to directories are not followed)
**-n**, **\--non-interactive**\[=false\] Do not run in interactive mode
**-p**, **\--no-progress**\[=false\] Do not show progress in
non-interactive mode
**-u**, **\--no-unicode**\[=false\] Do not use Unicode symbols (for size bar)
**-s**, **\--summarize**\[=false\] Show only a total in non-interactive mode
**-t**, **\--top**\[=0\] Show only top X largest files in non-interactive mode
**-d**, **\--show-disks**\[=false\] Show all mounted disks
**-a**, **\--show-apparent-size**\[=false\] Show apparent size
**-C**, **\--show-item-count**\[=false\] Show number of items in directory
**-M**, **\--show-mtime**\[=false\] Show latest mtime of items in directory
**\--si**\[=false\] Show sizes with decimal SI prefixes (kB, MB, GB) instead of binary prefixes (KiB, MiB, GiB)
**\--no-prefix**\[=false\] Show sizes as raw numbers without any prefixes (SI or binary) in non-interactive mode
**\--no-mouse**\[=false\] Do not use mouse
**\--no-delete**\[=false\] Do not allow deletions
**-f**, **\--input-file** Import analysis from JSON file. If the file is \"-\", read from standard input.
**-o**, **\--output-file** Export all info into file as JSON. If the file is \"-\", write to standard output.
**\--config-file**=\"$HOME/.gdu.yaml\" Read config from file
**\--write-config**\[=false\] Write current configuration to file (default is $HOME/.gdu.yaml)
**-g**, **\--const-gc**\[=false\] Enable memory garbage collection during analysis with constant level set by GOGC
**\--enable-profiling**\[=false\] Enable collection of profiling data and provide it on http://localhost:6060/debug/pprof/
**\--use-storage**\[=false\] Use persistent key-value storage for analysis data (experimental)
**-r**, **\--read-from-storage**\[=false\] Read analysis data from persistent key-value storage
**-v**, **\--version**\[=false\] Print version
# FILE FLAGS
Files and directories may be prefixed by a one-character
flag with following meaning:
**!**
: An error occurred while reading this directory.
**.**
: An error occurred while reading a subdirectory, size may be not correct.
**\@**
: File is symlink or socket.
**H**
: Same file was already counted (hard link).
**e**
: Directory is empty.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

View File

@ -1,194 +0,0 @@
Name: gdu
Version: 5.30.1
Release: 2
Summary: Pretty fast disk usage analyzer written in Go
License: MIT
URL: https://github.com/dundee/gdu
Source0: https://github.com/dundee/gdu/archive/refs/tags/v%{version}.tar.gz
BuildRequires: golang
BuildRequires: systemd-rpm-macros
BuildRequires: git
Provides: %{name} = %{version}
%description
Pretty fast disk usage analyzer written in Go.
%global debug_package %{nil}
%prep
%autosetup -n %{name}-%{version}
%build
export GOINSECURE=go.opencensus.io
GO111MODULE=on CGO_ENABLED=0 go build \
-trimpath \
-buildmode=pie \
-mod=readonly \
-modcacherw \
-ldflags \
"-s -w \
-X 'b612.me/apps/b612/gdu/build.Version=v%{version}' \
-X 'b612.me/apps/b612/gdu/build.User=$(id -u -n)' \
-X 'b612.me/apps/b612/gdu/build.Time=$(LC_ALL=en_US.UTF-8 date)'" \
-o %{name} b612.me/apps/b612/gdu/cmd/gdu
%install
rm -rf $RPM_BUILD_ROOT
install -Dpm 0755 %{name} %{buildroot}%{_bindir}/%{name}
install -Dpm 0755 %{name}.1 $RPM_BUILD_ROOT%{_mandir}/man1/gdu.1
%check
%post
%preun
%files
%{_bindir}/gdu
%{_mandir}/man1/gdu.1.gz
%changelog
* Tue Feb 4 2025 - Danie de Jager - 5.30.1-2
- fix: set "GOINSECURE=go.opencensus.io"
* Mon Dec 30 2024 Daniel Milde - 5.30.1-1
- fix: set default colors when config file does not exist
* Mon Dec 30 2024 Daniel Milde - 5.30.0-1
- feat: show top largest files using -t or --top option in #391
- feat: introduce more style options in #396
* Mon Jun 17 2024 Daniel Milde - 5.29.0-1
- feat: support for reading gzip, bzip2 and xz files by @dundee in #363
- feat: add --show-mtime (-M) option by @dundee in #350
- feat: add option --no-unicode to disable unicode symbols by @dundee in #362
- fix: division by zero error in formatFileRow by @xroberx in #359
* Sun Apr 21 2024 Danie de Jager - 5.28.0-1
- feat: delete/empty items in background by @dundee in #336
- feat: add --show-item-count (-C) option by @ramgp in #332
- feat: add --no-delete option by @ramgp in #333
- feat: ignore item by pressing I by @dundee in #345
- feat: delete directory items in parallel by @dundee in #340
- feat: add --sequential option for sequential scanning by @dundee in #322
* Sun Feb 18 2024 Danie de Jager - 5.27.0-1
- feat: export in interactive mode by @kadogo in #298
- feat: handle vim-style navigation in confirmation by @samihda in #283
- fix: panic with Interface Conversion Nil Error by @ShivamB25 in #274
- fix: Enter key properly working when reading analysis from file by @dundee in #312
- fix: check if type matches for selected device by @dundee in #318
- ci: package gdu in docker container by @rare-magma in #313
- ci: add values for building gdu with tito by @daniejstriata in #288
- ci: change Winget Releaser job to ubuntu-latest by @sitiom in #271
* Tue Feb 13 2024 Danie de Jager - 5.26.0-1
- feat: use key-value store for analysis data in #297
- feat: use profile-guided optimization in #286
* Fri Dec 1 2023 Danie de Jager - 5.25.0-2
- Improved SPEC to build on AL2023.
* Tue Jun 6 2023 Danie de Jager - 5.25.0-1
- feat: use unicode block elements in size bar in #255
* Thu Jun 1 2023 Danie de Jager - 5.24.0-1
- feat: add ctrl+z for job control by @yurenchen000 in #250
- feat: upgrade dependencies by @dundee in #252
* Thu May 11 2023 Danie de Jager - 5.23.0-2
- Compiled with golang 1.19.9
* Tue Apr 11 2023 Danie de Jager - 5.23.0-1
- feat: added configuration option to change CWD when browsing directories by @leapfog in #230
- fix: do not show help modal when confirm modal is already opened by @dundee in #237
* Mon Feb 6 2023 Danie de Jager - 5.22.0-1
- feat: added option to follow symlinks in #206
- fix: ignore mouse events when modal is opened in #205
- Updated SPEC file used for rpm creation by @daniejstriata in #198
* Mon Jan 9 2023 Danie de Jager - 5.21.1-2
- updated SPEC file to support builds on Fedora
* Mon Jan 9 2023 Danie de Jager - 5.21.1-1
- fix: correct open command for Win
* Wed Jan 4 2023 Danie de Jager - 5.21.0-1
- feat: mark multiple items for deletion by @dundee in #193
- feat: move cursor to next row when marked by @dundee in #194
- Use GNU tar on Darwin to fix build error by @sryze in #188
* Mon Oct 24 2022 Danie de Jager - 5.20.0-1
- feat: set default sorting using config option
- feat: open file or directory in external program
- fix: check reference type
* Wed Sep 28 2022 Danie de Jager - 5.19.0-1
- feat: upgrade all dependencies
- feat: bump go version to 1.18
- feat: format negative numbers correctly
- feat: try to read config from ~/.config/gdu/gdu.yaml first
- test: export formatting
- docs: config file default locations
* Sun Sep 18 2022 Danie de Jager - 5.18.1-1
- fix: correct config file option regex
- fix: read non-default config file properly in #175
- feat: crop current item path to 70 chars in #173
- feat: show elapsed time in progress modal
- feat: configuration option for setting maximum length of the path for current item in the progress modal in #174
* Tue Sep 13 2022 Danie de Jager - 5.17.1-1
- fix: nul log file for Windows (#171)
- fix: increase the vertical size of the progress modal (#172)
- feat: added possibility to change text and background color of the selected row by @dundee in #170
* Thu Sep 8 2022 Danie de Jager - 5.16.0-1
- feat: support for reading (and writing) configuration to YAML file
- feat: initial mouse support by @dundee in #165
- add mtime for Windows by @mcoret in #157
- openbsd fixes by @dundee in #164
* Wed Aug 10 2022 Danie de Jager - 5.15.0-1
- feat: show sizes as raw numbers without prefixes by @dundee in #147
- feat: natural sorting by @dundee in #156
- fix: honor --summarize when reading analysis by @Riatre in #149
- fix: upgrade dependencies by @phanirithvij in #153
- ci: generate release tarballs with vendor directory by @CyberTailor in #148
* Mon Jul 18 2022 Danie de Jager - 5.14.0-2
* Thu May 26 2022 Danie de Jager - 5.14.0-1
- sort items by name if usage/size/count is the same (#143)
* Mon Feb 21 2022 Danie de Jager - 5.13.2
- able to go back to devices list from analyzed directory
* Thu Feb 10 2022 Danie de Jager - 5.13.1
- properly count only the first hard link size on a rescan
- do not panic if path does not start with a slash
* Sat Jan 29 2022 Danie de Jager - 5.13.0-1
- lower memory usage
- possibility to toggle between bar graph relative to the size of the directory or the biggest file
- added option --si for showing sizes with decimal SI prefixes
- fixed freeze when r key binding is being hold
* Tue Dec 14 2021 Danie de Jager - 5.12.1-1
- Bump to 5.12.1-1
- fixed listing devices on NetBSD
- escape file names (#111)
- fixed filtering
* Fri Dec 3 2021 Danie de Jager - 5.12.0-1
- Bump to 5.12.0-1
* Fri Dec 3 2021 Danie de Jager - 5.11.0-2
- Compile with go 1.17.4
* Sun Nov 28 2021 Danie de Jager - 5.11.0-1
- Bump to 5.11.0
* Tue Nov 23 2021 Danie de Jager - 5.10.1-1
- Bump to 5.10.1
* Wed Nov 10 2021 Danie de Jager - 5.10.0-1
- Bump to 5.10.01
* Mon Oct 25 2021 Danie de Jager - 5.9.0-1
- Bump to 5.9.0
* Mon Sep 27 2021 Danie de Jager - 5.8.1-2
- Remove pandoc requirement.
* Sun Sep 26 2021 Danie de Jager - 5.8.1-1
- Bump to 5.8.1
* Thu Sep 23 2021 Danie de Jager - 5.8.0-2
- Bump to 5.8.0
* Tue Sep 7 2021 Danie de Jager - 5.7.0-1
- Bump to 5.7.0
* Sat Aug 28 2021 Danie de Jager - 5.6.2-1
- Bump to 5.6.2
- Compiled with go 1.17
* Fri Aug 27 2021 Danie de Jager - 5.6.1-1
- Bump to 5.6.1
* Mon Aug 23 2021 Danie de Jager - 5.6.0-1
- Bump to 5.6.0
* Fri Aug 13 2021 Danie de Jager - 5.5.0-2
- Compiled with go 1.16.7
* Mon Aug 2 2021 Danie de Jager - 5.5.0-1
- Bump to 5.5.0
* Mon Jul 26 2021 Danie de Jager - 5.4.0-1
- Bump to 5.4.0
* Thu Jul 22 2021 Danie de Jager - 5.3.0-2
- First release

View File

@ -1,23 +0,0 @@
package common
import "b612.me/apps/b612/gdu/pkg/fs"
// CurrentProgress struct
type CurrentProgress struct {
CurrentItemName string
ItemCount int
TotalSize int64
}
// ShouldDirBeIgnored whether path should be ignored
type ShouldDirBeIgnored func(name, path string) bool
// Analyzer is type for dir analyzing function
type Analyzer interface {
AnalyzeDir(path string, ignore ShouldDirBeIgnored, constGC bool) fs.Item
SetFollowSymlinks(bool)
SetShowAnnexedSize(bool)
GetProgressChan() chan CurrentProgress
GetDone() SignalGroup
ResetProgress()
}

View File

@ -1,21 +0,0 @@
package common
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
// TermApplication is interface for the terminal UI app
type TermApplication interface {
Run() error
Stop()
Suspend(f func()) bool
SetRoot(root tview.Primitive, fullscreen bool) *tview.Application
SetFocus(p tview.Primitive) *tview.Application
SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *tview.Application
SetMouseCapture(
capture func(event *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction),
) *tview.Application
QueueUpdateDraw(f func()) *tview.Application
SetBeforeDrawFunc(func(screen tcell.Screen) bool) *tview.Application
}

View File

@ -1,152 +0,0 @@
package common
import (
"bufio"
"os"
"path/filepath"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
)
// CreateIgnorePattern creates one pattern from all path patterns
func CreateIgnorePattern(paths []string) (*regexp.Regexp, error) {
var err error
for i, path := range paths {
if _, err = regexp.Compile(path); err != nil {
return nil, err
}
if !filepath.IsAbs(path) {
absPath, err := filepath.Abs(path)
if err == nil {
paths = append(paths, absPath)
}
} else {
relPath, err := filepath.Rel("/", path)
if err == nil {
paths = append(paths, relPath)
}
}
paths[i] = "(" + path + ")"
}
ignore := `^` + strings.Join(paths, "|") + `$`
return regexp.Compile(ignore)
}
// SetIgnoreDirPaths sets paths to ignore
func (ui *UI) SetIgnoreDirPaths(paths []string) {
log.Printf("Ignoring dirs %s", strings.Join(paths, ", "))
ui.IgnoreDirPaths = make(map[string]struct{}, len(paths)*2)
for _, path := range paths {
ui.IgnoreDirPaths[path] = struct{}{}
if !filepath.IsAbs(path) {
if absPath, err := filepath.Abs(path); err == nil {
ui.IgnoreDirPaths[absPath] = struct{}{}
}
} else {
if relPath, err := filepath.Rel("/", path); err == nil {
ui.IgnoreDirPaths[relPath] = struct{}{}
}
}
}
}
// SetIgnoreDirPatterns sets regular patterns of dirs to ignore
func (ui *UI) SetIgnoreDirPatterns(paths []string) error {
var err error
log.Printf("Ignoring dir patterns %s", strings.Join(paths, ", "))
ui.IgnoreDirPathPatterns, err = CreateIgnorePattern(paths)
return err
}
// SetIgnoreFromFile sets regular patterns of dirs to ignore
func (ui *UI) SetIgnoreFromFile(ignoreFile string) error {
var err error
var paths []string
log.Printf("Reading ignoring dir patterns from file '%s'", ignoreFile)
file, err := os.Open(ignoreFile)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
paths = append(paths, scanner.Text())
}
if err := scanner.Err(); err != nil {
return err
}
ui.IgnoreDirPathPatterns, err = CreateIgnorePattern(paths)
return err
}
// SetIgnoreHidden sets flags if hidden dirs should be ignored
func (ui *UI) SetIgnoreHidden(value bool) {
log.Printf("Ignoring hidden dirs")
ui.IgnoreHidden = value
}
// ShouldDirBeIgnored returns true if given path should be ignored
func (ui *UI) ShouldDirBeIgnored(name, path string) bool {
_, shouldIgnore := ui.IgnoreDirPaths[path]
if shouldIgnore {
log.Printf("Directory %s ignored", path)
}
return shouldIgnore
}
// ShouldDirBeIgnoredUsingPattern returns true if given path should be ignored
func (ui *UI) ShouldDirBeIgnoredUsingPattern(name, path string) bool {
shouldIgnore := ui.IgnoreDirPathPatterns.MatchString(path)
if shouldIgnore {
log.Printf("Directory %s ignored", path)
}
return shouldIgnore
}
// IsHiddenDir returns if the dir name begins with dot
func (ui *UI) IsHiddenDir(name, path string) bool {
shouldIgnore := name[0] == '.'
if shouldIgnore {
log.Printf("Directory %s ignored", path)
}
return shouldIgnore
}
// CreateIgnoreFunc returns function for detecting if dir should be ignored
// nolint: gocyclo // Why: This function is a switch statement that is not too complex
func (ui *UI) CreateIgnoreFunc() ShouldDirBeIgnored {
switch {
case len(ui.IgnoreDirPaths) > 0 && ui.IgnoreDirPathPatterns == nil && !ui.IgnoreHidden:
return ui.ShouldDirBeIgnored
case len(ui.IgnoreDirPaths) > 0 && ui.IgnoreDirPathPatterns != nil && !ui.IgnoreHidden:
return func(name, path string) bool {
return ui.ShouldDirBeIgnored(name, path) || ui.ShouldDirBeIgnoredUsingPattern(name, path)
}
case len(ui.IgnoreDirPaths) > 0 && ui.IgnoreDirPathPatterns != nil && ui.IgnoreHidden:
return func(name, path string) bool {
return ui.ShouldDirBeIgnored(name, path) || ui.ShouldDirBeIgnoredUsingPattern(name, path) || ui.IsHiddenDir(name, path)
}
case len(ui.IgnoreDirPaths) == 0 && ui.IgnoreDirPathPatterns != nil && ui.IgnoreHidden:
return func(name, path string) bool {
return ui.ShouldDirBeIgnoredUsingPattern(name, path) || ui.IsHiddenDir(name, path)
}
case len(ui.IgnoreDirPaths) == 0 && ui.IgnoreDirPathPatterns != nil && !ui.IgnoreHidden:
return ui.ShouldDirBeIgnoredUsingPattern
case len(ui.IgnoreDirPaths) == 0 && ui.IgnoreDirPathPatterns == nil && ui.IgnoreHidden:
return ui.IsHiddenDir
case len(ui.IgnoreDirPaths) > 0 && ui.IgnoreDirPathPatterns == nil && ui.IgnoreHidden:
return func(name, path string) bool {
return ui.ShouldDirBeIgnored(name, path) || ui.IsHiddenDir(name, path)
}
default:
return func(name, path string) bool { return false }
}
}

View File

@ -1,206 +0,0 @@
package common_test
import (
"os"
"path/filepath"
"testing"
log "github.com/sirupsen/logrus"
"b612.me/apps/b612/gdu/internal/common"
"github.com/stretchr/testify/assert"
)
func init() {
log.SetLevel(log.WarnLevel)
}
func TestCreateIgnorePattern(t *testing.T) {
re, err := common.CreateIgnorePattern([]string{"[abc]+"})
assert.Nil(t, err)
assert.True(t, re.MatchString("aa"))
}
func TestCreateIgnorePatternWithErr(t *testing.T) {
re, err := common.CreateIgnorePattern([]string{"[[["})
assert.NotNil(t, err)
assert.Nil(t, re)
}
func TestEmptyIgnore(t *testing.T) {
ui := &common.UI{}
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.False(t, shouldBeIgnored("abc", "/abc"))
assert.False(t, shouldBeIgnored("xxx", "/xxx"))
}
func TestIgnoreByAbsPath(t *testing.T) {
ui := &common.UI{}
ui.SetIgnoreDirPaths([]string{"/abc"})
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("abc", "/abc"))
assert.False(t, shouldBeIgnored("xxx", "/xxx"))
}
func TestIgnoreByPattern(t *testing.T) {
ui := &common.UI{}
err := ui.SetIgnoreDirPatterns([]string{"/[abc]+"})
assert.Nil(t, err)
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("aaa", "/aaa"))
assert.True(t, shouldBeIgnored("aaa", "/aaabc"))
assert.False(t, shouldBeIgnored("xxx", "/xxx"))
}
func TestIgnoreFromFile(t *testing.T) {
file, err := os.OpenFile("ignore", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
panic(err)
}
defer file.Close()
if _, err := file.WriteString("/aaa\n"); err != nil {
panic(err)
}
if _, err := file.WriteString("/aaabc\n"); err != nil {
panic(err)
}
if _, err := file.WriteString("/[abd]+\n"); err != nil {
panic(err)
}
ui := &common.UI{}
err = ui.SetIgnoreFromFile("ignore")
assert.Nil(t, err)
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("aaa", "/aaa"))
assert.True(t, shouldBeIgnored("aaabc", "/aaabc"))
assert.True(t, shouldBeIgnored("aaabd", "/aaabd"))
assert.False(t, shouldBeIgnored("xxx", "/xxx"))
}
func TestIgnoreFromNotExistingFile(t *testing.T) {
ui := &common.UI{}
err := ui.SetIgnoreFromFile("xxx")
assert.NotNil(t, err)
}
func TestIgnoreHidden(t *testing.T) {
ui := &common.UI{}
ui.SetIgnoreHidden(true)
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored(".git", "/aaa/.git"))
assert.True(t, shouldBeIgnored(".bbb", "/aaa/.bbb"))
assert.False(t, shouldBeIgnored("xxx", "/xxx"))
}
func TestIgnoreByAbsPathAndHidden(t *testing.T) {
ui := &common.UI{}
ui.SetIgnoreDirPaths([]string{"/abc"})
ui.SetIgnoreHidden(true)
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("abc", "/abc"))
assert.True(t, shouldBeIgnored(".git", "/aaa/.git"))
assert.True(t, shouldBeIgnored(".bbb", "/aaa/.bbb"))
assert.False(t, shouldBeIgnored("xxx", "/xxx"))
}
func TestIgnoreByAbsPathAndPattern(t *testing.T) {
ui := &common.UI{}
ui.SetIgnoreDirPaths([]string{"/abc"})
err := ui.SetIgnoreDirPatterns([]string{"/[abc]+"})
assert.Nil(t, err)
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("abc", "/abc"))
assert.True(t, shouldBeIgnored("aabc", "/aabc"))
assert.True(t, shouldBeIgnored("ccc", "/ccc"))
assert.False(t, shouldBeIgnored("xxx", "/xxx"))
}
func TestIgnoreByPatternAndHidden(t *testing.T) {
ui := &common.UI{}
err := ui.SetIgnoreDirPatterns([]string{"/[abc]+"})
assert.Nil(t, err)
ui.SetIgnoreHidden(true)
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("abbc", "/abbc"))
assert.True(t, shouldBeIgnored(".git", "/aaa/.git"))
assert.True(t, shouldBeIgnored(".bbb", "/aaa/.bbb"))
assert.False(t, shouldBeIgnored("xxx", "/xxx"))
}
func TestIgnoreByAll(t *testing.T) {
ui := &common.UI{}
ui.SetIgnoreDirPaths([]string{"/abc"})
err := ui.SetIgnoreDirPatterns([]string{"/[abc]+"})
assert.Nil(t, err)
ui.SetIgnoreHidden(true)
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("abc", "/abc"))
assert.True(t, shouldBeIgnored("aabc", "/aabc"))
assert.True(t, shouldBeIgnored(".git", "/aaa/.git"))
assert.True(t, shouldBeIgnored(".bbb", "/aaa/.bbb"))
assert.False(t, shouldBeIgnored("xxx", "/xxx"))
}
func TestIgnoreByRelativePath(t *testing.T) {
ui := &common.UI{}
ui.SetIgnoreDirPaths([]string{"test_dir/abc"})
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("abc", "test_dir/abc"))
absPath, err := filepath.Abs("test_dir/abc")
assert.Nil(t, err)
assert.True(t, shouldBeIgnored("abc", absPath))
assert.False(t, shouldBeIgnored("xxx", "test_dir/xxx"))
}
func TestIgnoreByRelativePattern(t *testing.T) {
ui := &common.UI{}
err := ui.SetIgnoreDirPatterns([]string{"test_dir/[abc]+"})
assert.Nil(t, err)
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("abc", "test_dir/abc"))
absPath, err := filepath.Abs("test_dir/abc")
assert.Nil(t, err)
assert.True(t, shouldBeIgnored("abc", absPath))
assert.False(t, shouldBeIgnored("xxx", "test_dir/xxx"))
}
func TestIgnoreFromFileWithRelativePaths(t *testing.T) {
file, err := os.OpenFile("ignore", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
panic(err)
}
defer file.Close()
if _, err := file.WriteString("test_dir/aaa\n"); err != nil {
panic(err)
}
if _, err := file.WriteString("node_modules/[^/]+\n"); err != nil {
panic(err)
}
ui := &common.UI{}
err = ui.SetIgnoreFromFile("ignore")
assert.Nil(t, err)
shouldBeIgnored := ui.CreateIgnoreFunc()
assert.True(t, shouldBeIgnored("aaa", "test_dir/aaa"))
absPath, err := filepath.Abs("test_dir/aaa")
assert.Nil(t, err)
assert.True(t, shouldBeIgnored("aaa", absPath))
assert.False(t, shouldBeIgnored("xxx", "test_dir/xxx"))
}

Some files were not shown because too many files have changed in this diff Show More