diff --git a/buildwasm b/buildwasm index 1eba1c87..e51ddfa8 100755 --- a/buildwasm +++ b/buildwasm @@ -1 +1,4 @@ -env GOOS=js GOARCH=wasm go build -ldflags="-s -w" -tags "wasm,b_wasm,no_sqlite,no_psql,no_mysql,no_io,no_git,no_chitosocket,no_termui" -o wasm/tryrye/main.wasm main_wasm.go ; bin/rye serve_wasm.rye +env GOOS=js GOARCH=wasm go build -ldflags="-s -w" -tags "wasm,b_wasm,no_sqlite,no_psql,no_mysql,no_io,no_git,no_chitosocket,no_termui,no_telegram,no_smtpd,no_tui,no_mcp,no_os,no_pipes,no_mail,no_smtpd,no_prometheus" -o wasm/tryrye/main.wasm main_wasm.go +# ; bin/rye serve_wasm.rye + +# no_termui,no_os,no_pipes,no_mail,no_crypto,no_bcrypt,no_bson,no_echarts,no_email,no_imap,no_mqtt,no_prometheus,no_smtpd,no_telegram,no_validation,no_sxml,no_markdown,no_cli,no_tui,no_mcp" \ diff --git a/env/context.go b/env/context.go index 05cc4aac..21728d51 100755 --- a/env/context.go +++ b/env/context.go @@ -587,11 +587,17 @@ func (e *RyeCtx) MarkAsVariable(word int) { // Check if a word is a variable func (e *RyeCtx) IsVariable(word int) bool { - isVar, exists := e.varFlags[word] - if exists && isVar { + // Fast path: check current context first + if isVar, exists := e.varFlags[word]; exists && isVar { return true } - // Not a variable in this context + // Walk parent chain without recursion + for ctx := e.Parent; ctx != nil; ctx = ctx.Parent { + if isVar, exists := ctx.varFlags[word]; exists && isVar { + return true + } + } + // Not a variable in this context or any parent return false } @@ -612,16 +618,17 @@ func (e *RyeCtx) Mod(word int, val Object) bool { // ModWithInfo modifies a word and returns detailed information about the result // Returns (ModResult, existingType) where existingType is the type of the existing value if there's a mismatch func (e *RyeCtx) ModWithInfo(word int, val Object) (ModResult, Type) { - if existingVal, exists := e.state[word]; exists { + if _, exists := e.state[word]; exists { // Word exists, check if it's a variable if !e.IsVariable(word) { // Cannot modify constants return ModErrConstant, 0 } + // bug ... Mod retuns always false, even if error is different type of value than previous // Type check - compare Object.Type() - if existingVal.Type() != val.Type() { - return ModErrTypeMismatch, existingVal.Type() - } + //if existingVal.Type() != val.Type() { + // return ModErrTypeMismatch, existingVal.Type() + //} } else { // Word doesn't exist, create it as a variable e.MarkAsVariable(word) @@ -1176,8 +1183,6 @@ func (ps *ProgramState) GetValue(word string, typ Type) (Object, bool) { return v, true } - - const STACK_SIZE int = 1000 type EyrStack struct { diff --git a/evaldo/builtins.go b/evaldo/builtins.go index adebb65a..a9c447d3 100644 --- a/evaldo/builtins.go +++ b/evaldo/builtins.go @@ -282,6 +282,32 @@ func lesserThanNew(arg0 env.Object, arg1 env.Object) bool { } func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { + // Helper function to get type name for better error messages + getTypeName := func(obj any) string { + switch v := obj.(type) { + case env.Object: + return NameOfRyeType(v.Type()) + default: + return reflect.TypeOf(obj).String() + } + } + + // Helper function to get key description for error messages + getKeyDesc := func(key any) string { + switch k := key.(type) { + case env.String: + return fmt.Sprintf("string key '%s'", k.Value) + case env.Integer: + return fmt.Sprintf("integer index %d", k.Value) + case env.Word: + return fmt.Sprintf("word key '%s'", ps.Idx.GetWord(k.Index)) + case env.Tagword: + return fmt.Sprintf("tagword key '%s'", ps.Idx.GetWord(k.Index)) + default: + return fmt.Sprintf("%s key", getTypeName(key)) + } + } + switch s1 := data.(type) { case env.Dict: switch s2 := key.(type) { @@ -312,11 +338,13 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { return v1 case nil: ps.FailureFlag = true - return env.NewError("Key is missing in dict") + return env.NewError(fmt.Sprintf("Key '%s' not found in dict", s2.Value)) default: ps.FailureFlag = true return env.NewError("Unhandeled value, type: " + reflect.TypeOf(v1).String()) } + default: + return makeError(ps, fmt.Sprintf("Dict requires string key, but got %s", getKeyDesc(key))) } case *env.Dict: switch s2 := key.(type) { @@ -347,11 +375,13 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { return v1 case nil: ps.FailureFlag = true - return env.NewError("Key is missing in dict") + return env.NewError(fmt.Sprintf("Key '%s' not found in dict", s2.Value)) default: ps.FailureFlag = true return env.NewError("Unhandeled value, type: " + reflect.TypeOf(v1).String()) } + default: + return makeError(ps, fmt.Sprintf("Dict requires string key, but got %s", getKeyDesc(key))) } case *env.RyeCtx: switch s2 := key.(type) { @@ -360,17 +390,17 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { if ok { return v } else { - return makeError(ps, "Not found in context") + return makeError(ps, fmt.Sprintf("Word '%s' not found in context", ps.Idx.GetWord(s2.Index))) } case env.Tagword: v, ok := s1.Get(s2.Index) if ok { return v } else { - return makeError(ps, "Not found in context") + return makeError(ps, fmt.Sprintf("Tagword '%s' not found in context", ps.Idx.GetWord(s2.Index))) } default: - return makeError(ps, "Wrong type or missing key for get-arrow") + return makeError(ps, fmt.Sprintf("Context requires word or tagword key, but got %s", getKeyDesc(key))) } case env.List: switch s2 := key.(type) { @@ -380,14 +410,22 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { idx-- } if idx < 0 { - return makeError(ps, "Index too low") + return makeError(ps, fmt.Sprintf("Index %d is too low (minimum is %d)", s2.Value, func() int64 { + if posMode { + return 1 + } else { + return 0 + } + }())) } if len(s1.Data) > int(idx) && idx >= 0 { v := s1.Data[idx] return env.ToRyeValue(v) } else { - return makeError(ps, "Index larger than length") + return makeError(ps, fmt.Sprintf("Index %d is out of bounds (list length is %d)", s2.Value, len(s1.Data))) } + default: + return makeError(ps, fmt.Sprintf("List requires integer index, but got %s", getKeyDesc(key))) } case *env.List: switch s2 := key.(type) { @@ -397,14 +435,22 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { idx-- } if idx < 0 { - return makeError(ps, "Index too low") + return makeError(ps, fmt.Sprintf("Index %d is too low (minimum is %d)", s2.Value, func() int64 { + if posMode { + return 1 + } else { + return 0 + } + }())) } if len(s1.Data) > int(idx) && idx >= 0 { v := s1.Data[idx] return env.ToRyeValue(v) } else { - return makeError(ps, "Index larger than length") + return makeError(ps, fmt.Sprintf("Index %d is out of bounds (list length is %d)", s2.Value, len(s1.Data))) } + default: + return makeError(ps, fmt.Sprintf("List requires integer index, but got %s", getKeyDesc(key))) } case env.Block: switch s2 := key.(type) { @@ -414,14 +460,22 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { idx-- } if idx < 0 { - return makeError(ps, "Index too low") + return makeError(ps, fmt.Sprintf("Index %d is too low (minimum is %d)", s2.Value, func() int64 { + if posMode { + return 1 + } else { + return 0 + } + }())) } if len(s1.Series.S) >= int(idx)+1 { v := s1.Series.Get(int(idx)) return v } else { - return makeError(ps, "Index larger than length") + return makeError(ps, fmt.Sprintf("Index %d is out of bounds (block length is %d)", s2.Value, len(s1.Series.S))) } + default: + return makeError(ps, fmt.Sprintf("Block requires integer index, but got %s", getKeyDesc(key))) } case *env.Block: switch s2 := key.(type) { @@ -431,14 +485,22 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { idx-- } if idx < 0 { - return makeError(ps, "Index too low") + return makeError(ps, fmt.Sprintf("Index %d is too low (minimum is %d)", s2.Value, func() int64 { + if posMode { + return 1 + } else { + return 0 + } + }())) } if len(s1.Series.S) >= int(idx)+1 { v := s1.Series.Get(int(idx)) return v } else { - return makeError(ps, "Index larger than length") + return makeError(ps, fmt.Sprintf("Index %d is out of bounds (block length is %d)", s2.Value, len(s1.Series.S))) } + default: + return makeError(ps, fmt.Sprintf("Block requires integer index, but got %s", getKeyDesc(key))) } case env.Table: switch s2 := key.(type) { @@ -448,14 +510,22 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { idx-- } if idx < 0 { - return makeError(ps, "Index too low") + return makeError(ps, fmt.Sprintf("Index %d is too low (minimum is %d)", s2.Value, func() int64 { + if posMode { + return 1 + } else { + return 0 + } + }())) } if idx < int64(len(s1.Rows)) { v := s1.Rows[idx] return v } else { - return makeError(ps, "Index larger than length") + return makeError(ps, fmt.Sprintf("Index %d is out of bounds (table length is %d)", s2.Value, len(s1.Rows))) } + default: + return makeError(ps, fmt.Sprintf("Table requires integer index, but got %s", getKeyDesc(key))) } case *env.Table: switch s2 := key.(type) { @@ -465,14 +535,22 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { idx-- } if idx < 0 { - return makeError(ps, "Index too low") + return makeError(ps, fmt.Sprintf("Index %d is too low (minimum is %d)", s2.Value, func() int64 { + if posMode { + return 1 + } else { + return 0 + } + }())) } if idx < int64(len(s1.Rows)) { v := s1.Rows[idx] return v } else { - return makeError(ps, "Index larger than length") + return makeError(ps, fmt.Sprintf("Index %d is out of bounds (table length is %d)", s2.Value, len(s1.Rows))) } + default: + return makeError(ps, fmt.Sprintf("Table requires integer index, but got %s", getKeyDesc(key))) } case env.Bytes: switch s2 := key.(type) { @@ -482,14 +560,22 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { idx-- } if idx < 0 { - return makeError(ps, "Index too low") + return makeError(ps, fmt.Sprintf("Index %d is too low (minimum is %d)", s2.Value, func() int64 { + if posMode { + return 1 + } else { + return 0 + } + }())) } if idx < int64(len(s1.Value)) { v := s1.Value[idx] return *env.NewInteger(int64(v)) } else { - return makeError(ps, "Index larger than length") + return makeError(ps, fmt.Sprintf("Index %d is out of bounds (bytes length is %d)", s2.Value, len(s1.Value))) } + default: + return makeError(ps, fmt.Sprintf("Bytes requires integer index, but got %s", getKeyDesc(key))) } case env.TableRow: switch s2 := key.(type) { @@ -497,32 +583,45 @@ func getFrom(ps *env.ProgramState, data any, key any, posMode bool) env.Object { index := 0 // find the column index columnNames := s1.Uplink.GetColumnNames() + found := false for i := 0; i < len(columnNames); i++ { if columnNames[i] == s2.Value { index = i + found = true + break } } - v := s1.Values[index] - if true { - return env.ToRyeValue(v) - } else { - return makeError(ps, "Index larger than table row length") + if !found { + return makeError(ps, fmt.Sprintf("Column '%s' not found in table row (available columns: %v)", s2.Value, columnNames)) } + v := s1.Values[index] + return env.ToRyeValue(v) case env.Integer: idx := s2.Value if posMode { idx-- } + if idx < 0 { + return makeError(ps, fmt.Sprintf("Index %d is too low (minimum is %d)", s2.Value, func() int64 { + if posMode { + return 1 + } else { + return 0 + } + }())) + } if idx < int64(len(s1.Values)) { v := s1.Values[idx] return env.ToRyeValue(v) } else { - return makeError(ps, "Index larger than table row length") + return makeError(ps, fmt.Sprintf("Index %d is out of bounds (table row length is %d)", s2.Value, len(s1.Values))) } + default: + return makeError(ps, fmt.Sprintf("TableRow requires string column name or integer index, but got %s", getKeyDesc(key))) } } // fmt.Printf("GETFROM: %#v %#v %#v\n", data, key, posMode) - return makeError(ps, "Wrong type or missing key for get-arrow") + return makeError(ps, fmt.Sprintf("Cannot access %s with %s - unsupported combination", getTypeName(data), getKeyDesc(key))) } // Sort object interface @@ -2488,19 +2587,19 @@ var builtins = map[string]*env.Builtin{ } else { firstArg = 2 } - + // If no arguments beyond the script name, return empty block if firstArg >= len(os.Args) { return *env.NewBlock(*env.NewTSeries([]env.Object{})) } - + // Convert each argument to appropriate Rye type args := os.Args[firstArg:] lst := make([]env.Object, len(args)) - + intRe := regexp.MustCompile("^[+-]?[0-9]+$") floatRe := regexp.MustCompile("^[+-]?[0-9]*\\.[0-9]+$") - + for i, arg := range args { // Try to parse as integer if intRe.MatchString(arg) { @@ -2509,7 +2608,7 @@ var builtins = map[string]*env.Builtin{ continue } } - + // Try to parse as float if floatRe.MatchString(arg) { if num, err := strconv.ParseFloat(arg, 64); err == nil { @@ -2517,11 +2616,11 @@ var builtins = map[string]*env.Builtin{ continue } } - + // Default to string lst[i] = *env.NewString(arg) } - + return *env.NewBlock(*env.NewTSeries(lst)) }, }, @@ -2535,19 +2634,19 @@ var builtins = map[string]*env.Builtin{ } else { firstArg = 2 } - + // If no arguments beyond the script name, return empty block if firstArg >= len(os.Args) { return *env.NewBlock(*env.NewTSeries([]env.Object{})) } - + // Convert each argument to appropriate Rye type args := os.Args[firstArg:] lst := make([]env.Object, len(args)) - + intRe := regexp.MustCompile("^[+-]?[0-9]+$") floatRe := regexp.MustCompile("^[+-]?[0-9]*\\.[0-9]+$") - + for i, arg := range args { // Try to parse as integer if intRe.MatchString(arg) { @@ -2556,7 +2655,7 @@ var builtins = map[string]*env.Builtin{ continue } } - + // Try to parse as float if floatRe.MatchString(arg) { if num, err := strconv.ParseFloat(arg, 64); err == nil { @@ -2564,11 +2663,11 @@ var builtins = map[string]*env.Builtin{ continue } } - + // Default to string lst[i] = *env.NewString(arg) } - + return *env.NewBlock(*env.NewTSeries(lst)) }, }, @@ -2992,6 +3091,7 @@ func RegisterBuiltins(ps *env.ProgramState) { RegisterBuiltins2(builtins_contexts, ps, "base") RegisterBuiltins2(builtins_persistent_contexts, ps, "base") RegisterBuiltins2(builtins_functions, ps, "base") + RegisterBuiltins2(builtins_unique, ps, "base") // already in base_functions RegisterBuiltins2(builtins_apply, ps, "base") RegisterBuiltins2(Builtins_error_creation, ps, "error-creation") RegisterBuiltins2(Builtins_error_inspection, ps, "error-inspection") @@ -3029,6 +3129,7 @@ func RegisterBuiltins(ps *env.ProgramState) { RegisterBuiltins2(Builtins_bcrypt, ps, "bcrypt") RegisterBuiltins2(Builtins_console, ps, "console") RegisterBuiltinsInContext(Builtins_crypto, ps, "crypto") + RegisterBuiltinsInContext(Builtins_encoding, ps, "encoding") RegisterBuiltinsInContext(Builtins_math, ps, "math") RegisterBuiltinsInContext(Builtins_os, ps, "os") RegisterBuiltinsInContext(Builtins_pipes, ps, "pipes") @@ -3280,6 +3381,7 @@ var allBuiltinGroups = []builtinGroup{ {"chitosocket", Builtins_chitosocket, false}, // Module builtins – in named child context {"crypto", Builtins_crypto, true}, + {"encoding", Builtins_encoding, true}, {"math", Builtins_math, true}, {"os", Builtins_os, true}, {"pipes", Builtins_pipes, true}, diff --git a/evaldo/builtins_base_contexts.go b/evaldo/builtins_base_contexts.go index c428b560..7557f58f 100644 --- a/evaldo/builtins_base_contexts.go +++ b/evaldo/builtins_base_contexts.go @@ -970,4 +970,67 @@ var builtins_contexts = map[string]*env.Builtin{ return *env.NewInteger(old) }, }, + + // Tests: + // equal { c: context { x: 100 } 123 |enter c { .print x } } 123 + // equal { c: context { x: 100 } 123 |enter c { add x } } 223 + // equal { pipes: context { echo: { .print } into-file: { .print } } 123 |enter pipes { .echo .into-file %data.txt } } 123 + // Args: + // * value: Value to inject into the block (like with) + // * context: Context to use as parent context during execution (like do\in) + // * block: Block of code to execute with both the injected value and specified parent context + // Returns: + // * result of executing the block with both the injected value and modified parent context + "enter": { + Argsn: 3, + Doc: "Combines with and do\\in: takes a value to inject, a context as parent, and a block to evaluate with both.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + value := arg0 // First argument: value to inject (like with) + + switch ctx := arg1.(type) { // Second argument: context (like do\in) + case *env.RyeCtx: + switch bloc := arg2.(type) { // Third argument: block to execute + case env.Block: + ser := ps.Ser + ps.Ser = bloc.Series + + // First, set up the parent context like do\in does + tempCtx := ctx + for { + // If it's parent is the same as current context is + // we set it's parent to our parent, skip our context + if tempCtx.Parent == ps.Ctx { + tempCtx.Parent = ps.Ctx.Parent + break + } + if tempCtx.Parent != nil { + tempCtx = tempCtx.Parent + } else { + break + } + } + + // Save current parent context + temp := ps.Ctx.Parent + // Set argument context as parent + ps.Ctx.Parent = ctx + + // Now evaluate the block with value injection like with does + EvalBlockInj(ps, value, true) + + MaybeDisplayFailureOrError(ps, ps.Idx, "enter") + + // Restore original parent context + ps.Ctx.Parent = temp + ps.Ser = ser + return ps.Res + + default: + return MakeArgError(ps, 3, []env.Type{env.BlockType}, "enter") + } + default: + return MakeArgError(ps, 2, []env.Type{env.ContextType}, "enter") + } + }, + }, } diff --git a/evaldo/builtins_base_printing.go b/evaldo/builtins_base_printing.go index c0c052c7..482a0e98 100644 --- a/evaldo/builtins_base_printing.go +++ b/evaldo/builtins_base_printing.go @@ -67,8 +67,8 @@ func displayRyeValue(ps *env.ProgramState, arg0 env.Object, interactive bool) (e if len(items) == 0 { return bloc, "" } - mdBlock := env.NewBlock(*env.NewTSeries(items)) - obj, esc := term.DisplayBlock(*mdBlock, ps.Idx) + convertedItems := convertMarkdownDisplayItems(items) + obj, esc := term.DisplayMarkdownItems(convertedItems, ps.Idx) if !esc { return obj, "" } @@ -77,8 +77,8 @@ func displayRyeValue(ps *env.ProgramState, arg0 env.Object, interactive bool) (e if len(items) == 0 { return bloc, "" } - mdBlock := env.NewBlock(*env.NewTSeries(items)) - obj, esc := term.DisplayBlock(*mdBlock, ps.Idx) + convertedItems := convertMarkdownDisplayItems(items) + obj, esc := term.DisplayMarkdownItems(convertedItems, ps.Idx) if !esc { return obj, "" } diff --git a/evaldo/builtins_console.go b/evaldo/builtins_console.go index c041e6b2..5a8e5d9c 100644 --- a/evaldo/builtins_console.go +++ b/evaldo/builtins_console.go @@ -55,7 +55,7 @@ var Builtins_console = map[string]*env.Builtin{ } } */ - DoRyeRepl(ps, "rye", ShowResults, false) + DoRyeRepl(ps, "rye", ShowResults, false, "") fmt.Println("-------------------------------------------------------------") ps.Ser = ser return ps.Res diff --git a/evaldo/builtins_encoding.go b/evaldo/builtins_encoding.go new file mode 100644 index 00000000..d5f62e0c --- /dev/null +++ b/evaldo/builtins_encoding.go @@ -0,0 +1,420 @@ +package evaldo + +import ( + "encoding/hex" + "strings" + "unicode" + + "golang.org/x/text/encoding" + "golang.org/x/text/encoding/charmap" + + "github.com/refaktor/rye/env" +) + +var Builtins_encoding = map[string]*env.Builtin{ + + // + // ##### Encoding and Text Processing ##### "Functions for encoding/decoding text and hex strings" + // + + // Tests: + // equal { "48656c6c6f20576f726c64" |hex\\decode-string } "Hello World" + // equal { "48656c6c6f20576f726c64" |hex\\decode-string |type? } 'string + // equal { "invalid" |hex\\decode-string |disarm |type? } 'error + // Args: + // * hex-string: hexadecimal string to decode + // Returns: + // * string containing the decoded bytes as a UTF-8 string + "decode\\hex-string": { + Argsn: 1, + Doc: "Decodes a hexadecimal string to a UTF-8 string.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch hexStr := arg0.(type) { + case env.String: + decoded, err := hex.DecodeString(hexStr.Value) + if err != nil { + ps.FailureFlag = true + return MakeBuiltinError(ps, "Failed to decode hex string: "+err.Error(), "hex\\decode-string") + } + return *env.NewString(string(decoded)) + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.StringType}, "hex\\decode-string") + } + }, + }, + + // Tests: + // equal { "Hello World" |hex\\encode-string } "48656c6c6f20576f726c64" + // equal { "Hello World" |hex\\encode-string |type? } 'string + // equal { "" |hex\\encode-string } "" + // Args: + // * string: string to encode + // Returns: + // * hexadecimal string representation + "encode\\hex-string": { + Argsn: 1, + Doc: "Encodes a string to its hexadecimal representation.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch str := arg0.(type) { + case env.String: + encoded := hex.EncodeToString([]byte(str.Value)) + return *env.NewString(encoded) + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.StringType}, "hex\\encode-string") + } + }, + }, + + // Tests: + // equal { " 70 44 01 4b \n 55 50 4e 51 " |hex\\clean-string } "7044014b55504e51" + // equal { "70 44 01 4b\t55 50 4e 51\r\n52 0a" |hex\\clean-string } "7044014b55504e51520a" + // equal { "7044014b55504e51520a" |hex\\clean-string } "7044014b55504e51520a" + // Args: + // * hex-string: raw hex string with possible whitespace + // Returns: + // * cleaned hex string with whitespace removed and even length + "clean\\hex-string": { + Argsn: 1, + Doc: "Removes all whitespace from a hex string and ensures even length.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch hexStr := arg0.(type) { + case env.String: + var sb strings.Builder + for _, r := range hexStr.Value { + if !unicode.IsSpace(r) { + sb.WriteRune(r) + } + } + + // Ensure even length for valid hex string + result := sb.String() + if len(result)%2 != 0 { + result = result[:len(result)-1] + } + return *env.NewString(result) + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.StringType}, "hex\\clean-string") + } + }, + }, + + // Tests: + // equal { charmap\\windows-1250 |type? } 'native + // equal { charmap\\windows-1250 |kind? } 'charmap-encoding + // Args: + // * none + // Returns: + // * Windows-1250 encoding as a native value + "charmap\\windows-1250": { + Argsn: 0, + Doc: "Returns the Windows-1250 (CP1250) character encoding for Central European languages.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return *env.NewNative(ps.Idx, charmap.Windows1250, "charmap-encoding") + }, + }, + + // Tests: + // equal { charmap\\iso-8859-1 |type? } 'native + // equal { charmap\\iso-8859-1 |kind? } 'charmap-encoding + // Args: + // * none + // Returns: + // * ISO-8859-1 encoding as a native value + "charmap\\iso-8859-1": { + Argsn: 0, + Doc: "Returns the ISO-8859-1 (Latin-1) character encoding.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return *env.NewNative(ps.Idx, charmap.ISO8859_1, "charmap-encoding") + }, + }, + + // Tests: + // equal { charmap\\iso-8859-2 |type? } 'native + // equal { charmap\\iso-8859-2 |kind? } 'charmap-encoding + // Args: + // * none + // Returns: + // * ISO-8859-2 encoding as a native value + "charmap\\iso-8859-2": { + Argsn: 0, + Doc: "Returns the ISO-8859-2 (Latin-2) character encoding for Central European languages.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return *env.NewNative(ps.Idx, charmap.ISO8859_2, "charmap-encoding") + }, + }, + + // Tests: + // equal { charmap\\windows-1252 |type? } 'native + // equal { charmap\\windows-1252 |kind? } 'charmap-encoding + // Args: + // * none + // Returns: + // * Windows-1252 encoding as a native value + "charmap\\windows-1252": { + Argsn: 0, + Doc: "Returns the Windows-1252 character encoding for Western European languages.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return *env.NewNative(ps.Idx, charmap.Windows1252, "charmap-encoding") + }, + }, + + // Tests: + // equal { charmap\\code-page-437 |type? } 'native + // equal { charmap\\code-page-437 |kind? } 'charmap-encoding + // Args: + // * none + // Returns: + // * Code Page 437 encoding as a native value + "charmap\\code-page-437": { + Argsn: 0, + Doc: "Returns the Code Page 437 (DOS/IBM PC) character encoding.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return *env.NewNative(ps.Idx, charmap.CodePage437, "charmap-encoding") + }, + }, + + // Tests: + // equal { charmap\\code-page-850 |type? } 'native + // equal { charmap\\code-page-850 |kind? } 'charmap-encoding + // Args: + // * none + // Returns: + // * Code Page 850 encoding as a native value + "charmap\\code-page-850": { + Argsn: 0, + Doc: "Returns the Code Page 850 (DOS Latin-1) character encoding.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return *env.NewNative(ps.Idx, charmap.CodePage850, "charmap-encoding") + }, + }, + + // Tests: + // equal { charmap\\koi8r |type? } 'native + // equal { charmap\\koi8r |kind? } 'charmap-encoding + // Args: + // * none + // Returns: + // * KOI8-R encoding as a native value + "charmap\\koi8r": { + Argsn: 0, + Doc: "Returns the KOI8-R character encoding for Russian.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return *env.NewNative(ps.Idx, charmap.KOI8R, "charmap-encoding") + }, + }, + + // Tests: + // equal { charmap\\windows-1250 |charmap-encoding\\new-decoder |type? } 'native + // equal { charmap\\windows-1250 |charmap-encoding\\new-decoder |kind? } 'text-decoder + // Args: + // * encoding: charmap encoding as a native value + // Returns: + // * text decoder as a native value + "charmap-encoding//Decoder": { + Argsn: 1, + Doc: "Creates a new text decoder for the given character encoding.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch encoding := arg0.(type) { + case env.Native: + if ps.Idx.GetWord(encoding.GetKind()) != "charmap-encoding" { + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "charmap-encoding\\new-decoder") + } + enc := encoding.Value.(*charmap.Charmap) + decoder := enc.NewDecoder() + return *env.NewNative(ps.Idx, decoder, "text-decoder") + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "charmap-encoding\\new-decoder") + } + }, + }, + + // Tests: + // equal { charmap\\windows-1250 |charmap-encoding\\new-encoder |type? } 'native + // equal { charmap\\windows-1250 |charmap-encoding\\new-encoder |kind? } 'text-encoder + // Args: + // * encoding: charmap encoding as a native value + // Returns: + // * text encoder as a native value + "charmap-encoding//Encoder": { + Argsn: 1, + Doc: "Creates a new text encoder for the given character encoding.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch encoding := arg0.(type) { + case env.Native: + if ps.Idx.GetWord(encoding.GetKind()) != "charmap-encoding" { + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "charmap-encoding\\new-encoder") + } + enc := encoding.Value.(*charmap.Charmap) + encoder := enc.NewEncoder() + return *env.NewNative(ps.Idx, encoder, "text-encoder") + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "charmap-encoding\\new-encoder") + } + }, + }, + + // Tests: + // equal { charmap\\windows-1250 |charmap-encoding\\new-decoder "Plačilo računa" |text-decoder\\string |contains? "č" } 1 + // equal { charmap\\windows-1250 |charmap-encoding\\new-decoder "Hello" |text-decoder\\string } "Hello" + // equal { charmap\\windows-1250 |charmap-encoding\\new-decoder "" |text-decoder\\string } "" + // Args: + // * decoder: text decoder as a native value + // * input: string to decode + // Returns: + // * decoded string + "text-decoder//Decode": { + Argsn: 2, + Doc: "Decodes a string using the given text decoder.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch decoder := arg0.(type) { + case env.Native: + if ps.Idx.GetWord(decoder.GetKind()) != "text-decoder" { + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "text-decoder\\string") + } + switch input := arg1.(type) { + case env.String: + dec := decoder.Value.(*encoding.Decoder) + decoded, err := dec.String(input.Value) + if err != nil { + ps.FailureFlag = true + return MakeBuiltinError(ps, "Failed to decode string: "+err.Error(), "text-decoder\\string") + } + return *env.NewString(decoded) + default: + ps.FailureFlag = true + return MakeArgError(ps, 2, []env.Type{env.StringType}, "text-decoder\\string") + } + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "text-decoder\\string") + } + }, + }, + + // Tests: + // equal { charmap\\windows-1250 |charmap-encoding\\new-encoder "Hello" |text-encoder\\string } "Hello" + // equal { charmap\\windows-1250 |charmap-encoding\\new-encoder "Plačilo" |text-encoder\\string |hex\\encode-string } "506c61e8696c6f" + // Args: + // * encoder: text encoder as a native value + // * input: string to encode + // Returns: + // * encoded string + "text-encoder//Encode": { + Argsn: 2, + Doc: "Encodes a string using the given text encoder.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch encoder := arg0.(type) { + case env.Native: + if ps.Idx.GetWord(encoder.GetKind()) != "text-encoder" { + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "text-encoder\\string") + } + switch input := arg1.(type) { + case env.String: + enc := encoder.Value.(*encoding.Encoder) + encoded, err := enc.String(input.Value) + if err != nil { + ps.FailureFlag = true + return MakeBuiltinError(ps, "Failed to encode string: "+err.Error(), "text-encoder\\string") + } + return *env.NewString(encoded) + default: + ps.FailureFlag = true + return MakeArgError(ps, 2, []env.Type{env.StringType}, "text-encoder\\string") + } + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "text-encoder\\string") + } + }, + }, + + // Tests: + // equal { " " |is-space? } 1 + // equal { "\t" |is-space? } 1 + // equal { "\n" |is-space? } 1 + // equal { "\r" |is-space? } 1 + // equal { "a" |is-space? } 0 + // equal { "A" |is-space? } 0 + // equal { "1" |is-space? } 0 + // Args: + // * input: string to check (should be single character) + // Returns: + // * integer 1 if character is whitespace, 0 otherwise + "is-space?": { + Argsn: 1, + Doc: "Checks if a single character string is a whitespace character.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch str := arg0.(type) { + case env.String: + if len(str.Value) == 0 { + return *env.NewInteger(0) + } + // Check first rune of the string + for _, r := range str.Value { + if unicode.IsSpace(r) { + return *env.NewInteger(1) + } + return *env.NewInteger(0) + } + return *env.NewInteger(0) + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.StringType}, "is-space?") + } + }, + }, + + // Tests: + // equal { "Hello World" |remove-all-space } "HelloWorld" + // equal { " Hello World " |remove-all-space } "HelloWorld" + // equal { "\t\n Hello \r\n World \t" |remove-all-space } "HelloWorld" + // equal { "" |remove-all-space } "" + // Args: + // * input: string to process + // Returns: + // * string with all whitespace characters removed + "remove-all-space": { + Argsn: 1, + Doc: "Removes all whitespace characters from a string.", + Pure: true, + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch str := arg0.(type) { + case env.String: + var sb strings.Builder + for _, r := range str.Value { + if !unicode.IsSpace(r) { + sb.WriteRune(r) + } + } + return *env.NewString(sb.String()) + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.StringType}, "remove-all-space") + } + }, + }, +} diff --git a/evaldo/builtins_error_handling.go b/evaldo/builtins_error_handling.go index 35e1207b..d5c6b489 100644 --- a/evaldo/builtins_error_handling.go +++ b/evaldo/builtins_error_handling.go @@ -80,6 +80,22 @@ var ErrorCreationBuiltins = map[string]*env.Builtin{ }, }, + // Tests: + // equal { failure "error message" |type? } 'error + // equal { failure "error message" |message? } "error message" + // equal { failure 404 |status? } 404 + // Args: + // * error_info: String message, Integer code, or block for multiple parameters + // Returns: + // * error object without setting any flags + "empty": { + Argsn: 0, + Doc: "Creates an 404 error object without setting any flags.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return MakeRyeError(ps, *env.NewInteger(404), nil) + }, + }, + // Tests: // equal { failure\wrap "outer error" failure "inner error" |message? } "outer error" // equal { failure\wrap "outer error" failure "inner error" |type? } 'error diff --git a/evaldo/builtins_io.go b/evaldo/builtins_io.go index deac5883..93e44cd6 100755 --- a/evaldo/builtins_io.go +++ b/evaldo/builtins_io.go @@ -7,6 +7,7 @@ import ( "bufio" "fmt" "io" + "net" "os" "path/filepath" "strings" @@ -1831,4 +1832,119 @@ var Builtins_io = map[string]*env.Builtin{ } }, }, + + // + // ##### Unix Domain Socket Operations ##### "Unix domain socket operations for IPC" + // + // Args: + // * path: uri representing the Unix socket path to connect to + // Returns: + // * native unix-connection object + "unix-uri//Open": { + Argsn: 1, + Doc: "Opens a connection to a Unix domain socket.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch s := arg0.(type) { + case env.Uri: + conn, err := net.Dial("unix", s.Path) + if err != nil { + ps.FailureFlag = true + return MakeBuiltinError(ps, "Error connecting to Unix socket: "+err.Error(), "unix-uri//Open") + } + return *env.NewNative(ps.Idx, conn, "unix-connection") + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.UriType}, "unix-uri//Open") + } + }, + }, + + // Args: + // * connection: native unix-connection object + // * data: string to write to the socket + // Returns: + // * the connection object if successful (allows chaining) + "unix-connection//Write": { + Argsn: 2, + Doc: "Writes data to a Unix domain socket connection.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch conn := arg0.(type) { + case env.Native: + if ps.Idx.GetWord(conn.GetKind()) != "unix-connection" { + ps.FailureFlag = true + return MakeBuiltinError(ps, "Expected unix-connection object", "unix-connection//Write") + } + switch data := arg1.(type) { + case env.String: + _, err := conn.Value.(net.Conn).Write([]byte(data.Value)) + if err != nil { + ps.FailureFlag = true + return MakeBuiltinError(ps, "Error writing to Unix socket: "+err.Error(), "unix-connection//Write") + } + return arg0 // Return the connection for chaining + default: + ps.FailureFlag = true + return MakeArgError(ps, 2, []env.Type{env.StringType}, "unix-connection//Write") + } + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "unix-connection//Write") + } + }, + }, + + // Args: + // * connection: native unix-connection object + // Returns: + // * string containing data read from the socket + "unix-connection//Read": { + Argsn: 1, + Doc: "Reads data from a Unix domain socket connection.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch conn := arg0.(type) { + case env.Native: + if ps.Idx.GetWord(conn.GetKind()) != "unix-connection" { + ps.FailureFlag = true + return MakeBuiltinError(ps, "Expected unix-connection object", "unix-connection//Read") + } + buf := make([]byte, 1024) + n, err := conn.Value.(net.Conn).Read(buf) + if err != nil { + ps.FailureFlag = true + return MakeBuiltinError(ps, "Error reading from Unix socket: "+err.Error(), "unix-connection//Read") + } + return *env.NewString(string(buf[:n])) + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "unix-connection//Read") + } + }, + }, + + // Args: + // * connection: native unix-connection object + // Returns: + // * empty string if successful + "unix-connection//Close": { + Argsn: 1, + Doc: "Closes a Unix domain socket connection.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch conn := arg0.(type) { + case env.Native: + if ps.Idx.GetWord(conn.GetKind()) != "unix-connection" { + ps.FailureFlag = true + return MakeBuiltinError(ps, "Expected unix-connection object", "unix-connection//Close") + } + err := conn.Value.(net.Conn).Close() + if err != nil { + ps.FailureFlag = true + return MakeBuiltinError(ps, err.Error(), "unix-connection//Close") + } + return *env.NewString("") + default: + ps.FailureFlag = true + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "unix-connection//Close") + } + }, + }, } diff --git a/evaldo/builtins_js_interop.go b/evaldo/builtins_js_interop.go index a407f3fb..efd131b1 100644 --- a/evaldo/builtins_js_interop.go +++ b/evaldo/builtins_js_interop.go @@ -4,6 +4,8 @@ package evaldo import ( + "regexp" + "runtime" "syscall/js" // "encoding/json" @@ -11,6 +13,14 @@ import ( "github.com/refaktor/rye/env" ) +// ANSI escape code regex for stripping in browser console output +var ansiRegexWASM = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) + +// stripAnsiCodes removes ANSI escape sequences from a string +func stripAnsiCodes(s string) string { + return ansiRegexWASM.ReplaceAllString(s, "") +} + // JavaScript interop functions for Rye WASM var Builtins_js_interop = map[string]*env.Builtin{ @@ -360,16 +370,19 @@ var Builtins_js_interop = map[string]*env.Builtin{ // * the message string "js-log": { Argsn: 1, - Doc: "Logs a message to the browser console.", + Doc: "Logs a message to the browser console (ANSI codes are stripped).", Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { switch message := arg0.(type) { case env.String: - js.Global().Get("console").Call("log", "Rye: "+message.Value) + // Strip ANSI codes before logging to browser console + cleanMessage := stripAnsiCodes(message.Value) + js.Global().Get("console").Call("log", "Rye: "+cleanMessage) return message default: - // Convert other types to string + // Convert other types to string and strip ANSI codes str := arg0.Inspect(*ps.Idx) - js.Global().Get("console").Call("log", "Rye: "+str) + cleanStr := stripAnsiCodes(str) + js.Global().Get("console").Call("log", "Rye: "+cleanStr) return *env.NewString(str) } }, @@ -404,16 +417,19 @@ var Builtins_js_interop = map[string]*env.Builtin{ // * the message string "js-alert": { Argsn: 1, - Doc: "Shows a browser alert dialog with the given message.", + Doc: "Shows a browser alert dialog with the given message (ANSI codes are stripped).", Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { switch message := arg0.(type) { case env.String: - js.Global().Call("alert", message.Value) + // Strip ANSI codes before showing browser alert + cleanMessage := stripAnsiCodes(message.Value) + js.Global().Call("alert", cleanMessage) return message default: - // Convert other types to string + // Convert other types to string and strip ANSI codes str := arg0.Inspect(*ps.Idx) - js.Global().Call("alert", str) + cleanStr := stripAnsiCodes(str) + js.Global().Call("alert", cleanStr) return *env.NewString(str) } }, @@ -429,7 +445,7 @@ var Builtins_js_interop = map[string]*env.Builtin{ // * String containing the user input, or void if cancelled "js-prompt": { Argsn: -1, // Variable arguments (1 or 2) - Doc: "Shows a browser prompt dialog and returns user input.", + Doc: "Shows a browser prompt dialog and returns user input (ANSI codes are stripped from message).", Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { // Check if arg0 is nil (no arguments) if arg0 == nil { @@ -438,19 +454,24 @@ var Builtins_js_interop = map[string]*env.Builtin{ switch message := arg0.(type) { case env.String: + // Strip ANSI codes from the message + cleanMessage := stripAnsiCodes(message.Value) var result js.Value // Check if default value is provided (arg1 is not nil) if arg1 != nil { switch defaultVal := arg1.(type) { case env.String: - result = js.Global().Call("prompt", message.Value, defaultVal.Value) + // Strip ANSI codes from default value too + cleanDefault := stripAnsiCodes(defaultVal.Value) + result = js.Global().Call("prompt", cleanMessage, cleanDefault) default: - // Convert other types to string for default value + // Convert other types to string for default value and strip ANSI defaultStr := arg1.Inspect(*ps.Idx) - result = js.Global().Call("prompt", message.Value, defaultStr) + cleanDefault := stripAnsiCodes(defaultStr) + result = js.Global().Call("prompt", cleanMessage, cleanDefault) } } else { - result = js.Global().Call("prompt", message.Value) + result = js.Global().Call("prompt", cleanMessage) } // Check if user cancelled (returns null) @@ -459,14 +480,16 @@ var Builtins_js_interop = map[string]*env.Builtin{ } return *env.NewString(result.String()) default: - // Convert other types to string for message + // Convert other types to string for message and strip ANSI str := arg0.Inspect(*ps.Idx) + cleanStr := stripAnsiCodes(str) var result js.Value if arg1 != nil { defaultStr := arg1.Inspect(*ps.Idx) - result = js.Global().Call("prompt", str, defaultStr) + cleanDefault := stripAnsiCodes(defaultStr) + result = js.Global().Call("prompt", cleanStr, cleanDefault) } else { - result = js.Global().Call("prompt", str) + result = js.Global().Call("prompt", cleanStr) } if result.IsNull() { return env.NewVoid() @@ -590,6 +613,47 @@ var Builtins_js_interop = map[string]*env.Builtin{ } }, }, + + // Tests: + // js-sleep 100 + // js-sleep 1000 + // Args: + // * milliseconds: Integer number of milliseconds to sleep + // Returns: + // * Integer milliseconds actually slept + "js-sleep": { + Argsn: 1, + Doc: "Non-blocking sleep that yields control to JavaScript event loop, allowing async operations to complete.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch ms := arg0.(type) { + case env.Integer: + if ms.Value <= 0 { + return ms // Return immediately for zero or negative values + } + + // For WASM, we use a different approach - we just yield briefly + // and return immediately instead of trying to do actual timing + // This allows the JavaScript event loop to process callbacks + runtime.Gosched() + return ms + default: + return MakeArgError(ps, 1, []env.Type{env.IntegerType}, "js-sleep") + } + }, + }, + + // js-yield + // Yields control to JavaScript event loop without delay + // This is safer than js-sleep for avoiding deadlocks + "js-yield": { + Argsn: 0, + Doc: "Yields control to JavaScript event loop without delay.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + // Simple yield without any channel operations + runtime.Gosched() + return env.Void{} + }, + }, } // Helper function to convert Rye values to JavaScript values diff --git a/evaldo/builtins_markdown.go b/evaldo/builtins_markdown.go index f0cd4349..a6fbe876 100644 --- a/evaldo/builtins_markdown.go +++ b/evaldo/builtins_markdown.go @@ -625,7 +625,70 @@ func markdownParagraphText(node *ast.Paragraph, source []byte) string { return buf.String() } -func markdownDisplayItems(source string) []env.Object { +// wrapTextToWidth wraps text to fit within the specified width, properly handling line breaks +func wrapTextToWidth(text string, width int) []string { + if width <= 0 { + return []string{text} + } + + lines := strings.Split(text, "\n") + wrappedLines := make([]string, 0) + + for _, line := range lines { + if len(line) <= width { + wrappedLines = append(wrappedLines, line) + continue + } + + // Wrap long lines + for len(line) > width { + // Find the best break point (space or punctuation) + breakPoint := width + for i := width - 1; i >= width/2; i-- { + if line[i] == ' ' || line[i] == '\t' || line[i] == '-' { + breakPoint = i + 1 + break + } + } + + wrappedLines = append(wrappedLines, strings.TrimRight(line[:breakPoint], " \t")) + line = strings.TrimLeft(line[breakPoint:], " \t") + } + + if len(line) > 0 { + wrappedLines = append(wrappedLines, line) + } + } + + return wrappedLines +} + +// MarkdownDisplayItem represents a selectable markdown element with its type and content +type MarkdownDisplayItem struct { + Type string // "heading", "paragraph", "code", "list", "quote", "hr", "link" + Content string // Raw content + DisplayLines []string // Formatted lines for display + Level int // For headings (1-6), or 0 for others + Language string // For code blocks +} + +// convertMarkdownDisplayItems converts MarkdownDisplayItem slice to interface{} slice for DisplayMarkdownItems +func convertMarkdownDisplayItems(items []MarkdownDisplayItem) []interface{} { + converted := make([]interface{}, len(items)) + for i, item := range items { + itemMap := map[string]interface{}{ + "Type": item.Type, + "Content": item.Content, + "DisplayLines": item.DisplayLines, + "Level": item.Level, + "Language": item.Language, + } + converted[i] = itemMap + } + return converted +} + +func markdownDisplayItems(source string) []MarkdownDisplayItem { sourceBytes := []byte(source) md := goldmark.New( goldmark.WithExtensions( @@ -636,8 +699,17 @@ func markdownDisplayItems(source string) []env.Object { ), ) + // Get terminal width for proper text wrapping + size, err := term.GetTerminalSize() + width := size.Width + if err != nil || width <= 0 { + width = 80 // Fallback default + } + // Reserve some space for display margins and cursor + displayWidth := width - 4 + doc := md.Parser().Parse(text.NewReader(sourceBytes)) - items := make([]env.Object, 0) + items := make([]MarkdownDisplayItem, 0) ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { @@ -648,8 +720,21 @@ func markdownDisplayItems(source string) []env.Object { case *ast.Heading: headingText := strings.TrimSpace(string(node.Text(sourceBytes))) if headingText != "" { - label := term.StrBold() + "H" + strconv.Itoa(node.Level) + ": " + headingText + term.StrCloseProps() + "\n" - items = append(items, *env.NewString(label)) + fullHeading := "H" + strconv.Itoa(node.Level) + ": " + headingText + // Wrap heading if it's too long + wrappedLines := wrapTextToWidth(fullHeading, displayWidth) + displayLines := make([]string, len(wrappedLines)) + for i, line := range wrappedLines { + displayLines[i] = term.StrBold() + line + term.StrCloseProps() + } + + item := MarkdownDisplayItem{ + Type: "heading", + Content: headingText, + DisplayLines: displayLines, + Level: node.Level, + } + items = append(items, item) } case *ast.Paragraph: child := node.FirstChild() @@ -660,17 +745,174 @@ func markdownDisplayItems(source string) []env.Object { if linkText == "" { linkText = destination } - items = append(items, *env.NewString(fmt.Sprintf("Link: %s -> %s\n", linkText, destination))) + linkLine := fmt.Sprintf("Link: %s -> %s", linkText, destination) + // Wrap link if it's too long + wrappedLines := wrapTextToWidth(linkLine, displayWidth) + + item := MarkdownDisplayItem{ + Type: "link", + Content: destination, + DisplayLines: wrappedLines, + } + items = append(items, item) return ast.WalkSkipChildren, nil } } paragraph := markdownParagraphText(node, sourceBytes) if paragraph == "" { - items = append(items, *env.NewString("\n")) + item := MarkdownDisplayItem{ + Type: "paragraph", + Content: "", + DisplayLines: []string{""}, + } + items = append(items, item) return ast.WalkContinue, nil } - items = append(items, *env.NewString(paragraph + "\n")) + + // Remove trailing newline for wrapping + paragraph = strings.TrimRight(paragraph, "\n") + + // Wrap paragraph text to fit terminal width + wrappedLines := wrapTextToWidth(paragraph, displayWidth) + + item := MarkdownDisplayItem{ + Type: "paragraph", + Content: paragraph, + DisplayLines: wrappedLines, + } + items = append(items, item) + case *ast.CodeBlock, *ast.FencedCodeBlock: + // Handle code blocks + var content string + var language string + if fenced, ok := node.(*ast.FencedCodeBlock); ok { + language = string(fenced.Language(sourceBytes)) + var buf bytes.Buffer + for i := 0; i < fenced.Lines().Len(); i++ { + line := fenced.Lines().At(i) + buf.Write(line.Value(sourceBytes)) + } + content = buf.String() + } else if codeBlock, ok := node.(*ast.CodeBlock); ok { + var buf bytes.Buffer + for i := 0; i < codeBlock.Lines().Len(); i++ { + line := codeBlock.Lines().At(i) + buf.Write(line.Value(sourceBytes)) + } + content = buf.String() + } + + if content != "" { + codeLines := strings.Split(strings.TrimRight(content, "\n"), "\n") + displayLines := make([]string, len(codeLines)) + for i, line := range codeLines { + // Don't wrap code lines, just truncate if too long + if len(line) > displayWidth { + line = line[:displayWidth-3] + "..." + } + displayLines[i] = " " + line // Indent code + } + + item := MarkdownDisplayItem{ + Type: "code", + Content: content, + DisplayLines: displayLines, + Language: language, + } + items = append(items, item) + } + case *ast.List: + // Handle lists as complete blocks - collect all list items + var listItems []string + var listContent strings.Builder + + for c := node.FirstChild(); c != nil; c = c.NextSibling() { + if listItem, ok := c.(*ast.ListItem); ok { + var itemText bytes.Buffer + for ic := listItem.FirstChild(); ic != nil; ic = ic.NextSibling() { + if para, ok := ic.(*ast.Paragraph); ok { + for pc := para.FirstChild(); pc != nil; pc = pc.NextSibling() { + if text, ok := pc.(*ast.Text); ok { + itemText.Write(text.Text(sourceBytes)) + } + } + } + } + + if itemText.Len() > 0 { + itemContent := itemText.String() + listItems = append(listItems, itemContent) + listContent.WriteString("• " + itemContent + "\n") + } + } + } + + if len(listItems) > 0 { + displayLines := make([]string, 0) + for _, itemContent := range listItems { + prefix := "• " + itemWidth := displayWidth - len(prefix) + wrappedLines := wrapTextToWidth(itemContent, itemWidth) + + for i, line := range wrappedLines { + if i == 0 { + displayLines = append(displayLines, prefix+line) + } else { + displayLines = append(displayLines, " "+line) + } + } + } + + item := MarkdownDisplayItem{ + Type: "list", + Content: strings.TrimRight(listContent.String(), "\n"), + DisplayLines: displayLines, + } + items = append(items, item) + } + + return ast.WalkSkipChildren, nil + case *ast.Blockquote: + // Handle blockquotes + var buf bytes.Buffer + for c := node.FirstChild(); c != nil; c = c.NextSibling() { + if para, ok := c.(*ast.Paragraph); ok { + for pc := para.FirstChild(); pc != nil; pc = pc.NextSibling() { + if text, ok := pc.(*ast.Text); ok { + buf.Write(text.Text(sourceBytes)) + } + } + } + } + + if buf.Len() > 0 { + quoteText := strings.TrimSpace(buf.String()) + prefix := "> " + quoteWidth := displayWidth - len(prefix) + wrappedLines := wrapTextToWidth(quoteText, quoteWidth) + + displayLines := make([]string, len(wrappedLines)) + for i, line := range wrappedLines { + displayLines[i] = prefix + line + } + + item := MarkdownDisplayItem{ + Type: "quote", + Content: quoteText, + DisplayLines: displayLines, + } + items = append(items, item) + } + case *ast.ThematicBreak: + // Handle horizontal rules + ruler := strings.Repeat("─", min(displayWidth, 40)) + item := MarkdownDisplayItem{ + Type: "hr", + Content: "---", + DisplayLines: []string{ruler}, + } + items = append(items, item) } return ast.WalkContinue, nil @@ -679,6 +921,14 @@ func markdownDisplayItems(source string) []env.Object { return items } +// Helper function for min +func min(a, b int) int { + if a < b { + return a + } + return b +} + var Builtins_markdown = map[string]*env.Builtin{ // diff --git a/evaldo/builtins_matrix.go b/evaldo/builtins_matrix.go index 11520c09..81ba6668 100644 --- a/evaldo/builtins_matrix.go +++ b/evaldo/builtins_matrix.go @@ -1,6 +1,7 @@ package evaldo import ( + "fmt" "math" "math/rand" @@ -280,7 +281,7 @@ var Builtins_matrix = map[string]*env.Builtin{ r := int(row.Value) c := int(col.Value) if r < 0 || r >= m.Rows || c < 0 || c >= m.Cols { - return MakeBuiltinError(ps, "Index out of bounds", "mat-get") + return MakeBuiltinError(ps, fmt.Sprintf("Matrix indices out of bounds: row %d, col %d (matrix is %dx%d)", r, c, m.Rows, m.Cols), "mat-get") } return *env.NewDecimal(m.Get(r, c)) default: @@ -317,7 +318,7 @@ var Builtins_matrix = map[string]*env.Builtin{ r := int(row.Value) c := int(col.Value) if r < 0 || r >= m.Rows || c < 0 || c >= m.Cols { - return MakeBuiltinError(ps, "Index out of bounds", "mat-set!") + return MakeBuiltinError(ps, fmt.Sprintf("Matrix indices out of bounds: row %d, col %d (matrix is %dx%d)", r, c, m.Rows, m.Cols), "mat-set!") } var val float64 switch v := arg3.(type) { @@ -359,7 +360,7 @@ var Builtins_matrix = map[string]*env.Builtin{ case env.Integer: r := int(row.Value) if r < 0 || r >= m.Rows { - return MakeBuiltinError(ps, "Row index out of bounds", "mat-row") + return MakeBuiltinError(ps, fmt.Sprintf("Row index %d out of bounds (matrix has %d rows)", r, m.Rows), "mat-row") } data := make(govector.Vector, m.Cols) for j := 0; j < m.Cols; j++ { @@ -392,7 +393,7 @@ var Builtins_matrix = map[string]*env.Builtin{ case env.Integer: c := int(col.Value) if c < 0 || c >= m.Cols { - return MakeBuiltinError(ps, "Column index out of bounds", "mat-col") + return MakeBuiltinError(ps, fmt.Sprintf("Column index %d out of bounds (matrix has %d columns)", c, m.Cols), "mat-col") } data := make(govector.Vector, m.Rows) for i := 0; i < m.Rows; i++ { @@ -975,7 +976,7 @@ var Builtins_matrix = map[string]*env.Builtin{ case env.Integer: c := int(col.Value) if c < 0 || c >= m.Cols { - return MakeBuiltinError(ps, "Column index out of bounds", "mat-set-col!") + return MakeBuiltinError(ps, fmt.Sprintf("Column index %d out of bounds (matrix has %d columns)", c, m.Cols), "mat-set-col!") } switch vals := arg2.(type) { case env.Vector: diff --git a/evaldo/builtins_os.go b/evaldo/builtins_os.go index 3a1a1b91..5dbf8b43 100644 --- a/evaldo/builtins_os.go +++ b/evaldo/builtins_os.go @@ -476,45 +476,243 @@ var Builtins_os = map[string]*env.Builtin{ }, // Args: - // * filter: word 'dirs' or 'files' to filter by type, string for partial name matching, or regexp to match names + // * filter: file-uri to list directory, word 'dirs' or 'files' to filter by type, string for partial name matching, or regexp to match names // Returns: - // * block of uris representing filtered files or directories in the current directory + // * block of uris representing filtered files or directories in the specified directory or current directory "ls\\": { Argsn: 1, - Doc: "Lists files or directories in the current directory based on filter. Use 'dirs' for directories only, 'files' for files only, a string for partial name matching, or a regexp to match names.", + Doc: "Lists files or directories with absolute paths when possible. If argument is a file-uri, lists that directory (or returns failure if not a directory). Otherwise filters current directory: 'dirs' for directories only, 'files' for files only, a string for partial name matching, or a regexp to match names.", Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { - files, err := os.ReadDir(".") + var targetDir string = "." + var doFiltering bool = true + var filterArg env.Object = arg0 + + // Check if argument is a file-uri + if uri, ok := arg0.(env.Uri); ok { + targetPath := resolvePath(ps.WorkingPath, uri.GetPath()) + + // Check if path exists and is a directory + info, err := os.Stat(targetPath) + if err != nil { + if os.IsNotExist(err) { + return MakeBuiltinError(ps, fmt.Sprintf("path doesn't exist: %s", uri.GetPath()), "ls\\") + } + if os.IsPermission(err) { + return MakeBuiltinError(ps, fmt.Sprintf("permission denied: %s", uri.GetPath()), "ls\\") + } + return MakeBuiltinError(ps, fmt.Sprintf("error accessing path %s: %s", uri.GetPath(), err.Error()), "ls\\") + } + + if !info.IsDir() { + return MakeBuiltinError(ps, fmt.Sprintf("not a directory (is file): %s", uri.GetPath()), "ls\\") + } + + targetDir = targetPath + doFiltering = false + } + + files, err := os.ReadDir(targetDir) if err != nil { return MakeBuiltinError(ps, "Error reading directory:"+err.Error(), "ls\\") } var items []env.Object - switch filterArg := arg0.(type) { - case env.Word: - filter := ps.Idx.GetWord(filterArg.Index) - if filter != "dirs" && filter != "files" { - return MakeBuiltinError(ps, "Word filter must be 'dirs' or 'files'", "ls\\") + if !doFiltering { + // If we're listing a specific directory, return all files with absolute paths + for _, file := range files { + absPath, err := filepath.Abs(filepath.Join(targetDir, file.Name())) + if err != nil { + // Fallback to relative path if absolute path fails + items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) + } else { + items = append(items, *env.NewUri1(ps.Idx, "file://"+absPath)) + } + } + } else { + // Apply filtering based on the argument type + switch filterArg := filterArg.(type) { + case env.Word: + filter := ps.Idx.GetWord(filterArg.Index) + if filter != "dirs" && filter != "files" { + return MakeBuiltinError(ps, "Word filter must be 'dirs' or 'files'", "ls\\") + } + + for _, file := range files { + include := false + if filter == "dirs" { + include = file.IsDir() + } else if filter == "files" { + include = !file.IsDir() + } + + if include { + absPath, err := filepath.Abs(filepath.Join(targetDir, file.Name())) + if err != nil { + // Fallback to relative path if absolute path fails + items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) + } else { + items = append(items, *env.NewUri1(ps.Idx, "file://"+absPath)) + } + } + } + + case env.String: + // String does partial matching on file/directory names + pattern := filterArg.Value + for _, file := range files { + if strings.Contains(file.Name(), pattern) { + absPath, err := filepath.Abs(filepath.Join(targetDir, file.Name())) + if err != nil { + // Fallback to relative path if absolute path fails + items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) + } else { + items = append(items, *env.NewUri1(ps.Idx, "file://"+absPath)) + } + } + } + + case env.Native: + // Check if it's a regexp + if ps.Idx.GetWord(filterArg.Kind.Index) != "regexp" { + return MakeBuiltinError(ps, "Native object must be a regexp", "ls\\") + } + + regex := filterArg.Value.(*regexp.Regexp) + for _, file := range files { + if regex.MatchString(file.Name()) { + absPath, err := filepath.Abs(filepath.Join(targetDir, file.Name())) + if err != nil { + // Fallback to relative path if absolute path fails + items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) + } else { + items = append(items, *env.NewUri1(ps.Idx, "file://"+absPath)) + } + } + } + + default: + return MakeArgError(ps, 1, []env.Type{env.UriType, env.WordType, env.StringType, env.NativeType}, "ls\\") + } + } + + return *env.NewBlock(*env.NewTSeries(items)) + }, + }, + + // Args: + // * none + // Returns: + // * block of uris representing directories in the current directory + "ls\\dirs": { + Argsn: 0, + Doc: "Lists only directories in the current directory.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + files, err := os.ReadDir(".") + if err != nil { + return MakeBuiltinError(ps, "Error reading directory:"+err.Error(), "ls\\dirs") + } + + var items []env.Object + for _, file := range files { + if file.IsDir() { + items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) + } + } + return *env.NewBlock(*env.NewTSeries(items)) + }, + }, + + // Args: + // * none + // Returns: + // * block of uris representing files (non-directories) in the current directory + "ls\\files": { + Argsn: 0, + Doc: "Lists only files (non-directories) in the current directory.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + files, err := os.ReadDir(".") + if err != nil { + return MakeBuiltinError(ps, "Error reading directory:"+err.Error(), "ls\\files") + } + + var items []env.Object + for _, file := range files { + if !file.IsDir() { + items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) } + } + return *env.NewBlock(*env.NewTSeries(items)) + }, + }, + + // Args: + // * filter: string for partial name matching, or regexp to match names + // Returns: + // * block of uris representing filtered directories in the current directory + "ls\\dirs\\": { + Argsn: 1, + Doc: "Lists directories in the current directory with filtering. Use a string for partial name matching, or a regexp to match names.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + files, err := os.ReadDir(".") + if err != nil { + return MakeBuiltinError(ps, "Error reading directory:"+err.Error(), "ls\\dirs\\") + } + var items []env.Object + + switch filterArg := arg0.(type) { + case env.String: + // String does partial matching on directory names + pattern := filterArg.Value for _, file := range files { - include := false - if filter == "dirs" { - include = file.IsDir() - } else if filter == "files" { - include = !file.IsDir() + if file.IsDir() && strings.Contains(file.Name(), pattern) { + items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) } + } - if include { + case env.Native: + // Check if it's a regexp + if ps.Idx.GetWord(filterArg.Kind.Index) != "regexp" { + return MakeBuiltinError(ps, "Native object must be a regexp", "ls\\dirs\\") + } + + regex := filterArg.Value.(*regexp.Regexp) + for _, file := range files { + if file.IsDir() && regex.MatchString(file.Name()) { items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) } } + default: + return MakeArgError(ps, 1, []env.Type{env.StringType, env.NativeType}, "ls\\dirs\\") + } + + return *env.NewBlock(*env.NewTSeries(items)) + }, + }, + + // Args: + // * filter: string for partial name matching, or regexp to match names + // Returns: + // * block of uris representing filtered files (non-directories) in the current directory + "ls\\files\\": { + Argsn: 1, + Doc: "Lists files (non-directories) in the current directory with filtering. Use a string for partial name matching, or a regexp to match names.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + files, err := os.ReadDir(".") + if err != nil { + return MakeBuiltinError(ps, "Error reading directory:"+err.Error(), "ls\\files\\") + } + + var items []env.Object + + switch filterArg := arg0.(type) { case env.String: - // String does partial matching on file/directory names + // String does partial matching on file names pattern := filterArg.Value for _, file := range files { - if strings.Contains(file.Name(), pattern) { + if !file.IsDir() && strings.Contains(file.Name(), pattern) { items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) } } @@ -522,24 +720,340 @@ var Builtins_os = map[string]*env.Builtin{ case env.Native: // Check if it's a regexp if ps.Idx.GetWord(filterArg.Kind.Index) != "regexp" { - return MakeBuiltinError(ps, "Native object must be a regexp", "ls\\") + return MakeBuiltinError(ps, "Native object must be a regexp", "ls\\files\\") } regex := filterArg.Value.(*regexp.Regexp) for _, file := range files { - if regex.MatchString(file.Name()) { + if !file.IsDir() && regex.MatchString(file.Name()) { items = append(items, *env.NewUri1(ps.Idx, "file://"+file.Name())) } } default: - return MakeArgError(ps, 1, []env.Type{env.WordType, env.StringType, env.NativeType}, "ls\\") + return MakeArgError(ps, 1, []env.Type{env.StringType, env.NativeType}, "ls\\files\\") } return *env.NewBlock(*env.NewTSeries(items)) }, }, + // Args: + // * path: uri representing source path + // * link: uri representing link path to create + // Returns: + // * link uri if successful + // Tags: #file #link + "symlink": { + Argsn: 2, + Doc: "Creates a symbolic link.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch src := arg0.(type) { + case env.Uri: + switch link := arg1.(type) { + case env.Uri: + srcPath := resolvePath(ps.WorkingPath, src.GetPath()) + linkPath := resolvePath(ps.WorkingPath, link.GetPath()) + err := os.Symlink(srcPath, linkPath) + if err != nil { + return MakeBuiltinError(ps, "Error creating symlink: "+err.Error(), "symlink") + } + return arg1 + default: + return MakeArgError(ps, 2, []env.Type{env.UriType}, "symlink") + } + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "symlink") + } + }, + }, + + // Args: + // * path: uri representing a path to check + // Returns: + // * boolean: true if path is a symbolic link + // Tags: #file #check + "is-symlink?": { + Argsn: 1, + Doc: "Checks if a path is a symbolic link.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + filePath := resolvePath(ps.WorkingPath, path.GetPath()) + info, err := os.Lstat(filePath) // Use Lstat to not follow symlinks + if err != nil { + if os.IsNotExist(err) { + return *env.NewBoolean(false) + } + return MakeBuiltinError(ps, "Error checking path: "+err.Error(), "is-symlink?") + } + return *env.NewBoolean(info.Mode()&os.ModeSymlink != 0) + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "is-symlink?") + } + }, + }, + + // Args: + // * path: uri representing a symbolic link + // Returns: + // * uri representing the target of the symlink + // Tags: #file #link + "readlink": { + Argsn: 1, + Doc: "Reads the target of a symbolic link.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + linkPath := resolvePath(ps.WorkingPath, path.GetPath()) + target, err := os.Readlink(linkPath) + if err != nil { + return MakeBuiltinError(ps, "Error reading symlink: "+err.Error(), "readlink") + } + return *env.NewUri1(ps.Idx, "file://"+target) + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "readlink") + } + }, + }, + + // Args: + // * path: uri representing file or directory path + // * mode: integer representing file mode (e.g., 0755) + // Returns: + // * path uri if successful + // Tags: #file #permissions + "chmod": { + Argsn: 2, + Doc: "Changes file or directory permissions.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + switch mode := arg1.(type) { + case env.Integer: + filePath := resolvePath(ps.WorkingPath, path.GetPath()) + err := os.Chmod(filePath, os.FileMode(mode.Value)) + if err != nil { + return MakeBuiltinError(ps, "Error changing permissions: "+err.Error(), "chmod") + } + return arg0 + default: + return MakeArgError(ps, 2, []env.Type{env.IntegerType}, "chmod") + } + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "chmod") + } + }, + }, + + // Args: + // * path: uri representing file or directory + // Returns: + // * string representing the absolute path + // Tags: #file #path + "abs-path": { + Argsn: 1, + Doc: "Returns the absolute path for a given path.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + absPath, err := filepath.Abs(resolvePath(ps.WorkingPath, path.GetPath())) + if err != nil { + return MakeBuiltinError(ps, "Error getting absolute path: "+err.Error(), "abs-path") + } + return *env.NewUri1(ps.Idx, "file://"+absPath) + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "abs-path") + } + }, + }, + + // Args: + // * path: uri representing a path + // Returns: + // * uri representing the directory containing the path + // Tags: #file #path + "dirname": { + Argsn: 1, + Doc: "Returns the directory part of a path.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + dir := filepath.Dir(path.GetPath()) + return *env.NewUri1(ps.Idx, "file://"+dir) + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "dirname") + } + }, + }, + + // Args: + // * path: uri representing a path + // Returns: + // * string representing the filename part of the path + // Tags: #file #path + "basename": { + Argsn: 1, + Doc: "Returns the filename part of a path.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + base := filepath.Base(path.GetPath()) + return *env.NewString(base) + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "basename") + } + }, + }, + + // Args: + // * path: uri representing a file path + // Returns: + // * string representing the file extension (including the dot) + // Tags: #file #path + "file-ext": { + Argsn: 1, + Doc: "Returns the file extension of a path (including the dot).", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + ext := filepath.Ext(path.GetPath()) + return *env.NewString(ext) + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "file-ext") + } + }, + }, + + // Args: + // * paths: uri or block of uris to join + // Returns: + // * uri representing the joined path + // Tags: #file #path + "join-path": { + Argsn: 1, + Doc: "Joins path elements into a single path.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch paths := arg0.(type) { + case env.Block: + pathStrs := make([]string, paths.Series.Len()) + for i := 0; i < paths.Series.Len(); i++ { + item := paths.Series.Get(i) + switch p := item.(type) { + case env.Uri: + pathStrs[i] = p.GetPath() + case env.String: + pathStrs[i] = p.Value + default: + return MakeBuiltinError(ps, "Block must contain only uris or strings", "join-path") + } + } + joined := filepath.Join(pathStrs...) + return *env.NewUri1(ps.Idx, "file://"+joined) + case env.Uri: + // If single uri passed, just return it + return arg0 + default: + return MakeArgError(ps, 1, []env.Type{env.UriType, env.BlockType}, "join-path") + } + }, + }, + + // Args: + // * none + // Returns: + // * string representing the current user's username + // Tags: #system #user + "whoami": { + Argsn: 0, + Doc: "Returns the current user's username.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + username := os.Getenv("USER") + if username == "" { + username = os.Getenv("USERNAME") // Windows + } + if username == "" { + return MakeBuiltinError(ps, "Unable to determine username", "whoami") + } + return *env.NewString(username) + }, + }, + + // Args: + // * command: string representing the command to check + // Returns: + // * uri representing the path to the executable, or error if not found + // Tags: #system #command + "which": { + Argsn: 1, + Doc: "Finds the path to an executable command.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch cmd := arg0.(type) { + case env.String: + path, err := findExecutable(cmd.Value) + if err != nil { + return MakeBuiltinError(ps, "Command not found: "+cmd.Value, "which") + } + return *env.NewUri1(ps.Idx, "file://"+path) + default: + return MakeArgError(ps, 1, []env.Type{env.StringType}, "which") + } + }, + }, + + // Args: + // * source: uri representing source directory + // * destination: uri representing destination directory + // Returns: + // * destination uri if successful + // Tags: #file #copy #recursive + "cp-r": { + Argsn: 2, + Doc: "Recursively copies a directory and all its contents.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch src := arg0.(type) { + case env.Uri: + switch dst := arg1.(type) { + case env.Uri: + srcPath := resolvePath(ps.WorkingPath, src.GetPath()) + dstPath := resolvePath(ps.WorkingPath, dst.GetPath()) + err := copyRecursive(srcPath, dstPath) + if err != nil { + return MakeBuiltinError(ps, "Error copying recursively: "+err.Error(), "cp-r") + } + return arg1 + default: + return MakeArgError(ps, 2, []env.Type{env.UriType}, "cp-r") + } + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "cp-r") + } + }, + }, + + // Args: + // * path: uri representing directory path to create + // Returns: + // * path uri if successful + // Tags: #file #directory + "mkdir-p": { + Argsn: 1, + Doc: "Creates a directory and any necessary parent directories.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + dirPath := resolvePath(ps.WorkingPath, path.GetPath()) + err := os.MkdirAll(dirPath, 0755) + if err != nil { + return MakeBuiltinError(ps, "Error creating directory: "+err.Error(), "mkdir-p") + } + return arg0 + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "mkdir-p") + } + }, + }, + // Args: // * none // Returns: @@ -1228,6 +1742,171 @@ var Builtins_os = map[string]*env.Builtin{ } }, }, + + // + // ##### Additional OS utilities ##### "Common OS operations made simple" + // + + // Gets the size of a file in bytes. + // Args: + // * path: uri representing the file + // Returns: + // * integer size in bytes + // Tags: #file #size + "file-size": { + Argsn: 1, + Doc: "Gets the size of a file in bytes.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + filePath := resolvePath(ps.WorkingPath, path.GetPath()) + info, err := os.Stat(filePath) + if err != nil { + return MakeBuiltinError(ps, "Error getting file size: "+err.Error(), "file-size") + } + return *env.NewInteger(info.Size()) + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "file-size") + } + }, + }, + + // Touches a file (creates if doesn't exist, updates access time if exists). + // Args: + // * path: uri representing the file to touch + // Returns: + // * path uri if successful + // Tags: #file #create + "touch": { + Argsn: 1, + Doc: "Creates a file if it doesn't exist, or updates its access time if it does.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch path := arg0.(type) { + case env.Uri: + filePath := resolvePath(ps.WorkingPath, path.GetPath()) + now := time.Now() + err := os.Chtimes(filePath, now, now) + if os.IsNotExist(err) { + // File doesn't exist, create it + file, err := os.Create(filePath) + if err != nil { + return MakeBuiltinError(ps, "Error creating file: "+err.Error(), "touch") + } + file.Close() + } else if err != nil { + return MakeBuiltinError(ps, "Error touching file: "+err.Error(), "touch") + } + return arg0 + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "touch") + } + }, + }, + + // Creates a hard link to a file. + // Args: + // * source: uri representing the source file + // * link: uri representing the hard link to create + // Returns: + // * link uri if successful + // Tags: #file #link + "hardlink": { + Argsn: 2, + Doc: "Creates a hard link to a file.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch src := arg0.(type) { + case env.Uri: + switch link := arg1.(type) { + case env.Uri: + srcPath := resolvePath(ps.WorkingPath, src.GetPath()) + linkPath := resolvePath(ps.WorkingPath, link.GetPath()) + err := os.Link(srcPath, linkPath) + if err != nil { + return MakeBuiltinError(ps, "Error creating hard link: "+err.Error(), "hardlink") + } + return arg1 + default: + return MakeArgError(ps, 2, []env.Type{env.UriType}, "hardlink") + } + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "hardlink") + } + }, + }, + + // Gets file or directory count in a directory. + // Args: + // * path: uri representing the directory (optional, defaults to current dir) + // Returns: + // * integer count of items + // Tags: #file #count + "count-dir": { + Argsn: 1, + Doc: "Counts the number of files and directories in a directory.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + var dirPath string + switch path := arg0.(type) { + case env.Uri: + dirPath = resolvePath(ps.WorkingPath, path.GetPath()) + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "count-dir") + } + + files, err := os.ReadDir(dirPath) + if err != nil { + return MakeBuiltinError(ps, "Error reading directory: "+err.Error(), "count-dir") + } + return *env.NewInteger(int64(len(files))) + }, + }, + + // Gets the current process ID. + // Args: + // * none + // Returns: + // * integer process ID + // Tags: #system #process + "pid": { + Argsn: 0, + Doc: "Gets the current process ID.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return *env.NewInteger(int64(os.Getpid())) + }, + }, + + // Gets the parent process ID. + // Args: + // * none + // Returns: + // * integer parent process ID + // Tags: #system #process + "ppid": { + Argsn: 0, + Doc: "Gets the parent process ID.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + return *env.NewInteger(int64(os.Getppid())) + }, + }, + + // Sleep for a specified duration. + // Args: + // * duration: integer seconds to sleep + // Returns: + // * integer seconds slept + // Tags: #system #time + "sleep": { + Argsn: 1, + Doc: "Sleeps for the specified number of seconds.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch seconds := arg0.(type) { + case env.Integer: + time.Sleep(time.Duration(seconds.Value) * time.Second) + return arg0 + default: + return MakeArgError(ps, 1, []env.Type{env.IntegerType}, "sleep") + } + }, + }, } // resolvePath resolves a path - if it's absolute, returns it as-is; if relative, joins with workingPath @@ -1478,6 +2157,108 @@ func extractZip(srcPath, dstPath string) error { return nil } +// Helper function to find an executable in PATH +func findExecutable(cmd string) (string, error) { + // Check if command is an absolute path + if filepath.IsAbs(cmd) { + if _, err := os.Stat(cmd); err == nil { + return cmd, nil + } + return "", fmt.Errorf("command not found") + } + + // Search in PATH + pathEnv := os.Getenv("PATH") + if pathEnv == "" { + return "", fmt.Errorf("PATH environment variable not set") + } + + pathDirs := strings.Split(pathEnv, string(os.PathListSeparator)) + for _, dir := range pathDirs { + if dir == "" { + continue + } + fullPath := filepath.Join(dir, cmd) + + // Try with common executable extensions on Windows + extensions := []string{""} + if strings.Contains(strings.ToLower(os.Getenv("OS")), "windows") { + extensions = []string{"", ".exe", ".bat", ".cmd", ".com"} + } + + for _, ext := range extensions { + testPath := fullPath + ext + if info, err := os.Stat(testPath); err == nil && !info.IsDir() { + // Check if file is executable (basic check) + if info.Mode().Perm()&0111 != 0 || ext != "" { + return testPath, nil + } + } + } + } + return "", fmt.Errorf("command not found in PATH") +} + +// Helper function to recursively copy directories +func copyRecursive(src, dst string) error { + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + if srcInfo.IsDir() { + // Create destination directory + if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil { + return err + } + + // Read source directory + entries, err := os.ReadDir(src) + if err != nil { + return err + } + + // Copy each entry recursively + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if err := copyRecursive(srcPath, dstPath); err != nil { + return err + } + } + } else { + // Copy file + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + // Create destination directory if needed + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + + dstFile, err := os.Create(dst) + if err != nil { + return err + } + defer dstFile.Close() + + if _, err := io.Copy(dstFile, srcFile); err != nil { + return err + } + + // Copy permissions + if err := os.Chmod(dst, srcInfo.Mode()); err != nil { + return err + } + } + + return nil +} + func proccesTableBase() *env.Table { return env.NewTable([]string{ "User", diff --git a/evaldo/builtins_sxml.go b/evaldo/builtins_sxml.go index 0aebc661..7e32745a 100644 --- a/evaldo/builtins_sxml.go +++ b/evaldo/builtins_sxml.go @@ -265,7 +265,7 @@ var Builtins_sxml = map[string]*env.Builtin{ // * block: SXML processing block with tag handlers // Returns: // * result of processing the XML - "reader//do-sxml": { + "reader//Do-sxml": { Argsn: 2, Doc: "Processes XML using a streaming SAX-like approach with tag handlers.", Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { @@ -279,6 +279,8 @@ var Builtins_sxml = map[string]*env.Builtin{ }, }, + // TODO ... make a do-sxml function that accepts a string + // Tests: // ; TODO: method resolution for rye-sxml-start//Attr? not working - needs investigation // ; stdout { diff --git a/evaldo/builtins_term.go b/evaldo/builtins_term.go index fee786c2..937944bc 100644 --- a/evaldo/builtins_term.go +++ b/evaldo/builtins_term.go @@ -6,6 +6,7 @@ package evaldo import ( "fmt" "os" + "strings" "time" "github.com/muesli/reflow/indent" @@ -850,4 +851,71 @@ var Builtins_term = map[string]*env.Builtin{ } }, }, + + // Tests: + // pane "Title" "Content line 1\nContent line 2" + // Args: + // * title: String for the header title + // * content: String content to display in the pane + // Returns: + // * Integer 1 (success indicator) + "pane": { + Argsn: 2, + Doc: "Displays content in a bordered pane with a title header.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + title, ok1 := arg0.(env.String) + if !ok1 { + return MakeArgError(ps, 1, []env.Type{env.StringType}, "pane") + } + content, ok2 := arg1.(env.String) + if !ok2 { + return MakeArgError(ps, 2, []env.Type{env.StringType}, "pane") + } + + // Split content into lines + lines := strings.Split(content.Value, "\n") + + // Calculate the width needed + maxContentWidth := 0 + for _, line := range lines { + if len(line) > maxContentWidth { + maxContentWidth = len(line) + } + } + + // Title width with padding: "─ title ─" + titleWidth := len(title.Value) + 4 // "─ " + title + " ─" + + // Choose the larger of content width and title width, with minimum padding + paneWidth := maxContentWidth + 2 // content + 2 spaces padding + if titleWidth > paneWidth { + paneWidth = titleWidth + } + if paneWidth < 10 { // minimum width + paneWidth = 10 + } + + // Build the pane + // Top border with title + titlePadding := paneWidth - len(title.Value) - 4 // subtract "─ title ─" + leftPadding := titlePadding / 2 + rightPadding := titlePadding - leftPadding + + topBorder := "╭" + strings.Repeat("─", leftPadding) + "─ " + title.Value + " ─" + strings.Repeat("─", rightPadding) + "╮" + fmt.Println(topBorder) + + // Content lines + for _, line := range lines { + contentPadding := paneWidth - len(line) + rightContentPadding := contentPadding + fmt.Printf("│%s%s│\n", line, strings.Repeat(" ", rightContentPadding)) + } + + // Bottom border + bottomBorder := "╰" + strings.Repeat("─", paneWidth) + "╯" + fmt.Println(bottomBorder) + + return *env.NewInteger(1) + }, + }, } diff --git a/evaldo/builtins_unique.go b/evaldo/builtins_unique.go new file mode 100644 index 00000000..8b2c1c67 --- /dev/null +++ b/evaldo/builtins_unique.go @@ -0,0 +1,142 @@ +package evaldo + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "sync/atomic" + "time" + + "github.com/refaktor/rye/env" +) + +// Global counter for process-unique IDs - thread-safe using atomic operations +var processUniqueCounter int64 + +// processStartTime stores when this process started for unique ID generation +var processStartTime int64 + +func init() { + // Initialize the start time when the package loads + processStartTime = time.Now().UnixNano() +} + +// generateRandomString creates a cryptographically secure random string of specified length +func generateRandomString(length int) (string, error) { + bytes := make([]byte, length) + _, err := rand.Read(bytes) + if err != nil { + return "", err + } + return hex.EncodeToString(bytes)[:length], nil +} + +// generateProcessUniqueID creates a unique ID for this process run +func generateProcessUniqueID() (string, error) { + // Get process ID + pid := os.Getpid() + + // Get atomic counter value and increment + counter := atomic.AddInt64(&processUniqueCounter, 1) + + // Get current nanosecond timestamp + nowNano := time.Now().UnixNano() + + // Generate a short random component for extra uniqueness + randomStr, err := generateRandomString(6) + if err != nil { + return "", err + } + + // Combine all components: rye_[PID]_[COUNTER]_[NANOS]_[RANDOM] + uniqueID := fmt.Sprintf("rye_%d_%d_%d_%s", pid, counter, nowNano, randomStr) + return uniqueID, nil +} + +var builtins_unique = map[string]*env.Builtin{ + + // + // ##### Unique ID Generation ##### "Process-unique identifier generation functions" + // + + // Tests: + // different { Rye-itself//unique-id } { Rye-itself//unique-id } + // does { Rye-itself//unique-id } |type? |= 'string + // Args: + // * rye-ctx: Rye runtime context (automatically provided) + // Returns: + // * string: Process-unique identifier + "Rye-itself//Unique-id?": { + Argsn: 1, + Doc: "Generates a process-unique identifier string. Each call within the same process run is guaranteed to return a different ID.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + // arg0 should be Rye context, but we don't need to validate it for this implementation + uniqueID, err := generateProcessUniqueID() + if err != nil { + return MakeBuiltinError(ps, fmt.Sprintf("Failed to generate unique ID: %v", err), "Rye-itself//unique-id") + } + return *env.NewString(uniqueID) + }, + }, + + // Tests: + // different { Rye-itself//unique-temp-dir } { Rye-itself//unique-temp-dir } + // does { Rye-itself//unique-temp-dir } |type? |= 'uri + // Args: + // * rye-ctx: Rye runtime context (automatically provided) + // Returns: + // * uri: File URI pointing to a unique temporary directory + "Rye-itself//Temp-dir?": { + Argsn: 1, + Doc: "Creates a unique temporary directory and returns its file URI. The directory is guaranteed to be unique for this process run.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + // Generate unique ID for directory name + uniqueID, err := generateProcessUniqueID() + if err != nil { + return MakeBuiltinError(ps, fmt.Sprintf("Failed to generate unique ID: %v", err), "Rye-itself//unique-temp-dir") + } + + // Create the full path in system temp directory + tempDir := os.TempDir() + uniqueDirPath := filepath.Join(tempDir, uniqueID) + + // Create the directory + err = os.MkdirAll(uniqueDirPath, 0755) + if err != nil { + return MakeBuiltinError(ps, fmt.Sprintf("Failed to create temp directory: %v", err), "Rye-itself//unique-temp-dir") + } + + // Return as file URI + return *env.NewFileUri(ps.Idx, uniqueDirPath) + }, + }, + + // Tests: + // different { Rye-itself//unique-temp-file } { Rye-itself//unique-temp-file } + // does { Rye-itself//unique-temp-file } |type? |= 'uri + // Args: + // * rye-ctx: Rye runtime context (automatically provided) + // Returns: + // * uri: File URI pointing to a unique temporary file path + "Rye-itself//Temp-file?": { + Argsn: 1, + Doc: "Generates a unique temporary file path and returns its file URI. The file path is guaranteed to be unique for this process run. Note: the file is not created, only the path is generated.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + // Generate unique ID for file name + uniqueID, err := generateProcessUniqueID() + if err != nil { + return MakeBuiltinError(ps, fmt.Sprintf("Failed to generate unique ID: %v", err), "Rye-itself//unique-temp-file") + } + + // Create the full path in system temp directory with .tmp extension + tempDir := os.TempDir() + uniqueFilePath := filepath.Join(tempDir, uniqueID+".tmp") + + // Return as file URI (don't create the file, just return the path) + return *env.NewFileUri(ps.Idx, uniqueFilePath) + }, + }, +} + diff --git a/evaldo/builtins_validation.go b/evaldo/builtins_validation.go index 4e9b522e..3827f662 100644 --- a/evaldo/builtins_validation.go +++ b/evaldo/builtins_validation.go @@ -258,7 +258,9 @@ func evalWord(word env.Word, es *env.ProgramState, val any) (any, env.Object) { return val, nil // TODO ... make error } case "required": - if val == nil { + if _, ok := val.(*env.Error); ok { + return val, *env.NewString("required") + } else if val == nil { return val, *env.NewString("required") } else { return val, nil diff --git a/evaldo/debug_options.go b/evaldo/debug_options.go index dce08ce4..2f76abce 100644 --- a/evaldo/debug_options.go +++ b/evaldo/debug_options.go @@ -125,7 +125,7 @@ func HandleEnterConsoleOption(es *env.ProgramState, genv *env.Idxs) { es.ErrorFlag = false // Use the existing REPL system - DoRyeRepl(es, "rye", true, false) + DoRyeRepl(es, "rye", true, false, "") } // HandleListContextOption lists the current context diff --git a/evaldo/repl.go b/evaldo/repl.go index 81ca6948..93e0a4b8 100644 --- a/evaldo/repl.go +++ b/evaldo/repl.go @@ -560,7 +560,7 @@ func isCursorAtBottom() bool { // TODO --- doesn't seem to work and probably don return true || os.Getenv("TERM_LINES") != "" && os.Getenv("TERM_LINES") == os.Getenv("TERM_ROW") } -func DoRyeRepl(es *env.ProgramState, dialect string, showResults bool, localHist bool) { // here because of some odd options we were experimentally adding +func DoRyeRepl(es *env.ProgramState, dialect string, showResults bool, localHist bool, histFile string) { // here because of some odd options we were experimentally adding // Configure log to not include date/time prefix for cleaner output log.SetFlags(0) @@ -652,8 +652,14 @@ func DoRyeRepl(es *env.ProgramState, dialect string, showResults bool, localHist // Improved error handling for history file operations histFn := history_fn - if localHist { + if histFile != "" { + histFn = histFile + log.Printf("Using custom history file: %s", histFn) + } else if localHist { histFn = "local_rye_history" + log.Printf("Using local history file: %s", histFn) + } else { + log.Printf("Using default history file: %s", histFn) } f, err := os.Open(histFn) diff --git a/go.mod b/go.mod index 3957c0c5..872b71f4 100644 --- a/go.mod +++ b/go.mod @@ -70,11 +70,22 @@ require ( ) require ( + github.com/mlange-42/ark v0.8.3 + github.com/spf13/cobra v1.10.2 + github.com/tliron/glsp v0.2.2 + google.golang.org/genai v1.59.0 +) + +require ( + cloud.google.com/go v0.116.0 // indirect + cloud.google.com/go/auth v0.9.3 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect filippo.io/hpke v0.4.0 // indirect github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect github.com/StirlingMarketingGroup/go-retry v0.0.0-20190512160921-94a8eb23e893 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blevesearch/zapx/v17 v17.1.2 // indirect @@ -92,19 +103,27 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/headzoo/surf v1.0.1 // indirect github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/inbucket/html2text v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jhillyerd/enmime/v2 v2.3.0 // indirect github.com/klauspost/compress v1.18.3 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.2.0 // indirect github.com/olekukonko/ll v0.1.8 // indirect github.com/olekukonko/tablewriter v1.1.4 // indirect + github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect @@ -112,6 +131,9 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/xid v1.6.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sasha-s/go-deadlock v0.3.1 // indirect + github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/sqs/go-xoauth2 v0.0.0-20120917012134-0911dad68e56 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/tidwall/gjson v1.18.0 // indirect @@ -119,12 +141,17 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/tiendc/go-deepcopy v1.7.2 // indirect + github.com/tliron/commonlog v0.2.8 // indirect + github.com/tliron/kutil v0.3.11 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.81.1 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect diff --git a/go.sum b/go.sum index 817f982b..3ffb3ca5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,14 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M= c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= +cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0= filippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= @@ -8,6 +17,7 @@ filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= github.com/BrianLeishman/go-imap v0.1.28 h1:+IGG0hSdiwoUlJKIP6rF/OfnEUt0YZzHsBVBXdC7Rnk= github.com/BrianLeishman/go-imap v0.1.28/go.mod h1:gmLtGYOsDv+wnfRAgvw/zg46gNMuLQf+VaoZd4Ig/Tk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ= github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo= github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.1 h1:IpUgup6ucCE4wB59wAP0Y2qSApYjFhSfGVjShUBoVSw= @@ -62,6 +72,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3 github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8= github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -112,14 +124,18 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4Yn github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -140,6 +156,10 @@ github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2I github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/elastic/go-seccomp-bpf v1.6.0 h1:NYduiYxRJ0ZkIyQVwlSskcqPPSg6ynu5pK0/d7SQATs= github.com/elastic/go-seccomp-bpf v1.6.0/go.mod h1:5tFsTvH4NtWGfpjsOQD53H8HdVQ+zSZFRUDSGevC0Kc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -187,10 +207,30 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -201,12 +241,18 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -219,8 +265,12 @@ github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca h1:utFgFwgxaqx5OthzE3DS github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca/go.mod h1:8926sG02TCOX4RFRzIMFIzRw4xuc/TwO2gtN7teMJZ4= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs= github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= @@ -273,6 +323,8 @@ github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/a github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8= github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4= +github.com/mlange-42/ark v0.8.3 h1:vgmV4EiwqYbW9adiO576zapzW/O0BhkuUT2IXlqJ43s= +github.com/mlange-42/ark v0.8.3/go.mod h1:gkS9cuklENPTmSjL2z4DcJgJsIVqF1yNwFlx48Hz/Sw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -284,6 +336,8 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= @@ -298,6 +352,8 @@ github.com/ollama/ollama v0.30.0 h1:sUw0oK1SOgKwSg5UwXiMfwa6V8Eg8yWOGuiRxTqy3MM= github.com/ollama/ollama v0.30.0/go.mod h1:TjwyryJftKpcf7ByoIuZWso/Wx2Jr2AcGubxadv13dY= github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -309,6 +365,7 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= @@ -335,10 +392,13 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sairash/chitosocket v1.0.2 h1:MxenGnZxdMClk4IroLmdrARpY32KW9WryrffRj1i0do= github.com/sairash/chitosocket v1.0.2/go.mod h1:da73+aWNPA8SHR1PO53HEPGW9uIq2mBiyTvEl1xBPVk= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= @@ -349,12 +409,23 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U= +github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/sqs/go-xoauth2 v0.0.0-20120917012134-0911dad68e56 h1:KCgKdj+ha4CgnVHIiJYGKzgZk3HfCc6XssESfOa6atM= github.com/sqs/go-xoauth2 v0.0.0-20120917012134-0911dad68e56/go.mod h1:ghDEBrT4oFcM4rv18bzcZaAWXbHPGpDa4e2hh9oXL8A= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= @@ -377,6 +448,12 @@ github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08 github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/tliron/commonlog v0.2.8 h1:vpKrEsZX4nlneC9673pXpeKqv3cFLxwpzNEZF1qiaQQ= +github.com/tliron/commonlog v0.2.8/go.mod h1:HgQZrJEuiKLLRvUixtPWGcmTmWWtKkCtywF6x9X5Spw= +github.com/tliron/glsp v0.2.2 h1:IKPfwpE8Lu8yB6Dayta+IyRMAbTVunudeauEgjXBt+c= +github.com/tliron/glsp v0.2.2/go.mod h1:GMVWDNeODxHzmDPvYbYTCs7yHVaEATfYtXiYJ9w1nBg= +github.com/tliron/kutil v0.3.11 h1:kongR0dhrrn9FR/3QRFoUfQe27t78/xQvrU9aXIy5bk= +github.com/tliron/kutil v0.3.11/go.mod h1:4IqOAAdpJuDxYbJxMv4nL8LSH0mPofSrdwIv8u99PDc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -398,6 +475,8 @@ go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= @@ -410,7 +489,9 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= @@ -418,14 +499,24 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -436,6 +527,9 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -445,9 +539,12 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -487,6 +584,10 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= @@ -496,6 +597,35 @@ golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genai v1.59.0 h1:xp+ydkJFW8hO0hTUaAkr8TrLM9HFP3NYAwFhPd0nDqA= +google.golang.org/genai v1.59.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= +google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= @@ -511,10 +641,13 @@ gopkg.in/headzoo/surf.v1 v1.0.1 h1:oDBy9b5NlTb2Hvl3hF8NN+Qy7ypC9/g5YDP85pPh13k= gopkg.in/headzoo/surf.v1 v1.0.1/go.mod h1:T0BH8276y+OPL0E4tisxCFjBVIAKGbwdYU7AS7/EpQQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA= kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= diff --git a/runner/runner.go b/runner/runner.go index d669edf4..6bf62cd0 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -46,6 +46,7 @@ var ( version = flag.Bool("version", false, "Displays the version of Rye.") localhist = flag.Bool("localhist", false, "Store console history to local file local_rye_history") + histfile = flag.String("histfile", "", "Store console history to specified file") // Seccomp options (Linux only) - using pure Go library SeccompProfile = flag.String("seccomp-profile", "", "Seccomp profile to use: strict, readonly") @@ -257,6 +258,7 @@ func DoMain(regfn func(*env.ProgramState) error) { fmt.Println("\033[33m rye -unshare -unshare-net=false . \033[36m# run main.rye isolated but with network access kept") fmt.Println("\033[33m rye -unshare -unshare-fs=false . \033[36m# isolate network/pid/uts but keep full filesystem access") fmt.Println("\033[33m rye -localhist \033[36m# append console history to local file local_rye_history") + fmt.Println("\033[33m rye -histfile hist.rye \033[36m# append console history to specified file hist.rye") fmt.Println("\033[33m rye -http 8080 \033[36m# start HTTP REPL mode on port 8080 (localhost only)") fmt.Println("\033[33m rye -http 8080 main.rye \033[36m# load main.rye and expose via HTTP console on port 8080") fmt.Println("\033[0m\n Thank you for trying out \033[1mRye\033[22m ...") @@ -1104,7 +1106,7 @@ func main_rye_file(file string, sig bool, subc bool, here bool, interactive bool // evaldo.MaybeDisplayFailureOrError(ps, ps.Idx, "main rye file #2") if interactive { - evaldo.DoRyeRepl(ps, "rye", evaldo.ShowResults, *localhist) + evaldo.DoRyeRepl(ps, "rye", evaldo.ShowResults, *localhist, *histfile) } else { if file == "" && evaldo.ShowResults { // TODO -- move this to some instance ... to ProgramState? or is more of a ReplState? fmt.Println(ps.Res.Print(*ps.Idx)) @@ -1301,7 +1303,7 @@ func main_rye_repl(_ io.Reader, _ io.Writer, subc bool, here bool, lang string, // Start dual REPL evaldo.DoRyeDualRepl(es, rightEs, lang, evaldo.ShowResults) } else { - evaldo.DoRyeRepl(es, lang, evaldo.ShowResults, *localhist) + evaldo.DoRyeRepl(es, lang, evaldo.ShowResults, *localhist, *histfile) } } diff --git a/term/microliner.go b/term/microliner.go index e28486db..ad942208 100644 --- a/term/microliner.go +++ b/term/microliner.go @@ -456,9 +456,15 @@ func (s *MLState) tabComplete(p []rune, line []rune, pos int, mode int) ([]rune, // Set flag to indicate we're in tab completion mode s.inTabCompletion = true + + // Extract the current word being completed + s.currentTabWord = s.extractCurrentWord(string(line), pos) + fmt.Printf("Tab completion started for: '%s'\n", s.currentTabWord) + defer func() { // Always clear the flag and suggestion space when exiting tab completion s.inTabCompletion = false + s.currentTabWord = "" s.clearSuggestionSpace() }() @@ -535,6 +541,31 @@ func (s *MLState) tabComplete(p []rune, line []rune, pos int, mode int) ([]rune, } return originalLine, originalPos, next, nil } + // Check for Ctrl+X to display word info while in tab completion + if next.Ctrl && strings.ToLower(next.Key) == "x" { + s.sendBack("\n") // Move to a new line first + fmt.Printf("Ctrl+X triggered! Word: '%s', Has state: %v\n", s.currentTabWord, s.programState != nil) + if s.programState != nil && s.currentTabWord != "" { + // Display word info without exiting tab completion + + // Get word information + wordInfo := FindWordInfo(s.programState, s.currentTabWord) + if wordInfo != "" { + fmt.Print(wordInfo) + } else { + fmt.Printf("No documentation found for '%s'\n", s.currentTabWord) + } + + // Refresh the completion display + err = s.refresh(p, completedLine, newPos) + if err != nil { + return line, pos, KeyEvent{Code: 27}, fmt.Errorf("failed to refresh display: %w", err) + } + s.displayTabSuggestions(list, currentIndex, mode) + direction = 0 // Don't change selection + continue + } + } return completedLine, newPos, next, nil } @@ -629,6 +660,7 @@ type MLState struct { inTabCompletion bool // Flag to track if we're in tab completion mode suggestionSpace int // Number of lines reserved for suggestions (0 = none reserved) ctrlSMode int // Ctrl+S cycles through modes: 1=context, 2=generics (0 is Tab-only) + currentTabWord string // The current word being tab-completed isWasmMode bool // Flag to track if we're in WASM mode (xterm.js) promptFunc func() string // Optional dynamic prompt function; called before each input line killRing [][]rune // Kill ring for cut/paste operations @@ -1790,8 +1822,23 @@ startOfHere: // Mode indicator is shown in displayTabSuggestions, not here line, pos, next, _ = s.tabComplete(p, line, pos, s.ctrlSMode) goto haveNext - case "x": // display last returned value interactively - if s.programState != nil && s.programState.Res != nil && s.displayValue != nil { + case "x": // display last returned value interactively, or word info if in tab completion + if s.inTabCompletion && s.currentTabWord != "" && s.programState != nil { + // We're in tab completion mode - show word information + s.sendBack("\n") // Move to a new line + + // Get word information + wordInfo := FindWordInfo(s.programState, s.currentTabWord) + if wordInfo != "" { + fmt.Print(wordInfo) + } else { + fmt.Printf("No documentation found for '%s'\n", s.currentTabWord) + } + + // Force refresh to redraw the prompt + s.needRefresh = true + + } else if s.programState != nil && s.programState.Res != nil && s.displayValue != nil { // Move to a new line s.sendBack("\n") @@ -2303,3 +2350,27 @@ startOfHere: } // return string(line), nil } + +// extractCurrentWord extracts the word at the cursor position for tab completion +func (s *MLState) extractCurrentWord(line string, pos int) string { + if pos < 0 || pos > len(line) { + return "" + } + + // Parse word context similar to the REPL completer + var wordpart string + spacePos := strings.LastIndex(line[:pos], " ") + + if spacePos < 0 { + wordpart = line[:pos] + } else { + wordpart = strings.TrimSpace(line[spacePos:pos]) + } + + // Remove op-prefix ("." or "|") if present, but keep the prefix for context + if strings.HasPrefix(wordpart, ".") || strings.HasPrefix(wordpart, "|") { + return wordpart + } + + return wordpart +} diff --git a/term/term.go b/term/term.go index 15bf7018..3edf553c 100644 --- a/term/term.go +++ b/term/term.go @@ -1663,6 +1663,626 @@ func DisplayTextArea(width, height int, text string) (env.Object, bool) { } } +// DisplayMarkdown provides an interactive display for markdown content with proper terminal-aware rendering +func DisplayMarkdown(items []env.Object, idx *env.Idxs) (env.Object, bool) { + mode := 0 // 0 - human, 1 - dev + totalItems := len(items) + size, err := GetTerminalSize() + height := size.Height + if err != nil { + height = 20 // Fallback default + } + pageSize := height - 4 // Reserve lines for prompts/instructions + if pageSize < 1 { + pageSize = 1 + } + totalPages := (totalItems + pageSize - 1) / pageSize // Ceiling division + if totalPages == 0 { + totalPages = 1 + } + + // If totalPages <= 1, use inline interactive mode + if totalPages <= 1 { + HideCur() + curr := 0 + moveUp := 0 + + defer func() { + ShowCur() + }() + + INLINE_DODO: + if moveUp > 0 { + CurUp(moveUp) + } + SaveCurPos() + + totalLines := 0 + // Print all items with cursor highlighting + for i, v := range items { + ClearLine() + if i == curr { + ColorBrGreen() + Bold() + termPrint("» ") + } else { + termPrint(" ") + } + + var valueStr string + switch ob := v.(type) { + case env.Object: + if mode == 0 { + valueStr = ob.Print(*idx) + } else { + valueStr = ob.Inspect(*idx) + } + default: + valueStr = fmt.Sprint(ob) + } + termPrintln(valueStr) + // Count the actual number of lines this entry takes (including newlines in the value) + totalLines += strings.Count(valueStr, "\n") + 1 + CloseProps() + } + + moveUp = totalLines + + for { + ascii, keyCode, err := GetChar() + + if (ascii == 3 || ascii == 27) || err != nil { + return *env.NewBlock(*env.NewTSeries(items)), true // Return full block on Ctrl+C or Esc + } + + if ascii == 13 { + if curr < totalItems { + return items[curr], false // Return selected item on Enter + } + return nil, true + } + + if ascii == 77 || ascii == 109 { // 'm' or 'M' for mode toggle + mode = 1 - mode + goto INLINE_DODO + } + + if keyCode == 40 { // Down arrow + curr++ + if curr >= totalItems { + curr = 0 // Wrap to top + } + goto INLINE_DODO + } else if keyCode == 38 { // Up arrow + curr-- + if curr < 0 { + curr = totalItems - 1 // Wrap to bottom + } + goto INLINE_DODO + } + } + } + + // Full-screen paginated mode for blocks that need pagination + HideCur() + currentPage := 0 + localCurr := 0 + moveUp := 0 + +DODO: + if moveUp > 0 { + CurUp(moveUp) + } + SaveCurPos() + start := currentPage * pageSize + end := start + pageSize + if end > totalItems { + end = totalItems + } + + totalLines := 0 + + // Print the current page of items + for i := 0; i < pageSize; i++ { + ClearLine() + globalIndex := start + i + if globalIndex < end { + item := items[globalIndex] + if i == localCurr { + ColorBrGreen() + Bold() + termPrint("» ") + } else { + termPrint(" ") + } + + var valueStr string + switch ob := item.(type) { + case env.Object: + if mode == 0 { + valueStr = ob.Print(*idx) + } else { + valueStr = ob.Inspect(*idx) + } + default: + valueStr = fmt.Sprint(ob) + } + termPrintln(valueStr) + // Count the actual number of lines this entry takes + totalLines += strings.Count(valueStr, "\n") + 1 + CloseProps() + } else { + // Empty line for incomplete pages + termPrintln("") + totalLines++ + } + } + + // Footer with navigation info + termPrintln(fmt.Sprintf("Page %d/%d | ↑/↓: navigate, Enter: select, n/p: page, m: mode, Esc: exit", + currentPage+1, totalPages)) + totalLines++ + + moveUp = totalLines + + for { + ascii, keyCode, err := GetChar() + + if (ascii == 3 || ascii == 27) || err != nil { + ShowCur() + return *env.NewBlock(*env.NewTSeries(items)), true // Return full block on Ctrl+C or Esc + } + + if ascii == 13 { // Enter key + globalIndex := start + localCurr + if globalIndex < totalItems { + ShowCur() + return items[globalIndex], false // Return selected item + } + } + + // Mode toggle + if ascii == 77 || ascii == 109 { // 'm' or 'M' for mode toggle + mode = 1 - mode + goto DODO + } + + // Page navigation + if ascii == 110 || ascii == 78 { // 'n' or 'N' for next page + if currentPage < totalPages-1 { + currentPage++ + localCurr = 0 + goto DODO + } + } + + if ascii == 112 || ascii == 80 { // 'p' or 'P' for previous page + if currentPage > 0 { + currentPage-- + localCurr = 0 + goto DODO + } + } + + // Item navigation within current page + maxLocalIndex := end - start - 1 + if keyCode == 40 { // Down arrow + if localCurr < maxLocalIndex { + localCurr++ + } else { + // Wrap to next page if possible, or to top of current page + if currentPage < totalPages-1 { + currentPage++ + localCurr = 0 + } else { + localCurr = 0 // Wrap to top of current page + } + } + goto DODO + } else if keyCode == 38 { // Up arrow + if localCurr > 0 { + localCurr-- + } else { + // Wrap to previous page if possible, or to bottom of current page + if currentPage > 0 { + currentPage-- + // Set cursor to last item of previous page + prevPageStart := currentPage * pageSize + prevPageEnd := prevPageStart + pageSize + if prevPageEnd > totalItems { + prevPageEnd = totalItems + } + localCurr = prevPageEnd - prevPageStart - 1 + } else { + localCurr = maxLocalIndex // Wrap to bottom of current page + } + } + goto DODO + } + } +} + // GetChar and GetChar2 functions are implemented in platform-specific files: // - term_unix.go for Unix/Linux systems // - term_windows.go for Windows systems + +// DisplayMarkdownItems displays markdown items with proper block selection and type+content return +func DisplayMarkdownItems(items interface{}, idx *env.Idxs) (env.Object, bool) { + // Convert items to proper type - we expect []MarkdownDisplayItem but work with interface{} + var markdownItems []interface{} + + // Handle the interface{} input - it should be a slice of MarkdownDisplayItem + if slice, ok := items.([]interface{}); ok { + markdownItems = slice + } else { + // Fallback for unexpected types + return nil, true + } + + totalItems := len(markdownItems) + if totalItems == 0 { + return nil, true + } + + size, err := GetTerminalSize() + height := size.Height + if err != nil { + height = 20 // Fallback default + } + pageSize := height - 4 // Reserve lines for prompts/instructions + if pageSize < 1 { + pageSize = 1 + } + + // Calculate display lines needed + totalDisplayLines := 0 + itemDisplayCounts := make([]int, totalItems) + + for i, item := range markdownItems { + // Extract display lines from the item using reflection or type assertion + if itemMap, ok := item.(map[string]interface{}); ok { + if displayLines, ok := itemMap["DisplayLines"].([]string); ok { + itemDisplayCounts[i] = len(displayLines) + totalDisplayLines += len(displayLines) + } else { + itemDisplayCounts[i] = 1 + totalDisplayLines++ + } + } else { + itemDisplayCounts[i] = 1 + totalDisplayLines++ + } + } + + // Determine if we need pagination based on total display lines vs page size + totalDisplayLinesWithSpacing := totalDisplayLines + (totalItems - 1) // Add spacing between items + needsPagination := totalDisplayLinesWithSpacing > pageSize + + // Calculate pages needed (for display purposes) + totalPages := (totalDisplayLinesWithSpacing + pageSize - 1) / pageSize + if totalPages == 0 { + totalPages = 1 + } + + // If content fits on one page, use inline interactive mode + if !needsPagination { + HideCur() + curr := 0 + moveUp := 0 + + defer func() { + ShowCur() + }() + + INLINE_DODO: + if moveUp > 0 { + CurUp(moveUp) + } + SaveCurPos() + + currentLine := 0 + // Print all items with cursor highlighting + for i, item := range markdownItems { + isSelected := (i == curr) + + // Extract item data + var displayLines []string + + if itemMap, ok := item.(map[string]interface{}); ok { + if dl, ok := itemMap["DisplayLines"].([]string); ok { + displayLines = dl + } + } + + // Display the item + for lineIdx, line := range displayLines { + ClearLine() + + // Show selection indicator on first line of item + if lineIdx == 0 { + if isSelected { + ColorBrGreen() + Bold() + termPrint("» ") + } else { + termPrint(" ") + } + } else { + // Continuation lines get indented + termPrint(" ") + } + + termPrintln(line) + CloseProps() + currentLine++ + } + + // Add empty line after each item except the last + if i < totalItems-1 && len(displayLines) > 0 { + ClearLine() + termPrintln("") + currentLine++ + } + } + + moveUp = currentLine + + for { + ascii, keyCode, err := GetChar() + + if (ascii == 3 || ascii == 27) || err != nil { + return nil, true // Escape on Ctrl+C or Esc + } + + if ascii == 13 { + // Return a block with type and content + if curr < totalItems { + item := markdownItems[curr] + if itemMap, ok := item.(map[string]interface{}); ok { + itemType := "text" + content := "" + + if t, ok := itemMap["Type"].(string); ok { + itemType = t + } + if c, ok := itemMap["Content"].(string); ok { + content = c + } + + // Return a block with [type content] + series := env.NewTSeries([]env.Object{*env.NewString(itemType), *env.NewString(content)}) + return *env.NewBlock(*series), false + } + } + return nil, true + } + + if keyCode == 40 { // Down arrow + curr++ + if curr >= totalItems { + curr = 0 // Wrap to top + } + goto INLINE_DODO + } else if keyCode == 38 { // Up arrow + curr-- + if curr < 0 { + curr = totalItems - 1 // Wrap to bottom + } + goto INLINE_DODO + } + } + } + + // Full-screen paginated mode for markdown that needs pagination + HideCur() + currentPage := 0 + localCurr := 0 // Current selection within visible items on page + moveUp := 0 + +DODO: + if moveUp > 0 { + CurUp(moveUp) + } + SaveCurPos() + + // Simple item-based pagination + // Calculate which items fit on current page based on their total display lines + visibleItems := make([]int, 0) + currentLines := 0 + start := 0 + + // Find the starting item for this page by skipping previous pages + skipLines := currentPage * pageSize + currentDisplayLine := 0 + + // Find start item + for i := 0; i < totalItems; i++ { + itemLinesWithSpacing := itemDisplayCounts[i] + if i < totalItems-1 { + itemLinesWithSpacing++ // Add spacing + } + + if currentDisplayLine+itemLinesWithSpacing <= skipLines { + currentDisplayLine += itemLinesWithSpacing + start = i + 1 + } else { + break + } + } + + // Find items that fit on current page + currentLines = 0 + for i := start; i < totalItems && currentLines < pageSize; i++ { + itemLines := itemDisplayCounts[i] + spacingLine := 0 + if i < totalItems-1 { + spacingLine = 1 + } + + if currentLines+itemLines+spacingLine <= pageSize { + visibleItems = append(visibleItems, i) + currentLines += itemLines + spacingLine + } else { + break + } + } + + displayedItems := len(visibleItems) + + // Ensure localCurr is within bounds + if localCurr >= displayedItems && displayedItems > 0 { + localCurr = displayedItems - 1 + } + + // Clear page + for i := 0; i < pageSize; i++ { + ClearLine() + termPrintln("") + } + + // Reset position and render content + CurUp(pageSize) + currentLineInPage := 0 + + for idx, itemIndex := range visibleItems { + if currentLineInPage >= pageSize { + break + } + + isSelected := (idx == localCurr) + item := markdownItems[itemIndex] + + // Extract item data + var displayLines []string + if itemMap, ok := item.(map[string]interface{}); ok { + if dl, ok := itemMap["DisplayLines"].([]string); ok { + displayLines = dl + } + } + + // Display the item + for lineIdx, line := range displayLines { + if currentLineInPage >= pageSize { + break // Stop if we run out of space + } + + ClearLine() + + // Show selection indicator on first line of item + if lineIdx == 0 { + if isSelected { + ColorBrGreen() + Bold() + termPrint("» ") + } else { + termPrint(" ") + } + } else { + // Continuation lines get indented + termPrint(" ") + } + + if isSelected { + ColorBrGreen() + } + + termPrintln(line) + CloseProps() + currentLineInPage++ + } + + // Add spacing line after each item except the last on page + if idx < len(visibleItems)-1 && currentLineInPage < pageSize { + ClearLine() + termPrintln("") + currentLineInPage++ + } + } + + // Move to position for footer + for currentLineInPage < pageSize { + CurDown(1) + currentLineInPage++ + } + + // Print footer showing page info + termPrintln(fmt.Sprintf("Page %d/%d (n=next, p=prev, Enter=select)", currentPage+1, totalPages)) + moveUp = pageSize + 1 + + defer func() { + ShowCur() + }() + + for { + ascii, keyCode, err := GetChar() + + if (ascii == 3 || ascii == 27) || err != nil { + return nil, true + } + + if ascii == 13 { + if localCurr < displayedItems { + globalIndex := visibleItems[localCurr] + item := markdownItems[globalIndex] + if itemMap, ok := item.(map[string]interface{}); ok { + itemType := "text" + content := "" + + if t, ok := itemMap["Type"].(string); ok { + itemType = t + } + if c, ok := itemMap["Content"].(string); ok { + content = c + } + + // Return a block with [type content] + series := env.NewTSeries([]env.Object{*env.NewString(itemType), *env.NewString(content)}) + return *env.NewBlock(*series), false + } + } + return nil, true + } + + if ascii == 110 || ascii == 78 { // 'n' or 'N' + if currentPage < totalPages-1 { + currentPage++ + localCurr = 0 + goto DODO + } + } else if ascii == 112 || ascii == 80 { // 'p' or 'P' + if currentPage > 0 { + currentPage-- + localCurr = 0 + goto DODO + } + } + + if keyCode == 40 { // Down arrow + localCurr++ + if localCurr >= displayedItems { + if currentPage < totalPages-1 { + currentPage++ + localCurr = 0 + } else { + currentPage = 0 + localCurr = 0 + } + } + goto DODO + } else if keyCode == 38 { // Up arrow + localCurr-- + if localCurr < 0 { + if currentPage > 0 { + currentPage-- + localCurr = 0 + goto DODO + } else { + currentPage = totalPages - 1 + localCurr = 0 + goto DODO + } + } + goto DODO + } + } +} diff --git a/term/terminal_query.go b/term/terminal_query.go new file mode 100644 index 00000000..f7e3b562 --- /dev/null +++ b/term/terminal_query.go @@ -0,0 +1,148 @@ +//go:build !windows && !wasm +// +build !windows,!wasm + +package term + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/pkg/term" + goterm "golang.org/x/term" +) + +// CursorPosition represents the current cursor position +type CursorPosition struct { + Row int + Col int +} + +// TerminalSize represents the terminal dimensions +type TerminalSize struct { + Width int + Height int +} + +// GetTerminalSize returns the current terminal size (width and height in characters) +func GetTerminalSize() (TerminalSize, error) { + width, height, err := goterm.GetSize(int(os.Stdout.Fd())) + if err != nil { + return TerminalSize{Width: 80, Height: 24}, err + } + return TerminalSize{Width: width, Height: height}, nil +} + +// GetTerminalHeight returns just the terminal height in rows +func GetTerminalHeight() int { + size, err := GetTerminalSize() + if err != nil { + return 24 // fallback + } + return size.Height +} + +// QueryCursorPosition queries the terminal for the current cursor position using ANSI escape sequences +// Returns the current row and column (1-based indexing as returned by terminal) +func QueryCursorPosition() (CursorPosition, error) { + t, err := term.Open("/dev/tty") + if err != nil { + return CursorPosition{}, fmt.Errorf("failed to open terminal: %w", err) + } + defer t.Close() + + // Set raw mode to read the response + if err = term.RawMode(t); err != nil { + return CursorPosition{}, fmt.Errorf("failed to set raw mode: %w", err) + } + defer t.Restore() + + // Send cursor position query (CPR - Cursor Position Report) + _, err = t.Write([]byte("\033[6n")) + if err != nil { + return CursorPosition{}, fmt.Errorf("failed to write query: %w", err) + } + + // Read the response with timeout + response := make([]byte, 32) + + // Set a read timeout + done := make(chan bool, 1) + var numRead int + var readErr error + + go func() { + numRead, readErr = t.Read(response) + done <- true + }() + + select { + case <-done: + if readErr != nil { + return CursorPosition{}, fmt.Errorf("failed to read response: %w", readErr) + } + case <-time.After(100 * time.Millisecond): + return CursorPosition{}, fmt.Errorf("timeout waiting for cursor position response") + } + + // Parse the response: ESC[{row};{col}R + responseStr := string(response[:numRead]) + if !strings.HasPrefix(responseStr, "\033[") || !strings.HasSuffix(responseStr, "R") { + return CursorPosition{}, fmt.Errorf("invalid response format: %s", responseStr) + } + + // Extract the row;col part + coords := responseStr[2 : len(responseStr)-1] // Remove ESC[ and R + parts := strings.Split(coords, ";") + if len(parts) != 2 { + return CursorPosition{}, fmt.Errorf("invalid coordinate format: %s", coords) + } + + row, err := strconv.Atoi(parts[0]) + if err != nil { + return CursorPosition{}, fmt.Errorf("invalid row number: %s", parts[0]) + } + + col, err := strconv.Atoi(parts[1]) + if err != nil { + return CursorPosition{}, fmt.Errorf("invalid column number: %s", parts[1]) + } + + return CursorPosition{Row: row, Col: col}, nil +} + +// GetCurrentRow returns just the current row position +func GetCurrentRow() (int, error) { + pos, err := QueryCursorPosition() + if err != nil { + return 0, err + } + return pos.Row, nil +} + +// GetCurrentColumn returns just the current column position +func GetCurrentColumn() (int, error) { + pos, err := QueryCursorPosition() + if err != nil { + return 0, err + } + return pos.Col, nil +} + +// IsNearBottomOfTerminal checks if cursor is near the bottom of the terminal +// Returns true if within 'threshold' lines of the bottom +func IsNearBottomOfTerminal(threshold int) (bool, error) { + pos, err := QueryCursorPosition() + if err != nil { + return false, err + } + + size, err := GetTerminalSize() + if err != nil { + return false, err + } + + return (size.Height - pos.Row) <= threshold, nil +} diff --git a/term/terminal_query_wasm.go b/term/terminal_query_wasm.go new file mode 100644 index 00000000..6de3a8a3 --- /dev/null +++ b/term/terminal_query_wasm.go @@ -0,0 +1,76 @@ +//go:build wasm +// +build wasm + +package term + +import ( + "fmt" +) + +// CursorPosition represents the current cursor position +type CursorPosition struct { + Row int + Col int +} + +// TerminalSize represents the terminal dimensions +type TerminalSize struct { + Width int + Height int +} + +// GetTerminalSize returns the current terminal size (width and height in characters) +// In WASM, we return default values since we don't have direct terminal access +func GetTerminalSize() (TerminalSize, error) { + // In WASM environment, return reasonable defaults + // These could be made configurable via external variables + return TerminalSize{Width: 80, Height: 24}, nil +} + +// GetTerminalHeight returns just the terminal height in rows +func GetTerminalHeight() int { + size, _ := GetTerminalSize() + return size.Height +} + +// QueryCursorPosition queries the terminal for the current cursor position using ANSI escape sequences +// Returns the current row and column (1-based indexing as returned by terminal) +// Note: In WASM, this is not supported +func QueryCursorPosition() (CursorPosition, error) { + // WASM doesn't have direct terminal access + return CursorPosition{}, fmt.Errorf("cursor position query not supported in WASM environment") +} + +// GetCurrentRow returns just the current row position +func GetCurrentRow() (int, error) { + pos, err := QueryCursorPosition() + if err != nil { + return 0, err + } + return pos.Row, nil +} + +// GetCurrentColumn returns just the current column position +func GetCurrentColumn() (int, error) { + pos, err := QueryCursorPosition() + if err != nil { + return 0, err + } + return pos.Col, nil +} + +// IsNearBottomOfTerminal checks if cursor is near the bottom of the terminal +// Returns true if within 'threshold' lines of the bottom +func IsNearBottomOfTerminal(threshold int) (bool, error) { + pos, err := QueryCursorPosition() + if err != nil { + return false, err + } + + size, err := GetTerminalSize() + if err != nil { + return false, err + } + + return (size.Height - pos.Row) <= threshold, nil +} diff --git a/term/terminal_query_windows.go b/term/terminal_query_windows.go new file mode 100644 index 00000000..766f978b --- /dev/null +++ b/term/terminal_query_windows.go @@ -0,0 +1,84 @@ +//go:build windows +// +build windows + +package term + +import ( + "fmt" + "os" + + goterm "golang.org/x/term" +) + +// CursorPosition represents the current cursor position +type CursorPosition struct { + Row int + Col int +} + +// TerminalSize represents the terminal dimensions +type TerminalSize struct { + Width int + Height int +} + +// GetTerminalSize returns the current terminal size (width and height in characters) +func GetTerminalSize() (TerminalSize, error) { + width, height, err := goterm.GetSize(int(os.Stdout.Fd())) + if err != nil { + return TerminalSize{Width: 80, Height: 24}, err + } + return TerminalSize{Width: width, Height: height}, nil +} + +// GetTerminalHeight returns just the terminal height in rows +func GetTerminalHeight() int { + size, err := GetTerminalSize() + if err != nil { + return 24 // fallback + } + return size.Height +} + +// QueryCursorPosition queries the terminal for the current cursor position using ANSI escape sequences +// Returns the current row and column (1-based indexing as returned by terminal) +// Note: On Windows, this may not work in all terminal environments +func QueryCursorPosition() (CursorPosition, error) { + // Windows implementation would need platform-specific code + // For now, return an error indicating it's not supported + return CursorPosition{}, fmt.Errorf("cursor position query not implemented for Windows") +} + +// GetCurrentRow returns just the current row position +func GetCurrentRow() (int, error) { + pos, err := QueryCursorPosition() + if err != nil { + return 0, err + } + return pos.Row, nil +} + +// GetCurrentColumn returns just the current column position +func GetCurrentColumn() (int, error) { + pos, err := QueryCursorPosition() + if err != nil { + return 0, err + } + return pos.Col, nil +} + +// IsNearBottomOfTerminal checks if cursor is near the bottom of the terminal +// Returns true if within 'threshold' lines of the bottom +func IsNearBottomOfTerminal(threshold int) (bool, error) { + pos, err := QueryCursorPosition() + if err != nil { + return false, err + } + + size, err := GetTerminalSize() + if err != nil { + return false, err + } + + return (size.Height - pos.Row) <= threshold, nil +} diff --git a/term/word_info.go b/term/word_info.go new file mode 100644 index 00000000..2bd7b412 --- /dev/null +++ b/term/word_info.go @@ -0,0 +1,313 @@ +package term + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/refaktor/rye/env" + "github.com/refaktor/rye/loader" +) + +// FindWordInfo searches for documentation about a word in RYE_HOME/info/*.info.rye files +func FindWordInfo(ps *env.ProgramState, word string) string { + if word == "" { + return "" + } + + // Strip any prefix operators + cleanWord := word + if strings.HasPrefix(word, ".") || strings.HasPrefix(word, "|") { + cleanWord = word[1:] + } + + if cleanWord == "" { + return "" + } + + // Find RYE_HOME or use current directory structure + ryeHome := os.Getenv("RYE_HOME") + if ryeHome == "" { + // Try to find info directory relative to current working directory + if wd, err := os.Getwd(); err == nil { + // Look for info directory in current directory or parent directories + testDirs := []string{ + filepath.Join(wd, "info"), + filepath.Join(wd, "tests"), // Since the example files are in tests/ directory + } + for _, dir := range testDirs { + if _, err := os.Stat(dir); err == nil { + ryeHome = filepath.Dir(dir) + break + } + } + } + } + + if ryeHome == "" { + return "" + } + + // Look for info files + infoDirs := []string{ + filepath.Join(ryeHome, "info"), + filepath.Join(ryeHome, "tests"), // Fallback for the current structure + } + + for _, infoDir := range infoDirs { + if info := searchInfoInDirectory(ps, infoDir, cleanWord); info != "" { + return info + } + } + + return "" +} + +// searchInfoInDirectory searches for word info in *.info.rye files within a directory +func searchInfoInDirectory(ps *env.ProgramState, infoDir, word string) string { + if _, err := os.Stat(infoDir); os.IsNotExist(err) { + return "" + } + + files, err := filepath.Glob(filepath.Join(infoDir, "*.info.rye")) + if err != nil { + return "" + } + + for _, file := range files { + if info := searchWordInFile(ps, file, word); info != "" { + return info + } + } + + return "" +} + +// searchWordInFile searches for word documentation in a specific info.rye file +func searchWordInFile(ps *env.ProgramState, filename, word string) string { + content, err := os.ReadFile(filename) + if err != nil { + return "" + } + + // Parse the file using Rye's no_peg parser + block, _ := loader.LoadStringNoPEG(string(content), false) + blockObj, ok := block.(env.Block) + if !ok { + return "" + } + + // Search for the word in the parsed structure + return searchWordInBlock(ps, blockObj, word) +} + +// searchWordInBlock recursively searches for a word's documentation in a parsed block +func searchWordInBlock(ps *env.ProgramState, block env.Block, targetWord string) string { + series := block.Series + items := series.GetAll() + + for i := 0; i < len(items); i++ { + item := items[i] + + // Look for 'group' followed by the word name + if word, ok := item.(env.Word); ok { + if ps.Idx.GetWord(word.Index) == "group" && i+1 < len(items) { + // Next item should be the word name + if nextItem, ok := items[i+1].(env.String); ok { + wordName := nextItem.Value + if wordName == targetWord { + // Found the target word! Now collect its documentation + return extractWordDocumentation(ps, items, i, targetWord) + } + } + } + } + + // If this is a block, search recursively + if subBlock, ok := item.(env.Block); ok { + if result := searchWordInBlock(ps, subBlock, targetWord); result != "" { + return result + } + } + } + + return "" +} + +// extractWordDocumentation extracts the documentation for a word starting at the given index +func extractWordDocumentation(ps *env.ProgramState, items []env.Object, startIdx int, wordName string) string { + var result strings.Builder + + result.WriteString(fmt.Sprintf("\n\033[1;36m%s\033[0m\n", wordName)) + result.WriteString("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") + + // Look for the description (string after the word name) + if startIdx+2 < len(items) { + if desc, ok := items[startIdx+2].(env.String); ok { + result.WriteString("\033[33mDescription:\033[0m ") + result.WriteString(desc.Value) + result.WriteString("\n\n") + } + } + + // Look for the specification block (next block after description) + if startIdx+3 < len(items) { + if specBlock, ok := items[startIdx+3].(env.Block); ok { + extractSpecification(ps, &result, specBlock) + } + } + + // Look for test examples (next block after specification) + if startIdx+4 < len(items) { + if testBlock, ok := items[startIdx+4].(env.Block); ok { + extractExamples(ps, &result, testBlock) + } + } + + return result.String() +} + +// extractSpecification extracts argument and return information from the specification block +func extractSpecification(ps *env.ProgramState, result *strings.Builder, block env.Block) { + items := block.Series.GetAll() + + for i := 0; i < len(items); i++ { + item := items[i] + + if word, ok := item.(env.Word); ok { + wordName := ps.Idx.GetWord(word.Index) + + switch wordName { + case "argsn": + if i+1 < len(items) { + if argCount, ok := items[i+1].(env.Integer); ok { + result.WriteString(fmt.Sprintf("\033[32mArguments:\033[0m %d\n", argCount.Value)) + } + } + case "arg": + if i+1 < len(items) { + if argDesc, ok := items[i+1].(env.String); ok { + result.WriteString(fmt.Sprintf(" • %s\n", argDesc.Value)) + } + } + case "returns": + if i+1 < len(items) { + if returnDesc, ok := items[i+1].(env.String); ok { + result.WriteString(fmt.Sprintf("\033[35mReturns:\033[0m %s\n", returnDesc.Value)) + } + } + case "pure": + result.WriteString("\033[36mPure function\033[0m (no side effects)\n") + case "argtypes": + if i+1 < len(items) { + if typeBlock, ok := items[i+1].(env.Block); ok { + extractArgTypes(ps, result, typeBlock) + } + } + } + } + } + result.WriteString("\n") +} + +// extractArgTypes extracts argument type information +func extractArgTypes(ps *env.ProgramState, result *strings.Builder, block env.Block) { + items := block.Series.GetAll() + argNum := 1 + + for _, item := range items { + if argBlock, ok := item.(env.Block); ok { + if len(argBlock.Series.GetAll()) > 1 { + if num, ok := argBlock.Series.GetAll()[0].(env.Integer); ok { + argNum = int(num.Value) + } + if typeList, ok := argBlock.Series.GetAll()[1].(env.Block); ok { + types := make([]string, 0) + for _, typeItem := range typeList.Series.GetAll() { + if typeWord, ok := typeItem.(env.Word); ok { + types = append(types, ps.Idx.GetWord(typeWord.Index)) + } + } + if len(types) > 0 { + result.WriteString(fmt.Sprintf(" Arg %d types: %s\n", argNum, strings.Join(types, ", "))) + } + } + } + } + } +} + +// extractExamples extracts test examples from the test block +func extractExamples(ps *env.ProgramState, result *strings.Builder, block env.Block) { + items := block.Series.GetAll() + + if len(items) > 0 { + result.WriteString("\033[33mExamples:\033[0m\n") + + for _, item := range items { + if testBlock, ok := item.(env.Block); ok { + testItems := testBlock.Series.GetAll() + + for _, testItem := range testItems { + if word, ok := testItem.(env.Word); ok { + testType := ps.Idx.GetWord(word.Index) + if testType == "equal" { + // Handle 'equal' test + if len(testItems) >= 3 { + if codeBlock, ok := testItems[1].(env.Block); ok { + expectedValue := testItems[2] + codeStr := formatBlockForDisplay(ps, codeBlock) + expectedStr := formatValueForDisplay(ps, expectedValue) + result.WriteString(fmt.Sprintf(" %s ; => %s\n", codeStr, expectedStr)) + } + } + } else if testType == "error" { + // Handle 'error' test + if len(testItems) >= 2 { + if codeBlock, ok := testItems[1].(env.Block); ok { + codeStr := formatBlockForDisplay(ps, codeBlock) + result.WriteString(fmt.Sprintf(" %s ; => ERROR\n", codeStr)) + } + } + } + } + } + } + } + result.WriteString("\n") + } +} + +// formatBlockForDisplay formats a block for display in examples +func formatBlockForDisplay(ps *env.ProgramState, block env.Block) string { + var result strings.Builder + items := block.Series.GetAll() + + for i, item := range items { + if i > 0 { + result.WriteString(" ") + } + result.WriteString(formatValueForDisplay(ps, item)) + } + + return result.String() +} + +// formatValueForDisplay formats a value for display +func formatValueForDisplay(ps *env.ProgramState, value env.Object) string { + switch v := value.(type) { + case env.String: + return fmt.Sprintf(`"%s"`, v.Value) + case env.Integer: + return fmt.Sprintf("%d", v.Value) + case env.Decimal: + return fmt.Sprintf("%.1f", v.Value) + case env.Word: + return ps.Idx.GetWord(v.Index) + case env.Block: + return "{ " + formatBlockForDisplay(ps, v) + " }" + default: + return fmt.Sprintf("%v", v) + } +} \ No newline at end of file diff --git a/tests/base.info.rye b/tests/base.info.rye index 39c580c8..acab019d 100644 --- a/tests/base.info.rye +++ b/tests/base.info.rye @@ -1107,6 +1107,30 @@ section "Strings " "" { { } + group "contains\\flag" + "Checks if a block contains a specific flag (long or short), returning true if found or false if not found." + { + argsn 2 + pure + argtypes { + 1 [ Block ] + 2 [ Flagword ] + } + arg `collection: block of strings to search in` + arg `value: Flag value to search for` + returns `true if the collection contains the flag in short or long form, false otherwise` + } + + { + equal { contains\flag "-help -yello -ho" -h|help } true + equal { contains\flag "hello -help ho" -h|help } true + equal { contains\flag "hello yello -ho" -h|help } false + equal { contains\flag "-hello yello ho" -h|help } false + } + + { + } + group "has-suffix" "Checks if a string ends with a specific suffix, returning true if it does or false if it doesn't." { @@ -1349,14 +1373,14 @@ section "Strings " "" { } group "join" - "Concatenates all strings, numbers or URIs in a block or list into a single string with no separator. If the first element is a URI, returns a URI." + "Concatenates all strings, numbers (integers and decimals) or URIs in a block or list into a single string with no separator. If the first element is a URI, returns a URI." { argsn 1 pure argtypes { 1 [ List Block ] } - arg `collection: Block or list of strings, numbers or URIs to join` + arg `collection: Block or list of strings, numbers (integers and decimals) or URIs to join` returns `a single string with all values concatenated together, or a URI if first element is a URI` } @@ -1364,6 +1388,7 @@ section "Strings " "" { equal { join { "Mary" "Anne" } } "MaryAnne" equal { join { "Spot" "Fido" "Rex" } } "SpotFidoRex" equal { join { 1 2 3 } } "123" + equal { join { 1.5 2.25 3.0 } } "1.5000002.2500003.000000" equal { join { https://example.com/ "path" } |type? } 'uri equal { join { } } "" } @@ -1372,7 +1397,7 @@ section "Strings " "" { } group "join\\with" - "Concatenates all strings or numbers in a block or list into a single string with a specified delimiter between values." + "Concatenates all strings, integers or decimals in a block or list into a single string with a specified delimiter between values." { argsn 2 pure @@ -1380,7 +1405,7 @@ section "Strings " "" { 1 [ List Block ] 2 [ String ] } - arg `collection: Block or list of strings or numbers to join` + arg `collection: Block or list of strings, integers or decimals to join` arg `delimiter: String to insert between each value` returns `a single string with all values concatenated with the delimiter between them` } @@ -1389,6 +1414,7 @@ section "Strings " "" { equal { join\with { "Mary" "Anne" } " " } "Mary Anne" equal { join\with { "Spot" "Fido" "Rex" } "/" } "Spot/Fido/Rex" equal { join\with { 1 2 3 } "-" } "1-2-3" + equal { join\with { 1.5 2.25 3.0 } "-" } "1.500000-2.250000-3.000000" } { @@ -4850,7 +4876,7 @@ section "Contexts " "Context related functions" { } group "bind!" - "Binds a context to a parent context, allowing it to access the parent's values." + "Binds a context as a parent context to first context." { argsn 2 argtypes { @@ -4869,6 +4895,68 @@ section "Contexts " "Context related functions" { { } + group "bind" + "Creates a clone of the first context and binds the second context as its parent." + { + argsn 2 + argtypes { + 1 [ Context ] + 2 [ Context ] + } + arg `child: Context object to clone and bind` + arg `parent: Context object to bind to as parent` + returns `a cloned child context with its parent set to the specified parent context` + } + + { + equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p cc/x } 123 + equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p cc/y } 456 + equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p do\inside c { x:: 999 } cc/x } 123 ; clone unchanged + equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p do\inside c { x:: 999 } c/x } 999 ; original modified + } + + { + } + + group "anchor" + "Creates a clone of a context and sets current context as its parent." + { + argsn 1 + argtypes { + 1 [ Context ] + } + arg `ctx: Context object to clone and anchor to current context` + returns `a cloned context with current context set as parent` + } + + { + equal { c: context { x: 123 } cc: anchor c cc/x } 123 + equal { c: context { x: 123 } cc: anchor c do\inside c { x:: 999 } cc/x } 123 ; clone unchanged + equal { c: context { x: 123 } cc: anchor c do\inside c { x:: 999 } c/x } 999 ; original modified + } + + { + } + + group "anchor!" + "Sets current context as parent of the given context (modifies the context in place)." + { + argsn 1 + argtypes { + 1 [ Context ] + } + arg `ctx: Context object to anchor to current context` + returns `the modified context with current context set as parent` + } + + { + equal { c: context { x: 123 } anchor! c c/x } 123 + equal { c: context { x: 123 } anchor! c do\inside c { x:: 999 } c/x } 999 ; original modified + } + + { + } + group "unbind" "Removes the parent relationship from a context, making it a standalone context." { @@ -5320,6 +5408,29 @@ section "Contexts " "Context related functions" { { } + group "enter" + "Combines with and do\\in: takes a value to inject, a context as parent, and a block to evaluate with both." + { + argsn 3 + argtypes { + 2 [ Context ] + 3 [ Block ] + } + arg `value: Value to inject into the block (like with)` + arg `context: Context to use as parent context during execution (like do\in)` + arg `block: Block of code to execute with both the injected value and specified parent context` + returns `result of executing the block with both the injected value and modified parent context` + } + + { + equal { c: context { x: 100 } 123 |enter c { .print x } } 123 + equal { c: context { x: 100 } 123 |enter c { add x } } 223 + equal { pipes: context { echo: { .print } into-file: { .print } } 123 |enter pipes { .echo .into-file %data.txt } } 123 + } + + { + } + } section "Flow control " "Functions for conditional execution and branching logic" { diff --git a/tests/formats.info.rye b/tests/formats.info.rye index 43330cb3..440f883a 100644 --- a/tests/formats.info.rye +++ b/tests/formats.info.rye @@ -280,7 +280,7 @@ section "BSON " "BSON encoding and decoding" { } section "SXML " "Streaming, SAX-like XML processing" { - group "reader//do-sxml" + group "reader//Do-sxml" "Processes XML using a streaming SAX-like approach with tag handlers." { argsn 2 diff --git a/tests/io.info.rye b/tests/io.info.rye index 986c904e..4d1e9c48 100644 --- a/tests/io.info.rye +++ b/tests/io.info.rye @@ -1041,6 +1041,79 @@ section "File Monitoring " "File watching and tailing operations" { } +section "Unix Domain Socket Operations " "Unix domain socket operations for IPC" { + group "unix-uri//Open" + "Opens a connection to a Unix domain socket." + { + argsn 1 + argtypes { + 1 [ Uri ] + } + arg `path: uri representing the Unix socket path to connect to` + returns `native unix-connection object` + } + + { + } + + { + } + + group "unix-connection//Write" + "Writes data to a Unix domain socket connection." + { + argsn 2 + argtypes { + 1 [ Native ] + 2 [ String ] + } + arg `connection: native unix-connection object` + arg `data: string to write to the socket` + returns `the connection object if successful (allows chaining)` + } + + { + } + + { + } + + group "unix-connection//Read" + "Reads data from a Unix domain socket connection." + { + argsn 1 + argtypes { + 1 [ Native ] + } + arg `connection: native unix-connection object` + returns `string containing data read from the socket` + } + + { + } + + { + } + + group "unix-connection//Close" + "Closes a Unix domain socket connection." + { + argsn 1 + argtypes { + 1 [ Native ] + } + arg `connection: native unix-connection object` + returns `empty string if successful` + } + + { + } + + { + } + +} + section "Command Operations " "Running other programs" { group "cmd" "Create a command."