Compare commits
4 Commits
v1.0.3
...
1795eb64d1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1795eb64d1 | |||
| 355f0f918f | |||
| 5d3c86119e | |||
| 8c602e3db0 |
@@ -4,10 +4,7 @@
|
||||
"description": "Database Relations Specification Tool for Go",
|
||||
"language": "go"
|
||||
},
|
||||
"agent": {
|
||||
"preferred": "Explore",
|
||||
"description": "Use Explore agent for fast codebase navigation and Go project exploration"
|
||||
},
|
||||
|
||||
"codeStyle": {
|
||||
"useGofmt": true,
|
||||
"lineLength": 100,
|
||||
|
||||
2
Makefile
2
Makefile
@@ -36,7 +36,7 @@ COMPOSE_CMD := $(shell \
|
||||
|
||||
all: lint test build ## Run linting, tests, and build
|
||||
|
||||
build: ## Build the binary
|
||||
build: deps ## Build the binary
|
||||
@echo "Building $(BINARY_NAME)..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/relspec
|
||||
|
||||
11
TODO.md
11
TODO.md
@@ -22,6 +22,17 @@
|
||||
- [✔️] GraphQL schema generation
|
||||
|
||||
|
||||
## UI
|
||||
- [✔️] Basic UI (I went with tview)
|
||||
- [✔️] Save / Load Database
|
||||
- [✔️] Schemas / Domains / Tables
|
||||
- [ ] Add Relations
|
||||
- [ ] Add Indexes
|
||||
- [ ] Add Views
|
||||
- [ ] Add Sequences
|
||||
- [ ] Add Scripts
|
||||
- [ ] Domain / Table Assignment
|
||||
|
||||
## Documentation
|
||||
- [ ] API documentation (godoc)
|
||||
- [ ] Usage examples for each format combination
|
||||
|
||||
334
cmd/relspec/edit.go
Normal file
334
cmd/relspec/edit.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/bun"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/dbml"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/dctx"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/drawdb"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/drizzle"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/gorm"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/graphql"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/json"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/pgsql"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/prisma"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/typeorm"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/yaml"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/ui"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
wbun "git.warky.dev/wdevs/relspecgo/pkg/writers/bun"
|
||||
wdbml "git.warky.dev/wdevs/relspecgo/pkg/writers/dbml"
|
||||
wdctx "git.warky.dev/wdevs/relspecgo/pkg/writers/dctx"
|
||||
wdrawdb "git.warky.dev/wdevs/relspecgo/pkg/writers/drawdb"
|
||||
wdrizzle "git.warky.dev/wdevs/relspecgo/pkg/writers/drizzle"
|
||||
wgorm "git.warky.dev/wdevs/relspecgo/pkg/writers/gorm"
|
||||
wgraphql "git.warky.dev/wdevs/relspecgo/pkg/writers/graphql"
|
||||
wjson "git.warky.dev/wdevs/relspecgo/pkg/writers/json"
|
||||
wpgsql "git.warky.dev/wdevs/relspecgo/pkg/writers/pgsql"
|
||||
wprisma "git.warky.dev/wdevs/relspecgo/pkg/writers/prisma"
|
||||
wtypeorm "git.warky.dev/wdevs/relspecgo/pkg/writers/typeorm"
|
||||
wyaml "git.warky.dev/wdevs/relspecgo/pkg/writers/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
editSourceType string
|
||||
editSourcePath string
|
||||
editSourceConn string
|
||||
editTargetType string
|
||||
editTargetPath string
|
||||
editSchemaFilter string
|
||||
)
|
||||
|
||||
var editCmd = &cobra.Command{
|
||||
Use: "edit",
|
||||
Short: "Edit database schema interactively with TUI",
|
||||
Long: `Edit database schemas from various formats using an interactive terminal UI.
|
||||
|
||||
Allows you to:
|
||||
- List and navigate schemas and tables
|
||||
- Create, edit, and delete schemas
|
||||
- Create, edit, and delete tables
|
||||
- Add, edit, and delete columns
|
||||
- Set table and column properties
|
||||
- Add constraints, indexes, and relationships
|
||||
|
||||
Supports reading from and writing to all supported formats:
|
||||
Input formats:
|
||||
- dbml: DBML schema files
|
||||
- dctx: DCTX schema files
|
||||
- drawdb: DrawDB JSON files
|
||||
- graphql: GraphQL schema files (.graphql, SDL)
|
||||
- json: JSON database schema
|
||||
- yaml: YAML database schema
|
||||
- gorm: GORM model files (Go, file or directory)
|
||||
- bun: Bun model files (Go, file or directory)
|
||||
- drizzle: Drizzle ORM schema files (TypeScript, file or directory)
|
||||
- prisma: Prisma schema files (.prisma)
|
||||
- typeorm: TypeORM entity files (TypeScript)
|
||||
- pgsql: PostgreSQL database (live connection)
|
||||
|
||||
Output formats:
|
||||
- dbml: DBML schema files
|
||||
- dctx: DCTX schema files
|
||||
- drawdb: DrawDB JSON files
|
||||
- graphql: GraphQL schema files (.graphql, SDL)
|
||||
- json: JSON database schema
|
||||
- yaml: YAML database schema
|
||||
- gorm: GORM model files (Go)
|
||||
- bun: Bun model files (Go)
|
||||
- drizzle: Drizzle ORM schema files (TypeScript)
|
||||
- prisma: Prisma schema files (.prisma)
|
||||
- typeorm: TypeORM entity files (TypeScript)
|
||||
- pgsql: PostgreSQL SQL schema
|
||||
|
||||
PostgreSQL Connection String Examples:
|
||||
postgres://username:password@localhost:5432/database_name
|
||||
postgres://username:password@localhost/database_name
|
||||
postgresql://user:pass@host:5432/dbname?sslmode=disable
|
||||
postgresql://user:pass@host/dbname?sslmode=require
|
||||
host=localhost port=5432 user=username password=pass dbname=mydb sslmode=disable
|
||||
|
||||
Examples:
|
||||
# Edit a DBML schema file
|
||||
relspec edit --from dbml --from-path schema.dbml --to dbml --to-path schema.dbml
|
||||
|
||||
# Edit a PostgreSQL database
|
||||
relspec edit --from pgsql --from-conn "postgres://user:pass@localhost/mydb" \
|
||||
--to pgsql --to-conn "postgres://user:pass@localhost/mydb"
|
||||
|
||||
# Edit JSON schema and output to GORM
|
||||
relspec edit --from json --from-path db.json --to gorm --to-path models/
|
||||
|
||||
# Edit GORM models in place
|
||||
relspec edit --from gorm --from-path ./models --to gorm --to-path ./models`,
|
||||
RunE: runEdit,
|
||||
}
|
||||
|
||||
func init() {
|
||||
editCmd.Flags().StringVar(&editSourceType, "from", "", "Source format (dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql)")
|
||||
editCmd.Flags().StringVar(&editSourcePath, "from-path", "", "Source file path (for file-based formats)")
|
||||
editCmd.Flags().StringVar(&editSourceConn, "from-conn", "", "Source connection string (for database formats)")
|
||||
editCmd.Flags().StringVar(&editTargetType, "to", "", "Target format (dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql)")
|
||||
editCmd.Flags().StringVar(&editTargetPath, "to-path", "", "Target file path (for file-based formats)")
|
||||
editCmd.Flags().StringVar(&editSchemaFilter, "schema", "", "Filter to a specific schema by name")
|
||||
|
||||
// Flags are now optional - if not provided, UI will prompt for load/save options
|
||||
}
|
||||
|
||||
func runEdit(cmd *cobra.Command, args []string) error {
|
||||
fmt.Fprintf(os.Stderr, "\n=== RelSpec Schema Editor ===\n")
|
||||
fmt.Fprintf(os.Stderr, "Started at: %s\n\n", getCurrentTimestamp())
|
||||
|
||||
var db *models.Database
|
||||
var loadConfig *ui.LoadConfig
|
||||
var saveConfig *ui.SaveConfig
|
||||
var err error
|
||||
|
||||
// Check if source parameters are provided
|
||||
if editSourceType != "" {
|
||||
// Read source database
|
||||
fmt.Fprintf(os.Stderr, "[1/3] Reading source schema...\n")
|
||||
fmt.Fprintf(os.Stderr, " Format: %s\n", editSourceType)
|
||||
if editSourcePath != "" {
|
||||
fmt.Fprintf(os.Stderr, " Path: %s\n", editSourcePath)
|
||||
}
|
||||
if editSourceConn != "" {
|
||||
fmt.Fprintf(os.Stderr, " Conn: %s\n", maskPassword(editSourceConn))
|
||||
}
|
||||
|
||||
db, err = readDatabaseForEdit(editSourceType, editSourcePath, editSourceConn, "Source")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read source: %w", err)
|
||||
}
|
||||
|
||||
// Apply schema filter if specified
|
||||
if editSchemaFilter != "" {
|
||||
db = filterDatabaseBySchema(db, editSchemaFilter)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, " ✓ Successfully read database '%s'\n", db.Name)
|
||||
fmt.Fprintf(os.Stderr, " Found: %d schema(s)\n", len(db.Schemas))
|
||||
|
||||
totalTables := 0
|
||||
for _, schema := range db.Schemas {
|
||||
totalTables += len(schema.Tables)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, " Found: %d table(s)\n\n", totalTables)
|
||||
|
||||
// Store load config
|
||||
loadConfig = &ui.LoadConfig{
|
||||
SourceType: editSourceType,
|
||||
FilePath: editSourcePath,
|
||||
ConnString: editSourceConn,
|
||||
}
|
||||
} else {
|
||||
// No source parameters provided, UI will show load screen
|
||||
fmt.Fprintf(os.Stderr, "[1/2] No source specified, editor will prompt for database\n\n")
|
||||
}
|
||||
|
||||
// Store save config if target parameters are provided
|
||||
if editTargetType != "" {
|
||||
saveConfig = &ui.SaveConfig{
|
||||
TargetType: editTargetType,
|
||||
FilePath: editTargetPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Launch interactive TUI
|
||||
if editSourceType != "" {
|
||||
fmt.Fprintf(os.Stderr, "[2/3] Launching interactive editor...\n")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[2/2] Launching interactive editor...\n")
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, " Use arrow keys and shortcuts to navigate\n")
|
||||
fmt.Fprintf(os.Stderr, " Press ? for help\n\n")
|
||||
|
||||
editor := ui.NewSchemaEditorWithConfigs(db, loadConfig, saveConfig)
|
||||
if err := editor.Run(); err != nil {
|
||||
return fmt.Errorf("editor failed: %w", err)
|
||||
}
|
||||
|
||||
// Only write to output if target parameters were provided and database was loaded from command line
|
||||
if editTargetType != "" && editSourceType != "" && db != nil {
|
||||
fmt.Fprintf(os.Stderr, "[3/3] Writing changes to output...\n")
|
||||
fmt.Fprintf(os.Stderr, " Format: %s\n", editTargetType)
|
||||
if editTargetPath != "" {
|
||||
fmt.Fprintf(os.Stderr, " Path: %s\n", editTargetPath)
|
||||
}
|
||||
|
||||
// Get the potentially modified database from the editor
|
||||
err = writeDatabaseForEdit(editTargetType, editTargetPath, "", editor.GetDatabase(), "Target")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write output: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, " ✓ Successfully written database\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\n=== Edit complete ===\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readDatabaseForEdit(dbType, filePath, connString, label string) (*models.Database, error) {
|
||||
var reader readers.Reader
|
||||
|
||||
switch strings.ToLower(dbType) {
|
||||
case "dbml":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for DBML format", label)
|
||||
}
|
||||
reader = dbml.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "dctx":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for DCTX format", label)
|
||||
}
|
||||
reader = dctx.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "drawdb":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for DrawDB format", label)
|
||||
}
|
||||
reader = drawdb.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "graphql":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for GraphQL format", label)
|
||||
}
|
||||
reader = graphql.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "json":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for JSON format", label)
|
||||
}
|
||||
reader = json.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "yaml":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for YAML format", label)
|
||||
}
|
||||
reader = yaml.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "gorm":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for GORM format", label)
|
||||
}
|
||||
reader = gorm.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "bun":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for Bun format", label)
|
||||
}
|
||||
reader = bun.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "drizzle":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for Drizzle format", label)
|
||||
}
|
||||
reader = drizzle.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "prisma":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for Prisma format", label)
|
||||
}
|
||||
reader = prisma.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "typeorm":
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("%s: file path is required for TypeORM format", label)
|
||||
}
|
||||
reader = typeorm.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "pgsql":
|
||||
if connString == "" {
|
||||
return nil, fmt.Errorf("%s: connection string is required for PostgreSQL format", label)
|
||||
}
|
||||
reader = pgsql.NewReader(&readers.ReaderOptions{ConnectionString: connString})
|
||||
default:
|
||||
return nil, fmt.Errorf("%s: unsupported format: %s", label, dbType)
|
||||
}
|
||||
|
||||
db, err := reader.ReadDatabase()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", label, err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func writeDatabaseForEdit(dbType, filePath, connString string, db *models.Database, label string) error {
|
||||
var writer writers.Writer
|
||||
|
||||
switch strings.ToLower(dbType) {
|
||||
case "dbml":
|
||||
writer = wdbml.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "dctx":
|
||||
writer = wdctx.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "drawdb":
|
||||
writer = wdrawdb.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "graphql":
|
||||
writer = wgraphql.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "json":
|
||||
writer = wjson.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "yaml":
|
||||
writer = wyaml.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "gorm":
|
||||
writer = wgorm.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "bun":
|
||||
writer = wbun.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "drizzle":
|
||||
writer = wdrizzle.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "prisma":
|
||||
writer = wprisma.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "typeorm":
|
||||
writer = wtypeorm.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "pgsql":
|
||||
writer = wpgsql.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
default:
|
||||
return fmt.Errorf("%s: unsupported format: %s", label, dbType)
|
||||
}
|
||||
|
||||
err := writer.WriteDatabase(db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", label, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -20,4 +20,6 @@ func init() {
|
||||
rootCmd.AddCommand(diffCmd)
|
||||
rootCmd.AddCommand(inspectCmd)
|
||||
rootCmd.AddCommand(scriptsCmd)
|
||||
rootCmd.AddCommand(templCmd)
|
||||
rootCmd.AddCommand(editCmd)
|
||||
}
|
||||
|
||||
167
cmd/relspec/templ.go
Normal file
167
cmd/relspec/templ.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
wtemplate "git.warky.dev/wdevs/relspecgo/pkg/writers/template"
|
||||
)
|
||||
|
||||
var (
|
||||
templSourceType string
|
||||
templSourcePath string
|
||||
templSourceConn string
|
||||
templTemplatePath string
|
||||
templOutputPath string
|
||||
templSchemaFilter string
|
||||
templMode string
|
||||
templFilenamePattern string
|
||||
)
|
||||
|
||||
var templCmd = &cobra.Command{
|
||||
Use: "templ",
|
||||
Short: "Apply custom templates to database schemas",
|
||||
Long: `Apply custom Go text templates to database schemas with flexible execution modes.
|
||||
|
||||
The templ command allows you to transform database schemas using custom Go text
|
||||
templates. It supports multiple execution modes for different use cases:
|
||||
|
||||
Execution Modes:
|
||||
database Execute template once for entire database (single output file)
|
||||
schema Execute template once per schema (one file per schema)
|
||||
script Execute template once per script (one file per script)
|
||||
table Execute template once per table (one file per table)
|
||||
|
||||
Supported Input Formats:
|
||||
dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql
|
||||
|
||||
Template Functions:
|
||||
String utilities: toUpper, toLower, toCamelCase, toPascalCase, toSnakeCase, toKebabCase,
|
||||
pluralize, singularize, title, trim, split, join, replace
|
||||
|
||||
Type conversion: sqlToGo, sqlToTypeScript, sqlToJava, sqlToPython, sqlToRust,
|
||||
sqlToCSharp, sqlToPhp
|
||||
|
||||
Filtering: filterTables, filterColumns, filterPrimaryKeys, filterForeignKeys,
|
||||
filterNullable, filterNotNull, filterColumnsByType
|
||||
|
||||
Formatting: toJSON, toJSONPretty, toYAML, indent, escape, comment
|
||||
|
||||
Loop helpers: enumerate, batch, reverse, first, last, skip, take, concat,
|
||||
unique, sortBy, groupBy
|
||||
|
||||
Safe access: get, getOr, getPath, has, keys, values, merge, pick, omit,
|
||||
sliceContains, indexOf, pluck
|
||||
|
||||
Examples:
|
||||
# Generate documentation from PostgreSQL database
|
||||
relspec templ --from pgsql --from-conn "postgres://user:pass@localhost/db" \
|
||||
--template docs.tmpl --output schema-docs.md
|
||||
|
||||
# Generate one TypeScript model file per table
|
||||
relspec templ --from dbml --from-path schema.dbml \
|
||||
--template ts-model.tmpl --mode table \
|
||||
--output ./models/ \
|
||||
--filename-pattern "{{.Name | toCamelCase}}.ts"
|
||||
|
||||
# Generate schema documentation files
|
||||
relspec templ --from json --from-path db.json \
|
||||
--template schema.tmpl --mode schema \
|
||||
--output ./docs/ \
|
||||
--filename-pattern "{{.Name}}_schema.md"`,
|
||||
RunE: runTempl,
|
||||
}
|
||||
|
||||
func init() {
|
||||
templCmd.Flags().StringVar(&templSourceType, "from", "", "Source format (dbml, pgsql, json, etc.)")
|
||||
templCmd.Flags().StringVar(&templSourcePath, "from-path", "", "Source file path (for file-based sources)")
|
||||
templCmd.Flags().StringVar(&templSourceConn, "from-conn", "", "Source connection string (for database sources)")
|
||||
templCmd.Flags().StringVar(&templTemplatePath, "template", "", "Template file path (required)")
|
||||
templCmd.Flags().StringVar(&templOutputPath, "output", "", "Output path (file or directory, empty for stdout)")
|
||||
templCmd.Flags().StringVar(&templSchemaFilter, "schema", "", "Filter to specific schema")
|
||||
templCmd.Flags().StringVar(&templMode, "mode", "database", "Execution mode: database, schema, script, or table")
|
||||
templCmd.Flags().StringVar(&templFilenamePattern, "filename-pattern", "{{.Name}}.txt", "Filename pattern for multi-output modes")
|
||||
|
||||
_ = templCmd.MarkFlagRequired("from")
|
||||
_ = templCmd.MarkFlagRequired("template")
|
||||
}
|
||||
|
||||
func runTempl(cmd *cobra.Command, args []string) error {
|
||||
// Print header
|
||||
fmt.Fprintf(os.Stderr, "=== RelSpec Template Execution ===\n")
|
||||
fmt.Fprintf(os.Stderr, "Started at: %s\n\n", getCurrentTimestamp())
|
||||
|
||||
// Read database using the same function as convert
|
||||
fmt.Fprintf(os.Stderr, "Reading from %s...\n", templSourceType)
|
||||
db, err := readDatabaseForConvert(templSourceType, templSourcePath, templSourceConn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read source: %w", err)
|
||||
}
|
||||
|
||||
// Print database stats
|
||||
schemaCount := len(db.Schemas)
|
||||
tableCount := 0
|
||||
for _, schema := range db.Schemas {
|
||||
tableCount += len(schema.Tables)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "✓ Successfully read database: %s\n", db.Name)
|
||||
fmt.Fprintf(os.Stderr, " Schemas: %d\n", schemaCount)
|
||||
fmt.Fprintf(os.Stderr, " Tables: %d\n\n", tableCount)
|
||||
|
||||
// Apply schema filter if specified
|
||||
if templSchemaFilter != "" {
|
||||
fmt.Fprintf(os.Stderr, "Filtering to schema: %s\n", templSchemaFilter)
|
||||
found := false
|
||||
for _, schema := range db.Schemas {
|
||||
if schema.Name == templSchemaFilter {
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("schema not found: %s", templSchemaFilter)
|
||||
}
|
||||
}
|
||||
|
||||
// Create template writer
|
||||
fmt.Fprintf(os.Stderr, "Loading template: %s\n", templTemplatePath)
|
||||
fmt.Fprintf(os.Stderr, "Execution mode: %s\n", templMode)
|
||||
|
||||
metadata := map[string]interface{}{
|
||||
"template_path": templTemplatePath,
|
||||
"mode": templMode,
|
||||
"filename_pattern": templFilenamePattern,
|
||||
}
|
||||
|
||||
writerOpts := &writers.WriterOptions{
|
||||
OutputPath: templOutputPath,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
writer, err := wtemplate.NewWriter(writerOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create template writer: %w", err)
|
||||
}
|
||||
|
||||
// Execute template
|
||||
fmt.Fprintf(os.Stderr, "\nExecuting template...\n")
|
||||
if err := writer.WriteDatabase(db); err != nil {
|
||||
return fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
// Print success message
|
||||
fmt.Fprintf(os.Stderr, "\n✓ Template executed successfully\n")
|
||||
if templOutputPath != "" {
|
||||
fmt.Fprintf(os.Stderr, "Output written to: %s\n", templOutputPath)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Output written to stdout\n")
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Completed at: %s\n", getCurrentTimestamp())
|
||||
|
||||
return nil
|
||||
}
|
||||
149
docs/DOMAINS_DRAWDB.md
Normal file
149
docs/DOMAINS_DRAWDB.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Domains and DrawDB Areas Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Domains provide a way to organize tables from potentially multiple schemas into logical business groupings. When working with DrawDB format, domains are automatically imported/exported as **Subject Areas** - a native DrawDB feature for visually grouping tables.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Writing Domains to DrawDB (Export)
|
||||
|
||||
When you export a database with domains to DrawDB format:
|
||||
|
||||
1. **Schema Areas** are created automatically for each schema (existing behavior)
|
||||
2. **Domain Areas** are created for each domain, calculated based on the positions of the tables they contain
|
||||
3. The domain area bounds are automatically calculated to encompass all its tables with a small padding
|
||||
|
||||
```go
|
||||
// Example: Creating a domain and exporting to DrawDB
|
||||
db := models.InitDatabase("mydb")
|
||||
|
||||
// Create an "authentication" domain
|
||||
authDomain := models.InitDomain("authentication")
|
||||
authDomain.Tables = append(authDomain.Tables,
|
||||
models.InitDomainTable("users", "public"),
|
||||
models.InitDomainTable("roles", "public"),
|
||||
models.InitDomainTable("permissions", "public"),
|
||||
)
|
||||
db.Domains = append(db.Domains, authDomain)
|
||||
|
||||
// Create a "financial" domain spanning multiple schemas
|
||||
finDomain := models.InitDomain("financial")
|
||||
finDomain.Tables = append(finDomain.Tables,
|
||||
models.InitDomainTable("accounts", "public"),
|
||||
models.InitDomainTable("transactions", "public"),
|
||||
models.InitDomainTable("ledger", "finance"), // Different schema!
|
||||
)
|
||||
db.Domains = append(db.Domains, finDomain)
|
||||
|
||||
// Write to DrawDB - domains become subject areas
|
||||
writer := drawdb.NewWriter(&writers.WriterOptions{
|
||||
OutputPath: "schema.json",
|
||||
})
|
||||
writer.WriteDatabase(db)
|
||||
```
|
||||
|
||||
The resulting DrawDB JSON will have Subject Areas for both:
|
||||
- "authentication" area containing the auth tables
|
||||
- "financial" area containing the financial tables from both schemas
|
||||
|
||||
### Reading Domains from DrawDB (Import)
|
||||
|
||||
When you import a DrawDB file with Subject Areas:
|
||||
|
||||
1. **Subject Areas** are automatically converted to **Domains**
|
||||
2. Tables are assigned to a domain if they fall within the area's visual bounds
|
||||
3. Table references include both the table name and schema name
|
||||
|
||||
```go
|
||||
// Example: Reading DrawDB with areas
|
||||
reader := drawdb.NewReader(&readers.ReaderOptions{
|
||||
FilePath: "schema.json",
|
||||
})
|
||||
|
||||
db, err := reader.ReadDatabase()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Access domains
|
||||
for _, domain := range db.Domains {
|
||||
fmt.Printf("Domain: %s\n", domain.Name)
|
||||
for _, domainTable := range domain.Tables {
|
||||
fmt.Printf(" - %s.%s\n", domainTable.SchemaName, domainTable.TableName)
|
||||
|
||||
// Access the actual table reference if loaded
|
||||
if domainTable.RefTable != nil {
|
||||
fmt.Printf(" Description: %s\n", domainTable.RefTable.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Domain Structure
|
||||
|
||||
```go
|
||||
type Domain struct {
|
||||
Name string // Domain name (e.g., "authentication", "user_data")
|
||||
Description string // Optional human-readable description
|
||||
Tables []*DomainTable // Tables belonging to this domain
|
||||
Comment string // Optional comment
|
||||
Metadata map[string]any // Extensible metadata
|
||||
Sequence uint // Ordering hint
|
||||
}
|
||||
|
||||
type DomainTable struct {
|
||||
TableName string // Table name
|
||||
SchemaName string // Schema containing the table
|
||||
Sequence uint // Ordering hint
|
||||
RefTable *Table // Pointer to actual table (in-memory only, not serialized)
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Schema Domains
|
||||
|
||||
One of the key features of domains is that they can span multiple schemas:
|
||||
|
||||
```
|
||||
Domain: "user_data"
|
||||
├── public.users
|
||||
├── public.profiles
|
||||
├── public.user_preferences
|
||||
├── auth.user_sessions
|
||||
└── auth.mfa_devices
|
||||
```
|
||||
|
||||
This allows you to organize related tables even when they're stored in different schemas.
|
||||
|
||||
## Visual Organization in DrawDB
|
||||
|
||||
When viewing the exported DrawDB file in DrawDB Editor:
|
||||
|
||||
1. **Schema areas** appear in one color (original behavior)
|
||||
2. **Domain areas** appear in a different color
|
||||
3. Domain area bounds are calculated to fit all contained tables
|
||||
4. Areas can overlap - a table can visually belong to multiple areas
|
||||
|
||||
## Integration with Other Formats
|
||||
|
||||
Currently, domain/area integration is implemented for DrawDB format.
|
||||
|
||||
To implement similar functionality for other formats:
|
||||
|
||||
1. Identify if the format has a native grouping/area feature
|
||||
2. Add conversion logic in the reader to map format areas → Domain model
|
||||
3. Add conversion logic in the writer to map Domain model → format areas
|
||||
|
||||
Example formats that could support domains:
|
||||
- **DBML**: Could use DBML's `TableGroup` feature
|
||||
- **DrawDB**: ✅ Already implemented (Subject Areas)
|
||||
- **GraphQL**: Could use schema directives
|
||||
- **Custom formats**: Implement as needed
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Keep domains focused**: Each domain should represent a distinct business area
|
||||
2. **Document purposes**: Use Description and Comment fields to explain each domain
|
||||
3. **Use meaningful names**: Domain names should clearly reflect their purpose
|
||||
4. **Maintain schema consistency**: Keep related tables together in the same schema when possible
|
||||
5. **Use metadata**: Store tool-specific information in the Metadata field
|
||||
572
docs/TEMPLATE_MODE.md
Normal file
572
docs/TEMPLATE_MODE.md
Normal file
@@ -0,0 +1,572 @@
|
||||
# RelSpec Template Mode
|
||||
|
||||
The `templ` command allows you to transform database schemas using custom Go text templates. It provides powerful template functions and flexible execution modes for generating any type of output from your database schema.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [Execution Modes](#execution-modes)
|
||||
- [Template Functions](#template-functions)
|
||||
- [String Utilities](#string-utilities)
|
||||
- [Type Conversion](#type-conversion)
|
||||
- [Filtering](#filtering)
|
||||
- [Formatting](#formatting)
|
||||
- [Loop Helpers](#loop-helpers)
|
||||
- [Sorting Helpers](#sorting-helpers)
|
||||
- [Safe Access](#safe-access)
|
||||
- [Utility Functions](#utility-functions)
|
||||
- [Data Model](#data-model)
|
||||
- [Examples](#examples)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Generate documentation from a database
|
||||
relspec templ --from pgsql --from-conn "postgres://user:pass@localhost/db" \
|
||||
--template docs.tmpl --output schema-docs.md
|
||||
|
||||
# Generate TypeScript models (one file per table)
|
||||
relspec templ --from dbml --from-path schema.dbml \
|
||||
--template model.tmpl --mode table \
|
||||
--output ./models/ \
|
||||
--filename-pattern "{{.Name | toCamelCase}}.ts"
|
||||
|
||||
# Output to stdout
|
||||
relspec templ --from json --from-path schema.json \
|
||||
--template report.tmpl
|
||||
```
|
||||
|
||||
## Execution Modes
|
||||
|
||||
The `--mode` flag controls how the template is executed:
|
||||
|
||||
| Mode | Description | Output | When to Use |
|
||||
|------|-------------|--------|-------------|
|
||||
| `database` | Execute once for entire database | Single file | Documentation, reports, overview files |
|
||||
| `schema` | Execute once per schema | One file per schema | Schema-specific documentation |
|
||||
| `domain` | Execute once per domain | One file per domain | Domain-based documentation, domain exports |
|
||||
| `script` | Execute once per script | One file per script | Script processing |
|
||||
| `table` | Execute once per table | One file per table | Model generation, table docs |
|
||||
|
||||
### Filename Patterns
|
||||
|
||||
For multi-file modes (`schema`, `domain`, `script`, `table`), use `--filename-pattern` to control output filenames:
|
||||
|
||||
```bash
|
||||
# Default pattern
|
||||
--filename-pattern "{{.Name}}.txt"
|
||||
|
||||
# With transformations
|
||||
--filename-pattern "{{.Name | toCamelCase}}.ts"
|
||||
|
||||
# Nested directories
|
||||
--filename-pattern "{{.Schema}}/{{.Name}}.md"
|
||||
|
||||
# Complex patterns
|
||||
--filename-pattern "{{.ParentSchema.Name}}/models/{{.Name | toPascalCase}}Model.java"
|
||||
```
|
||||
|
||||
## Template Functions
|
||||
|
||||
### String Utilities
|
||||
|
||||
Transform and manipulate strings in your templates.
|
||||
|
||||
| Function | Description | Example | Output |
|
||||
|----------|-------------|---------|--------|
|
||||
| `toUpper` | Convert to uppercase | `{{ "hello" \| toUpper }}` | `HELLO` |
|
||||
| `toLower` | Convert to lowercase | `{{ "HELLO" \| toLower }}` | `hello` |
|
||||
| `toCamelCase` | Convert to camelCase | `{{ "user_name" \| toCamelCase }}` | `userName` |
|
||||
| `toPascalCase` | Convert to PascalCase | `{{ "user_name" \| toPascalCase }}` | `UserName` |
|
||||
| `toSnakeCase` | Convert to snake_case | `{{ "UserName" \| toSnakeCase }}` | `user_name` |
|
||||
| `toKebabCase` | Convert to kebab-case | `{{ "UserName" \| toKebabCase }}` | `user-name` |
|
||||
| `pluralize` | Convert to plural | `{{ "user" \| pluralize }}` | `users` |
|
||||
| `singularize` | Convert to singular | `{{ "users" \| singularize }}` | `user` |
|
||||
| `title` | Capitalize first letter | `{{ "hello world" \| title }}` | `Hello World` |
|
||||
| `trim` | Trim whitespace | `{{ " hello " \| trim }}` | `hello` |
|
||||
| `trimPrefix` | Remove prefix | `{{ trimPrefix "tbl_users" "tbl_" }}` | `users` |
|
||||
| `trimSuffix` | Remove suffix | `{{ trimSuffix "users_old" "_old" }}` | `users` |
|
||||
| `replace` | Replace occurrences | `{{ replace "hello" "l" "L" -1 }}` | `heLLo` |
|
||||
| `stringContains` | Check if contains substring | `{{ stringContains "hello" "ell" }}` | `true` |
|
||||
| `hasPrefix` | Check if starts with | `{{ hasPrefix "hello" "hel" }}` | `true` |
|
||||
| `hasSuffix` | Check if ends with | `{{ hasSuffix "hello" "llo" }}` | `true` |
|
||||
| `split` | Split by separator | `{{ split "a,b,c" "," }}` | `[a b c]` |
|
||||
| `join` | Join with separator | `{{ join (list "a" "b") "," }}` | `a,b` |
|
||||
|
||||
### Type Conversion
|
||||
|
||||
Convert SQL types to various programming language types.
|
||||
|
||||
| Function | Parameters | Description | Example |
|
||||
|----------|------------|-------------|---------|
|
||||
| `sqlToGo` | `sqlType`, `nullable` | SQL to Go | `{{ sqlToGo "varchar" true }}` → `string` |
|
||||
| `sqlToTypeScript` | `sqlType`, `nullable` | SQL to TypeScript | `{{ sqlToTypeScript "integer" false }}` → `number \| null` |
|
||||
| `sqlToJava` | `sqlType`, `nullable` | SQL to Java | `{{ sqlToJava "varchar" true }}` → `String` |
|
||||
| `sqlToPython` | `sqlType` | SQL to Python | `{{ sqlToPython "integer" }}` → `int` |
|
||||
| `sqlToRust` | `sqlType`, `nullable` | SQL to Rust | `{{ sqlToRust "varchar" false }}` → `Option<String>` |
|
||||
| `sqlToCSharp` | `sqlType`, `nullable` | SQL to C# | `{{ sqlToCSharp "integer" false }}` → `int?` |
|
||||
| `sqlToPhp` | `sqlType`, `nullable` | SQL to PHP | `{{ sqlToPhp "varchar" false }}` → `?string` |
|
||||
|
||||
**Supported SQL Types:**
|
||||
- Integer: `integer`, `int`, `smallint`, `bigint`, `serial`, `bigserial`
|
||||
- String: `text`, `varchar`, `char`, `character`, `citext`
|
||||
- Boolean: `boolean`, `bool`
|
||||
- Float: `real`, `float`, `double precision`, `numeric`, `decimal`
|
||||
- Date/Time: `timestamp`, `date`, `time`, `timestamptz`
|
||||
- Binary: `bytea`
|
||||
- Special: `uuid`, `json`, `jsonb`, `array`
|
||||
|
||||
### Filtering
|
||||
|
||||
Filter and select specific database objects.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `filterTables` | Filter tables by pattern | `{{ filterTables .Schema.Tables "user_*" }}` |
|
||||
| `filterTablesByPattern` | Alias for filterTables | `{{ filterTablesByPattern .Schema.Tables "temp_*" }}` |
|
||||
| `filterColumns` | Filter columns by pattern | `{{ filterColumns .Table.Columns "*_id" }}` |
|
||||
| `filterColumnsByType` | Filter by SQL type | `{{ filterColumnsByType .Table.Columns "varchar" }}` |
|
||||
| `filterPrimaryKeys` | Get primary key columns | `{{ filterPrimaryKeys .Table.Columns }}` |
|
||||
| `filterForeignKeys` | Get foreign key constraints | `{{ filterForeignKeys .Table.Constraints }}` |
|
||||
| `filterUniqueConstraints` | Get unique constraints | `{{ filterUniqueConstraints .Table.Constraints }}` |
|
||||
| `filterCheckConstraints` | Get check constraints | `{{ filterCheckConstraints .Table.Constraints }}` |
|
||||
| `filterNullable` | Get nullable columns | `{{ filterNullable .Table.Columns }}` |
|
||||
| `filterNotNull` | Get non-nullable columns | `{{ filterNotNull .Table.Columns }}` |
|
||||
|
||||
**Pattern Matching:**
|
||||
- `*` - Match any characters
|
||||
- `?` - Match single character
|
||||
- Example: `user_*` matches `user_profile`, `user_settings`
|
||||
|
||||
### Formatting
|
||||
|
||||
Format output and add structure to generated code.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `toJSON` | Convert to JSON | `{{ .Database \| toJSON }}` |
|
||||
| `toJSONPretty` | Pretty-print JSON | `{{ toJSONPretty .Table " " }}` |
|
||||
| `toYAML` | Convert to YAML | `{{ .Schema \| toYAML }}` |
|
||||
| `indent` | Indent by spaces | `{{ indent .Column.Description 4 }}` |
|
||||
| `indentWith` | Indent with prefix | `{{ indentWith .Comment " " }}` |
|
||||
| `escape` | Escape special chars | `{{ escape .Column.Default }}` |
|
||||
| `escapeQuotes` | Escape quotes only | `{{ escapeQuotes .String }}` |
|
||||
| `comment` | Add comment prefix | `{{ comment .Description "//" }}` |
|
||||
| `quoteString` | Add quotes | `{{ quoteString "value" }}` → `"value"` |
|
||||
| `unquoteString` | Remove quotes | `{{ unquoteString "\"value\"" }}` → `value` |
|
||||
|
||||
**Comment Styles:**
|
||||
- `//` - C/Go/JavaScript style
|
||||
- `#` - Python/Shell style
|
||||
- `--` - SQL style
|
||||
- `/* */` - Block comment style
|
||||
|
||||
### Loop Helpers
|
||||
|
||||
Iterate and manipulate collections.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `enumerate` | Add index to items | `{{ range enumerate .Tables }}{{ .Index }}: {{ .Value.Name }}{{ end }}` |
|
||||
| `batch` | Split into chunks | `{{ range batch .Columns 3 }}...{{ end }}` |
|
||||
| `chunk` | Alias for batch | `{{ range chunk .Columns 5 }}...{{ end }}` |
|
||||
| `reverse` | Reverse order | `{{ range reverse .Tables }}...{{ end }}` |
|
||||
| `first` | Get first N items | `{{ range first .Tables 5 }}...{{ end }}` |
|
||||
| `last` | Get last N items | `{{ range last .Tables 3 }}...{{ end }}` |
|
||||
| `skip` | Skip first N items | `{{ range skip .Tables 2 }}...{{ end }}` |
|
||||
| `take` | Take first N (alias) | `{{ range take .Tables 10 }}...{{ end }}` |
|
||||
| `concat` | Concatenate slices | `{{ $all := concat .Schema1.Tables .Schema2.Tables }}` |
|
||||
| `unique` | Remove duplicates | `{{ $unique := unique .Items }}` |
|
||||
| `sortBy` | Sort by field | `{{ $sorted := sortBy .Tables "Name" }}` |
|
||||
| `groupBy` | Group by field | `{{ $grouped := groupBy .Tables "Schema" }}` |
|
||||
|
||||
### Sorting Helpers
|
||||
|
||||
Sort database objects by name or sequence number. All sort functions modify the slice in-place.
|
||||
|
||||
**Schema Sorting:**
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sortSchemasByName` | Sort schemas by name | `{{ sortSchemasByName .Database.Schemas false }}` |
|
||||
| `sortSchemasBySequence` | Sort schemas by sequence | `{{ sortSchemasBySequence .Database.Schemas false }}` |
|
||||
|
||||
**Table Sorting:**
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sortTablesByName` | Sort tables by name | `{{ sortTablesByName .Schema.Tables false }}` |
|
||||
| `sortTablesBySequence` | Sort tables by sequence | `{{ sortTablesBySequence .Schema.Tables true }}` |
|
||||
|
||||
**Column Sorting:**
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sortColumnsMapByName` | Convert column map to sorted slice by name | `{{ $cols := sortColumnsMapByName .Table.Columns false }}` |
|
||||
| `sortColumnsMapBySequence` | Convert column map to sorted slice by sequence | `{{ $cols := sortColumnsMapBySequence .Table.Columns false }}` |
|
||||
| `sortColumnsByName` | Sort column slice by name | `{{ sortColumnsByName $columns false }}` |
|
||||
| `sortColumnsBySequence` | Sort column slice by sequence | `{{ sortColumnsBySequence $columns true }}` |
|
||||
|
||||
**Other Object Sorting:**
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sortViewsByName` | Sort views by name | `{{ sortViewsByName .Schema.Views false }}` |
|
||||
| `sortViewsBySequence` | Sort views by sequence | `{{ sortViewsBySequence .Schema.Views false }}` |
|
||||
| `sortSequencesByName` | Sort sequences by name | `{{ sortSequencesByName .Schema.Sequences false }}` |
|
||||
| `sortSequencesBySequence` | Sort sequences by sequence | `{{ sortSequencesBySequence .Schema.Sequences false }}` |
|
||||
| `sortIndexesMapByName` | Convert index map to sorted slice by name | `{{ $idx := sortIndexesMapByName .Table.Indexes false }}` |
|
||||
| `sortIndexesMapBySequence` | Convert index map to sorted slice by sequence | `{{ $idx := sortIndexesMapBySequence .Table.Indexes false }}` |
|
||||
| `sortIndexesByName` | Sort index slice by name | `{{ sortIndexesByName $indexes false }}` |
|
||||
| `sortIndexesBySequence` | Sort index slice by sequence | `{{ sortIndexesBySequence $indexes false }}` |
|
||||
| `sortConstraintsMapByName` | Convert constraint map to sorted slice by name | `{{ $cons := sortConstraintsMapByName .Table.Constraints false }}` |
|
||||
| `sortConstraintsByName` | Sort constraint slice by name | `{{ sortConstraintsByName $constraints false }}` |
|
||||
| `sortRelationshipsMapByName` | Convert relationship map to sorted slice by name | `{{ $rels := sortRelationshipsMapByName .Table.Relationships false }}` |
|
||||
| `sortRelationshipsByName` | Sort relationship slice by name | `{{ sortRelationshipsByName $relationships false }}` |
|
||||
| `sortScriptsByName` | Sort scripts by name | `{{ sortScriptsByName .Schema.Scripts false }}` |
|
||||
| `sortEnumsByName` | Sort enums by name | `{{ sortEnumsByName .Schema.Enums false }}` |
|
||||
|
||||
**Sort Parameters:**
|
||||
- Second parameter: `false` = ascending, `true` = descending
|
||||
- Example: `{{ sortTablesByName .Schema.Tables true }}` sorts descending (Z-A)
|
||||
|
||||
### Safe Access
|
||||
|
||||
Safely access nested data without panicking.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `get` | Get map value | `{{ get .Metadata "key" }}` |
|
||||
| `getOr` | Get with default | `{{ getOr .Metadata "key" "default" }}` |
|
||||
| `getPath` | Nested access | `{{ getPath .Config "database.host" }}` |
|
||||
| `getPathOr` | Nested with default | `{{ getPathOr .Config "db.port" 5432 }}` |
|
||||
| `safeIndex` | Safe array access | `{{ safeIndex .Tables 0 }}` |
|
||||
| `safeIndexOr` | Safe with default | `{{ safeIndexOr .Tables 0 nil }}` |
|
||||
| `has` | Check key exists | `{{ if has .Metadata "key" }}...{{ end }}` |
|
||||
| `hasPath` | Check nested path | `{{ if hasPath .Config "db.host" }}...{{ end }}` |
|
||||
| `keys` | Get map keys | `{{ range keys .Metadata }}...{{ end }}` |
|
||||
| `values` | Get map values | `{{ range values .Table.Columns }}...{{ end }}` |
|
||||
| `merge` | Merge maps | `{{ $merged := merge .Map1 .Map2 }}` |
|
||||
| `pick` | Select keys | `{{ $subset := pick .Metadata "name" "desc" }}` |
|
||||
| `omit` | Exclude keys | `{{ $filtered := omit .Metadata "internal" }}` |
|
||||
| `sliceContains` | Check contains | `{{ if sliceContains .Names "admin" }}...{{ end }}` |
|
||||
| `indexOf` | Find index | `{{ $idx := indexOf .Names "admin" }}` |
|
||||
| `pluck` | Extract field | `{{ $names := pluck .Tables "Name" }}` |
|
||||
|
||||
### Utility Functions
|
||||
|
||||
General-purpose template helpers.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `add` | Add numbers | `{{ add 5 3 }}` → `8` |
|
||||
| `sub` | Subtract | `{{ sub 10 3 }}` → `7` |
|
||||
| `mul` | Multiply | `{{ mul 4 5 }}` → `20` |
|
||||
| `div` | Divide | `{{ div 10 2 }}` → `5` |
|
||||
| `mod` | Modulo | `{{ mod 10 3 }}` → `1` |
|
||||
| `default` | Default value | `{{ default "unknown" .Name }}` |
|
||||
| `dict` | Create map | `{{ $m := dict "key1" "val1" "key2" "val2" }}` |
|
||||
| `list` | Create list | `{{ $l := list "a" "b" "c" }}` |
|
||||
| `seq` | Number sequence | `{{ range seq 1 5 }}{{ . }}{{ end }}` → `12345` |
|
||||
|
||||
## Data Model
|
||||
|
||||
The data available in templates depends on the execution mode:
|
||||
|
||||
### Database Mode
|
||||
```go
|
||||
.Database // *models.Database - Full database
|
||||
.ParentDatabase // *models.Database - Same as .Database
|
||||
.FlatColumns // []*models.FlatColumn - All columns flattened
|
||||
.FlatTables // []*models.FlatTable - All tables flattened
|
||||
.FlatConstraints // []*models.FlatConstraint - All constraints
|
||||
.FlatRelationships // []*models.FlatRelationship - All relationships
|
||||
.Summary // *models.DatabaseSummary - Statistics
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Schema Mode
|
||||
```go
|
||||
.Schema // *models.Schema - Current schema
|
||||
.ParentDatabase // *models.Database - Parent database context
|
||||
.FlatColumns // []*models.FlatColumn - Schema's columns flattened
|
||||
.FlatTables // []*models.FlatTable - Schema's tables flattened
|
||||
.FlatConstraints // []*models.FlatConstraint - Schema's constraints
|
||||
.FlatRelationships // []*models.FlatRelationship - Schema's relationships
|
||||
.Summary // *models.DatabaseSummary - Statistics
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Domain Mode
|
||||
```go
|
||||
.Domain // *models.Domain - Current domain
|
||||
.ParentDatabase // *models.Database - Parent database context
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Table Mode
|
||||
```go
|
||||
.Table // *models.Table - Current table
|
||||
.ParentSchema // *models.Schema - Parent schema
|
||||
.ParentDatabase // *models.Database - Parent database context
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Script Mode
|
||||
```go
|
||||
.Script // *models.Script - Current script
|
||||
.ParentSchema // *models.Schema - Parent schema
|
||||
.ParentDatabase // *models.Database - Parent database context
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Model Structures
|
||||
|
||||
**Database:**
|
||||
- `.Name` - Database name
|
||||
- `.Schemas` - List of schemas
|
||||
- `.Domains` - List of domains (business domain groupings)
|
||||
- `.Description`, `.Comment` - Documentation
|
||||
|
||||
**Schema:**
|
||||
- `.Name` - Schema name
|
||||
- `.Tables` - List of tables
|
||||
- `.Views`, `.Sequences`, `.Scripts` - Other objects
|
||||
- `.Enums` - Enum types
|
||||
|
||||
**Domain:**
|
||||
- `.Name` - Domain name
|
||||
- `.Tables` - List of DomainTable references
|
||||
- `.Description`, `.Comment` - Documentation
|
||||
- `.Metadata` - Custom metadata map
|
||||
|
||||
**DomainTable:**
|
||||
- `.TableName` - Name of the table
|
||||
- `.SchemaName` - Schema containing the table
|
||||
- `.RefTable` - Pointer to actual Table object (if loaded)
|
||||
|
||||
**Table:**
|
||||
- `.Name` - Table name
|
||||
- `.Schema` - Schema name
|
||||
- `.Columns` - Map of columns (use `values` function to iterate)
|
||||
- `.Constraints` - Map of constraints
|
||||
- `.Indexes` - Map of indexes
|
||||
- `.Relationships` - Map of relationships
|
||||
- `.Description`, `.Comment` - Documentation
|
||||
|
||||
**Column:**
|
||||
- `.Name` - Column name
|
||||
- `.Type` - SQL type
|
||||
- `.NotNull` - Is NOT NULL
|
||||
- `.IsPrimaryKey` - Is primary key
|
||||
- `.Default` - Default value
|
||||
- `.Description`, `.Comment` - Documentation
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: TypeScript Interfaces (Table Mode)
|
||||
|
||||
**Template:** `typescript-interface.tmpl`
|
||||
```typescript
|
||||
// Generated from {{ .ParentDatabase.Name }}.{{ .ParentSchema.Name }}.{{ .Table.Name }}
|
||||
|
||||
export interface {{ .Table.Name | toPascalCase }} {
|
||||
{{- range .Table.Columns | values }}
|
||||
{{ .Name | toCamelCase }}: {{ sqlToTypeScript .Type .NotNull }};
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{- $fks := filterForeignKeys .Table.Constraints }}
|
||||
{{- if $fks }}
|
||||
|
||||
// Foreign Keys:
|
||||
{{- range $fks }}
|
||||
// - {{ .Name }}: references {{ .ReferencedTable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
**Command:**
|
||||
```bash
|
||||
relspec templ --from pgsql --from-conn "..." \
|
||||
--template typescript-interface.tmpl \
|
||||
--mode table \
|
||||
--output ./src/types/ \
|
||||
--filename-pattern "{{.Name | toCamelCase}}.ts"
|
||||
```
|
||||
|
||||
### Example 2: Markdown Documentation (Database Mode)
|
||||
|
||||
**Template:** `database-docs.tmpl`
|
||||
```markdown
|
||||
# Database: {{ .Database.Name }}
|
||||
|
||||
{{ if .Database.Description }}{{ .Database.Description }}{{ end }}
|
||||
|
||||
**Statistics:**
|
||||
- Schemas: {{ len .Database.Schemas }}
|
||||
- Tables: {{ .Summary.TotalTables }}
|
||||
- Columns: {{ .Summary.TotalColumns }}
|
||||
|
||||
{{ range .Database.Schemas }}
|
||||
## Schema: {{ .Name }}
|
||||
|
||||
{{ range .Tables }}
|
||||
### {{ .Name }}
|
||||
|
||||
{{ if .Description }}{{ .Description }}{{ end }}
|
||||
|
||||
**Columns:**
|
||||
|
||||
| Column | Type | Nullable | PK | Description |
|
||||
|--------|------|----------|----|----|
|
||||
{{- range .Columns | values }}
|
||||
| {{ .Name }} | `{{ .Type }}` | {{ if .NotNull }}No{{ else }}Yes{{ end }} | {{ if .IsPrimaryKey }}✓{{ end }} | {{ .Description }} |
|
||||
{{- end }}
|
||||
|
||||
{{- $fks := filterForeignKeys .Constraints }}
|
||||
{{- if $fks }}
|
||||
|
||||
**Foreign Keys:**
|
||||
|
||||
{{ range $fks }}
|
||||
- `{{ .Name }}`: {{ join .Columns ", " }} → {{ .ReferencedTable }}({{ join .ReferencedColumns ", " }})
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
### Example 3: Python SQLAlchemy Models (Table Mode)
|
||||
|
||||
**Template:** `python-model.tmpl`
|
||||
```python
|
||||
"""{{ .Table.Name | toPascalCase }} model for {{ .ParentDatabase.Name }}.{{ .ParentSchema.Name }}"""
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class {{ .Table.Name | toPascalCase }}(Base):
|
||||
"""{{ if .Table.Description }}{{ .Table.Description }}{{ else }}{{ .Table.Name }} table{{ end }}"""
|
||||
|
||||
__tablename__ = "{{ .Table.Name }}"
|
||||
__table_args__ = {"schema": "{{ .ParentSchema.Name }}"}
|
||||
|
||||
{{- range .Table.Columns | values }}
|
||||
{{ .Name }} = Column({{ sqlToPython .Type }}{{ if .IsPrimaryKey }}, primary_key=True{{ end }}{{ if .NotNull }}, nullable=False{{ end }})
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
### Example 4: GraphQL Schema (Schema Mode)
|
||||
|
||||
**Template:** `graphql-schema.tmpl`
|
||||
```graphql
|
||||
"""{{ .Schema.Name }} schema"""
|
||||
|
||||
{{ range .Schema.Tables }}
|
||||
type {{ .Name | toPascalCase }} {
|
||||
{{- range .Columns | values }}
|
||||
{{ .Name | toCamelCase }}: {{ sqlToTypeScript .Type .NotNull | replace " | null" "" }}{{ if not .NotNull }}{{ end }}
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
input {{ .Name | toPascalCase }}Input {
|
||||
{{- $cols := filterNotNull .Columns | filterPrimaryKeys }}
|
||||
{{- range $cols }}
|
||||
{{ .Name | toCamelCase }}: {{ sqlToTypeScript .Type true | replace " | null" "" }}!
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
### Example 5: SQL Migration (Database Mode)
|
||||
|
||||
**Template:** `migration.tmpl`
|
||||
```sql
|
||||
-- Migration for {{ .Database.Name }}
|
||||
-- Generated: {{ .Metadata.timestamp }}
|
||||
|
||||
BEGIN;
|
||||
|
||||
{{ range .Database.Schemas }}
|
||||
-- Schema: {{ .Name }}
|
||||
CREATE SCHEMA IF NOT EXISTS {{ .Name }};
|
||||
|
||||
{{ range .Tables }}
|
||||
CREATE TABLE {{ $.Database.Name }}.{{ .Schema }}.{{ .Name }} (
|
||||
{{- range $i, $col := .Columns | values }}
|
||||
{{- if $i }},{{ end }}
|
||||
{{ $col.Name }} {{ $col.Type }}{{ if $col.NotNull }} NOT NULL{{ end }}{{ if $col.Default }} DEFAULT {{ $col.Default }}{{ end }}
|
||||
{{- end }}
|
||||
);
|
||||
|
||||
{{- $pks := filterPrimaryKeys .Columns }}
|
||||
{{- if $pks }}
|
||||
|
||||
ALTER TABLE {{ $.Database.Name }}.{{ .Schema }}.{{ .Name }}
|
||||
ADD PRIMARY KEY ({{ range $i, $pk := $pks }}{{ if $i }}, {{ end }}{{ $pk.Name }}{{ end }});
|
||||
{{- end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Hyphen for Whitespace Control:**
|
||||
```
|
||||
{{- removes whitespace before
|
||||
-}} removes whitespace after
|
||||
```
|
||||
|
||||
2. **Store Intermediate Results:**
|
||||
```
|
||||
{{ $pks := filterPrimaryKeys .Table.Columns }}
|
||||
{{ if $pks }}...{{ end }}
|
||||
```
|
||||
|
||||
3. **Check Before Accessing:**
|
||||
```
|
||||
{{ if .Table.Description }}{{ .Table.Description }}{{ end }}
|
||||
```
|
||||
|
||||
4. **Use Safe Access for Maps:**
|
||||
```
|
||||
{{ getOr .Metadata "key" "default-value" }}
|
||||
```
|
||||
|
||||
5. **Iterate Map Values:**
|
||||
```
|
||||
{{ range .Table.Columns | values }}...{{ end }}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Error: "wrong type for value"**
|
||||
- Check function parameter order (e.g., `sqlToGo .Type .NotNull` not `.NotNull .Type`)
|
||||
|
||||
**Error: "can't evaluate field"**
|
||||
- Field doesn't exist on the object
|
||||
- Use `{{ if .Field }}` to check before accessing
|
||||
|
||||
**Empty Output:**
|
||||
- Check your mode matches your template expectations
|
||||
- Verify data exists (use `{{ .Database | toJSON }}` to inspect)
|
||||
|
||||
**Whitespace Issues:**
|
||||
- Use `{{-` and `-}}` to control whitespace
|
||||
- Run output through a formatter if needed
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Go Template Documentation](https://pkg.go.dev/text/template)
|
||||
- [RelSpec Documentation](../README.md)
|
||||
- [Model Structure Reference](../pkg/models/)
|
||||
- [Example Templates](../examples/templates/)
|
||||
9
go.mod
9
go.mod
@@ -3,23 +3,30 @@ module git.warky.dev/wdevs/relspecgo
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell/v2 v2.8.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/rivo/tview v0.42.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/uptrace/bun v1.2.16
|
||||
golang.org/x/text v0.28.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
@@ -27,5 +34,5 @@ require (
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/term v0.34.0 // indirect
|
||||
)
|
||||
|
||||
79
go.sum
79
go.sum
@@ -3,6 +3,11 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
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=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
||||
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -21,11 +26,21 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
|
||||
github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
@@ -48,15 +63,79 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
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-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=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
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=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
74
pkg/commontypes/csharp.go
Normal file
74
pkg/commontypes/csharp.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package commontypes
|
||||
|
||||
import "strings"
|
||||
|
||||
// CSharpTypeMap maps PostgreSQL types to C# types
|
||||
var CSharpTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "int",
|
||||
"int": "int",
|
||||
"int4": "int",
|
||||
"smallint": "short",
|
||||
"int2": "short",
|
||||
"bigint": "long",
|
||||
"int8": "long",
|
||||
"serial": "int",
|
||||
"bigserial": "long",
|
||||
"smallserial": "short",
|
||||
|
||||
// String types
|
||||
"text": "string",
|
||||
"varchar": "string",
|
||||
"char": "string",
|
||||
"character": "string",
|
||||
"citext": "string",
|
||||
"bpchar": "string",
|
||||
"uuid": "Guid",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "float",
|
||||
"float4": "float",
|
||||
"double precision": "double",
|
||||
"float8": "double",
|
||||
"numeric": "decimal",
|
||||
"decimal": "decimal",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "DateTime",
|
||||
"timestamp without time zone": "DateTime",
|
||||
"timestamp with time zone": "DateTimeOffset",
|
||||
"timestamptz": "DateTimeOffset",
|
||||
"date": "DateTime",
|
||||
"time": "TimeSpan",
|
||||
"time without time zone": "TimeSpan",
|
||||
"time with time zone": "DateTimeOffset",
|
||||
"timetz": "DateTimeOffset",
|
||||
|
||||
// Binary
|
||||
"bytea": "byte[]",
|
||||
|
||||
// JSON
|
||||
"json": "string",
|
||||
"jsonb": "string",
|
||||
}
|
||||
|
||||
// SQLToCSharp converts SQL types to C# types
|
||||
func SQLToCSharp(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
csType, ok := CSharpTypeMap[baseType]
|
||||
if !ok {
|
||||
csType = "object"
|
||||
}
|
||||
|
||||
// Handle nullable value types (reference types are already nullable)
|
||||
if !nullable && csType != "string" && !strings.HasSuffix(csType, "[]") && csType != "object" {
|
||||
return csType + "?"
|
||||
}
|
||||
|
||||
return csType
|
||||
}
|
||||
89
pkg/commontypes/golang.go
Normal file
89
pkg/commontypes/golang.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package commontypes
|
||||
|
||||
import "strings"
|
||||
|
||||
// GoTypeMap maps PostgreSQL types to Go types
|
||||
var GoTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "int32",
|
||||
"int": "int32",
|
||||
"int4": "int32",
|
||||
"smallint": "int16",
|
||||
"int2": "int16",
|
||||
"bigint": "int64",
|
||||
"int8": "int64",
|
||||
"serial": "int32",
|
||||
"bigserial": "int64",
|
||||
"smallserial": "int16",
|
||||
|
||||
// String types
|
||||
"text": "string",
|
||||
"varchar": "string",
|
||||
"char": "string",
|
||||
"character": "string",
|
||||
"citext": "string",
|
||||
"bpchar": "string",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "float32",
|
||||
"float4": "float32",
|
||||
"double precision": "float64",
|
||||
"float8": "float64",
|
||||
"numeric": "float64",
|
||||
"decimal": "float64",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "time.Time",
|
||||
"timestamp without time zone": "time.Time",
|
||||
"timestamp with time zone": "time.Time",
|
||||
"timestamptz": "time.Time",
|
||||
"date": "time.Time",
|
||||
"time": "time.Time",
|
||||
"time without time zone": "time.Time",
|
||||
"time with time zone": "time.Time",
|
||||
"timetz": "time.Time",
|
||||
|
||||
// Binary
|
||||
"bytea": "[]byte",
|
||||
|
||||
// UUID
|
||||
"uuid": "string",
|
||||
|
||||
// JSON
|
||||
"json": "string",
|
||||
"jsonb": "string",
|
||||
|
||||
// Array
|
||||
"array": "[]string",
|
||||
}
|
||||
|
||||
// SQLToGo converts SQL types to Go types
|
||||
func SQLToGo(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
goType, ok := GoTypeMap[baseType]
|
||||
if !ok {
|
||||
goType = "interface{}"
|
||||
}
|
||||
|
||||
// Handle nullable types
|
||||
if nullable {
|
||||
return goType
|
||||
}
|
||||
|
||||
// For nullable, use pointer types (except for slices and interfaces)
|
||||
if !strings.HasPrefix(goType, "[]") && goType != "interface{}" {
|
||||
return "*" + goType
|
||||
}
|
||||
|
||||
return goType
|
||||
}
|
||||
|
||||
// NeedsTimeImport checks if a Go type requires the time package
|
||||
func NeedsTimeImport(goType string) bool {
|
||||
return strings.Contains(goType, "time.Time")
|
||||
}
|
||||
68
pkg/commontypes/java.go
Normal file
68
pkg/commontypes/java.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package commontypes
|
||||
|
||||
// JavaTypeMap maps PostgreSQL types to Java types
|
||||
var JavaTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "Integer",
|
||||
"int": "Integer",
|
||||
"int4": "Integer",
|
||||
"smallint": "Short",
|
||||
"int2": "Short",
|
||||
"bigint": "Long",
|
||||
"int8": "Long",
|
||||
"serial": "Integer",
|
||||
"bigserial": "Long",
|
||||
"smallserial": "Short",
|
||||
|
||||
// String types
|
||||
"text": "String",
|
||||
"varchar": "String",
|
||||
"char": "String",
|
||||
"character": "String",
|
||||
"citext": "String",
|
||||
"bpchar": "String",
|
||||
"uuid": "UUID",
|
||||
|
||||
// Boolean
|
||||
"boolean": "Boolean",
|
||||
"bool": "Boolean",
|
||||
|
||||
// Float types
|
||||
"real": "Float",
|
||||
"float4": "Float",
|
||||
"double precision": "Double",
|
||||
"float8": "Double",
|
||||
"numeric": "BigDecimal",
|
||||
"decimal": "BigDecimal",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "Timestamp",
|
||||
"timestamp without time zone": "Timestamp",
|
||||
"timestamp with time zone": "Timestamp",
|
||||
"timestamptz": "Timestamp",
|
||||
"date": "Date",
|
||||
"time": "Time",
|
||||
"time without time zone": "Time",
|
||||
"time with time zone": "Time",
|
||||
"timetz": "Time",
|
||||
|
||||
// Binary
|
||||
"bytea": "byte[]",
|
||||
|
||||
// JSON
|
||||
"json": "String",
|
||||
"jsonb": "String",
|
||||
}
|
||||
|
||||
// SQLToJava converts SQL types to Java types
|
||||
func SQLToJava(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
javaType, ok := JavaTypeMap[baseType]
|
||||
if !ok {
|
||||
javaType = "Object"
|
||||
}
|
||||
|
||||
// Java uses wrapper classes for nullable types by default
|
||||
return javaType
|
||||
}
|
||||
72
pkg/commontypes/php.go
Normal file
72
pkg/commontypes/php.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package commontypes
|
||||
|
||||
// PHPTypeMap maps PostgreSQL types to PHP types
|
||||
var PHPTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "int",
|
||||
"int": "int",
|
||||
"int4": "int",
|
||||
"smallint": "int",
|
||||
"int2": "int",
|
||||
"bigint": "int",
|
||||
"int8": "int",
|
||||
"serial": "int",
|
||||
"bigserial": "int",
|
||||
"smallserial": "int",
|
||||
|
||||
// String types
|
||||
"text": "string",
|
||||
"varchar": "string",
|
||||
"char": "string",
|
||||
"character": "string",
|
||||
"citext": "string",
|
||||
"bpchar": "string",
|
||||
"uuid": "string",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "float",
|
||||
"float4": "float",
|
||||
"double precision": "float",
|
||||
"float8": "float",
|
||||
"numeric": "float",
|
||||
"decimal": "float",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "\\DateTime",
|
||||
"timestamp without time zone": "\\DateTime",
|
||||
"timestamp with time zone": "\\DateTime",
|
||||
"timestamptz": "\\DateTime",
|
||||
"date": "\\DateTime",
|
||||
"time": "\\DateTime",
|
||||
"time without time zone": "\\DateTime",
|
||||
"time with time zone": "\\DateTime",
|
||||
"timetz": "\\DateTime",
|
||||
|
||||
// Binary
|
||||
"bytea": "string",
|
||||
|
||||
// JSON
|
||||
"json": "array",
|
||||
"jsonb": "array",
|
||||
}
|
||||
|
||||
// SQLToPhp converts SQL types to PHP types
|
||||
func SQLToPhp(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
phpType, ok := PHPTypeMap[baseType]
|
||||
if !ok {
|
||||
phpType = "mixed"
|
||||
}
|
||||
|
||||
// PHP 7.1+ supports nullable types with ?Type syntax
|
||||
if !nullable && phpType != "mixed" {
|
||||
return "?" + phpType
|
||||
}
|
||||
|
||||
return phpType
|
||||
}
|
||||
71
pkg/commontypes/python.go
Normal file
71
pkg/commontypes/python.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package commontypes
|
||||
|
||||
// PythonTypeMap maps PostgreSQL types to Python types
|
||||
var PythonTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "int",
|
||||
"int": "int",
|
||||
"int4": "int",
|
||||
"smallint": "int",
|
||||
"int2": "int",
|
||||
"bigint": "int",
|
||||
"int8": "int",
|
||||
"serial": "int",
|
||||
"bigserial": "int",
|
||||
"smallserial": "int",
|
||||
|
||||
// String types
|
||||
"text": "str",
|
||||
"varchar": "str",
|
||||
"char": "str",
|
||||
"character": "str",
|
||||
"citext": "str",
|
||||
"bpchar": "str",
|
||||
"uuid": "UUID",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "float",
|
||||
"float4": "float",
|
||||
"double precision": "float",
|
||||
"float8": "float",
|
||||
"numeric": "Decimal",
|
||||
"decimal": "Decimal",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "datetime",
|
||||
"timestamp without time zone": "datetime",
|
||||
"timestamp with time zone": "datetime",
|
||||
"timestamptz": "datetime",
|
||||
"date": "date",
|
||||
"time": "time",
|
||||
"time without time zone": "time",
|
||||
"time with time zone": "time",
|
||||
"timetz": "time",
|
||||
|
||||
// Binary
|
||||
"bytea": "bytes",
|
||||
|
||||
// JSON
|
||||
"json": "dict",
|
||||
"jsonb": "dict",
|
||||
|
||||
// Array
|
||||
"array": "list",
|
||||
}
|
||||
|
||||
// SQLToPython converts SQL types to Python types
|
||||
func SQLToPython(sqlType string) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
pyType, ok := PythonTypeMap[baseType]
|
||||
if !ok {
|
||||
pyType = "Any"
|
||||
}
|
||||
|
||||
// Python uses Optional[Type] for nullable, but we return the base type
|
||||
return pyType
|
||||
}
|
||||
72
pkg/commontypes/rust.go
Normal file
72
pkg/commontypes/rust.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package commontypes
|
||||
|
||||
// RustTypeMap maps PostgreSQL types to Rust types
|
||||
var RustTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "i32",
|
||||
"int": "i32",
|
||||
"int4": "i32",
|
||||
"smallint": "i16",
|
||||
"int2": "i16",
|
||||
"bigint": "i64",
|
||||
"int8": "i64",
|
||||
"serial": "i32",
|
||||
"bigserial": "i64",
|
||||
"smallserial": "i16",
|
||||
|
||||
// String types
|
||||
"text": "String",
|
||||
"varchar": "String",
|
||||
"char": "String",
|
||||
"character": "String",
|
||||
"citext": "String",
|
||||
"bpchar": "String",
|
||||
"uuid": "String",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "f32",
|
||||
"float4": "f32",
|
||||
"double precision": "f64",
|
||||
"float8": "f64",
|
||||
"numeric": "f64",
|
||||
"decimal": "f64",
|
||||
|
||||
// Date/Time types (using chrono crate)
|
||||
"timestamp": "NaiveDateTime",
|
||||
"timestamp without time zone": "NaiveDateTime",
|
||||
"timestamp with time zone": "DateTime<Utc>",
|
||||
"timestamptz": "DateTime<Utc>",
|
||||
"date": "NaiveDate",
|
||||
"time": "NaiveTime",
|
||||
"time without time zone": "NaiveTime",
|
||||
"time with time zone": "DateTime<Utc>",
|
||||
"timetz": "DateTime<Utc>",
|
||||
|
||||
// Binary
|
||||
"bytea": "Vec<u8>",
|
||||
|
||||
// JSON
|
||||
"json": "serde_json::Value",
|
||||
"jsonb": "serde_json::Value",
|
||||
}
|
||||
|
||||
// SQLToRust converts SQL types to Rust types
|
||||
func SQLToRust(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
rustType, ok := RustTypeMap[baseType]
|
||||
if !ok {
|
||||
rustType = "String"
|
||||
}
|
||||
|
||||
// Handle nullable types with Option<T>
|
||||
if nullable {
|
||||
return rustType
|
||||
}
|
||||
|
||||
return "Option<" + rustType + ">"
|
||||
}
|
||||
22
pkg/commontypes/sql.go
Normal file
22
pkg/commontypes/sql.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package commontypes
|
||||
|
||||
import "strings"
|
||||
|
||||
// ExtractBaseType extracts the base type from a SQL type string
|
||||
// Examples: varchar(100) → varchar, numeric(10,2) → numeric
|
||||
func ExtractBaseType(sqlType string) string {
|
||||
sqlType = strings.ToLower(strings.TrimSpace(sqlType))
|
||||
|
||||
// Remove everything after '('
|
||||
if idx := strings.Index(sqlType, "("); idx > 0 {
|
||||
sqlType = sqlType[:idx]
|
||||
}
|
||||
|
||||
return sqlType
|
||||
}
|
||||
|
||||
// NormalizeType normalizes a SQL type to its base form
|
||||
// Alias for ExtractBaseType for backwards compatibility
|
||||
func NormalizeType(sqlType string) string {
|
||||
return ExtractBaseType(sqlType)
|
||||
}
|
||||
75
pkg/commontypes/typescript.go
Normal file
75
pkg/commontypes/typescript.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package commontypes
|
||||
|
||||
// TypeScriptTypeMap maps PostgreSQL types to TypeScript types
|
||||
var TypeScriptTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "number",
|
||||
"int": "number",
|
||||
"int4": "number",
|
||||
"smallint": "number",
|
||||
"int2": "number",
|
||||
"bigint": "number",
|
||||
"int8": "number",
|
||||
"serial": "number",
|
||||
"bigserial": "number",
|
||||
"smallserial": "number",
|
||||
|
||||
// String types
|
||||
"text": "string",
|
||||
"varchar": "string",
|
||||
"char": "string",
|
||||
"character": "string",
|
||||
"citext": "string",
|
||||
"bpchar": "string",
|
||||
"uuid": "string",
|
||||
|
||||
// Boolean
|
||||
"boolean": "boolean",
|
||||
"bool": "boolean",
|
||||
|
||||
// Float types
|
||||
"real": "number",
|
||||
"float4": "number",
|
||||
"double precision": "number",
|
||||
"float8": "number",
|
||||
"numeric": "number",
|
||||
"decimal": "number",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "Date",
|
||||
"timestamp without time zone": "Date",
|
||||
"timestamp with time zone": "Date",
|
||||
"timestamptz": "Date",
|
||||
"date": "Date",
|
||||
"time": "Date",
|
||||
"time without time zone": "Date",
|
||||
"time with time zone": "Date",
|
||||
"timetz": "Date",
|
||||
|
||||
// Binary
|
||||
"bytea": "Buffer",
|
||||
|
||||
// JSON
|
||||
"json": "any",
|
||||
"jsonb": "any",
|
||||
|
||||
// Array
|
||||
"array": "any[]",
|
||||
}
|
||||
|
||||
// SQLToTypeScript converts SQL types to TypeScript types
|
||||
func SQLToTypeScript(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
tsType, ok := TypeScriptTypeMap[baseType]
|
||||
if !ok {
|
||||
tsType = "any"
|
||||
}
|
||||
|
||||
// Handle nullable types
|
||||
if nullable {
|
||||
return tsType
|
||||
}
|
||||
|
||||
return tsType + " | null"
|
||||
}
|
||||
@@ -4,7 +4,10 @@
|
||||
// intermediate representation for converting between various database schema formats.
|
||||
package models
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DatabaseType represents the type of database system.
|
||||
type DatabaseType string
|
||||
@@ -21,10 +24,12 @@ type Database struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"`
|
||||
Schemas []*Schema `json:"schemas" yaml:"schemas" xml:"schemas"`
|
||||
Domains []*Domain `json:"domains,omitempty" yaml:"domains,omitempty" xml:"domains,omitempty"`
|
||||
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
|
||||
DatabaseType DatabaseType `json:"database_type,omitempty" yaml:"database_type,omitempty" xml:"database_type,omitempty"`
|
||||
DatabaseVersion string `json:"database_version,omitempty" yaml:"database_version,omitempty" xml:"database_version,omitempty"`
|
||||
SourceFormat string `json:"source_format,omitempty" yaml:"source_format,omitempty" xml:"source_format,omitempty"` // Source Format of the database.
|
||||
UpdatedAt string `json:"updatedat,omitempty" yaml:"updatedat,omitempty" xml:"updatedat,omitempty"`
|
||||
}
|
||||
|
||||
// SQLName returns the database name in lowercase for SQL compatibility.
|
||||
@@ -32,6 +37,37 @@ func (d *Database) SQLName() string {
|
||||
return strings.ToLower(d.Name)
|
||||
}
|
||||
|
||||
// UpdateDate sets the UpdatedAt field to the current time in RFC3339 format.
|
||||
func (d *Database) UpdateDate() {
|
||||
d.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// Domain represents a logical business domain grouping multiple tables from potentially different schemas.
|
||||
// Domains allow for organizing database tables by functional areas (e.g., authentication, user data, financial).
|
||||
type Domain struct {
|
||||
Name string `json:"name" yaml:"name" xml:"name"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"`
|
||||
Tables []*DomainTable `json:"tables" yaml:"tables" xml:"tables"`
|
||||
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty" xml:"-"`
|
||||
Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"`
|
||||
}
|
||||
|
||||
// SQLName returns the domain name in lowercase for SQL compatibility.
|
||||
func (d *Domain) SQLName() string {
|
||||
return strings.ToLower(d.Name)
|
||||
}
|
||||
|
||||
// DomainTable represents a reference to a specific table within a domain.
|
||||
// It identifies the table by name and schema, allowing a single domain to include
|
||||
// tables from multiple schemas.
|
||||
type DomainTable struct {
|
||||
TableName string `json:"table_name" yaml:"table_name" xml:"table_name"`
|
||||
SchemaName string `json:"schema_name" yaml:"schema_name" xml:"schema_name"`
|
||||
Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"`
|
||||
RefTable *Table `json:"-" yaml:"-" xml:"-"` // Excluded to prevent circular references
|
||||
}
|
||||
|
||||
// Schema represents a database schema, which is a logical grouping of database objects
|
||||
// such as tables, views, sequences, and relationships within a database.
|
||||
type Schema struct {
|
||||
@@ -49,6 +85,15 @@ type Schema struct {
|
||||
RefDatabase *Database `json:"-" yaml:"-" xml:"-"` // Excluded to prevent circular references
|
||||
Relations []*Relationship `json:"relations,omitempty" yaml:"relations,omitempty" xml:"-"`
|
||||
Enums []*Enum `json:"enums,omitempty" yaml:"enums,omitempty" xml:"enums"`
|
||||
UpdatedAt string `json:"updatedat,omitempty" yaml:"updatedat,omitempty" xml:"updatedat,omitempty"`
|
||||
}
|
||||
|
||||
// UpdaUpdateDateted sets the UpdatedAt field to the current time in RFC3339 format.
|
||||
func (d *Schema) UpdateDate() {
|
||||
d.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
if d.RefDatabase != nil {
|
||||
d.RefDatabase.UpdateDate()
|
||||
}
|
||||
}
|
||||
|
||||
// SQLName returns the schema name in lowercase for SQL compatibility.
|
||||
@@ -71,6 +116,15 @@ type Table struct {
|
||||
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty" xml:"-"`
|
||||
Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"`
|
||||
RefSchema *Schema `json:"-" yaml:"-" xml:"-"` // Excluded to prevent circular references
|
||||
UpdatedAt string `json:"updatedat,omitempty" yaml:"updatedat,omitempty" xml:"updatedat,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateDate sets the UpdatedAt field to the current time in RFC3339 format.
|
||||
func (d *Table) UpdateDate() {
|
||||
d.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
if d.RefSchema != nil {
|
||||
d.RefSchema.UpdateDate()
|
||||
}
|
||||
}
|
||||
|
||||
// SQLName returns the table name in lowercase for SQL compatibility.
|
||||
@@ -295,6 +349,7 @@ func InitDatabase(name string) *Database {
|
||||
return &Database{
|
||||
Name: name,
|
||||
Schemas: make([]*Schema, 0),
|
||||
Domains: make([]*Domain, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,3 +457,20 @@ func InitSequence(name, schema string) *Sequence {
|
||||
StartValue: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// InitDomain initializes a new Domain with empty slices and maps
|
||||
func InitDomain(name string) *Domain {
|
||||
return &Domain{
|
||||
Name: name,
|
||||
Tables: make([]*DomainTable, 0),
|
||||
Metadata: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// InitDomainTable initializes a new DomainTable reference
|
||||
func InitDomainTable(tableName, schemaName string) *DomainTable {
|
||||
return &DomainTable{
|
||||
TableName: tableName,
|
||||
SchemaName: schemaName,
|
||||
}
|
||||
}
|
||||
|
||||
282
pkg/models/sorting.go
Normal file
282
pkg/models/sorting.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SortOrder represents the sort direction
|
||||
type SortOrder bool
|
||||
|
||||
const (
|
||||
// Ascending sort order
|
||||
Ascending SortOrder = false
|
||||
// Descending sort order
|
||||
Descending SortOrder = true
|
||||
)
|
||||
|
||||
// Schema Sorting
|
||||
|
||||
// SortSchemasByName sorts schemas by name
|
||||
func SortSchemasByName(schemas []*Schema, desc bool) error {
|
||||
sort.SliceStable(schemas, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(schemas[i].Name), strings.ToLower(schemas[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortSchemasBySequence sorts schemas by sequence number
|
||||
func SortSchemasBySequence(schemas []*Schema, desc bool) error {
|
||||
sort.SliceStable(schemas, func(i, j int) bool {
|
||||
if desc {
|
||||
return schemas[i].Sequence > schemas[j].Sequence
|
||||
}
|
||||
return schemas[i].Sequence < schemas[j].Sequence
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Table Sorting
|
||||
|
||||
// SortTablesByName sorts tables by name
|
||||
func SortTablesByName(tables []*Table, desc bool) error {
|
||||
sort.SliceStable(tables, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(tables[i].Name), strings.ToLower(tables[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortTablesBySequence sorts tables by sequence number
|
||||
func SortTablesBySequence(tables []*Table, desc bool) error {
|
||||
sort.SliceStable(tables, func(i, j int) bool {
|
||||
if desc {
|
||||
return tables[i].Sequence > tables[j].Sequence
|
||||
}
|
||||
return tables[i].Sequence < tables[j].Sequence
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Column Sorting
|
||||
|
||||
// SortColumnsMapByName converts column map to sorted slice by name
|
||||
func SortColumnsMapByName(columns map[string]*Column, desc bool) []*Column {
|
||||
result := make([]*Column, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, col)
|
||||
}
|
||||
_ = SortColumnsByName(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortColumnsMapBySequence converts column map to sorted slice by sequence
|
||||
func SortColumnsMapBySequence(columns map[string]*Column, desc bool) []*Column {
|
||||
result := make([]*Column, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, col)
|
||||
}
|
||||
_ = SortColumnsBySequence(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortColumnsByName sorts columns by name
|
||||
func SortColumnsByName(columns []*Column, desc bool) error {
|
||||
sort.SliceStable(columns, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(columns[i].Name), strings.ToLower(columns[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortColumnsBySequence sorts columns by sequence number
|
||||
func SortColumnsBySequence(columns []*Column, desc bool) error {
|
||||
sort.SliceStable(columns, func(i, j int) bool {
|
||||
if desc {
|
||||
return columns[i].Sequence > columns[j].Sequence
|
||||
}
|
||||
return columns[i].Sequence < columns[j].Sequence
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// View Sorting
|
||||
|
||||
// SortViewsByName sorts views by name
|
||||
func SortViewsByName(views []*View, desc bool) error {
|
||||
sort.SliceStable(views, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(views[i].Name), strings.ToLower(views[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortViewsBySequence sorts views by sequence number
|
||||
func SortViewsBySequence(views []*View, desc bool) error {
|
||||
sort.SliceStable(views, func(i, j int) bool {
|
||||
if desc {
|
||||
return views[i].Sequence > views[j].Sequence
|
||||
}
|
||||
return views[i].Sequence < views[j].Sequence
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sequence Sorting
|
||||
|
||||
// SortSequencesByName sorts sequences by name
|
||||
func SortSequencesByName(sequences []*Sequence, desc bool) error {
|
||||
sort.SliceStable(sequences, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(sequences[i].Name), strings.ToLower(sequences[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortSequencesBySequence sorts sequences by sequence number
|
||||
func SortSequencesBySequence(sequences []*Sequence, desc bool) error {
|
||||
sort.SliceStable(sequences, func(i, j int) bool {
|
||||
if desc {
|
||||
return sequences[i].Sequence > sequences[j].Sequence
|
||||
}
|
||||
return sequences[i].Sequence < sequences[j].Sequence
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Index Sorting
|
||||
|
||||
// SortIndexesMapByName converts index map to sorted slice by name
|
||||
func SortIndexesMapByName(indexes map[string]*Index, desc bool) []*Index {
|
||||
result := make([]*Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
_ = SortIndexesByName(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortIndexesMapBySequence converts index map to sorted slice by sequence
|
||||
func SortIndexesMapBySequence(indexes map[string]*Index, desc bool) []*Index {
|
||||
result := make([]*Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
_ = SortIndexesBySequence(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortIndexesByName sorts indexes by name
|
||||
func SortIndexesByName(indexes []*Index, desc bool) error {
|
||||
sort.SliceStable(indexes, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(indexes[i].Name), strings.ToLower(indexes[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortIndexesBySequence sorts indexes by sequence number
|
||||
func SortIndexesBySequence(indexes []*Index, desc bool) error {
|
||||
sort.SliceStable(indexes, func(i, j int) bool {
|
||||
if desc {
|
||||
return indexes[i].Sequence > indexes[j].Sequence
|
||||
}
|
||||
return indexes[i].Sequence < indexes[j].Sequence
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Constraint Sorting
|
||||
|
||||
// SortConstraintsMapByName converts constraint map to sorted slice by name
|
||||
func SortConstraintsMapByName(constraints map[string]*Constraint, desc bool) []*Constraint {
|
||||
result := make([]*Constraint, 0, len(constraints))
|
||||
for _, c := range constraints {
|
||||
result = append(result, c)
|
||||
}
|
||||
_ = SortConstraintsByName(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortConstraintsByName sorts constraints by name
|
||||
func SortConstraintsByName(constraints []*Constraint, desc bool) error {
|
||||
sort.SliceStable(constraints, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(constraints[i].Name), strings.ToLower(constraints[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Relationship Sorting
|
||||
|
||||
// SortRelationshipsMapByName converts relationship map to sorted slice by name
|
||||
func SortRelationshipsMapByName(relationships map[string]*Relationship, desc bool) []*Relationship {
|
||||
result := make([]*Relationship, 0, len(relationships))
|
||||
for _, r := range relationships {
|
||||
result = append(result, r)
|
||||
}
|
||||
_ = SortRelationshipsByName(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortRelationshipsByName sorts relationships by name
|
||||
func SortRelationshipsByName(relationships []*Relationship, desc bool) error {
|
||||
sort.SliceStable(relationships, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(relationships[i].Name), strings.ToLower(relationships[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Script Sorting
|
||||
|
||||
// SortScriptsByName sorts scripts by name
|
||||
func SortScriptsByName(scripts []*Script, desc bool) error {
|
||||
sort.SliceStable(scripts, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(scripts[i].Name), strings.ToLower(scripts[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enum Sorting
|
||||
|
||||
// SortEnumsByName sorts enums by name
|
||||
func SortEnumsByName(enums []*Enum, desc bool) error {
|
||||
sort.SliceStable(enums, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(enums[i].Name), strings.ToLower(enums[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -140,6 +140,32 @@ func (r *Reader) convertToDatabase(drawSchema *drawdb.DrawDBSchema) (*models.Dat
|
||||
db.Schemas = append(db.Schemas, schema)
|
||||
}
|
||||
|
||||
// Convert DrawDB subject areas to domains
|
||||
for _, area := range drawSchema.SubjectAreas {
|
||||
domain := models.InitDomain(area.Name)
|
||||
|
||||
// Find all tables that visually belong to this area
|
||||
// A table belongs to an area if its position is within the area bounds
|
||||
for _, drawTable := range drawSchema.Tables {
|
||||
if drawTable.X >= area.X && drawTable.X <= (area.X+area.Width) &&
|
||||
drawTable.Y >= area.Y && drawTable.Y <= (area.Y+area.Height) {
|
||||
|
||||
schemaName := drawTable.Schema
|
||||
if schemaName == "" {
|
||||
schemaName = "public"
|
||||
}
|
||||
|
||||
domainTable := models.InitDomainTable(drawTable.Name, schemaName)
|
||||
domain.Tables = append(domain.Tables, domainTable)
|
||||
}
|
||||
}
|
||||
|
||||
// Only add domain if it has tables
|
||||
if len(domain.Tables) > 0 {
|
||||
db.Domains = append(db.Domains, domain)
|
||||
}
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
|
||||
326
pkg/reflectutil/helpers.go
Normal file
326
pkg/reflectutil/helpers.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package reflectutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Deref dereferences pointers until it reaches a non-pointer value
|
||||
// Returns the dereferenced value and true if successful, or the original value and false if nil
|
||||
func Deref(v reflect.Value) (reflect.Value, bool) {
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return v, false
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
// DerefInterface dereferences an interface{} until it reaches a non-pointer value
|
||||
func DerefInterface(i interface{}) reflect.Value {
|
||||
v := reflect.ValueOf(i)
|
||||
v, _ = Deref(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// GetFieldValue extracts a field value from a struct, map, or pointer
|
||||
// Returns nil if the field doesn't exist or can't be accessed
|
||||
func GetFieldValue(item interface{}, field string) interface{} {
|
||||
v := reflect.ValueOf(item)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
fieldVal := v.FieldByName(field)
|
||||
if fieldVal.IsValid() {
|
||||
return fieldVal.Interface()
|
||||
}
|
||||
return nil
|
||||
|
||||
case reflect.Map:
|
||||
keyVal := reflect.ValueOf(field)
|
||||
mapVal := v.MapIndex(keyVal)
|
||||
if mapVal.IsValid() {
|
||||
return mapVal.Interface()
|
||||
}
|
||||
return nil
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// IsSliceOrArray checks if an interface{} is a slice or array
|
||||
func IsSliceOrArray(i interface{}) bool {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
k := v.Kind()
|
||||
return k == reflect.Slice || k == reflect.Array
|
||||
}
|
||||
|
||||
// IsMap checks if an interface{} is a map
|
||||
func IsMap(i interface{}) bool {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return v.Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// SliceLen returns the length of a slice/array, or 0 if not a slice/array
|
||||
func SliceLen(i interface{}) int {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return 0
|
||||
}
|
||||
return v.Len()
|
||||
}
|
||||
|
||||
// MapLen returns the length of a map, or 0 if not a map
|
||||
func MapLen(i interface{}) int {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
if v.Kind() != reflect.Map {
|
||||
return 0
|
||||
}
|
||||
return v.Len()
|
||||
}
|
||||
|
||||
// SliceToInterfaces converts a slice/array to []interface{}
|
||||
// Returns empty slice if not a slice/array
|
||||
func SliceToInterfaces(i interface{}) []interface{} {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
result := make([]interface{}, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
result[i] = v.Index(i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MapKeys returns all keys from a map as []interface{}
|
||||
// Returns empty slice if not a map
|
||||
func MapKeys(i interface{}) []interface{} {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
keys := v.MapKeys()
|
||||
result := make([]interface{}, len(keys))
|
||||
for i, key := range keys {
|
||||
result[i] = key.Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MapValues returns all values from a map as []interface{}
|
||||
// Returns empty slice if not a map
|
||||
func MapValues(i interface{}) []interface{} {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
result := make([]interface{}, 0, v.Len())
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
result = append(result, iter.Value().Interface())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MapGet safely gets a value from a map by key
|
||||
// Returns nil if key doesn't exist or not a map
|
||||
func MapGet(m interface{}, key interface{}) interface{} {
|
||||
v := reflect.ValueOf(m)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return nil
|
||||
}
|
||||
|
||||
keyVal := reflect.ValueOf(key)
|
||||
mapVal := v.MapIndex(keyVal)
|
||||
if mapVal.IsValid() {
|
||||
return mapVal.Interface()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SliceIndex safely gets an element from a slice/array by index
|
||||
// Returns nil if index out of bounds or not a slice/array
|
||||
func SliceIndex(slice interface{}, index int) interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return nil
|
||||
}
|
||||
|
||||
if index < 0 || index >= v.Len() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.Index(index).Interface()
|
||||
}
|
||||
|
||||
// CompareValues compares two values for sorting
|
||||
// Returns -1 if a < b, 0 if a == b, 1 if a > b
|
||||
func CompareValues(a, b interface{}) int {
|
||||
if a == nil && b == nil {
|
||||
return 0
|
||||
}
|
||||
if a == nil {
|
||||
return -1
|
||||
}
|
||||
if b == nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
va := reflect.ValueOf(a)
|
||||
vb := reflect.ValueOf(b)
|
||||
|
||||
// Handle different types
|
||||
switch va.Kind() {
|
||||
case reflect.String:
|
||||
if vb.Kind() == reflect.String {
|
||||
as := va.String()
|
||||
bs := vb.String()
|
||||
if as < bs {
|
||||
return -1
|
||||
} else if as > bs {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if vb.Kind() >= reflect.Int && vb.Kind() <= reflect.Int64 {
|
||||
ai := va.Int()
|
||||
bi := vb.Int()
|
||||
if ai < bi {
|
||||
return -1
|
||||
} else if ai > bi {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if vb.Kind() >= reflect.Uint && vb.Kind() <= reflect.Uint64 {
|
||||
au := va.Uint()
|
||||
bu := vb.Uint()
|
||||
if au < bu {
|
||||
return -1
|
||||
} else if au > bu {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if vb.Kind() == reflect.Float32 || vb.Kind() == reflect.Float64 {
|
||||
af := va.Float()
|
||||
bf := vb.Float()
|
||||
if af < bf {
|
||||
return -1
|
||||
} else if af > bf {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetNestedValue gets a nested value using dot notation path
|
||||
// Example: GetNestedValue(obj, "database.schema.table")
|
||||
func GetNestedValue(m interface{}, path string) interface{} {
|
||||
if path == "" {
|
||||
return m
|
||||
}
|
||||
|
||||
parts := strings.Split(path, ".")
|
||||
current := m
|
||||
|
||||
for _, part := range parts {
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(current)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
keyVal := reflect.ValueOf(part)
|
||||
mapVal := v.MapIndex(keyVal)
|
||||
if !mapVal.IsValid() {
|
||||
return nil
|
||||
}
|
||||
current = mapVal.Interface()
|
||||
|
||||
case reflect.Struct:
|
||||
fieldVal := v.FieldByName(part)
|
||||
if !fieldVal.IsValid() {
|
||||
return nil
|
||||
}
|
||||
current = fieldVal.Interface()
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
// DeepEqual performs a deep equality check between two values
|
||||
func DeepEqual(a, b interface{}) bool {
|
||||
return reflect.DeepEqual(a, b)
|
||||
}
|
||||
95
pkg/ui/column_dataops.go
Normal file
95
pkg/ui/column_dataops.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package ui
|
||||
|
||||
import "git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
|
||||
// Column data operations - business logic for column management
|
||||
|
||||
// CreateColumn creates a new column and adds it to a table
|
||||
func (se *SchemaEditor) CreateColumn(schemaIndex, tableIndex int, name, dataType string, isPrimaryKey, isNotNull bool) *models.Column {
|
||||
table := se.GetTable(schemaIndex, tableIndex)
|
||||
if table == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if table.Columns == nil {
|
||||
table.Columns = make(map[string]*models.Column)
|
||||
}
|
||||
|
||||
newColumn := &models.Column{
|
||||
Name: name,
|
||||
Type: dataType,
|
||||
IsPrimaryKey: isPrimaryKey,
|
||||
NotNull: isNotNull,
|
||||
}
|
||||
table.UpdateDate()
|
||||
table.Columns[name] = newColumn
|
||||
return newColumn
|
||||
}
|
||||
|
||||
// UpdateColumn updates an existing column's properties
|
||||
func (se *SchemaEditor) UpdateColumn(schemaIndex, tableIndex int, oldName, newName, dataType string, isPrimaryKey, isNotNull bool, defaultValue interface{}, description string) bool {
|
||||
table := se.GetTable(schemaIndex, tableIndex)
|
||||
if table == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
column, exists := table.Columns[oldName]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
table.UpdateDate()
|
||||
|
||||
// If name changed, remove old entry and create new one
|
||||
if oldName != newName {
|
||||
delete(table.Columns, oldName)
|
||||
column.Name = newName
|
||||
table.Columns[newName] = column
|
||||
}
|
||||
|
||||
// Update properties
|
||||
column.Type = dataType
|
||||
column.IsPrimaryKey = isPrimaryKey
|
||||
column.NotNull = isNotNull
|
||||
column.Default = defaultValue
|
||||
column.Description = description
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// DeleteColumn removes a column from a table
|
||||
func (se *SchemaEditor) DeleteColumn(schemaIndex, tableIndex int, columnName string) bool {
|
||||
table := se.GetTable(schemaIndex, tableIndex)
|
||||
if table == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, exists := table.Columns[columnName]; !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
table.UpdateDate()
|
||||
|
||||
delete(table.Columns, columnName)
|
||||
return true
|
||||
}
|
||||
|
||||
// GetColumn returns a column by name
|
||||
func (se *SchemaEditor) GetColumn(schemaIndex, tableIndex int, columnName string) *models.Column {
|
||||
table := se.GetTable(schemaIndex, tableIndex)
|
||||
if table == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return table.Columns[columnName]
|
||||
}
|
||||
|
||||
// GetAllColumns returns all columns in a table
|
||||
func (se *SchemaEditor) GetAllColumns(schemaIndex, tableIndex int) map[string]*models.Column {
|
||||
table := se.GetTable(schemaIndex, tableIndex)
|
||||
if table == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return table.Columns
|
||||
}
|
||||
208
pkg/ui/column_screens.go
Normal file
208
pkg/ui/column_screens.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// showColumnEditor shows editor for a specific column
|
||||
func (se *SchemaEditor) showColumnEditor(schemaIndex, tableIndex, colIndex int, column *models.Column) {
|
||||
form := tview.NewForm()
|
||||
|
||||
// Store original name to handle renames
|
||||
originalName := column.Name
|
||||
|
||||
// Local variables to collect changes
|
||||
newName := column.Name
|
||||
newType := column.Type
|
||||
newIsPK := column.IsPrimaryKey
|
||||
newIsNotNull := column.NotNull
|
||||
newDefault := column.Default
|
||||
newDescription := column.Description
|
||||
|
||||
// Column type options: PostgreSQL, MySQL, SQL Server, and common SQL types
|
||||
columnTypes := []string{
|
||||
// Numeric Types
|
||||
"SMALLINT", "INTEGER", "BIGINT", "INT", "TINYINT", "FLOAT", "REAL", "DOUBLE PRECISION",
|
||||
"DECIMAL(10,2)", "NUMERIC", "DECIMAL", "NUMERIC(10,2)",
|
||||
// Character Types
|
||||
"CHAR", "VARCHAR", "VARCHAR(255)", "TEXT", "NCHAR", "NVARCHAR", "NVARCHAR(255)",
|
||||
// Boolean
|
||||
"BOOLEAN", "BOOL", "BIT",
|
||||
// Date/Time Types
|
||||
"DATE", "TIME", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "INTERVAL",
|
||||
"DATETIME", "DATETIME2", "DATEFIRST",
|
||||
// UUID and JSON
|
||||
"UUID", "GUID", "JSON", "JSONB",
|
||||
// Binary Types
|
||||
"BYTEA", "BLOB", "IMAGE", "VARBINARY", "VARBINARY(MAX)", "BINARY",
|
||||
// PostgreSQL Special Types
|
||||
"int4range", "int8range", "numrange", "tsrange", "tstzrange", "daterange",
|
||||
"HSTORE", "CITEXT", "INET", "MACADDR", "POINT", "LINE", "LSEG", "BOX", "PATH", "POLYGON", "CIRCLE",
|
||||
// Array Types
|
||||
"INTEGER ARRAY", "VARCHAR ARRAY", "TEXT ARRAY", "BIGINT ARRAY",
|
||||
// MySQL Specific
|
||||
"MEDIUMINT", "DOUBLE", "FLOAT(10,2)",
|
||||
// SQL Server Specific
|
||||
"MONEY", "SMALLMONEY", "SQL_VARIANT",
|
||||
}
|
||||
selectedTypeIndex := 0
|
||||
|
||||
// Add existing type if not already in the list
|
||||
typeExists := false
|
||||
for i, opt := range columnTypes {
|
||||
if opt == column.Type {
|
||||
selectedTypeIndex = i
|
||||
typeExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !typeExists && column.Type != "" {
|
||||
columnTypes = append(columnTypes, column.Type)
|
||||
selectedTypeIndex = len(columnTypes) - 1
|
||||
}
|
||||
|
||||
form.AddInputField("Column Name", column.Name, 40, nil, func(value string) {
|
||||
newName = value
|
||||
})
|
||||
|
||||
form.AddDropDown("Type", columnTypes, selectedTypeIndex, func(option string, index int) {
|
||||
newType = option
|
||||
})
|
||||
|
||||
form.AddCheckbox("Primary Key", column.IsPrimaryKey, func(checked bool) {
|
||||
newIsPK = checked
|
||||
})
|
||||
|
||||
form.AddCheckbox("Not Null", column.NotNull, func(checked bool) {
|
||||
newIsNotNull = checked
|
||||
})
|
||||
|
||||
defaultStr := ""
|
||||
if column.Default != nil {
|
||||
defaultStr = fmt.Sprintf("%v", column.Default)
|
||||
}
|
||||
form.AddInputField("Default Value", defaultStr, 40, nil, func(value string) {
|
||||
newDefault = value
|
||||
})
|
||||
|
||||
form.AddTextArea("Description", column.Description, 40, 5, 0, func(value string) {
|
||||
newDescription = value
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
// Apply changes using dataops
|
||||
se.UpdateColumn(schemaIndex, tableIndex, originalName, newName, newType, newIsPK, newIsNotNull, newDefault, newDescription)
|
||||
|
||||
se.pages.RemovePage("column-editor")
|
||||
se.pages.SwitchToPage("table-editor")
|
||||
})
|
||||
|
||||
form.AddButton("Delete", func() {
|
||||
se.showDeleteColumnConfirm(schemaIndex, tableIndex, originalName)
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
// Discard changes - don't apply them
|
||||
se.pages.RemovePage("column-editor")
|
||||
se.pages.SwitchToPage("table-editor")
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" Edit Column ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("column-editor", "table-editor")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("column-editor", form, true, true)
|
||||
}
|
||||
|
||||
// showNewColumnDialog shows dialog to create a new column
|
||||
func (se *SchemaEditor) showNewColumnDialog(schemaIndex, tableIndex int) {
|
||||
form := tview.NewForm()
|
||||
|
||||
columnName := ""
|
||||
dataType := "VARCHAR(255)"
|
||||
|
||||
// Column type options: PostgreSQL, MySQL, SQL Server, and common SQL types
|
||||
columnTypes := []string{
|
||||
// Numeric Types
|
||||
"SMALLINT", "INTEGER", "BIGINT", "INT", "TINYINT", "FLOAT", "REAL", "DOUBLE PRECISION",
|
||||
"DECIMAL(10,2)", "NUMERIC", "DECIMAL", "NUMERIC(10,2)",
|
||||
// Character Types
|
||||
"CHAR", "VARCHAR", "VARCHAR(255)", "TEXT", "NCHAR", "NVARCHAR", "NVARCHAR(255)",
|
||||
// Boolean
|
||||
"BOOLEAN", "BOOL", "BIT",
|
||||
// Date/Time Types
|
||||
"DATE", "TIME", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "INTERVAL",
|
||||
"DATETIME", "DATETIME2", "DATEFIRST",
|
||||
// UUID and JSON
|
||||
"UUID", "GUID", "JSON", "JSONB",
|
||||
// Binary Types
|
||||
"BYTEA", "BLOB", "IMAGE", "VARBINARY", "VARBINARY(MAX)", "BINARY",
|
||||
// PostgreSQL Special Types
|
||||
"int4range", "int8range", "numrange", "tsrange", "tstzrange", "daterange",
|
||||
"HSTORE", "CITEXT", "INET", "MACADDR", "POINT", "LINE", "LSEG", "BOX", "PATH", "POLYGON", "CIRCLE",
|
||||
// Array Types
|
||||
"INTEGER ARRAY", "VARCHAR ARRAY", "TEXT ARRAY", "BIGINT ARRAY",
|
||||
// MySQL Specific
|
||||
"MEDIUMINT", "DOUBLE", "FLOAT(10,2)",
|
||||
// SQL Server Specific
|
||||
"MONEY", "SMALLMONEY", "SQL_VARIANT",
|
||||
}
|
||||
selectedTypeIndex := 0
|
||||
|
||||
form.AddInputField("Column Name", "", 40, nil, func(value string) {
|
||||
columnName = value
|
||||
})
|
||||
|
||||
form.AddDropDown("Data Type", columnTypes, selectedTypeIndex, func(option string, index int) {
|
||||
dataType = option
|
||||
})
|
||||
|
||||
form.AddCheckbox("Primary Key", false, nil)
|
||||
form.AddCheckbox("Not Null", false, nil)
|
||||
form.AddCheckbox("Unique", false, nil)
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
if columnName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Get form values
|
||||
isPK := form.GetFormItemByLabel("Primary Key").(*tview.Checkbox).IsChecked()
|
||||
isNotNull := form.GetFormItemByLabel("Not Null").(*tview.Checkbox).IsChecked()
|
||||
|
||||
se.CreateColumn(schemaIndex, tableIndex, columnName, dataType, isPK, isNotNull)
|
||||
|
||||
table := se.db.Schemas[schemaIndex].Tables[tableIndex]
|
||||
se.pages.RemovePage("new-column")
|
||||
se.pages.RemovePage("table-editor")
|
||||
se.showTableEditor(schemaIndex, tableIndex, table)
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
table := se.db.Schemas[schemaIndex].Tables[tableIndex]
|
||||
se.pages.RemovePage("new-column")
|
||||
se.pages.RemovePage("table-editor")
|
||||
se.showTableEditor(schemaIndex, tableIndex, table)
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" New Column ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("new-column", "table-editor")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("new-column", form, true, true)
|
||||
}
|
||||
15
pkg/ui/database_dataops.go
Normal file
15
pkg/ui/database_dataops.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// updateDatabase updates database properties
|
||||
func (se *SchemaEditor) updateDatabase(name, description, comment, dbType, dbVersion string) {
|
||||
se.db.Name = name
|
||||
se.db.Description = description
|
||||
se.db.Comment = comment
|
||||
se.db.DatabaseType = models.DatabaseType(dbType)
|
||||
se.db.DatabaseVersion = dbVersion
|
||||
se.db.UpdateDate()
|
||||
}
|
||||
72
pkg/ui/database_screens.go
Normal file
72
pkg/ui/database_screens.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// showEditDatabaseForm displays a dialog to edit database properties
|
||||
func (se *SchemaEditor) showEditDatabaseForm() {
|
||||
form := tview.NewForm()
|
||||
|
||||
dbName := se.db.Name
|
||||
dbDescription := se.db.Description
|
||||
dbComment := se.db.Comment
|
||||
dbType := string(se.db.DatabaseType)
|
||||
dbVersion := se.db.DatabaseVersion
|
||||
|
||||
// Database type options
|
||||
dbTypeOptions := []string{"pgsql", "mssql", "sqlite"}
|
||||
selectedTypeIndex := 0
|
||||
for i, opt := range dbTypeOptions {
|
||||
if opt == dbType {
|
||||
selectedTypeIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
form.AddInputField("Database Name", dbName, 40, nil, func(value string) {
|
||||
dbName = value
|
||||
})
|
||||
|
||||
form.AddInputField("Description", dbDescription, 50, nil, func(value string) {
|
||||
dbDescription = value
|
||||
})
|
||||
|
||||
form.AddInputField("Comment", dbComment, 50, nil, func(value string) {
|
||||
dbComment = value
|
||||
})
|
||||
|
||||
form.AddDropDown("Database Type", dbTypeOptions, selectedTypeIndex, func(option string, index int) {
|
||||
dbType = option
|
||||
})
|
||||
|
||||
form.AddInputField("Database Version", dbVersion, 20, nil, func(value string) {
|
||||
dbVersion = value
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
if dbName == "" {
|
||||
return
|
||||
}
|
||||
se.updateDatabase(dbName, dbDescription, dbComment, dbType, dbVersion)
|
||||
se.pages.RemovePage("edit-database")
|
||||
se.pages.RemovePage("main")
|
||||
se.pages.AddPage("main", se.createMainMenu(), true, true)
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
se.pages.RemovePage("edit-database")
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" Edit Database ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("edit-database", "main")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("edit-database", form, true, true)
|
||||
}
|
||||
139
pkg/ui/dialogs.go
Normal file
139
pkg/ui/dialogs.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// showExitConfirmation shows a confirmation dialog when trying to exit without saving
|
||||
func (se *SchemaEditor) showExitConfirmation(pageToRemove, pageToSwitchTo string) {
|
||||
modal := tview.NewModal().
|
||||
SetText("Exit without saving changes?").
|
||||
AddButtons([]string{"Cancel", "No, exit without saving"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "No, exit without saving" {
|
||||
se.pages.RemovePage(pageToRemove)
|
||||
se.pages.SwitchToPage(pageToSwitchTo)
|
||||
}
|
||||
se.pages.RemovePage("exit-confirm")
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("exit-confirm")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("exit-confirm", modal, true, true)
|
||||
}
|
||||
|
||||
// showExitEditorConfirm shows confirmation dialog when trying to exit the entire editor
|
||||
func (se *SchemaEditor) showExitEditorConfirm() {
|
||||
modal := tview.NewModal().
|
||||
SetText("Exit RelSpec Editor? Press ESC again to confirm.").
|
||||
AddButtons([]string{"Cancel", "Exit"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Exit" {
|
||||
se.app.Stop()
|
||||
}
|
||||
se.pages.RemovePage("exit-editor-confirm")
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.app.Stop()
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("exit-editor-confirm", modal, true, true)
|
||||
}
|
||||
|
||||
// showDeleteSchemaConfirm shows confirmation dialog for schema deletion
|
||||
func (se *SchemaEditor) showDeleteSchemaConfirm(schemaIndex int) {
|
||||
modal := tview.NewModal().
|
||||
SetText(fmt.Sprintf("Delete schema '%s'? This will delete all tables in this schema.",
|
||||
se.db.Schemas[schemaIndex].Name)).
|
||||
AddButtons([]string{"Cancel", "Delete"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Delete" {
|
||||
se.DeleteSchema(schemaIndex)
|
||||
se.pages.RemovePage("schema-editor")
|
||||
se.pages.RemovePage("schemas")
|
||||
se.showSchemaList()
|
||||
}
|
||||
se.pages.RemovePage("confirm-delete-schema")
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("confirm-delete-schema")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("confirm-delete-schema", modal, true, true)
|
||||
}
|
||||
|
||||
// showDeleteTableConfirm shows confirmation dialog for table deletion
|
||||
func (se *SchemaEditor) showDeleteTableConfirm(schemaIndex, tableIndex int) {
|
||||
table := se.db.Schemas[schemaIndex].Tables[tableIndex]
|
||||
modal := tview.NewModal().
|
||||
SetText(fmt.Sprintf("Delete table '%s'? This action cannot be undone.",
|
||||
table.Name)).
|
||||
AddButtons([]string{"Cancel", "Delete"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Delete" {
|
||||
se.DeleteTable(schemaIndex, tableIndex)
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
se.pages.RemovePage("table-editor")
|
||||
se.pages.RemovePage("schema-editor")
|
||||
se.showSchemaEditor(schemaIndex, schema)
|
||||
}
|
||||
se.pages.RemovePage("confirm-delete-table")
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("confirm-delete-table")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("confirm-delete-table", modal, true, true)
|
||||
}
|
||||
|
||||
// showDeleteColumnConfirm shows confirmation dialog for column deletion
|
||||
func (se *SchemaEditor) showDeleteColumnConfirm(schemaIndex, tableIndex int, columnName string) {
|
||||
modal := tview.NewModal().
|
||||
SetText(fmt.Sprintf("Delete column '%s'? This action cannot be undone.",
|
||||
columnName)).
|
||||
AddButtons([]string{"Cancel", "Delete"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Delete" {
|
||||
se.DeleteColumn(schemaIndex, tableIndex, columnName)
|
||||
se.pages.RemovePage("column-editor")
|
||||
se.pages.RemovePage("confirm-delete-column")
|
||||
se.pages.SwitchToPage("table-editor")
|
||||
} else {
|
||||
se.pages.RemovePage("confirm-delete-column")
|
||||
}
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("confirm-delete-column")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("confirm-delete-column", modal, true, true)
|
||||
}
|
||||
35
pkg/ui/domain_dataops.go
Normal file
35
pkg/ui/domain_dataops.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// createDomain creates a new domain
|
||||
func (se *SchemaEditor) createDomain(name, description string) {
|
||||
domain := &models.Domain{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Tables: make([]*models.DomainTable, 0),
|
||||
Sequence: uint(len(se.db.Domains)),
|
||||
}
|
||||
|
||||
se.db.Domains = append(se.db.Domains, domain)
|
||||
se.showDomainList()
|
||||
}
|
||||
|
||||
// updateDomain updates an existing domain
|
||||
func (se *SchemaEditor) updateDomain(index int, name, description string) {
|
||||
if index >= 0 && index < len(se.db.Domains) {
|
||||
se.db.Domains[index].Name = name
|
||||
se.db.Domains[index].Description = description
|
||||
se.showDomainList()
|
||||
}
|
||||
}
|
||||
|
||||
// deleteDomain deletes a domain by index
|
||||
func (se *SchemaEditor) deleteDomain(index int) {
|
||||
if index >= 0 && index < len(se.db.Domains) {
|
||||
se.db.Domains = append(se.db.Domains[:index], se.db.Domains[index+1:]...)
|
||||
se.showDomainList()
|
||||
}
|
||||
}
|
||||
258
pkg/ui/domain_screens.go
Normal file
258
pkg/ui/domain_screens.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// showDomainList displays the domain management screen
|
||||
func (se *SchemaEditor) showDomainList() {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title
|
||||
title := tview.NewTextView().
|
||||
SetText("[::b]Manage Domains").
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter)
|
||||
|
||||
// Create domains table
|
||||
domainTable := tview.NewTable().SetBorders(true).SetSelectable(true, false).SetFixed(1, 0)
|
||||
|
||||
// Add header row
|
||||
headers := []string{"Name", "Sequence", "Total Tables", "Description"}
|
||||
headerWidths := []int{20, 15, 20}
|
||||
for i, header := range headers {
|
||||
padding := ""
|
||||
if i < len(headerWidths) {
|
||||
padding = strings.Repeat(" ", headerWidths[i]-len(header))
|
||||
}
|
||||
cell := tview.NewTableCell(header + padding).
|
||||
SetTextColor(tcell.ColorYellow).
|
||||
SetSelectable(false).
|
||||
SetAlign(tview.AlignLeft)
|
||||
domainTable.SetCell(0, i, cell)
|
||||
}
|
||||
|
||||
// Add existing domains
|
||||
for row, domain := range se.db.Domains {
|
||||
domain := domain // capture for closure
|
||||
|
||||
// Name - pad to 20 chars
|
||||
nameStr := fmt.Sprintf("%-20s", domain.Name)
|
||||
nameCell := tview.NewTableCell(nameStr).SetSelectable(true)
|
||||
domainTable.SetCell(row+1, 0, nameCell)
|
||||
|
||||
// Sequence - pad to 15 chars
|
||||
seqStr := fmt.Sprintf("%-15s", fmt.Sprintf("%d", domain.Sequence))
|
||||
seqCell := tview.NewTableCell(seqStr).SetSelectable(true)
|
||||
domainTable.SetCell(row+1, 1, seqCell)
|
||||
|
||||
// Total Tables - pad to 20 chars
|
||||
tablesStr := fmt.Sprintf("%-20s", fmt.Sprintf("%d", len(domain.Tables)))
|
||||
tablesCell := tview.NewTableCell(tablesStr).SetSelectable(true)
|
||||
domainTable.SetCell(row+1, 2, tablesCell)
|
||||
|
||||
// Description - no padding, takes remaining space
|
||||
descCell := tview.NewTableCell(domain.Description).SetSelectable(true)
|
||||
domainTable.SetCell(row+1, 3, descCell)
|
||||
}
|
||||
|
||||
domainTable.SetTitle(" Domains ").SetBorder(true).SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
// Action buttons flex
|
||||
btnFlex := tview.NewFlex()
|
||||
btnNewDomain := tview.NewButton("New Domain [n]").SetSelectedFunc(func() {
|
||||
se.showNewDomainDialog()
|
||||
})
|
||||
btnBack := tview.NewButton("Back [b]").SetSelectedFunc(func() {
|
||||
se.pages.SwitchToPage("main")
|
||||
se.pages.RemovePage("domains")
|
||||
})
|
||||
|
||||
// Set up button input captures for Tab/Shift+Tab navigation
|
||||
btnNewDomain.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(domainTable)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnBack)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnBack.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(btnNewDomain)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(domainTable)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnFlex.AddItem(btnNewDomain, 0, 1, true).
|
||||
AddItem(btnBack, 0, 1, false)
|
||||
|
||||
domainTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.SwitchToPage("main")
|
||||
se.pages.RemovePage("domains")
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnNewDomain)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyEnter {
|
||||
row, _ := domainTable.GetSelection()
|
||||
if row > 0 && row <= len(se.db.Domains) { // Skip header row
|
||||
domainIndex := row - 1
|
||||
se.showDomainEditor(domainIndex, se.db.Domains[domainIndex])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if event.Rune() == 'n' {
|
||||
se.showNewDomainDialog()
|
||||
return nil
|
||||
}
|
||||
if event.Rune() == 'b' {
|
||||
se.pages.SwitchToPage("main")
|
||||
se.pages.RemovePage("domains")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
flex.AddItem(title, 1, 0, false).
|
||||
AddItem(domainTable, 0, 1, true).
|
||||
AddItem(btnFlex, 1, 0, false)
|
||||
|
||||
se.pages.AddPage("domains", flex, true, true)
|
||||
}
|
||||
|
||||
// showNewDomainDialog displays a dialog to create a new domain
|
||||
func (se *SchemaEditor) showNewDomainDialog() {
|
||||
form := tview.NewForm()
|
||||
|
||||
domainName := ""
|
||||
domainDesc := ""
|
||||
|
||||
form.AddInputField("Name", "", 40, nil, func(value string) {
|
||||
domainName = value
|
||||
})
|
||||
|
||||
form.AddInputField("Description", "", 50, nil, func(value string) {
|
||||
domainDesc = value
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
if domainName == "" {
|
||||
return
|
||||
}
|
||||
se.createDomain(domainName, domainDesc)
|
||||
se.pages.RemovePage("new-domain")
|
||||
se.pages.RemovePage("domains")
|
||||
se.showDomainList()
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
se.pages.RemovePage("new-domain")
|
||||
se.pages.RemovePage("domains")
|
||||
se.showDomainList()
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" New Domain ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("new-domain", "domains")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("new-domain", form, true, true)
|
||||
}
|
||||
|
||||
// showDomainEditor displays a dialog to edit an existing domain
|
||||
func (se *SchemaEditor) showDomainEditor(index int, domain *models.Domain) {
|
||||
form := tview.NewForm()
|
||||
|
||||
domainName := domain.Name
|
||||
domainDesc := domain.Description
|
||||
|
||||
form.AddInputField("Name", domainName, 40, nil, func(value string) {
|
||||
domainName = value
|
||||
})
|
||||
|
||||
form.AddInputField("Description", domainDesc, 50, nil, func(value string) {
|
||||
domainDesc = value
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
if domainName == "" {
|
||||
return
|
||||
}
|
||||
se.updateDomain(index, domainName, domainDesc)
|
||||
se.pages.RemovePage("edit-domain")
|
||||
se.pages.RemovePage("domains")
|
||||
se.showDomainList()
|
||||
})
|
||||
|
||||
form.AddButton("Delete", func() {
|
||||
se.showDeleteDomainConfirm(index)
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
se.pages.RemovePage("edit-domain")
|
||||
se.pages.RemovePage("domains")
|
||||
se.showDomainList()
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" Edit Domain ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("edit-domain", "domains")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("edit-domain", form, true, true)
|
||||
}
|
||||
|
||||
// showDeleteDomainConfirm shows a confirmation dialog before deleting a domain
|
||||
func (se *SchemaEditor) showDeleteDomainConfirm(index int) {
|
||||
modal := tview.NewModal().
|
||||
SetText(fmt.Sprintf("Delete domain '%s'? This action cannot be undone.", se.db.Domains[index].Name)).
|
||||
AddButtons([]string{"Cancel", "Delete"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Delete" {
|
||||
se.deleteDomain(index)
|
||||
se.pages.RemovePage("delete-domain-confirm")
|
||||
se.pages.RemovePage("edit-domain")
|
||||
se.pages.RemovePage("domains")
|
||||
se.showDomainList()
|
||||
} else {
|
||||
se.pages.RemovePage("delete-domain-confirm")
|
||||
}
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("delete-domain-confirm")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddAndSwitchToPage("delete-domain-confirm", modal, true)
|
||||
}
|
||||
73
pkg/ui/editor.go
Normal file
73
pkg/ui/editor.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// SchemaEditor represents the interactive schema editor
|
||||
type SchemaEditor struct {
|
||||
db *models.Database
|
||||
app *tview.Application
|
||||
pages *tview.Pages
|
||||
loadConfig *LoadConfig
|
||||
saveConfig *SaveConfig
|
||||
}
|
||||
|
||||
// NewSchemaEditor creates a new schema editor
|
||||
func NewSchemaEditor(db *models.Database) *SchemaEditor {
|
||||
return &SchemaEditor{
|
||||
db: db,
|
||||
app: tview.NewApplication(),
|
||||
pages: tview.NewPages(),
|
||||
loadConfig: nil,
|
||||
saveConfig: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSchemaEditorWithConfigs creates a new schema editor with load/save configurations
|
||||
func NewSchemaEditorWithConfigs(db *models.Database, loadConfig *LoadConfig, saveConfig *SaveConfig) *SchemaEditor {
|
||||
return &SchemaEditor{
|
||||
db: db,
|
||||
app: tview.NewApplication(),
|
||||
pages: tview.NewPages(),
|
||||
loadConfig: loadConfig,
|
||||
saveConfig: saveConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the interactive editor
|
||||
func (se *SchemaEditor) Run() error {
|
||||
// If no database is loaded, show load screen
|
||||
if se.db == nil {
|
||||
se.showLoadScreen()
|
||||
} else {
|
||||
// Create main menu view
|
||||
mainMenu := se.createMainMenu()
|
||||
se.pages.AddPage("main", mainMenu, true, true)
|
||||
}
|
||||
|
||||
// Run the application
|
||||
if err := se.app.SetRoot(se.pages, true).Run(); err != nil {
|
||||
return fmt.Errorf("application error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatabase returns the current database
|
||||
func (se *SchemaEditor) GetDatabase() *models.Database {
|
||||
return se.db
|
||||
}
|
||||
|
||||
// Helper function to get sorted column names
|
||||
func getColumnNames(table *models.Table) []string {
|
||||
names := make([]string, 0, len(table.Columns))
|
||||
for name := range table.Columns {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
526
pkg/ui/load_save_screens.go
Normal file
526
pkg/ui/load_save_screens.go
Normal file
@@ -0,0 +1,526 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
rbun "git.warky.dev/wdevs/relspecgo/pkg/readers/bun"
|
||||
rdbml "git.warky.dev/wdevs/relspecgo/pkg/readers/dbml"
|
||||
rdctx "git.warky.dev/wdevs/relspecgo/pkg/readers/dctx"
|
||||
rdrawdb "git.warky.dev/wdevs/relspecgo/pkg/readers/drawdb"
|
||||
rdrizzle "git.warky.dev/wdevs/relspecgo/pkg/readers/drizzle"
|
||||
rgorm "git.warky.dev/wdevs/relspecgo/pkg/readers/gorm"
|
||||
rgraphql "git.warky.dev/wdevs/relspecgo/pkg/readers/graphql"
|
||||
rjson "git.warky.dev/wdevs/relspecgo/pkg/readers/json"
|
||||
rpgsql "git.warky.dev/wdevs/relspecgo/pkg/readers/pgsql"
|
||||
rprisma "git.warky.dev/wdevs/relspecgo/pkg/readers/prisma"
|
||||
rtypeorm "git.warky.dev/wdevs/relspecgo/pkg/readers/typeorm"
|
||||
ryaml "git.warky.dev/wdevs/relspecgo/pkg/readers/yaml"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
wbun "git.warky.dev/wdevs/relspecgo/pkg/writers/bun"
|
||||
wdbml "git.warky.dev/wdevs/relspecgo/pkg/writers/dbml"
|
||||
wdctx "git.warky.dev/wdevs/relspecgo/pkg/writers/dctx"
|
||||
wdrawdb "git.warky.dev/wdevs/relspecgo/pkg/writers/drawdb"
|
||||
wdrizzle "git.warky.dev/wdevs/relspecgo/pkg/writers/drizzle"
|
||||
wgorm "git.warky.dev/wdevs/relspecgo/pkg/writers/gorm"
|
||||
wgraphql "git.warky.dev/wdevs/relspecgo/pkg/writers/graphql"
|
||||
wjson "git.warky.dev/wdevs/relspecgo/pkg/writers/json"
|
||||
wpgsql "git.warky.dev/wdevs/relspecgo/pkg/writers/pgsql"
|
||||
wprisma "git.warky.dev/wdevs/relspecgo/pkg/writers/prisma"
|
||||
wtypeorm "git.warky.dev/wdevs/relspecgo/pkg/writers/typeorm"
|
||||
wyaml "git.warky.dev/wdevs/relspecgo/pkg/writers/yaml"
|
||||
)
|
||||
|
||||
// LoadConfig holds the configuration for loading a database
|
||||
type LoadConfig struct {
|
||||
SourceType string
|
||||
FilePath string
|
||||
ConnString string
|
||||
}
|
||||
|
||||
// SaveConfig holds the configuration for saving a database
|
||||
type SaveConfig struct {
|
||||
TargetType string
|
||||
FilePath string
|
||||
ConnString string
|
||||
}
|
||||
|
||||
// showLoadScreen displays the database load screen
|
||||
func (se *SchemaEditor) showLoadScreen() {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title
|
||||
title := tview.NewTextView().
|
||||
SetText("[::b]Load Database Schema").
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetDynamicColors(true)
|
||||
|
||||
// Form
|
||||
form := tview.NewForm()
|
||||
form.SetBorder(true).SetTitle(" Load Configuration ").SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
// Format selection
|
||||
formatOptions := []string{
|
||||
"dbml", "dctx", "drawdb", "graphql", "json", "yaml",
|
||||
"gorm", "bun", "drizzle", "prisma", "typeorm", "pgsql",
|
||||
}
|
||||
selectedFormat := 0
|
||||
currentFormat := formatOptions[selectedFormat]
|
||||
|
||||
// File path input
|
||||
filePath := ""
|
||||
connString := ""
|
||||
|
||||
form.AddDropDown("Format", formatOptions, 0, func(option string, index int) {
|
||||
selectedFormat = index
|
||||
currentFormat = option
|
||||
})
|
||||
|
||||
form.AddInputField("File Path", "", 50, nil, func(value string) {
|
||||
filePath = value
|
||||
})
|
||||
|
||||
form.AddInputField("Connection String", "", 50, nil, func(value string) {
|
||||
connString = value
|
||||
})
|
||||
|
||||
form.AddTextView("Help", getLoadHelpText(), 0, 5, true, false)
|
||||
|
||||
// Buttons
|
||||
form.AddButton("Load [l]", func() {
|
||||
se.loadDatabase(currentFormat, filePath, connString)
|
||||
})
|
||||
|
||||
form.AddButton("Create New [n]", func() {
|
||||
se.createNewDatabase()
|
||||
})
|
||||
|
||||
form.AddButton("Exit [q]", func() {
|
||||
se.app.Stop()
|
||||
})
|
||||
|
||||
// Keyboard shortcuts
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Key() {
|
||||
case tcell.KeyEscape:
|
||||
se.app.Stop()
|
||||
return nil
|
||||
}
|
||||
switch event.Rune() {
|
||||
case 'l':
|
||||
se.loadDatabase(currentFormat, filePath, connString)
|
||||
return nil
|
||||
case 'n':
|
||||
se.createNewDatabase()
|
||||
return nil
|
||||
case 'q':
|
||||
se.app.Stop()
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
// Tab navigation
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.app.Stop()
|
||||
return nil
|
||||
}
|
||||
if event.Rune() == 'l' || event.Rune() == 'n' || event.Rune() == 'q' {
|
||||
return event
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
flex.AddItem(title, 1, 0, false).
|
||||
AddItem(form, 0, 1, true)
|
||||
|
||||
se.pages.AddAndSwitchToPage("load-database", flex, true)
|
||||
}
|
||||
|
||||
// showSaveScreen displays the save database screen
|
||||
func (se *SchemaEditor) showSaveScreen() {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title
|
||||
title := tview.NewTextView().
|
||||
SetText("[::b]Save Database Schema").
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetDynamicColors(true)
|
||||
|
||||
// Form
|
||||
form := tview.NewForm()
|
||||
form.SetBorder(true).SetTitle(" Save Configuration ").SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
// Format selection
|
||||
formatOptions := []string{
|
||||
"dbml", "dctx", "drawdb", "graphql", "json", "yaml",
|
||||
"gorm", "bun", "drizzle", "prisma", "typeorm", "pgsql",
|
||||
}
|
||||
selectedFormat := 0
|
||||
currentFormat := formatOptions[selectedFormat]
|
||||
|
||||
// File path input
|
||||
filePath := ""
|
||||
if se.saveConfig != nil {
|
||||
// Pre-populate with existing save config
|
||||
for i, format := range formatOptions {
|
||||
if format == se.saveConfig.TargetType {
|
||||
selectedFormat = i
|
||||
currentFormat = format
|
||||
break
|
||||
}
|
||||
}
|
||||
filePath = se.saveConfig.FilePath
|
||||
}
|
||||
|
||||
form.AddDropDown("Format", formatOptions, selectedFormat, func(option string, index int) {
|
||||
selectedFormat = index
|
||||
currentFormat = option
|
||||
})
|
||||
|
||||
form.AddInputField("File Path", filePath, 50, nil, func(value string) {
|
||||
filePath = value
|
||||
})
|
||||
|
||||
form.AddTextView("Help", getSaveHelpText(), 0, 5, true, false)
|
||||
|
||||
// Buttons
|
||||
form.AddButton("Save [s]", func() {
|
||||
se.saveDatabase(currentFormat, filePath)
|
||||
})
|
||||
|
||||
form.AddButton("Update Existing Database [u]", func() {
|
||||
// Use saveConfig if available, otherwise use loadConfig
|
||||
if se.saveConfig != nil {
|
||||
se.showUpdateExistingDatabaseConfirm()
|
||||
} else if se.loadConfig != nil {
|
||||
se.showUpdateExistingDatabaseConfirm()
|
||||
} else {
|
||||
se.showErrorDialog("Error", "No database source found. Use Save instead.")
|
||||
}
|
||||
})
|
||||
|
||||
form.AddButton("Back [b]", func() {
|
||||
se.pages.RemovePage("save-database")
|
||||
se.pages.SwitchToPage("main")
|
||||
})
|
||||
|
||||
// Keyboard shortcuts
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Key() {
|
||||
case tcell.KeyEscape:
|
||||
se.pages.RemovePage("save-database")
|
||||
se.pages.SwitchToPage("main")
|
||||
return nil
|
||||
}
|
||||
switch event.Rune() {
|
||||
case 's':
|
||||
se.saveDatabase(currentFormat, filePath)
|
||||
return nil
|
||||
case 'u':
|
||||
// Use saveConfig if available, otherwise use loadConfig
|
||||
if se.saveConfig != nil {
|
||||
se.showUpdateExistingDatabaseConfirm()
|
||||
} else if se.loadConfig != nil {
|
||||
se.showUpdateExistingDatabaseConfirm()
|
||||
} else {
|
||||
se.showErrorDialog("Error", "No database source found. Use Save instead.")
|
||||
}
|
||||
return nil
|
||||
case 'b':
|
||||
se.pages.RemovePage("save-database")
|
||||
se.pages.SwitchToPage("main")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
flex.AddItem(title, 1, 0, false).
|
||||
AddItem(form, 0, 1, true)
|
||||
|
||||
se.pages.AddAndSwitchToPage("save-database", flex, true)
|
||||
}
|
||||
|
||||
// loadDatabase loads a database from the specified configuration
|
||||
func (se *SchemaEditor) loadDatabase(format, filePath, connString string) {
|
||||
// Validate input
|
||||
if format == "pgsql" {
|
||||
if connString == "" {
|
||||
se.showErrorDialog("Error", "Connection string is required for PostgreSQL")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if filePath == "" {
|
||||
se.showErrorDialog("Error", "File path is required for "+format)
|
||||
return
|
||||
}
|
||||
// Expand home directory
|
||||
if len(filePath) > 0 && filePath[0] == '~' {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
filePath = filepath.Join(home, filePath[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create reader
|
||||
var reader readers.Reader
|
||||
switch format {
|
||||
case "dbml":
|
||||
reader = rdbml.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "dctx":
|
||||
reader = rdctx.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "drawdb":
|
||||
reader = rdrawdb.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "graphql":
|
||||
reader = rgraphql.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "json":
|
||||
reader = rjson.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "yaml":
|
||||
reader = ryaml.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "gorm":
|
||||
reader = rgorm.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "bun":
|
||||
reader = rbun.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "drizzle":
|
||||
reader = rdrizzle.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "prisma":
|
||||
reader = rprisma.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "typeorm":
|
||||
reader = rtypeorm.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "pgsql":
|
||||
reader = rpgsql.NewReader(&readers.ReaderOptions{ConnectionString: connString})
|
||||
default:
|
||||
se.showErrorDialog("Error", "Unsupported format: "+format)
|
||||
return
|
||||
}
|
||||
|
||||
// Read database
|
||||
db, err := reader.ReadDatabase()
|
||||
if err != nil {
|
||||
se.showErrorDialog("Load Error", fmt.Sprintf("Failed to load database: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Store load config
|
||||
se.loadConfig = &LoadConfig{
|
||||
SourceType: format,
|
||||
FilePath: filePath,
|
||||
ConnString: connString,
|
||||
}
|
||||
|
||||
// Update database
|
||||
se.db = db
|
||||
|
||||
// Show success and switch to main menu
|
||||
se.showSuccessDialog("Load Complete", fmt.Sprintf("Successfully loaded database '%s'", db.Name), func() {
|
||||
se.pages.RemovePage("load-database")
|
||||
se.pages.RemovePage("main")
|
||||
se.pages.AddPage("main", se.createMainMenu(), true, true)
|
||||
})
|
||||
}
|
||||
|
||||
// saveDatabase saves the database to the specified configuration
|
||||
func (se *SchemaEditor) saveDatabase(format, filePath string) {
|
||||
// Validate input
|
||||
if format == "pgsql" {
|
||||
se.showErrorDialog("Error", "Direct PostgreSQL save is not supported from the UI. Use --to pgsql --to-path output.sql")
|
||||
return
|
||||
}
|
||||
|
||||
if filePath == "" {
|
||||
se.showErrorDialog("Error", "File path is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Expand home directory
|
||||
if len(filePath) > 0 && filePath[0] == '~' {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
filePath = filepath.Join(home, filePath[1:])
|
||||
}
|
||||
}
|
||||
|
||||
// Create writer
|
||||
var writer writers.Writer
|
||||
switch format {
|
||||
case "dbml":
|
||||
writer = wdbml.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "dctx":
|
||||
writer = wdctx.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "drawdb":
|
||||
writer = wdrawdb.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "graphql":
|
||||
writer = wgraphql.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "json":
|
||||
writer = wjson.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "yaml":
|
||||
writer = wyaml.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "gorm":
|
||||
writer = wgorm.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "bun":
|
||||
writer = wbun.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "drizzle":
|
||||
writer = wdrizzle.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "prisma":
|
||||
writer = wprisma.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "typeorm":
|
||||
writer = wtypeorm.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "pgsql":
|
||||
writer = wpgsql.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
default:
|
||||
se.showErrorDialog("Error", "Unsupported format: "+format)
|
||||
return
|
||||
}
|
||||
|
||||
// Write database
|
||||
err := writer.WriteDatabase(se.db)
|
||||
if err != nil {
|
||||
se.showErrorDialog("Save Error", fmt.Sprintf("Failed to save database: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Store save config
|
||||
se.saveConfig = &SaveConfig{
|
||||
TargetType: format,
|
||||
FilePath: filePath,
|
||||
}
|
||||
|
||||
// Show success
|
||||
se.showSuccessDialog("Save Complete", fmt.Sprintf("Successfully saved database to %s", filePath), func() {
|
||||
se.pages.RemovePage("save-database")
|
||||
se.pages.SwitchToPage("main")
|
||||
})
|
||||
}
|
||||
|
||||
// createNewDatabase creates a new empty database
|
||||
func (se *SchemaEditor) createNewDatabase() {
|
||||
// Create a new empty database
|
||||
se.db = &models.Database{
|
||||
Name: "New Database",
|
||||
Schemas: []*models.Schema{},
|
||||
}
|
||||
|
||||
// Clear load config
|
||||
se.loadConfig = nil
|
||||
|
||||
// Show success and switch to main menu
|
||||
se.showSuccessDialog("New Database", "Created new empty database", func() {
|
||||
se.pages.RemovePage("load-database")
|
||||
se.pages.AddPage("main", se.createMainMenu(), true, true)
|
||||
})
|
||||
}
|
||||
|
||||
// showErrorDialog displays an error dialog
|
||||
func (se *SchemaEditor) showErrorDialog(_title, message string) {
|
||||
modal := tview.NewModal().
|
||||
SetText(message).
|
||||
AddButtons([]string{"OK"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
se.pages.RemovePage("error-dialog")
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("error-dialog")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("error-dialog", modal, true, true)
|
||||
}
|
||||
|
||||
// showSuccessDialog displays a success dialog
|
||||
func (se *SchemaEditor) showSuccessDialog(_title, message string, onClose func()) {
|
||||
modal := tview.NewModal().
|
||||
SetText(message).
|
||||
AddButtons([]string{"OK"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
se.pages.RemovePage("success-dialog")
|
||||
if onClose != nil {
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("success-dialog")
|
||||
if onClose != nil {
|
||||
onClose()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("success-dialog", modal, true, true)
|
||||
}
|
||||
|
||||
// getLoadHelpText returns the help text for the load screen
|
||||
func getLoadHelpText() string {
|
||||
return `File-based formats: dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm
|
||||
Database formats: pgsql (requires connection string)
|
||||
|
||||
Examples:
|
||||
- File path: ~/schemas/mydb.dbml or /path/to/schema.json
|
||||
- Connection: postgres://user:pass@localhost/dbname`
|
||||
}
|
||||
|
||||
// showUpdateExistingDatabaseConfirm displays a confirmation dialog before updating existing database
|
||||
func (se *SchemaEditor) showUpdateExistingDatabaseConfirm() {
|
||||
// Use saveConfig if available, otherwise use loadConfig
|
||||
var targetType, targetPath string
|
||||
if se.saveConfig != nil {
|
||||
targetType = se.saveConfig.TargetType
|
||||
targetPath = se.saveConfig.FilePath
|
||||
} else if se.loadConfig != nil {
|
||||
targetType = se.loadConfig.SourceType
|
||||
targetPath = se.loadConfig.FilePath
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
confirmText := fmt.Sprintf("Update existing database?\n\nFormat: %s\nPath: %s\n\nThis will overwrite the source.",
|
||||
targetType, targetPath)
|
||||
|
||||
modal := tview.NewModal().
|
||||
SetText(confirmText).
|
||||
AddButtons([]string{"Cancel", "Update"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Update" {
|
||||
se.pages.RemovePage("update-confirm")
|
||||
se.pages.RemovePage("save-database")
|
||||
se.saveDatabase(targetType, targetPath)
|
||||
se.pages.SwitchToPage("main")
|
||||
} else {
|
||||
se.pages.RemovePage("update-confirm")
|
||||
}
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("update-confirm")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddAndSwitchToPage("update-confirm", modal, true)
|
||||
}
|
||||
|
||||
// getSaveHelpText returns the help text for the save screen
|
||||
func getSaveHelpText() string {
|
||||
return `File-based formats: dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql (SQL export)
|
||||
|
||||
Examples:
|
||||
- File: ~/schemas/mydb.dbml
|
||||
- Directory (for code formats): ./models/`
|
||||
}
|
||||
62
pkg/ui/main_menu.go
Normal file
62
pkg/ui/main_menu.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// createMainMenu creates the main menu screen
|
||||
func (se *SchemaEditor) createMainMenu() tview.Primitive {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title with database name
|
||||
dbName := se.db.Name
|
||||
if dbName == "" {
|
||||
dbName = "Untitled"
|
||||
}
|
||||
updateAtStr := ""
|
||||
if se.db.UpdatedAt != "" {
|
||||
updateAtStr = fmt.Sprintf("Updated @ %s", se.db.UpdatedAt)
|
||||
}
|
||||
titleText := fmt.Sprintf("[::b]RelSpec Schema Editor\n[::d]Database: %s %s\n[::d]Press arrow keys to navigate, Enter to select", dbName, updateAtStr)
|
||||
title := tview.NewTextView().
|
||||
SetText(titleText).
|
||||
SetDynamicColors(true)
|
||||
|
||||
// Menu options
|
||||
menu := tview.NewList().
|
||||
AddItem("Edit Database", "Edit database name, description, and properties", 'e', func() {
|
||||
se.showEditDatabaseForm()
|
||||
}).
|
||||
AddItem("Manage Schemas", "View, create, edit, and delete schemas", 's', func() {
|
||||
se.showSchemaList()
|
||||
}).
|
||||
AddItem("Manage Tables", "View and manage tables in schemas", 't', func() {
|
||||
se.showTableList()
|
||||
}).
|
||||
AddItem("Manage Domains", "View, create, edit, and delete domains", 'd', func() {
|
||||
se.showDomainList()
|
||||
}).
|
||||
AddItem("Save Database", "Save database to file or database", 'w', func() {
|
||||
se.showSaveScreen()
|
||||
}).
|
||||
AddItem("Exit Editor", "Exit the editor", 'q', func() {
|
||||
se.app.Stop()
|
||||
})
|
||||
|
||||
menu.SetBorder(true).SetTitle(" Menu ").SetTitleAlign(tview.AlignLeft)
|
||||
menu.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitEditorConfirm()
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
flex.AddItem(title, 5, 0, false).
|
||||
AddItem(menu, 0, 1, true)
|
||||
|
||||
return flex
|
||||
}
|
||||
55
pkg/ui/schema_dataops.go
Normal file
55
pkg/ui/schema_dataops.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package ui
|
||||
|
||||
import "git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
|
||||
// Schema data operations - business logic for schema management
|
||||
|
||||
// CreateSchema creates a new schema and adds it to the database
|
||||
func (se *SchemaEditor) CreateSchema(name, description string) *models.Schema {
|
||||
newSchema := &models.Schema{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Tables: make([]*models.Table, 0),
|
||||
Sequences: make([]*models.Sequence, 0),
|
||||
Enums: make([]*models.Enum, 0),
|
||||
}
|
||||
se.db.UpdateDate()
|
||||
se.db.Schemas = append(se.db.Schemas, newSchema)
|
||||
return newSchema
|
||||
}
|
||||
|
||||
// UpdateSchema updates an existing schema's properties
|
||||
func (se *SchemaEditor) UpdateSchema(schemaIndex int, name, owner, description string) {
|
||||
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||
return
|
||||
}
|
||||
se.db.UpdateDate()
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
schema.Name = name
|
||||
schema.Owner = owner
|
||||
schema.Description = description
|
||||
schema.UpdateDate()
|
||||
}
|
||||
|
||||
// DeleteSchema removes a schema from the database
|
||||
func (se *SchemaEditor) DeleteSchema(schemaIndex int) bool {
|
||||
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||
return false
|
||||
}
|
||||
se.db.UpdateDate()
|
||||
se.db.Schemas = append(se.db.Schemas[:schemaIndex], se.db.Schemas[schemaIndex+1:]...)
|
||||
return true
|
||||
}
|
||||
|
||||
// GetSchema returns a schema by index
|
||||
func (se *SchemaEditor) GetSchema(schemaIndex int) *models.Schema {
|
||||
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||
return nil
|
||||
}
|
||||
return se.db.Schemas[schemaIndex]
|
||||
}
|
||||
|
||||
// GetAllSchemas returns all schemas
|
||||
func (se *SchemaEditor) GetAllSchemas() []*models.Schema {
|
||||
return se.db.Schemas
|
||||
}
|
||||
351
pkg/ui/schema_screens.go
Normal file
351
pkg/ui/schema_screens.go
Normal file
@@ -0,0 +1,351 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// showSchemaList displays the schema management screen
|
||||
func (se *SchemaEditor) showSchemaList() {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title
|
||||
title := tview.NewTextView().
|
||||
SetText("[::b]Manage Schemas").
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter)
|
||||
|
||||
// Create schemas table
|
||||
schemaTable := tview.NewTable().SetBorders(true).SetSelectable(true, false).SetFixed(1, 0)
|
||||
|
||||
// Add header row with padding for full width
|
||||
headers := []string{"Name", "Sequence", "Total Tables", "Total Sequences", "Total Views", "Description"}
|
||||
headerWidths := []int{20, 15, 20, 20, 15} // Last column takes remaining space
|
||||
for i, header := range headers {
|
||||
padding := ""
|
||||
if i < len(headerWidths) {
|
||||
padding = strings.Repeat(" ", headerWidths[i]-len(header))
|
||||
}
|
||||
cell := tview.NewTableCell(header + padding).
|
||||
SetTextColor(tcell.ColorYellow).
|
||||
SetSelectable(false).
|
||||
SetAlign(tview.AlignLeft)
|
||||
schemaTable.SetCell(0, i, cell)
|
||||
}
|
||||
|
||||
// Add existing schemas
|
||||
for row, schema := range se.db.Schemas {
|
||||
schema := schema // capture for closure
|
||||
|
||||
// Name - pad to 20 chars
|
||||
nameStr := fmt.Sprintf("%-20s", schema.Name)
|
||||
nameCell := tview.NewTableCell(nameStr).SetSelectable(true)
|
||||
schemaTable.SetCell(row+1, 0, nameCell)
|
||||
|
||||
// Sequence - pad to 15 chars
|
||||
seqStr := fmt.Sprintf("%-15s", fmt.Sprintf("%d", schema.Sequence))
|
||||
seqCell := tview.NewTableCell(seqStr).SetSelectable(true)
|
||||
schemaTable.SetCell(row+1, 1, seqCell)
|
||||
|
||||
// Total Tables - pad to 20 chars
|
||||
tablesStr := fmt.Sprintf("%-20s", fmt.Sprintf("%d", len(schema.Tables)))
|
||||
tablesCell := tview.NewTableCell(tablesStr).SetSelectable(true)
|
||||
schemaTable.SetCell(row+1, 2, tablesCell)
|
||||
|
||||
// Total Sequences - pad to 20 chars
|
||||
sequencesStr := fmt.Sprintf("%-20s", fmt.Sprintf("%d", len(schema.Sequences)))
|
||||
sequencesCell := tview.NewTableCell(sequencesStr).SetSelectable(true)
|
||||
schemaTable.SetCell(row+1, 3, sequencesCell)
|
||||
|
||||
// Total Views - pad to 15 chars
|
||||
viewsStr := fmt.Sprintf("%-15s", fmt.Sprintf("%d", len(schema.Views)))
|
||||
viewsCell := tview.NewTableCell(viewsStr).SetSelectable(true)
|
||||
schemaTable.SetCell(row+1, 4, viewsCell)
|
||||
|
||||
// Description - no padding, takes remaining space
|
||||
descCell := tview.NewTableCell(schema.Description).SetSelectable(true)
|
||||
schemaTable.SetCell(row+1, 5, descCell)
|
||||
}
|
||||
|
||||
schemaTable.SetTitle(" Schemas ").SetBorder(true).SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
// Action buttons flex (define before input capture)
|
||||
btnFlex := tview.NewFlex()
|
||||
btnNewSchema := tview.NewButton("New Schema [n]").SetSelectedFunc(func() {
|
||||
se.showNewSchemaDialog()
|
||||
})
|
||||
btnBack := tview.NewButton("Back [b]").SetSelectedFunc(func() {
|
||||
se.pages.SwitchToPage("main")
|
||||
se.pages.RemovePage("schemas")
|
||||
})
|
||||
|
||||
// Set up button input captures for Tab/Shift+Tab navigation
|
||||
btnNewSchema.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(schemaTable)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnBack)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnBack.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(btnNewSchema)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(schemaTable)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnFlex.AddItem(btnNewSchema, 0, 1, true).
|
||||
AddItem(btnBack, 0, 1, false)
|
||||
|
||||
schemaTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.SwitchToPage("main")
|
||||
se.pages.RemovePage("schemas")
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnNewSchema)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyEnter {
|
||||
row, _ := schemaTable.GetSelection()
|
||||
if row > 0 && row <= len(se.db.Schemas) { // Skip header row
|
||||
schemaIndex := row - 1
|
||||
se.showSchemaEditor(schemaIndex, se.db.Schemas[schemaIndex])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if event.Rune() == 'n' {
|
||||
se.showNewSchemaDialog()
|
||||
return nil
|
||||
}
|
||||
if event.Rune() == 'b' {
|
||||
se.pages.SwitchToPage("main")
|
||||
se.pages.RemovePage("schemas")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
flex.AddItem(title, 1, 0, false).
|
||||
AddItem(schemaTable, 0, 1, true).
|
||||
AddItem(btnFlex, 1, 0, false)
|
||||
|
||||
se.pages.AddPage("schemas", flex, true, true)
|
||||
}
|
||||
|
||||
// showSchemaEditor shows the editor for a specific schema
|
||||
func (se *SchemaEditor) showSchemaEditor(index int, schema *models.Schema) {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title
|
||||
title := tview.NewTextView().
|
||||
SetText(fmt.Sprintf("[::b]Schema: %s", schema.Name)).
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter)
|
||||
|
||||
// Schema info display
|
||||
info := tview.NewTextView().SetDynamicColors(true)
|
||||
info.SetText(fmt.Sprintf("Tables: %d | Description: %s",
|
||||
len(schema.Tables), schema.Description))
|
||||
|
||||
// Table list
|
||||
tableList := tview.NewList().ShowSecondaryText(true)
|
||||
|
||||
for i, table := range schema.Tables {
|
||||
tableIndex := i
|
||||
table := table
|
||||
colCount := len(table.Columns)
|
||||
tableList.AddItem(table.Name, fmt.Sprintf("%d columns", colCount), rune('0'+i), func() {
|
||||
se.showTableEditor(index, tableIndex, table)
|
||||
})
|
||||
}
|
||||
|
||||
tableList.AddItem("[New Table]", "Add a new table to this schema", 'n', func() {
|
||||
se.showNewTableDialog(index)
|
||||
})
|
||||
|
||||
tableList.AddItem("[Edit Schema Info]", "Edit schema properties", 'e', func() {
|
||||
se.showEditSchemaDialog(index)
|
||||
})
|
||||
|
||||
tableList.AddItem("[Delete Schema]", "Delete this schema", 'd', func() {
|
||||
se.showDeleteSchemaConfirm(index)
|
||||
})
|
||||
|
||||
tableList.SetBorder(true).SetTitle(" Tables ").SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
// Action buttons (define before input capture)
|
||||
btnFlex := tview.NewFlex()
|
||||
btnNewTable := tview.NewButton("New Table [n]").SetSelectedFunc(func() {
|
||||
se.showNewTableDialog(index)
|
||||
})
|
||||
btnBack := tview.NewButton("Back to Schemas [b]").SetSelectedFunc(func() {
|
||||
se.pages.RemovePage("schema-editor")
|
||||
se.pages.SwitchToPage("schemas")
|
||||
})
|
||||
|
||||
// Set up button input captures for Tab/Shift+Tab navigation
|
||||
btnNewTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(tableList)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnBack)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnBack.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(btnNewTable)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(tableList)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
tableList.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("schema-editor")
|
||||
se.pages.SwitchToPage("schemas")
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnNewTable)
|
||||
return nil
|
||||
}
|
||||
if event.Rune() == 'b' {
|
||||
se.pages.RemovePage("schema-editor")
|
||||
se.pages.SwitchToPage("schemas")
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnFlex.AddItem(btnNewTable, 0, 1, true).
|
||||
AddItem(btnBack, 0, 1, false)
|
||||
|
||||
flex.AddItem(title, 1, 0, false).
|
||||
AddItem(info, 2, 0, false).
|
||||
AddItem(tableList, 0, 1, true).
|
||||
AddItem(btnFlex, 1, 0, false)
|
||||
|
||||
se.pages.AddPage("schema-editor", flex, true, true)
|
||||
}
|
||||
|
||||
// showNewSchemaDialog shows dialog to create a new schema
|
||||
func (se *SchemaEditor) showNewSchemaDialog() {
|
||||
form := tview.NewForm()
|
||||
|
||||
schemaName := ""
|
||||
description := ""
|
||||
|
||||
form.AddInputField("Schema Name", "", 40, nil, func(value string) {
|
||||
schemaName = value
|
||||
})
|
||||
|
||||
form.AddInputField("Description", "", 40, nil, func(value string) {
|
||||
description = value
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
if schemaName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
se.CreateSchema(schemaName, description)
|
||||
|
||||
se.pages.RemovePage("new-schema")
|
||||
se.pages.RemovePage("schemas")
|
||||
se.showSchemaList()
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
se.pages.RemovePage("new-schema")
|
||||
se.pages.RemovePage("schemas")
|
||||
se.showSchemaList()
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" New Schema ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("new-schema", "schemas")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("new-schema", form, true, true)
|
||||
}
|
||||
|
||||
// showEditSchemaDialog shows dialog to edit schema properties
|
||||
func (se *SchemaEditor) showEditSchemaDialog(schemaIndex int) {
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
form := tview.NewForm()
|
||||
|
||||
// Local variables to collect changes
|
||||
newName := schema.Name
|
||||
newOwner := schema.Owner
|
||||
newDescription := schema.Description
|
||||
|
||||
form.AddInputField("Schema Name", schema.Name, 40, nil, func(value string) {
|
||||
newName = value
|
||||
})
|
||||
|
||||
form.AddInputField("Owner", schema.Owner, 40, nil, func(value string) {
|
||||
newOwner = value
|
||||
})
|
||||
|
||||
form.AddTextArea("Description", schema.Description, 40, 5, 0, func(value string) {
|
||||
newDescription = value
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
// Apply changes using dataops
|
||||
se.UpdateSchema(schemaIndex, newName, newOwner, newDescription)
|
||||
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
se.pages.RemovePage("edit-schema")
|
||||
se.pages.RemovePage("schema-editor")
|
||||
se.showSchemaEditor(schemaIndex, schema)
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
// Discard changes - don't apply them
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
se.pages.RemovePage("edit-schema")
|
||||
se.pages.RemovePage("schema-editor")
|
||||
se.showSchemaEditor(schemaIndex, schema)
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" Edit Schema ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("edit-schema", "schema-editor")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("edit-schema", form, true, true)
|
||||
}
|
||||
88
pkg/ui/table_dataops.go
Normal file
88
pkg/ui/table_dataops.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package ui
|
||||
|
||||
import "git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
|
||||
// Table data operations - business logic for table management
|
||||
|
||||
// CreateTable creates a new table and adds it to a schema
|
||||
func (se *SchemaEditor) CreateTable(schemaIndex int, name, description string) *models.Table {
|
||||
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||
return nil
|
||||
}
|
||||
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
newTable := &models.Table{
|
||||
Name: name,
|
||||
Schema: schema.Name,
|
||||
Description: description,
|
||||
Columns: make(map[string]*models.Column),
|
||||
Constraints: make(map[string]*models.Constraint),
|
||||
Indexes: make(map[string]*models.Index),
|
||||
}
|
||||
schema.UpdateDate()
|
||||
schema.Tables = append(schema.Tables, newTable)
|
||||
return newTable
|
||||
}
|
||||
|
||||
// UpdateTable updates an existing table's properties
|
||||
func (se *SchemaEditor) UpdateTable(schemaIndex, tableIndex int, name, description string) {
|
||||
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||
return
|
||||
}
|
||||
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
if tableIndex < 0 || tableIndex >= len(schema.Tables) {
|
||||
return
|
||||
}
|
||||
schema.UpdateDate()
|
||||
table := schema.Tables[tableIndex]
|
||||
table.Name = name
|
||||
table.Description = description
|
||||
table.UpdateDate()
|
||||
}
|
||||
|
||||
// DeleteTable removes a table from a schema
|
||||
func (se *SchemaEditor) DeleteTable(schemaIndex, tableIndex int) bool {
|
||||
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||
return false
|
||||
}
|
||||
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
if tableIndex < 0 || tableIndex >= len(schema.Tables) {
|
||||
return false
|
||||
}
|
||||
schema.UpdateDate()
|
||||
schema.Tables = append(schema.Tables[:tableIndex], schema.Tables[tableIndex+1:]...)
|
||||
return true
|
||||
}
|
||||
|
||||
// GetTable returns a table by schema and table index
|
||||
func (se *SchemaEditor) GetTable(schemaIndex, tableIndex int) *models.Table {
|
||||
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||
return nil
|
||||
}
|
||||
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
if tableIndex < 0 || tableIndex >= len(schema.Tables) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return schema.Tables[tableIndex]
|
||||
}
|
||||
|
||||
// GetAllTables returns all tables across all schemas
|
||||
func (se *SchemaEditor) GetAllTables() []*models.Table {
|
||||
var tables []*models.Table
|
||||
for _, schema := range se.db.Schemas {
|
||||
tables = append(tables, schema.Tables...)
|
||||
}
|
||||
return tables
|
||||
}
|
||||
|
||||
// GetTablesInSchema returns all tables in a specific schema
|
||||
func (se *SchemaEditor) GetTablesInSchema(schemaIndex int) []*models.Table {
|
||||
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||
return nil
|
||||
}
|
||||
return se.db.Schemas[schemaIndex].Tables
|
||||
}
|
||||
530
pkg/ui/table_screens.go
Normal file
530
pkg/ui/table_screens.go
Normal file
@@ -0,0 +1,530 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// showTableList displays all tables across all schemas
|
||||
func (se *SchemaEditor) showTableList() {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title
|
||||
title := tview.NewTextView().
|
||||
SetText("[::b]All Tables").
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter)
|
||||
|
||||
// Create tables table
|
||||
tableTable := tview.NewTable().SetBorders(true).SetSelectable(true, false).SetFixed(1, 0)
|
||||
|
||||
// Add header row with padding for full width
|
||||
headers := []string{"Name", "Schema", "Sequence", "Total Columns", "Total Relations", "Total Indexes", "Description", "Comment"}
|
||||
headerWidths := []int{18, 15, 12, 14, 15, 14, 0, 12} // Description gets remainder
|
||||
for i, header := range headers {
|
||||
padding := ""
|
||||
if i < len(headerWidths) && headerWidths[i] > 0 {
|
||||
padding = strings.Repeat(" ", headerWidths[i]-len(header))
|
||||
}
|
||||
cell := tview.NewTableCell(header + padding).
|
||||
SetTextColor(tcell.ColorYellow).
|
||||
SetSelectable(false).
|
||||
SetAlign(tview.AlignLeft)
|
||||
tableTable.SetCell(0, i, cell)
|
||||
}
|
||||
|
||||
var tables []*models.Table
|
||||
var tableLocations []struct{ schemaIdx, tableIdx int }
|
||||
|
||||
for si, schema := range se.db.Schemas {
|
||||
for ti, table := range schema.Tables {
|
||||
tables = append(tables, table)
|
||||
tableLocations = append(tableLocations, struct{ schemaIdx, tableIdx int }{si, ti})
|
||||
}
|
||||
}
|
||||
|
||||
for row, table := range tables {
|
||||
tableIdx := tableLocations[row]
|
||||
schema := se.db.Schemas[tableIdx.schemaIdx]
|
||||
|
||||
// Name - pad to 18 chars
|
||||
nameStr := fmt.Sprintf("%-18s", table.Name)
|
||||
nameCell := tview.NewTableCell(nameStr).SetSelectable(true)
|
||||
tableTable.SetCell(row+1, 0, nameCell)
|
||||
|
||||
// Schema - pad to 15 chars
|
||||
schemaStr := fmt.Sprintf("%-15s", schema.Name)
|
||||
schemaCell := tview.NewTableCell(schemaStr).SetSelectable(true)
|
||||
tableTable.SetCell(row+1, 1, schemaCell)
|
||||
|
||||
// Sequence - pad to 12 chars
|
||||
seqStr := fmt.Sprintf("%-12s", fmt.Sprintf("%d", table.Sequence))
|
||||
seqCell := tview.NewTableCell(seqStr).SetSelectable(true)
|
||||
tableTable.SetCell(row+1, 2, seqCell)
|
||||
|
||||
// Total Columns - pad to 14 chars
|
||||
colsStr := fmt.Sprintf("%-14s", fmt.Sprintf("%d", len(table.Columns)))
|
||||
colsCell := tview.NewTableCell(colsStr).SetSelectable(true)
|
||||
tableTable.SetCell(row+1, 3, colsCell)
|
||||
|
||||
// Total Relations - pad to 15 chars
|
||||
relsStr := fmt.Sprintf("%-15s", fmt.Sprintf("%d", len(table.Relationships)))
|
||||
relsCell := tview.NewTableCell(relsStr).SetSelectable(true)
|
||||
tableTable.SetCell(row+1, 4, relsCell)
|
||||
|
||||
// Total Indexes - pad to 14 chars
|
||||
idxStr := fmt.Sprintf("%-14s", fmt.Sprintf("%d", len(table.Indexes)))
|
||||
idxCell := tview.NewTableCell(idxStr).SetSelectable(true)
|
||||
tableTable.SetCell(row+1, 5, idxCell)
|
||||
|
||||
// Description - no padding, takes remaining space
|
||||
descCell := tview.NewTableCell(table.Description).SetSelectable(true)
|
||||
tableTable.SetCell(row+1, 6, descCell)
|
||||
|
||||
// Comment - pad to 12 chars
|
||||
commentStr := fmt.Sprintf("%-12s", table.Comment)
|
||||
commentCell := tview.NewTableCell(commentStr).SetSelectable(true)
|
||||
tableTable.SetCell(row+1, 7, commentCell)
|
||||
}
|
||||
|
||||
tableTable.SetTitle(" All Tables ").SetBorder(true).SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
// Action buttons (define before input capture)
|
||||
btnFlex := tview.NewFlex()
|
||||
btnNewTable := tview.NewButton("New Table [n]").SetSelectedFunc(func() {
|
||||
se.showNewTableDialogFromList()
|
||||
})
|
||||
btnBack := tview.NewButton("Back [b]").SetSelectedFunc(func() {
|
||||
se.pages.SwitchToPage("main")
|
||||
se.pages.RemovePage("tables")
|
||||
})
|
||||
|
||||
// Set up button input captures for Tab/Shift+Tab navigation
|
||||
btnNewTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(tableTable)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnBack)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnBack.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(btnNewTable)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(tableTable)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnFlex.AddItem(btnNewTable, 0, 1, true).
|
||||
AddItem(btnBack, 0, 1, false)
|
||||
|
||||
tableTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.SwitchToPage("main")
|
||||
se.pages.RemovePage("tables")
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnNewTable)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyEnter {
|
||||
row, _ := tableTable.GetSelection()
|
||||
if row > 0 && row <= len(tables) { // Skip header row
|
||||
tableIdx := tableLocations[row-1]
|
||||
se.showTableEditor(tableIdx.schemaIdx, tableIdx.tableIdx, tables[row-1])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if event.Rune() == 'n' {
|
||||
se.showNewTableDialogFromList()
|
||||
return nil
|
||||
}
|
||||
if event.Rune() == 'b' {
|
||||
se.pages.SwitchToPage("main")
|
||||
se.pages.RemovePage("tables")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
flex.AddItem(title, 1, 0, false).
|
||||
AddItem(tableTable, 0, 1, true).
|
||||
AddItem(btnFlex, 1, 0, false)
|
||||
|
||||
se.pages.AddPage("tables", flex, true, true)
|
||||
}
|
||||
|
||||
// showTableEditor shows editor for a specific table
|
||||
func (se *SchemaEditor) showTableEditor(schemaIndex, tableIndex int, table *models.Table) {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title
|
||||
title := tview.NewTextView().
|
||||
SetText(fmt.Sprintf("[::b]Table: %s", table.Name)).
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter)
|
||||
|
||||
// Table info
|
||||
info := tview.NewTextView().SetDynamicColors(true)
|
||||
info.SetText(fmt.Sprintf("Schema: %s | Columns: %d | Description: %s",
|
||||
table.Schema, len(table.Columns), table.Description))
|
||||
|
||||
// Create columns table
|
||||
colTable := tview.NewTable().SetBorders(true).SetSelectable(true, false).SetFixed(1, 0)
|
||||
|
||||
// Add header row with padding for full width
|
||||
headers := []string{"Name", "Type", "Default", "KeyType", "Description"}
|
||||
headerWidths := []int{20, 18, 15, 15} // Last column takes remaining space
|
||||
for i, header := range headers {
|
||||
padding := ""
|
||||
if i < len(headerWidths) {
|
||||
padding = strings.Repeat(" ", headerWidths[i]-len(header))
|
||||
}
|
||||
cell := tview.NewTableCell(header + padding).
|
||||
SetTextColor(tcell.ColorYellow).
|
||||
SetSelectable(false).
|
||||
SetAlign(tview.AlignLeft)
|
||||
colTable.SetCell(0, i, cell)
|
||||
}
|
||||
|
||||
// Get sorted column names
|
||||
columnNames := getColumnNames(table)
|
||||
for row, colName := range columnNames {
|
||||
column := table.Columns[colName]
|
||||
|
||||
// Name - pad to 20 chars
|
||||
nameStr := fmt.Sprintf("%-20s", colName)
|
||||
nameCell := tview.NewTableCell(nameStr).SetSelectable(true)
|
||||
colTable.SetCell(row+1, 0, nameCell)
|
||||
|
||||
// Type - pad to 18 chars
|
||||
typeStr := fmt.Sprintf("%-18s", column.Type)
|
||||
typeCell := tview.NewTableCell(typeStr).SetSelectable(true)
|
||||
colTable.SetCell(row+1, 1, typeCell)
|
||||
|
||||
// Default - pad to 15 chars
|
||||
defaultStr := ""
|
||||
if column.Default != nil {
|
||||
defaultStr = fmt.Sprintf("%v", column.Default)
|
||||
}
|
||||
defaultStr = fmt.Sprintf("%-15s", defaultStr)
|
||||
defaultCell := tview.NewTableCell(defaultStr).SetSelectable(true)
|
||||
colTable.SetCell(row+1, 2, defaultCell)
|
||||
|
||||
// KeyType - pad to 15 chars
|
||||
keyTypeStr := ""
|
||||
if column.IsPrimaryKey {
|
||||
keyTypeStr = "PRIMARY"
|
||||
} else if column.NotNull {
|
||||
keyTypeStr = "NOT NULL"
|
||||
}
|
||||
keyTypeStr = fmt.Sprintf("%-15s", keyTypeStr)
|
||||
keyTypeCell := tview.NewTableCell(keyTypeStr).SetSelectable(true)
|
||||
colTable.SetCell(row+1, 3, keyTypeCell)
|
||||
|
||||
// Description
|
||||
descCell := tview.NewTableCell(column.Description).SetSelectable(true)
|
||||
colTable.SetCell(row+1, 4, descCell)
|
||||
}
|
||||
|
||||
colTable.SetTitle(" Columns ").SetBorder(true).SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
// Action buttons flex (define before input capture)
|
||||
btnFlex := tview.NewFlex()
|
||||
btnNewCol := tview.NewButton("Add Column [n]").SetSelectedFunc(func() {
|
||||
se.showNewColumnDialog(schemaIndex, tableIndex)
|
||||
})
|
||||
btnEditTable := tview.NewButton("Edit Table [e]").SetSelectedFunc(func() {
|
||||
se.showEditTableDialog(schemaIndex, tableIndex)
|
||||
})
|
||||
btnEditColumn := tview.NewButton("Edit Column [c]").SetSelectedFunc(func() {
|
||||
row, _ := colTable.GetSelection()
|
||||
if row > 0 && row <= len(columnNames) { // Skip header row
|
||||
colName := columnNames[row-1]
|
||||
column := table.Columns[colName]
|
||||
se.showColumnEditor(schemaIndex, tableIndex, row-1, column)
|
||||
}
|
||||
})
|
||||
btnDelTable := tview.NewButton("Delete Table [d]").SetSelectedFunc(func() {
|
||||
se.showDeleteTableConfirm(schemaIndex, tableIndex)
|
||||
})
|
||||
btnBack := tview.NewButton("Back to Schema [b]").SetSelectedFunc(func() {
|
||||
se.pages.RemovePage("table-editor")
|
||||
se.pages.SwitchToPage("schema-editor")
|
||||
})
|
||||
|
||||
// Set up button input captures for Tab/Shift+Tab navigation
|
||||
btnNewCol.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(colTable)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnEditColumn)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnEditColumn.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(btnNewCol)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnEditTable)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnEditTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(btnEditColumn)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnDelTable)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnDelTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(btnEditTable)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnBack)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnBack.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyBacktab {
|
||||
se.app.SetFocus(btnDelTable)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(colTable)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
btnFlex.AddItem(btnNewCol, 0, 1, true).
|
||||
AddItem(btnEditColumn, 0, 1, false).
|
||||
AddItem(btnEditTable, 0, 1, false).
|
||||
AddItem(btnDelTable, 0, 1, false).
|
||||
AddItem(btnBack, 0, 1, false)
|
||||
|
||||
colTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.SwitchToPage("schema-editor")
|
||||
se.pages.RemovePage("table-editor")
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyTab {
|
||||
se.app.SetFocus(btnNewCol)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyEnter {
|
||||
row, _ := colTable.GetSelection()
|
||||
if row > 0 { // Skip header row
|
||||
colName := columnNames[row-1]
|
||||
column := table.Columns[colName]
|
||||
se.showColumnEditor(schemaIndex, tableIndex, row-1, column)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if event.Rune() == 'c' {
|
||||
row, _ := colTable.GetSelection()
|
||||
if row > 0 && row <= len(columnNames) { // Skip header row
|
||||
colName := columnNames[row-1]
|
||||
column := table.Columns[colName]
|
||||
se.showColumnEditor(schemaIndex, tableIndex, row-1, column)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if event.Rune() == 'b' {
|
||||
se.pages.RemovePage("table-editor")
|
||||
se.pages.SwitchToPage("schema-editor")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
flex.AddItem(title, 1, 0, false).
|
||||
AddItem(info, 2, 0, false).
|
||||
AddItem(colTable, 0, 1, true).
|
||||
AddItem(btnFlex, 1, 0, false)
|
||||
|
||||
se.pages.AddPage("table-editor", flex, true, true)
|
||||
}
|
||||
|
||||
// showNewTableDialog shows dialog to create a new table
|
||||
func (se *SchemaEditor) showNewTableDialog(schemaIndex int) {
|
||||
form := tview.NewForm()
|
||||
|
||||
tableName := ""
|
||||
description := ""
|
||||
|
||||
form.AddInputField("Table Name", "", 40, nil, func(value string) {
|
||||
tableName = value
|
||||
})
|
||||
|
||||
form.AddInputField("Description", "", 40, nil, func(value string) {
|
||||
description = value
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
if tableName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
se.CreateTable(schemaIndex, tableName, description)
|
||||
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
se.pages.RemovePage("new-table")
|
||||
se.pages.RemovePage("schema-editor")
|
||||
se.showSchemaEditor(schemaIndex, schema)
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
schema := se.db.Schemas[schemaIndex]
|
||||
se.pages.RemovePage("new-table")
|
||||
se.pages.RemovePage("schema-editor")
|
||||
se.showSchemaEditor(schemaIndex, schema)
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" New Table ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("new-table", "schema-editor")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("new-table", form, true, true)
|
||||
}
|
||||
|
||||
// showNewTableDialogFromList shows dialog to create a new table with schema selection
|
||||
func (se *SchemaEditor) showNewTableDialogFromList() {
|
||||
form := tview.NewForm()
|
||||
|
||||
tableName := ""
|
||||
description := ""
|
||||
selectedSchemaIdx := 0
|
||||
|
||||
// Create schema dropdown options
|
||||
schemaOptions := make([]string, len(se.db.Schemas))
|
||||
for i, schema := range se.db.Schemas {
|
||||
schemaOptions[i] = schema.Name
|
||||
}
|
||||
|
||||
form.AddInputField("Table Name", "", 40, nil, func(value string) {
|
||||
tableName = value
|
||||
})
|
||||
|
||||
form.AddDropDown("Schema", schemaOptions, 0, func(option string, optionIndex int) {
|
||||
selectedSchemaIdx = optionIndex
|
||||
})
|
||||
|
||||
form.AddInputField("Description", "", 40, nil, func(value string) {
|
||||
description = value
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
if tableName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
se.CreateTable(selectedSchemaIdx, tableName, description)
|
||||
|
||||
se.pages.RemovePage("new-table-from-list")
|
||||
se.pages.RemovePage("tables")
|
||||
se.showTableList()
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
se.pages.RemovePage("new-table-from-list")
|
||||
se.pages.RemovePage("tables")
|
||||
se.showTableList()
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" New Table ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("new-table-from-list", "tables")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("new-table-from-list", form, true, true)
|
||||
}
|
||||
|
||||
// showEditTableDialog shows dialog to edit table properties
|
||||
func (se *SchemaEditor) showEditTableDialog(schemaIndex, tableIndex int) {
|
||||
table := se.db.Schemas[schemaIndex].Tables[tableIndex]
|
||||
form := tview.NewForm()
|
||||
|
||||
// Local variables to collect changes
|
||||
newName := table.Name
|
||||
newDescription := table.Description
|
||||
|
||||
form.AddInputField("Table Name", table.Name, 40, nil, func(value string) {
|
||||
newName = value
|
||||
})
|
||||
|
||||
form.AddTextArea("Description", table.Description, 40, 5, 0, func(value string) {
|
||||
newDescription = value
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
// Apply changes using dataops
|
||||
se.UpdateTable(schemaIndex, tableIndex, newName, newDescription)
|
||||
|
||||
table := se.db.Schemas[schemaIndex].Tables[tableIndex]
|
||||
se.pages.RemovePage("edit-table")
|
||||
se.pages.RemovePage("table-editor")
|
||||
se.showTableEditor(schemaIndex, tableIndex, table)
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
// Discard changes - don't apply them
|
||||
table := se.db.Schemas[schemaIndex].Tables[tableIndex]
|
||||
se.pages.RemovePage("edit-table")
|
||||
se.pages.RemovePage("table-editor")
|
||||
se.showTableEditor(schemaIndex, tableIndex, table)
|
||||
})
|
||||
|
||||
form.SetBorder(true).SetTitle(" Edit Table ").SetTitleAlign(tview.AlignLeft)
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.showExitConfirmation("edit-table", "table-editor")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("edit-table", form, true, true)
|
||||
}
|
||||
299
pkg/ui/ui_rules.md
Normal file
299
pkg/ui/ui_rules.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# UI Rules and Guidelines
|
||||
|
||||
## Layout Requirements
|
||||
|
||||
All layouts / forms must be in seperate files regarding their domain or entity.
|
||||
|
||||
### Screen Layout Structure
|
||||
|
||||
All screens must follow this consistent layout:
|
||||
|
||||
1. **Title at the Top** (1 row, fixed height)
|
||||
- Centered bold text: `[::b]Title Text`
|
||||
- Use `tview.NewTextView()` with `SetTextAlign(tview.AlignCenter)`
|
||||
- Enable dynamic colors: `SetDynamicColors(true)`
|
||||
|
||||
2. **Content in the Middle** (flexible height)
|
||||
- Tables, lists, forms, or info displays
|
||||
- Uses flex weight of 1 for dynamic sizing
|
||||
|
||||
3. **Action Buttons at the Bottom** (1 row, fixed height)
|
||||
- Must be in a horizontal flex container
|
||||
- Action buttons before Back button
|
||||
- Back button is always last
|
||||
|
||||
### Form Layout Structure
|
||||
|
||||
All forms must follow this button order:
|
||||
|
||||
1. **Save Button** (always first)
|
||||
- Label: "Save"
|
||||
- Primary action that commits changes
|
||||
|
||||
2. **Delete Button** (optional, only for edit forms)
|
||||
- Label: "Delete"
|
||||
- Only shown when editing existing items (not for new items)
|
||||
- Must show confirmation dialog before deletion
|
||||
|
||||
3. **Back Button** (always last)
|
||||
- Label: "Back"
|
||||
- Returns to previous screen without saving
|
||||
|
||||
**Button Order Examples:**
|
||||
- **New Item Forms:** Save, Back
|
||||
- **Edit Item Forms:** Save, Delete, Back
|
||||
|
||||
## Tab Navigation
|
||||
|
||||
All screens must implement circular tab navigation:
|
||||
|
||||
1. **Tab Key** - Moves focus to the next focusable element
|
||||
2. **Shift+Tab (BackTab)** - Moves focus to the previous focusable element
|
||||
3. **At the End** - Tab cycles back to the first element
|
||||
4. **At the Start** - Shift+Tab cycles back to the last element
|
||||
|
||||
**Navigation Flow Pattern:**
|
||||
- Each widget must handle both Tab and BackTab
|
||||
- First widget: BackTab → Last widget, Tab → Second widget
|
||||
- Middle widgets: BackTab → Previous widget, Tab → Next widget
|
||||
- Last widget: BackTab → Previous widget, Tab → First widget
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
### Standard Keys
|
||||
|
||||
- **ESC** - Cancel current operation or go back to previous screen
|
||||
- **Tab** - Move focus forward (circular)
|
||||
- **Shift+Tab** - Move focus backward (circular)
|
||||
- **Enter** - Activate/select current item in tables and lists
|
||||
|
||||
### Letter Key Shortcuts
|
||||
|
||||
- **'n'** - New (create new item)
|
||||
- **'b'** - Back (return to previous screen)
|
||||
- **'e'** - Edit (edit current item)
|
||||
- **'d'** - Delete (delete current item)
|
||||
- **'c'** - Edit Column (in table editor)
|
||||
- **'s'** - Manage Schemas (in main menu)
|
||||
- **'t'** - Manage Tables (in main menu)
|
||||
- **'q'** - Quit/Exit (in main menu)
|
||||
|
||||
## Consistency Requirements
|
||||
|
||||
1. **Layout Structure** - All screens: Title (top) → Content (middle) → Buttons (bottom)
|
||||
2. **Title Format** - Bold (`[::b]`), centered, dynamic colors enabled
|
||||
3. **Tables** - Fixed headers (row 0), borders enabled, selectable rows
|
||||
4. **Buttons** - Include keyboard shortcuts in labels (e.g., "Back [b]")
|
||||
5. **Forms** - Button order: Save, Delete (if edit), Back
|
||||
6. **Destructive Actions** - Always show confirmation dialogs
|
||||
7. **ESC Key** - All screens support ESC to go back
|
||||
8. **Action Buttons** - Positioned before Back button, in logical order
|
||||
9. **Data Refresh** - Always refresh the previous screen when returning from a form or dialog
|
||||
|
||||
## Widget Naming Conventions
|
||||
|
||||
- **Tables:** `schemaTable`, `tableTable`, `colTable`
|
||||
- **Buttons:** Prefix with `btn` (e.g., `btnBack`, `btnDelete`, `btnNewSchema`)
|
||||
- **Flex containers:** `btnFlex` for button containers, `flex` for main layout
|
||||
- **Forms:** `form`
|
||||
- **Lists:** `list`, `tableList`
|
||||
- **Text views:** `title`, `info`
|
||||
- Use camelCase for all variable names
|
||||
|
||||
## Page Naming Conventions
|
||||
|
||||
Use descriptive kebab-case names:
|
||||
|
||||
- **Main screens:** `main`, `schemas`, `tables`, `schema-editor`, `table-editor`, `column-editor`
|
||||
- **Load/Save screens:** `load-database`, `save-database`
|
||||
- **Creation dialogs:** `new-schema`, `new-table`, `new-column`, `new-table-from-list`
|
||||
- **Edit dialogs:** `edit-schema`, `edit-table`
|
||||
- **Confirmations:** `confirm-delete-schema`, `confirm-delete-table`, `confirm-delete-column`
|
||||
- **Exit confirmations:** `exit-confirm`, `exit-editor-confirm`
|
||||
- **Status dialogs:** `error-dialog`, `success-dialog`
|
||||
|
||||
## Dialog and Confirmation Rules
|
||||
|
||||
### Confirmation Dialogs
|
||||
|
||||
1. **Delete Confirmations** - Required for all destructive actions
|
||||
- Show item name in confirmation text
|
||||
- Buttons: "Cancel", "Delete"
|
||||
- ESC key dismisses dialog
|
||||
|
||||
2. **Exit Confirmations** - Required when exiting forms with potential unsaved changes
|
||||
- Text: "Exit without saving changes?"
|
||||
- Buttons: "Cancel", "No, exit without saving"
|
||||
- ESC key confirms exit
|
||||
|
||||
3. **Save Confirmations** - Optional, based on context
|
||||
- Use for critical data changes
|
||||
- Clear description of what will be saved
|
||||
|
||||
### Dialog Behavior
|
||||
|
||||
- All dialogs must capture ESC key for dismissal
|
||||
- Modal dialogs overlay current screen
|
||||
- Confirmation dialogs use `tview.NewModal()`
|
||||
- Remove dialog page after action completes
|
||||
|
||||
## Data Refresh Rules
|
||||
|
||||
When returning from any form or dialog, the previous screen must be refreshed to show updated data. If Tables exists in the screen, their data must be updated:
|
||||
|
||||
1. **After Save** - Remove and recreate the previous screen to display updated data
|
||||
2. **After Delete** - Remove and recreate the previous screen to display remaining data
|
||||
3. **After Cancel/Back** - Remove and recreate the previous screen (data may have changed)
|
||||
4. **Implementation Pattern** - Remove the current page, remove the previous page, then recreate the previous page with fresh data
|
||||
|
||||
**Why This Matters:**
|
||||
- Ensures users see their changes immediately
|
||||
- Prevents stale data from being displayed
|
||||
- Maintains data consistency across the UI
|
||||
- Avoids confusion from seeing outdated information
|
||||
|
||||
**Example Flow:**
|
||||
```
|
||||
User on Schema List → Opens Edit Schema Form → Saves Changes →
|
||||
Returns to Schema List (refreshed with updated schema data)
|
||||
```
|
||||
|
||||
## Big Loading/Saving Operations
|
||||
|
||||
When loading big changes, files or data, always give a load completed or load error dialog.
|
||||
Do the same with saving.
|
||||
This informs the user what happens.
|
||||
When data is dirty, always ask the user to save when trying to exit.
|
||||
|
||||
### Load/Save Screens
|
||||
|
||||
- **Load Screen** (`load-database`) - Shown when no source is specified via command line
|
||||
- Format dropdown (dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql)
|
||||
- File path input (for file-based formats)
|
||||
- Connection string input (for database formats like pgsql)
|
||||
- Load button [l] - Loads the database
|
||||
- Create New button [n] - Creates a new empty database
|
||||
- Exit button [q] - Exits the application
|
||||
- ESC key exits the application
|
||||
|
||||
- **Save Screen** (`save-database`) - Accessible from main menu with 'w' key
|
||||
- Format dropdown (same as load screen)
|
||||
- File path input
|
||||
- Help text explaining format requirements
|
||||
- Save button [s] - Saves the database
|
||||
- Back button [b] - Returns to main menu
|
||||
- ESC key returns to main menu
|
||||
- Pre-populated with existing save configuration if available
|
||||
|
||||
### Status Dialogs
|
||||
|
||||
- **Error Dialog** (`error-dialog`) - Shows error messages with OK button and ESC to dismiss
|
||||
- **Success Dialog** (`success-dialog`) - Shows success messages with OK button, ESC to dismiss, and optional callback on close
|
||||
|
||||
## Screen Organization
|
||||
|
||||
Organize UI code into these files:
|
||||
|
||||
### UI Files (Screens and Dialogs)
|
||||
|
||||
- **editor.go** - Core `SchemaEditor` struct, constructor, `Run()` method, helper functions
|
||||
- **main_menu.go** - Main menu screen
|
||||
- **load_save_screens.go** - Database load and save screens
|
||||
- **database_screens.go** - Database edit form
|
||||
- **schema_screens.go** - Schema list, schema editor, new/edit schema dialogs
|
||||
- **table_screens.go** - Tables list, table editor, new/edit table dialogs
|
||||
- **column_screens.go** - Column editor, new column dialog
|
||||
- **domain_screens.go** - Domain list, domain editor, new/edit domain dialogs
|
||||
- **dialogs.go** - Confirmation dialogs (exit, delete)
|
||||
|
||||
### Data Operations Files (Business Logic)
|
||||
|
||||
- **schema_dataops.go** - Schema CRUD operations (Create, Read, Update, Delete)
|
||||
- **table_dataops.go** - Table CRUD operations
|
||||
- **column_dataops.go** - Column CRUD operations
|
||||
|
||||
## Code Separation Rules
|
||||
|
||||
### UI vs Business Logic
|
||||
|
||||
1. **UI Files** - Handle only presentation and user interaction
|
||||
- Display data in tables, lists, and forms
|
||||
- Capture user input
|
||||
- Navigate between screens
|
||||
- Show/hide dialogs
|
||||
- Call dataops methods for actual data changes
|
||||
|
||||
2. **Dataops Files** - Handle only business logic and data manipulation
|
||||
- Create, read, update, delete operations
|
||||
- Data validation
|
||||
- Data structure manipulation
|
||||
- Return created/updated objects or success/failure status
|
||||
- No UI code or tview references
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
#### Creating New Items
|
||||
|
||||
**Bad (Direct Data Manipulation in UI):**
|
||||
```go
|
||||
form.AddButton("Save", func() {
|
||||
schema := &models.Schema{Name: name, Description: desc, ...}
|
||||
se.db.Schemas = append(se.db.Schemas, schema)
|
||||
})
|
||||
```
|
||||
|
||||
**Good (Using Dataops Methods):**
|
||||
```go
|
||||
form.AddButton("Save", func() {
|
||||
se.CreateSchema(name, description)
|
||||
})
|
||||
```
|
||||
|
||||
#### Editing Existing Items
|
||||
|
||||
**Bad (Modifying Data in onChange Callbacks):**
|
||||
```go
|
||||
form.AddInputField("Name", column.Name, 40, nil, func(value string) {
|
||||
column.Name = value // Changes immediately as user types!
|
||||
})
|
||||
form.AddButton("Save", func() {
|
||||
// Data already changed, just refresh screen
|
||||
})
|
||||
```
|
||||
|
||||
**Good (Local Variables + Dataops on Save):**
|
||||
```go
|
||||
// Store original values
|
||||
originalName := column.Name
|
||||
newName := column.Name
|
||||
|
||||
form.AddInputField("Name", column.Name, 40, nil, func(value string) {
|
||||
newName = value // Store in local variable
|
||||
})
|
||||
|
||||
form.AddButton("Save", func() {
|
||||
// Apply changes only when Save is clicked
|
||||
se.UpdateColumn(schemaIndex, tableIndex, originalName, newName, ...)
|
||||
// Then refresh screen
|
||||
})
|
||||
|
||||
form.AddButton("Back", func() {
|
||||
// Discard changes - don't apply local variables
|
||||
// Just refresh screen
|
||||
})
|
||||
```
|
||||
|
||||
### Why This Matters
|
||||
|
||||
**Edit Forms Must Use Local Variables:**
|
||||
1. **Deferred Changes** - Changes only apply when Save is clicked
|
||||
2. **Cancellable** - Back button discards changes without saving
|
||||
3. **Handles Renames** - Original name preserved to update map keys correctly
|
||||
4. **User Expectations** - Save means "commit changes", Back means "cancel"
|
||||
|
||||
This separation ensures:
|
||||
- Cleaner, more maintainable code
|
||||
- Reusable business logic
|
||||
- Easier testing
|
||||
- Clear separation of concerns
|
||||
- Proper change management (save vs cancel)
|
||||
@@ -127,6 +127,51 @@ func (w *Writer) databaseToDrawDB(d *models.Database) *DrawDBSchema {
|
||||
}
|
||||
}
|
||||
|
||||
// Create subject areas for domains
|
||||
for domainIdx, domainModel := range d.Domains {
|
||||
// Calculate bounds for all tables in this domain
|
||||
minX, minY := 999999, 999999
|
||||
maxX, maxY := 0, 0
|
||||
|
||||
domainTableCount := 0
|
||||
for _, domainTable := range domainModel.Tables {
|
||||
// Find the table in the schema to get its position
|
||||
for _, t := range schema.Tables {
|
||||
if t.Name == domainTable.TableName {
|
||||
if t.X < minX {
|
||||
minX = t.X
|
||||
}
|
||||
if t.Y < minY {
|
||||
minY = t.Y
|
||||
}
|
||||
if t.X+colWidth > maxX {
|
||||
maxX = t.X + colWidth
|
||||
}
|
||||
if t.Y+rowHeight > maxY {
|
||||
maxY = t.Y + rowHeight
|
||||
}
|
||||
domainTableCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only create area if domain has tables in this schema
|
||||
if domainTableCount > 0 {
|
||||
area := &DrawDBArea{
|
||||
ID: areaID,
|
||||
Name: domainModel.Name,
|
||||
Color: getColorForIndex(len(d.Schemas) + domainIdx), // Use different colors than schemas
|
||||
X: minX - 20,
|
||||
Y: minY - 20,
|
||||
Width: maxX - minX + 40,
|
||||
Height: maxY - minY + 40,
|
||||
}
|
||||
schema.SubjectAreas = append(schema.SubjectAreas, area)
|
||||
areaID++
|
||||
}
|
||||
}
|
||||
|
||||
// Add relationships
|
||||
for _, schemaModel := range d.Schemas {
|
||||
for _, table := range schemaModel.Tables {
|
||||
|
||||
276
pkg/writers/template/README.md
Normal file
276
pkg/writers/template/README.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# Template Writer
|
||||
|
||||
Custom template-based writer for RelSpec that allows users to generate any output format using Go text templates.
|
||||
|
||||
## Overview
|
||||
|
||||
The template writer provides a powerful and flexible way to transform database schemas into any desired format. It supports multiple execution modes and provides 80+ template functions for data transformation.
|
||||
|
||||
**For complete user documentation, see:** [/docs/TEMPLATE_MODE.md](../../../docs/TEMPLATE_MODE.md)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Package Structure
|
||||
|
||||
```
|
||||
pkg/writers/template/
|
||||
├── README.md # This file
|
||||
├── writer.go # Core writer with entrypoint mode logic
|
||||
├── template_data.go # Data structures passed to templates
|
||||
├── funcmap.go # Template function registry
|
||||
├── string_helpers.go # String manipulation functions
|
||||
├── type_mappers.go # SQL type conversion (delegates to commontypes)
|
||||
├── filters.go # Database object filtering
|
||||
├── formatters.go # JSON/YAML formatting utilities
|
||||
├── loop_helpers.go # Iteration and collection utilities
|
||||
├── safe_access.go # Safe map/array access functions
|
||||
└── errors.go # Custom error types
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **`pkg/commontypes`** - Centralized type mappings for Go, TypeScript, Java, Python, Rust, C#, PHP
|
||||
- **`pkg/reflectutil`** - Reflection utilities for safe type manipulation
|
||||
- **`pkg/models`** - Database schema models
|
||||
|
||||
## Writer Interface Implementation
|
||||
|
||||
Implements the standard `writers.Writer` interface:
|
||||
|
||||
```go
|
||||
type Writer interface {
|
||||
WriteDatabase(db *models.Database) error
|
||||
WriteSchema(schema *models.Schema) error
|
||||
WriteTable(table *models.Table) error
|
||||
}
|
||||
```
|
||||
|
||||
## Execution Modes
|
||||
|
||||
The writer supports four execution modes via `WriterOptions.Metadata["mode"]`:
|
||||
|
||||
| Mode | Data Passed | Output | Use Case |
|
||||
|------|-------------|--------|----------|
|
||||
| `database` | Full database | Single file | Reports, documentation |
|
||||
| `schema` | One schema at a time | File per schema | Schema-specific docs |
|
||||
| `script` | One script at a time | File per script | Script processing |
|
||||
| `table` | One table at a time | File per table | Model generation |
|
||||
|
||||
## Configuration
|
||||
|
||||
Writer is configured via `WriterOptions.Metadata`:
|
||||
|
||||
```go
|
||||
metadata := map[string]interface{}{
|
||||
"template_path": "/path/to/template.tmpl", // Required
|
||||
"mode": "table", // Default: "database"
|
||||
"filename_pattern": "{{.Name}}.ts", // Default: "{{.Name}}.txt"
|
||||
}
|
||||
```
|
||||
|
||||
## Template Data Structure
|
||||
|
||||
Templates receive a `TemplateData` struct:
|
||||
|
||||
```go
|
||||
type TemplateData struct {
|
||||
// Primary data (one populated based on mode)
|
||||
Database *models.Database
|
||||
Schema *models.Schema
|
||||
Script *models.Script
|
||||
Table *models.Table
|
||||
|
||||
// Parent context
|
||||
ParentSchema *models.Schema
|
||||
ParentDatabase *models.Database
|
||||
|
||||
// Pre-computed views
|
||||
FlatColumns []*models.FlatColumn
|
||||
FlatTables []*models.FlatTable
|
||||
FlatConstraints []*models.FlatConstraint
|
||||
FlatRelationships []*models.FlatRelationship
|
||||
Summary *models.DatabaseSummary
|
||||
|
||||
// User metadata
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
```
|
||||
|
||||
## Function Categories
|
||||
|
||||
### String Utilities (string_helpers.go)
|
||||
Case conversion, pluralization, trimming, splitting, joining
|
||||
|
||||
### Type Mappers (type_mappers.go)
|
||||
SQL type conversion to 7+ programming languages (delegates to `pkg/commontypes`)
|
||||
|
||||
### Filters (filters.go)
|
||||
Database object filtering by pattern, type, constraints
|
||||
|
||||
### Formatters (formatters.go)
|
||||
JSON/YAML serialization, indentation, escaping, commenting
|
||||
|
||||
### Loop Helpers (loop_helpers.go)
|
||||
Enumeration, batching, reversing, sorting, grouping (uses `pkg/reflectutil`)
|
||||
|
||||
### Safe Access (safe_access.go)
|
||||
Safe map/array access without panics (uses `pkg/reflectutil`)
|
||||
|
||||
## Adding New Functions
|
||||
|
||||
To add a new template function:
|
||||
|
||||
1. **Implement the function** in the appropriate file:
|
||||
```go
|
||||
// string_helpers.go
|
||||
func ToScreamingSnakeCase(s string) string {
|
||||
return strings.ToUpper(ToSnakeCase(s))
|
||||
}
|
||||
```
|
||||
|
||||
2. **Register in funcmap.go:**
|
||||
```go
|
||||
func BuildFuncMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
// ... existing functions
|
||||
"toScreamingSnakeCase": ToScreamingSnakeCase,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Document in /docs/TEMPLATE_MODE.md**
|
||||
|
||||
## Error Handling
|
||||
|
||||
Custom error types in `errors.go`:
|
||||
|
||||
- `TemplateLoadError` - Template file not found or unreadable
|
||||
- `TemplateParseError` - Invalid template syntax
|
||||
- `TemplateExecuteError` - Error during template execution
|
||||
|
||||
All errors wrap the underlying error for context.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
go test ./pkg/writers/template/...
|
||||
|
||||
# Test with example data
|
||||
cat > test.tmpl << 'EOF'
|
||||
{{ range .Database.Schemas }}
|
||||
Schema: {{ .Name }} ({{ len .Tables }} tables)
|
||||
{{ end }}
|
||||
EOF
|
||||
|
||||
relspec templ --from json --from-path schema.json --template test.tmpl
|
||||
```
|
||||
|
||||
## Multi-file Output
|
||||
|
||||
For multi-file modes, the writer:
|
||||
|
||||
1. Iterates through items (schemas/scripts/tables)
|
||||
2. Creates `TemplateData` for each item
|
||||
3. Executes template with item data
|
||||
4. Generates filename using `filename_pattern` template
|
||||
5. Writes output to generated filename
|
||||
|
||||
Output directory is created automatically if it doesn't exist.
|
||||
|
||||
## Filename Pattern Execution
|
||||
|
||||
The filename pattern is itself a template:
|
||||
|
||||
```go
|
||||
// Pattern: "{{.Schema}}/{{.Name | toCamelCase}}.ts"
|
||||
// For table "user_profile" in schema "public"
|
||||
// Generates: "public/userProfile.ts"
|
||||
```
|
||||
|
||||
Available in pattern template:
|
||||
- `.Name` - Item name (schema/script/table)
|
||||
- `.Schema` - Schema name (for scripts/tables)
|
||||
- All template functions
|
||||
|
||||
## Example Usage
|
||||
|
||||
### As a Library
|
||||
|
||||
```go
|
||||
import (
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers/template"
|
||||
)
|
||||
|
||||
// Create writer
|
||||
metadata := map[string]interface{}{
|
||||
"template_path": "model.tmpl",
|
||||
"mode": "table",
|
||||
"filename_pattern": "{{.Name}}.go",
|
||||
}
|
||||
|
||||
opts := &writers.WriterOptions{
|
||||
OutputPath: "./models/",
|
||||
PackageName: "models",
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
writer, err := template.NewWriter(opts)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Write database
|
||||
err = writer.WriteDatabase(db)
|
||||
```
|
||||
|
||||
### Via CLI
|
||||
|
||||
```bash
|
||||
relspec templ \
|
||||
--from pgsql \
|
||||
--from-conn "postgres://localhost/mydb" \
|
||||
--template model.tmpl \
|
||||
--mode table \
|
||||
--output ./models/ \
|
||||
--filename-pattern "{{.Name | toPascalCase}}.go"
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Template Parsing** - Template is parsed once in `NewWriter()`, not per execution
|
||||
2. **Reflection** - Loop and safe access helpers use reflection; cached where possible
|
||||
3. **Pre-computed Views** - `FlatColumns`, `FlatTables`, etc. computed once per data item
|
||||
4. **File I/O** - Multi-file mode creates directories as needed
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
|
||||
- [ ] Template caching for filename patterns
|
||||
- [ ] Parallel template execution for multi-file mode
|
||||
- [ ] Template function plugins
|
||||
- [ ] Custom function injection via metadata
|
||||
- [ ] Template includes/partials support
|
||||
- [ ] Dry-run mode to preview filenames
|
||||
- [ ] Progress reporting for large schemas
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
|
||||
1. Follow existing patterns (see similar functions)
|
||||
2. Add to appropriate category file
|
||||
3. Register in `funcmap.go`
|
||||
4. Update `/docs/TEMPLATE_MODE.md`
|
||||
5. Add tests
|
||||
6. Consider edge cases (nil, empty, invalid input)
|
||||
|
||||
## See Also
|
||||
|
||||
- [User Documentation](/docs/TEMPLATE_MODE.md) - Complete template function reference
|
||||
- [Common Types Package](../../commontypes/) - Centralized type mappings
|
||||
- [Reflect Utilities](../../reflectutil/) - Reflection helpers
|
||||
- [Models Package](../../models/) - Database schema models
|
||||
- [Go Template Docs](https://pkg.go.dev/text/template) - Official Go template documentation
|
||||
50
pkg/writers/template/errors.go
Normal file
50
pkg/writers/template/errors.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package template
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TemplateError represents an error that occurred during template operations
|
||||
type TemplateError struct {
|
||||
Phase string // "load", "parse", "execute"
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (e *TemplateError) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("template %s error: %s: %v", e.Phase, e.Message, e.Err)
|
||||
}
|
||||
return fmt.Sprintf("template %s error: %s", e.Phase, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped error
|
||||
func (e *TemplateError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// NewTemplateLoadError creates a new template load error
|
||||
func NewTemplateLoadError(msg string, err error) *TemplateError {
|
||||
return &TemplateError{
|
||||
Phase: "load",
|
||||
Message: msg,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTemplateParseError creates a new template parse error
|
||||
func NewTemplateParseError(msg string, err error) *TemplateError {
|
||||
return &TemplateError{
|
||||
Phase: "parse",
|
||||
Message: msg,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTemplateExecuteError creates a new template execution error
|
||||
func NewTemplateExecuteError(msg string, err error) *TemplateError {
|
||||
return &TemplateError{
|
||||
Phase: "execute",
|
||||
Message: msg,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
144
pkg/writers/template/filters.go
Normal file
144
pkg/writers/template/filters.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/commontypes"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// FilterTables filters tables using a predicate function
|
||||
// Usage: {{ $filtered := filterTables .Schema.Tables (func $t) { return hasPrefix $t.Name "user_" } }}
|
||||
// Note: Template functions can't pass Go funcs, so this is primarily for internal use
|
||||
func FilterTables(tables []*models.Table, pattern string) []*models.Table {
|
||||
if pattern == "" {
|
||||
return tables
|
||||
}
|
||||
|
||||
result := make([]*models.Table, 0)
|
||||
for _, table := range tables {
|
||||
if matchPattern(table.Name, pattern) {
|
||||
result = append(result, table)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterTablesByPattern filters tables by name pattern (glob-style)
|
||||
// Usage: {{ $userTables := filterTablesByPattern .Schema.Tables "user_*" }}
|
||||
func FilterTablesByPattern(tables []*models.Table, pattern string) []*models.Table {
|
||||
return FilterTables(tables, pattern)
|
||||
}
|
||||
|
||||
// FilterColumns filters columns from a map using a pattern
|
||||
// Usage: {{ $filtered := filterColumns .Table.Columns "*_id" }}
|
||||
func FilterColumns(columns map[string]*models.Column, pattern string) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
for _, col := range columns {
|
||||
if pattern == "" || matchPattern(col.Name, pattern) {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterColumnsByType filters columns by SQL type
|
||||
// Usage: {{ $stringCols := filterColumnsByType .Table.Columns "varchar" }}
|
||||
func FilterColumnsByType(columns map[string]*models.Column, sqlType string) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
baseType := commontypes.ExtractBaseType(sqlType)
|
||||
|
||||
for _, col := range columns {
|
||||
colBaseType := commontypes.ExtractBaseType(col.Type)
|
||||
if colBaseType == baseType {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterPrimaryKeys returns only columns that are primary keys
|
||||
// Usage: {{ $pks := filterPrimaryKeys .Table.Columns }}
|
||||
func FilterPrimaryKeys(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
for _, col := range columns {
|
||||
if col.IsPrimaryKey {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterForeignKeys returns only foreign key constraints
|
||||
// Usage: {{ $fks := filterForeignKeys .Table.Constraints }}
|
||||
func FilterForeignKeys(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0)
|
||||
for _, constraint := range constraints {
|
||||
if constraint.Type == models.ForeignKeyConstraint {
|
||||
result = append(result, constraint)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterUniqueConstraints returns only unique constraints
|
||||
// Usage: {{ $uniques := filterUniqueConstraints .Table.Constraints }}
|
||||
func FilterUniqueConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0)
|
||||
for _, constraint := range constraints {
|
||||
if constraint.Type == models.UniqueConstraint {
|
||||
result = append(result, constraint)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterCheckConstraints returns only check constraints
|
||||
// Usage: {{ $checks := filterCheckConstraints .Table.Constraints }}
|
||||
func FilterCheckConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0)
|
||||
for _, constraint := range constraints {
|
||||
if constraint.Type == models.CheckConstraint {
|
||||
result = append(result, constraint)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterNullable returns only nullable columns
|
||||
// Usage: {{ $nullables := filterNullable .Table.Columns }}
|
||||
func FilterNullable(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
for _, col := range columns {
|
||||
if !col.NotNull {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterNotNull returns only non-nullable columns
|
||||
// Usage: {{ $required := filterNotNull .Table.Columns }}
|
||||
func FilterNotNull(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
for _, col := range columns {
|
||||
if col.NotNull {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// matchPattern performs simple glob-style pattern matching
|
||||
// Supports: * (any characters), ? (single character)
|
||||
// Examples: "user_*" matches "user_profile", "user_settings"
|
||||
func matchPattern(s, pattern string) bool {
|
||||
// Use filepath.Match for glob-style pattern matching
|
||||
matched, err := filepath.Match(pattern, s)
|
||||
if err != nil {
|
||||
// If pattern is invalid, do exact match
|
||||
return strings.EqualFold(s, pattern)
|
||||
}
|
||||
return matched
|
||||
}
|
||||
157
pkg/writers/template/formatters.go
Normal file
157
pkg/writers/template/formatters.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ToJSON converts a value to JSON string
|
||||
// Usage: {{ .Database | toJSON }}
|
||||
func ToJSON(v interface{}) string {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("{\"error\": \"failed to marshal: %v\"}", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// ToJSONPretty converts a value to pretty-printed JSON string
|
||||
// Usage: {{ .Database | toJSONPretty " " }}
|
||||
func ToJSONPretty(v interface{}, indent string) string {
|
||||
data, err := json.MarshalIndent(v, "", indent)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("{\"error\": \"failed to marshal: %v\"}", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// ToYAML converts a value to YAML string
|
||||
// Usage: {{ .Database | toYAML }}
|
||||
func ToYAML(v interface{}) string {
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error: failed to marshal: %v", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// Indent indents each line of a string by the specified number of spaces
|
||||
// Usage: {{ .Column.Description | indent 4 }}
|
||||
func Indent(s string, spaces int) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
prefix := strings.Repeat(" ", spaces)
|
||||
lines := strings.Split(s, "\n")
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
lines[i] = prefix + line
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// IndentWith indents each line of a string with a custom prefix
|
||||
// Usage: {{ .Column.Description | indentWith " " }}
|
||||
func IndentWith(s string, prefix string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(s, "\n")
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
lines[i] = prefix + line
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// Escape escapes special characters in a string for use in code
|
||||
// Usage: {{ .Column.Default | escape }}
|
||||
func Escape(s string) string {
|
||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
||||
s = strings.ReplaceAll(s, "\n", "\\n")
|
||||
s = strings.ReplaceAll(s, "\r", "\\r")
|
||||
s = strings.ReplaceAll(s, "\t", "\\t")
|
||||
return s
|
||||
}
|
||||
|
||||
// EscapeQuotes escapes only quote characters
|
||||
// Usage: {{ .Column.Comment | escapeQuotes }}
|
||||
func EscapeQuotes(s string) string {
|
||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
||||
s = strings.ReplaceAll(s, "'", "\\'")
|
||||
return s
|
||||
}
|
||||
|
||||
// Comment adds comment prefix to a string
|
||||
// Supports: "//" (Go, C++, etc.), "#" (Python, shell), "--" (SQL), "/* */" (block)
|
||||
// Usage: {{ .Table.Description | comment "//" }}
|
||||
func Comment(s string, style string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(s, "\n")
|
||||
|
||||
switch style {
|
||||
case "//":
|
||||
for i, line := range lines {
|
||||
lines[i] = "// " + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
case "#":
|
||||
for i, line := range lines {
|
||||
lines[i] = "# " + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
case "--":
|
||||
for i, line := range lines {
|
||||
lines[i] = "-- " + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
case "/* */", "/**/":
|
||||
if len(lines) == 1 {
|
||||
return "/* " + lines[0] + " */"
|
||||
}
|
||||
result := "/*\n"
|
||||
for _, line := range lines {
|
||||
result += " * " + line + "\n"
|
||||
}
|
||||
result += " */"
|
||||
return result
|
||||
|
||||
default:
|
||||
// Default to // style
|
||||
for i, line := range lines {
|
||||
lines[i] = "// " + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// QuoteString adds quotes around a string
|
||||
// Usage: {{ .Column.Default | quoteString }}
|
||||
func QuoteString(s string) string {
|
||||
return "\"" + s + "\""
|
||||
}
|
||||
|
||||
// UnquoteString removes quotes from a string
|
||||
// Usage: {{ .Value | unquoteString }}
|
||||
func UnquoteString(s string) string {
|
||||
if len(s) >= 2 {
|
||||
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
171
pkg/writers/template/funcmap.go
Normal file
171
pkg/writers/template/funcmap.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// BuildFuncMap creates a template.FuncMap with all available helper functions
|
||||
func BuildFuncMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
// String manipulation functions
|
||||
"toUpper": ToUpper,
|
||||
"toLower": ToLower,
|
||||
"toCamelCase": ToCamelCase,
|
||||
"toPascalCase": ToPascalCase,
|
||||
"toSnakeCase": ToSnakeCase,
|
||||
"toKebabCase": ToKebabCase,
|
||||
"pluralize": Pluralize,
|
||||
"singularize": Singularize,
|
||||
"title": Title,
|
||||
"trim": Trim,
|
||||
"trimPrefix": TrimPrefix,
|
||||
"trimSuffix": TrimSuffix,
|
||||
"replace": Replace,
|
||||
"stringContains": StringContains, // Avoid conflict with slice contains
|
||||
"hasPrefix": HasPrefix,
|
||||
"hasSuffix": HasSuffix,
|
||||
"split": Split,
|
||||
"join": Join,
|
||||
|
||||
// Type conversion functions
|
||||
"sqlToGo": SQLToGo,
|
||||
"sqlToTypeScript": SQLToTypeScript,
|
||||
"sqlToJava": SQLToJava,
|
||||
"sqlToPython": SQLToPython,
|
||||
"sqlToRust": SQLToRust,
|
||||
"sqlToCSharp": SQLToCSharp,
|
||||
"sqlToPhp": SQLToPhp,
|
||||
|
||||
// Filtering functions
|
||||
"filterTables": FilterTables,
|
||||
"filterTablesByPattern": FilterTablesByPattern,
|
||||
"filterColumns": FilterColumns,
|
||||
"filterColumnsByType": FilterColumnsByType,
|
||||
"filterPrimaryKeys": FilterPrimaryKeys,
|
||||
"filterForeignKeys": FilterForeignKeys,
|
||||
"filterUniqueConstraints": FilterUniqueConstraints,
|
||||
"filterCheckConstraints": FilterCheckConstraints,
|
||||
"filterNullable": FilterNullable,
|
||||
"filterNotNull": FilterNotNull,
|
||||
|
||||
// Formatting functions
|
||||
"toJSON": ToJSON,
|
||||
"toJSONPretty": ToJSONPretty,
|
||||
"toYAML": ToYAML,
|
||||
"indent": Indent,
|
||||
"indentWith": IndentWith,
|
||||
"escape": Escape,
|
||||
"escapeQuotes": EscapeQuotes,
|
||||
"comment": Comment,
|
||||
"quoteString": QuoteString,
|
||||
"unquoteString": UnquoteString,
|
||||
|
||||
// Loop/iteration helper functions
|
||||
"enumerate": Enumerate,
|
||||
"batch": Batch,
|
||||
"chunk": Chunk,
|
||||
"reverse": Reverse,
|
||||
"first": First,
|
||||
"last": Last,
|
||||
"skip": Skip,
|
||||
"take": Take,
|
||||
"concat": Concat,
|
||||
"unique": Unique,
|
||||
"sortBy": SortBy,
|
||||
"groupBy": GroupBy,
|
||||
|
||||
// Safe access functions
|
||||
"get": Get,
|
||||
"getOr": GetOr,
|
||||
"getPath": GetPath,
|
||||
"getPathOr": GetPathOr,
|
||||
"safeIndex": SafeIndex,
|
||||
"safeIndexOr": SafeIndexOr,
|
||||
"has": Has,
|
||||
"hasPath": HasPath,
|
||||
"keys": Keys,
|
||||
"values": Values,
|
||||
"merge": Merge,
|
||||
"pick": Pick,
|
||||
"omit": Omit,
|
||||
"sliceContains": SliceContains,
|
||||
"indexOf": IndexOf,
|
||||
"pluck": Pluck,
|
||||
|
||||
// Sorting functions
|
||||
"sortSchemasByName": models.SortSchemasByName,
|
||||
"sortSchemasBySequence": models.SortSchemasBySequence,
|
||||
"sortTablesByName": models.SortTablesByName,
|
||||
"sortTablesBySequence": models.SortTablesBySequence,
|
||||
"sortColumnsByName": models.SortColumnsByName,
|
||||
"sortColumnsBySequence": models.SortColumnsBySequence,
|
||||
"sortColumnsMapByName": models.SortColumnsMapByName,
|
||||
"sortColumnsMapBySequence": models.SortColumnsMapBySequence,
|
||||
"sortViewsByName": models.SortViewsByName,
|
||||
"sortViewsBySequence": models.SortViewsBySequence,
|
||||
"sortSequencesByName": models.SortSequencesByName,
|
||||
"sortSequencesBySequence": models.SortSequencesBySequence,
|
||||
"sortIndexesByName": models.SortIndexesByName,
|
||||
"sortIndexesBySequence": models.SortIndexesBySequence,
|
||||
"sortIndexesMapByName": models.SortIndexesMapByName,
|
||||
"sortIndexesMapBySequence": models.SortIndexesMapBySequence,
|
||||
"sortConstraintsByName": models.SortConstraintsByName,
|
||||
"sortConstraintsMapByName": models.SortConstraintsMapByName,
|
||||
"sortRelationshipsByName": models.SortRelationshipsByName,
|
||||
"sortRelationshipsMapByName": models.SortRelationshipsMapByName,
|
||||
"sortScriptsByName": models.SortScriptsByName,
|
||||
"sortEnumsByName": models.SortEnumsByName,
|
||||
|
||||
// Utility functions (built-in Go template helpers + custom)
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"mul": func(a, b int) int { return a * b },
|
||||
"div": func(a, b int) int {
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return a / b
|
||||
},
|
||||
"mod": func(a, b int) int {
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return a % b
|
||||
},
|
||||
"default": func(defaultVal, val interface{}) interface{} {
|
||||
if val == nil {
|
||||
return defaultVal
|
||||
}
|
||||
return val
|
||||
},
|
||||
"dict": func(values ...interface{}) map[string]interface{} {
|
||||
if len(values)%2 != 0 {
|
||||
return nil
|
||||
}
|
||||
dict := make(map[string]interface{}, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict
|
||||
},
|
||||
"list": func(values ...interface{}) []interface{} {
|
||||
return values
|
||||
},
|
||||
"seq": func(start, end int) []int {
|
||||
if start > end {
|
||||
return []int{}
|
||||
}
|
||||
result := make([]int, end-start+1)
|
||||
for i := range result {
|
||||
result[i] = start + i
|
||||
}
|
||||
return result
|
||||
},
|
||||
}
|
||||
}
|
||||
282
pkg/writers/template/loop_helpers.go
Normal file
282
pkg/writers/template/loop_helpers.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/reflectutil"
|
||||
)
|
||||
|
||||
// EnumeratedItem represents an item with its index
|
||||
type EnumeratedItem struct {
|
||||
Index int
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// Enumerate returns a slice with index-value pairs
|
||||
// Usage: {{ range enumerate .Tables }}{{ .Index }}: {{ .Value.Name }}{{ end }}
|
||||
func Enumerate(slice interface{}) []EnumeratedItem {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []EnumeratedItem{}
|
||||
}
|
||||
|
||||
result := make([]EnumeratedItem, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
result[i] = EnumeratedItem{
|
||||
Index: i,
|
||||
Value: v.Index(i).Interface(),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Batch splits a slice into chunks of specified size
|
||||
// Usage: {{ range batch .Columns 3 }}...{{ end }}
|
||||
func Batch(slice interface{}, size int) [][]interface{} {
|
||||
if size <= 0 {
|
||||
return [][]interface{}{}
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return [][]interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
if length == 0 {
|
||||
return [][]interface{}{}
|
||||
}
|
||||
|
||||
numBatches := (length + size - 1) / size
|
||||
result := make([][]interface{}, numBatches)
|
||||
|
||||
for i := 0; i < numBatches; i++ {
|
||||
start := i * size
|
||||
end := start + size
|
||||
if end > length {
|
||||
end = length
|
||||
}
|
||||
|
||||
batch := make([]interface{}, end-start)
|
||||
for j := start; j < end; j++ {
|
||||
batch[j-start] = v.Index(j).Interface()
|
||||
}
|
||||
result[i] = batch
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Chunk is an alias for Batch
|
||||
func Chunk(slice interface{}, size int) [][]interface{} {
|
||||
return Batch(slice, size)
|
||||
}
|
||||
|
||||
// Reverse reverses a slice
|
||||
// Usage: {{ range reverse .Tables }}...{{ end }}
|
||||
func Reverse(slice interface{}) []interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
result := make([]interface{}, length)
|
||||
for i := 0; i < length; i++ {
|
||||
result[length-1-i] = v.Index(i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// First returns the first N items from a slice
|
||||
// Usage: {{ range first .Tables 5 }}...{{ end }}
|
||||
func First(slice interface{}, n int) []interface{} {
|
||||
if n <= 0 {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
if n > length {
|
||||
n = length
|
||||
}
|
||||
|
||||
result := make([]interface{}, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = v.Index(i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Last returns the last N items from a slice
|
||||
// Usage: {{ range last .Tables 5 }}...{{ end }}
|
||||
func Last(slice interface{}, n int) []interface{} {
|
||||
if n <= 0 {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
if n > length {
|
||||
n = length
|
||||
}
|
||||
|
||||
result := make([]interface{}, n)
|
||||
start := length - n
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = v.Index(start + i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Skip skips the first N items and returns the rest
|
||||
// Usage: {{ range skip .Tables 2 }}...{{ end }}
|
||||
func Skip(slice interface{}, n int) []interface{} {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
if n >= length {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
result := make([]interface{}, length-n)
|
||||
for i := n; i < length; i++ {
|
||||
result[i-n] = v.Index(i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Take returns the first N items (alias for First)
|
||||
// Usage: {{ range take .Tables 5 }}...{{ end }}
|
||||
func Take(slice interface{}, n int) []interface{} {
|
||||
return First(slice, n)
|
||||
}
|
||||
|
||||
// Concat concatenates multiple slices
|
||||
// Usage: {{ $all := concat .Schema1.Tables .Schema2.Tables }}
|
||||
func Concat(slices ...interface{}) []interface{} {
|
||||
result := make([]interface{}, 0)
|
||||
|
||||
for _, slice := range slices {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
result = append(result, v.Index(i).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Unique removes duplicates from a slice (compares by string representation)
|
||||
// Usage: {{ $unique := unique .Items }}
|
||||
func Unique(slice interface{}) []interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
seen := make(map[interface{}]bool)
|
||||
result := make([]interface{}, 0)
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i).Interface()
|
||||
if !seen[item] {
|
||||
seen[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SortBy sorts a slice by a field name (for structs) or key (for maps)
|
||||
// Usage: {{ $sorted := sortBy .Tables "Name" }}
|
||||
// Note: This is a basic implementation that works for simple cases
|
||||
func SortBy(slice interface{}, field string) []interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
// Convert to interface slice
|
||||
result := make([]interface{}, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
result[i] = v.Index(i).Interface()
|
||||
}
|
||||
|
||||
// Sort by field
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
vi := getFieldValue(result[i], field)
|
||||
vj := getFieldValue(result[j], field)
|
||||
return compareValues(vi, vj) < 0
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GroupBy groups a slice by a field value
|
||||
// Usage: {{ $grouped := groupBy .Tables "Schema" }}
|
||||
func GroupBy(slice interface{}, field string) map[interface{}][]interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return map[interface{}][]interface{}{}
|
||||
}
|
||||
|
||||
result := make(map[interface{}][]interface{})
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i).Interface()
|
||||
key := getFieldValue(item, field)
|
||||
result[key] = append(result[key], item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// CountIf counts items matching a condition
|
||||
// Note: Since templates can't pass functions, this is limited
|
||||
// Usage in code (not directly in templates)
|
||||
func CountIf(slice interface{}, matches func(interface{}) bool) int {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if matches(v.Index(i).Interface()) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// getFieldValue extracts a field value from a struct, map, or pointer
|
||||
func getFieldValue(item interface{}, field string) interface{} {
|
||||
return reflectutil.GetFieldValue(item, field)
|
||||
}
|
||||
|
||||
// compareValues compares two values for sorting
|
||||
func compareValues(a, b interface{}) int {
|
||||
return reflectutil.CompareValues(a, b)
|
||||
}
|
||||
288
pkg/writers/template/safe_access.go
Normal file
288
pkg/writers/template/safe_access.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/reflectutil"
|
||||
)
|
||||
|
||||
// Get safely gets a value from a map by key
|
||||
// Usage: {{ get .Metadata "key" }}
|
||||
func Get(m interface{}, key interface{}) interface{} {
|
||||
return reflectutil.MapGet(m, key)
|
||||
}
|
||||
|
||||
// GetOr safely gets a value from a map with a default fallback
|
||||
// Usage: {{ getOr .Metadata "key" "default" }}
|
||||
func GetOr(m interface{}, key interface{}, defaultValue interface{}) interface{} {
|
||||
result := Get(m, key)
|
||||
if result == nil {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetPath safely gets a nested value using dot notation
|
||||
// Usage: {{ getPath .Config "database.connection.host" }}
|
||||
func GetPath(m interface{}, path string) interface{} {
|
||||
return reflectutil.GetNestedValue(m, path)
|
||||
}
|
||||
|
||||
// GetPathOr safely gets a nested value with a default fallback
|
||||
// Usage: {{ getPathOr .Config "database.connection.host" "localhost" }}
|
||||
func GetPathOr(m interface{}, path string, defaultValue interface{}) interface{} {
|
||||
result := GetPath(m, path)
|
||||
if result == nil {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SafeIndex safely gets an element from a slice by index
|
||||
// Usage: {{ safeIndex .Tables 0 }}
|
||||
func SafeIndex(slice interface{}, index int) interface{} {
|
||||
return reflectutil.SliceIndex(slice, index)
|
||||
}
|
||||
|
||||
// SafeIndexOr safely gets an element from a slice with a default fallback
|
||||
// Usage: {{ safeIndexOr .Tables 0 "default" }}
|
||||
func SafeIndexOr(slice interface{}, index int, defaultValue interface{}) interface{} {
|
||||
result := SafeIndex(slice, index)
|
||||
if result == nil {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Has checks if a key exists in a map
|
||||
// Usage: {{ if has .Metadata "key" }}...{{ end }}
|
||||
func Has(m interface{}, key interface{}) bool {
|
||||
v := reflect.ValueOf(m)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return false
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return false
|
||||
}
|
||||
|
||||
keyVal := reflect.ValueOf(key)
|
||||
return v.MapIndex(keyVal).IsValid()
|
||||
}
|
||||
|
||||
// HasPath checks if a nested path exists
|
||||
// Usage: {{ if hasPath .Config "database.connection.host" }}...{{ end }}
|
||||
func HasPath(m interface{}, path string) bool {
|
||||
return GetPath(m, path) != nil
|
||||
}
|
||||
|
||||
// Keys returns all keys from a map
|
||||
// Usage: {{ range keys .Metadata }}...{{ end }}
|
||||
func Keys(m interface{}) []interface{} {
|
||||
return reflectutil.MapKeys(m)
|
||||
}
|
||||
|
||||
// Values returns all values from a map
|
||||
// Usage: {{ range values .Table.Columns }}...{{ end }}
|
||||
func Values(m interface{}) []interface{} {
|
||||
return reflectutil.MapValues(m)
|
||||
}
|
||||
|
||||
// Merge merges multiple maps into a new map
|
||||
// Usage: {{ $merged := merge .Map1 .Map2 }}
|
||||
func Merge(maps ...interface{}) map[interface{}]interface{} {
|
||||
result := make(map[interface{}]interface{})
|
||||
|
||||
for _, m := range maps {
|
||||
v := reflect.ValueOf(m)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
continue
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
continue
|
||||
}
|
||||
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
result[iter.Key().Interface()] = iter.Value().Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Pick returns a new map with only the specified keys
|
||||
// Usage: {{ $subset := pick .Metadata "name" "description" }}
|
||||
func Pick(m interface{}, keys ...interface{}) map[interface{}]interface{} {
|
||||
result := make(map[interface{}]interface{})
|
||||
v := reflect.ValueOf(m)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return result
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
keyVal := reflect.ValueOf(key)
|
||||
mapVal := v.MapIndex(keyVal)
|
||||
if mapVal.IsValid() {
|
||||
result[key] = mapVal.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Omit returns a new map without the specified keys
|
||||
// Usage: {{ $filtered := omit .Metadata "internal" "private" }}
|
||||
func Omit(m interface{}, keys ...interface{}) map[interface{}]interface{} {
|
||||
result := make(map[interface{}]interface{})
|
||||
v := reflect.ValueOf(m)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return result
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return result
|
||||
}
|
||||
|
||||
// Create a set of keys to omit
|
||||
omitSet := make(map[interface{}]bool)
|
||||
for _, key := range keys {
|
||||
omitSet[key] = true
|
||||
}
|
||||
|
||||
// Add all keys that are not in the omit set
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
key := iter.Key().Interface()
|
||||
if !omitSet[key] {
|
||||
result[key] = iter.Value().Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SliceContains checks if a slice contains a value
|
||||
// Usage: {{ if sliceContains .Names "admin" }}...{{ end }}
|
||||
func SliceContains(slice interface{}, value interface{}) bool {
|
||||
v := reflect.ValueOf(slice)
|
||||
v, ok := reflectutil.Deref(v)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if reflectutil.DeepEqual(v.Index(i).Interface(), value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IndexOf returns the index of a value in a slice, or -1 if not found
|
||||
// Usage: {{ $idx := indexOf .Names "admin" }}
|
||||
func IndexOf(slice interface{}, value interface{}) int {
|
||||
v := reflect.ValueOf(slice)
|
||||
v, ok := reflectutil.Deref(v)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if reflectutil.DeepEqual(v.Index(i).Interface(), value) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// Pluck extracts a field from each element in a slice
|
||||
// Usage: {{ $names := pluck .Tables "Name" }}
|
||||
func Pluck(slice interface{}, field string) []interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return []interface{}{}
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
result := make([]interface{}, 0, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i)
|
||||
|
||||
// Dereference item pointers
|
||||
for item.Kind() == reflect.Ptr {
|
||||
if item.IsNil() {
|
||||
result = append(result, nil)
|
||||
continue
|
||||
}
|
||||
item = item.Elem()
|
||||
}
|
||||
|
||||
switch item.Kind() {
|
||||
case reflect.Struct:
|
||||
fieldVal := item.FieldByName(field)
|
||||
if fieldVal.IsValid() {
|
||||
result = append(result, fieldVal.Interface())
|
||||
} else {
|
||||
result = append(result, nil)
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
keyVal := reflect.ValueOf(field)
|
||||
mapVal := item.MapIndex(keyVal)
|
||||
if mapVal.IsValid() {
|
||||
result = append(result, mapVal.Interface())
|
||||
} else {
|
||||
result = append(result, nil)
|
||||
}
|
||||
|
||||
default:
|
||||
result = append(result, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
316
pkg/writers/template/string_helpers.go
Normal file
316
pkg/writers/template/string_helpers.go
Normal file
@@ -0,0 +1,316 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// ToUpper converts a string to uppercase
|
||||
func ToUpper(s string) string {
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
|
||||
// ToLower converts a string to lowercase
|
||||
func ToLower(s string) string {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
// ToCamelCase converts snake_case to camelCase
|
||||
// Examples: user_name → userName, http_request → httpRequest
|
||||
func ToCamelCase(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(s, "_")
|
||||
for i, part := range parts {
|
||||
if i == 0 {
|
||||
parts[i] = strings.ToLower(part)
|
||||
} else {
|
||||
parts[i] = capitalize(part)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
// ToPascalCase converts snake_case to PascalCase
|
||||
// Examples: user_name → UserName, http_request → HTTPRequest
|
||||
func ToPascalCase(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(s, "_")
|
||||
for i, part := range parts {
|
||||
parts[i] = capitalize(part)
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
// ToSnakeCase converts PascalCase/camelCase to snake_case
|
||||
// Examples: UserName → user_name, HTTPRequest → http_request
|
||||
func ToSnakeCase(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
var prevUpper bool
|
||||
var nextUpper bool
|
||||
|
||||
runes := []rune(s)
|
||||
for i, r := range runes {
|
||||
isUpper := unicode.IsUpper(r)
|
||||
|
||||
if i+1 < len(runes) {
|
||||
nextUpper = unicode.IsUpper(runes[i+1])
|
||||
} else {
|
||||
nextUpper = false
|
||||
}
|
||||
|
||||
if i > 0 && isUpper {
|
||||
// Add underscore before uppercase letter if:
|
||||
// 1. Previous char was lowercase, OR
|
||||
// 2. Next char is lowercase (end of acronym)
|
||||
if !prevUpper || (!nextUpper && i+1 < len(runes)) {
|
||||
result.WriteRune('_')
|
||||
}
|
||||
}
|
||||
|
||||
result.WriteRune(unicode.ToLower(r))
|
||||
prevUpper = isUpper
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// ToKebabCase converts snake_case or PascalCase/camelCase to kebab-case
|
||||
// Examples: user_name → user-name, UserName → user-name
|
||||
func ToKebabCase(s string) string {
|
||||
// First convert to snake_case, then replace underscores with hyphens
|
||||
snakeCase := ToSnakeCase(s)
|
||||
return strings.ReplaceAll(snakeCase, "_", "-")
|
||||
}
|
||||
|
||||
// Title capitalizes the first letter of each word
|
||||
func Title(s string) string {
|
||||
caser := cases.Title(language.English)
|
||||
src := []byte(s)
|
||||
dest := []byte(s)
|
||||
_, _, _ = caser.Transform(dest, src, true)
|
||||
return string(dest)
|
||||
}
|
||||
|
||||
// Pluralize converts a singular word to plural
|
||||
// Basic implementation with common English rules
|
||||
func Pluralize(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Special cases
|
||||
irregular := map[string]string{
|
||||
"person": "people",
|
||||
"child": "children",
|
||||
"tooth": "teeth",
|
||||
"foot": "feet",
|
||||
"man": "men",
|
||||
"woman": "women",
|
||||
"mouse": "mice",
|
||||
"goose": "geese",
|
||||
"ox": "oxen",
|
||||
"datum": "data",
|
||||
"medium": "media",
|
||||
"analysis": "analyses",
|
||||
"crisis": "crises",
|
||||
"status": "statuses",
|
||||
}
|
||||
|
||||
if plural, ok := irregular[strings.ToLower(s)]; ok {
|
||||
return plural
|
||||
}
|
||||
|
||||
// Already plural (ends in 's' but not 'ss' or 'us')
|
||||
if strings.HasSuffix(s, "s") && !strings.HasSuffix(s, "ss") && !strings.HasSuffix(s, "us") {
|
||||
return s
|
||||
}
|
||||
|
||||
// Words ending in s, x, z, ch, sh
|
||||
if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "x") ||
|
||||
strings.HasSuffix(s, "z") || strings.HasSuffix(s, "ch") ||
|
||||
strings.HasSuffix(s, "sh") {
|
||||
return s + "es"
|
||||
}
|
||||
|
||||
// Words ending in consonant + y
|
||||
if len(s) >= 2 && strings.HasSuffix(s, "y") {
|
||||
prevChar := s[len(s)-2]
|
||||
if !isVowel(prevChar) {
|
||||
return s[:len(s)-1] + "ies"
|
||||
}
|
||||
}
|
||||
|
||||
// Words ending in f or fe
|
||||
if strings.HasSuffix(s, "f") {
|
||||
return s[:len(s)-1] + "ves"
|
||||
}
|
||||
if strings.HasSuffix(s, "fe") {
|
||||
return s[:len(s)-2] + "ves"
|
||||
}
|
||||
|
||||
// Words ending in consonant + o
|
||||
if len(s) >= 2 && strings.HasSuffix(s, "o") {
|
||||
prevChar := s[len(s)-2]
|
||||
if !isVowel(prevChar) {
|
||||
return s + "es"
|
||||
}
|
||||
}
|
||||
|
||||
// Default: add 's'
|
||||
return s + "s"
|
||||
}
|
||||
|
||||
// Singularize converts a plural word to singular
|
||||
// Basic implementation with common English rules
|
||||
func Singularize(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Special cases
|
||||
irregular := map[string]string{
|
||||
"people": "person",
|
||||
"children": "child",
|
||||
"teeth": "tooth",
|
||||
"feet": "foot",
|
||||
"men": "man",
|
||||
"women": "woman",
|
||||
"mice": "mouse",
|
||||
"geese": "goose",
|
||||
"oxen": "ox",
|
||||
"data": "datum",
|
||||
"media": "medium",
|
||||
"analyses": "analysis",
|
||||
"crises": "crisis",
|
||||
"statuses": "status",
|
||||
}
|
||||
|
||||
if singular, ok := irregular[strings.ToLower(s)]; ok {
|
||||
return singular
|
||||
}
|
||||
|
||||
// Words ending in ies
|
||||
if strings.HasSuffix(s, "ies") && len(s) > 3 {
|
||||
return s[:len(s)-3] + "y"
|
||||
}
|
||||
|
||||
// Words ending in ves
|
||||
if strings.HasSuffix(s, "ves") {
|
||||
return s[:len(s)-3] + "f"
|
||||
}
|
||||
|
||||
// Words ending in ses, xes, zes, ches, shes
|
||||
if strings.HasSuffix(s, "ses") || strings.HasSuffix(s, "xes") ||
|
||||
strings.HasSuffix(s, "zes") || strings.HasSuffix(s, "ches") ||
|
||||
strings.HasSuffix(s, "shes") {
|
||||
return s[:len(s)-2]
|
||||
}
|
||||
|
||||
// Words ending in s (not ss)
|
||||
if strings.HasSuffix(s, "s") && !strings.HasSuffix(s, "ss") {
|
||||
return s[:len(s)-1]
|
||||
}
|
||||
|
||||
// Already singular
|
||||
return s
|
||||
}
|
||||
|
||||
// Trim trims whitespace from both ends
|
||||
func Trim(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// TrimPrefix removes the prefix from the string if present
|
||||
func TrimPrefix(s, prefix string) string {
|
||||
return strings.TrimPrefix(s, prefix)
|
||||
}
|
||||
|
||||
// TrimSuffix removes the suffix from the string if present
|
||||
func TrimSuffix(s, suffix string) string {
|
||||
return strings.TrimSuffix(s, suffix)
|
||||
}
|
||||
|
||||
// Replace replaces occurrences of old with new (n times, or all if n < 0)
|
||||
func Replace(s, old, newstr string, n int) string {
|
||||
return strings.Replace(s, old, newstr, n)
|
||||
}
|
||||
|
||||
// StringContains checks if substr is within s
|
||||
func StringContains(s, substr string) bool {
|
||||
return strings.Contains(s, substr)
|
||||
}
|
||||
|
||||
// HasPrefix checks if string starts with prefix
|
||||
func HasPrefix(s, prefix string) bool {
|
||||
return strings.HasPrefix(s, prefix)
|
||||
}
|
||||
|
||||
// HasSuffix checks if string ends with suffix
|
||||
func HasSuffix(s, suffix string) bool {
|
||||
return strings.HasSuffix(s, suffix)
|
||||
}
|
||||
|
||||
// Split splits string by separator
|
||||
func Split(s, sep string) []string {
|
||||
return strings.Split(s, sep)
|
||||
}
|
||||
|
||||
// Join joins string slice with separator
|
||||
func Join(parts []string, sep string) string {
|
||||
return strings.Join(parts, sep)
|
||||
}
|
||||
|
||||
// capitalize capitalizes the first letter and handles common acronyms
|
||||
func capitalize(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
upper := strings.ToUpper(s)
|
||||
|
||||
// Handle common acronyms
|
||||
acronyms := map[string]bool{
|
||||
"ID": true,
|
||||
"UUID": true,
|
||||
"GUID": true,
|
||||
"URL": true,
|
||||
"URI": true,
|
||||
"HTTP": true,
|
||||
"HTTPS": true,
|
||||
"API": true,
|
||||
"JSON": true,
|
||||
"XML": true,
|
||||
"SQL": true,
|
||||
"HTML": true,
|
||||
"CSS": true,
|
||||
"RID": true,
|
||||
}
|
||||
|
||||
if acronyms[upper] {
|
||||
return upper
|
||||
}
|
||||
|
||||
// Capitalize first letter
|
||||
runes := []rune(s)
|
||||
runes[0] = unicode.ToUpper(runes[0])
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// isVowel checks if a byte is a vowel
|
||||
func isVowel(c byte) bool {
|
||||
c = byte(unicode.ToLower(rune(c)))
|
||||
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'
|
||||
}
|
||||
108
pkg/writers/template/template_data.go
Normal file
108
pkg/writers/template/template_data.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package template
|
||||
|
||||
import "git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
|
||||
// TemplateData wraps the model data with additional context for template execution
|
||||
type TemplateData struct {
|
||||
// One of these will be populated based on execution mode
|
||||
Database *models.Database
|
||||
Schema *models.Schema
|
||||
Domain *models.Domain
|
||||
Script *models.Script
|
||||
Table *models.Table
|
||||
|
||||
// Context information (parent references)
|
||||
ParentSchema *models.Schema // Set for table/script modes
|
||||
ParentDatabase *models.Database // Always set for full database context
|
||||
|
||||
// Pre-computed views for convenience
|
||||
FlatColumns []*models.FlatColumn
|
||||
FlatTables []*models.FlatTable
|
||||
FlatConstraints []*models.FlatConstraint
|
||||
FlatRelationships []*models.FlatRelationship
|
||||
Summary *models.DatabaseSummary
|
||||
|
||||
// User metadata from WriterOptions
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
// NewDatabaseData creates template data for database mode
|
||||
func NewDatabaseData(db *models.Database, metadata map[string]interface{}) *TemplateData {
|
||||
return &TemplateData{
|
||||
Database: db,
|
||||
ParentDatabase: db,
|
||||
FlatColumns: db.ToFlatColumns(),
|
||||
FlatTables: db.ToFlatTables(),
|
||||
FlatConstraints: db.ToFlatConstraints(),
|
||||
FlatRelationships: db.ToFlatRelationships(),
|
||||
Summary: db.ToSummary(),
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSchemaData creates template data for schema mode
|
||||
func NewSchemaData(schema *models.Schema, metadata map[string]interface{}) *TemplateData {
|
||||
// Create a temporary database with just this schema for context
|
||||
db := models.InitDatabase(schema.Name)
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
|
||||
return &TemplateData{
|
||||
Schema: schema,
|
||||
ParentDatabase: db,
|
||||
FlatColumns: db.ToFlatColumns(),
|
||||
FlatTables: db.ToFlatTables(),
|
||||
FlatConstraints: db.ToFlatConstraints(),
|
||||
FlatRelationships: db.ToFlatRelationships(),
|
||||
Summary: db.ToSummary(),
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDomainData creates template data for domain mode
|
||||
func NewDomainData(domain *models.Domain, db *models.Database, metadata map[string]interface{}) *TemplateData {
|
||||
return &TemplateData{
|
||||
Domain: domain,
|
||||
ParentDatabase: db,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// NewScriptData creates template data for script mode
|
||||
func NewScriptData(script *models.Script, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData {
|
||||
return &TemplateData{
|
||||
Script: script,
|
||||
ParentSchema: schema,
|
||||
ParentDatabase: db,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTableData creates template data for table mode
|
||||
func NewTableData(table *models.Table, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData {
|
||||
return &TemplateData{
|
||||
Table: table,
|
||||
ParentSchema: schema,
|
||||
ParentDatabase: db,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the primary name for the current template data (used for filename generation)
|
||||
func (td *TemplateData) Name() string {
|
||||
if td.Database != nil {
|
||||
return td.Database.Name
|
||||
}
|
||||
if td.Schema != nil {
|
||||
return td.Schema.Name
|
||||
}
|
||||
if td.Domain != nil {
|
||||
return td.Domain.Name
|
||||
}
|
||||
if td.Script != nil {
|
||||
return td.Script.Name
|
||||
}
|
||||
if td.Table != nil {
|
||||
return td.Table.Name
|
||||
}
|
||||
return "output"
|
||||
}
|
||||
38
pkg/writers/template/type_mappers.go
Normal file
38
pkg/writers/template/type_mappers.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package template
|
||||
|
||||
import "git.warky.dev/wdevs/relspecgo/pkg/commontypes"
|
||||
|
||||
// SQLToGo converts SQL types to Go types
|
||||
func SQLToGo(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToGo(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToTypeScript converts SQL types to TypeScript types
|
||||
func SQLToTypeScript(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToTypeScript(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToJava converts SQL types to Java types
|
||||
func SQLToJava(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToJava(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToPython converts SQL types to Python types
|
||||
func SQLToPython(sqlType string) string {
|
||||
return commontypes.SQLToPython(sqlType)
|
||||
}
|
||||
|
||||
// SQLToRust converts SQL types to Rust types
|
||||
func SQLToRust(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToRust(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToCSharp converts SQL types to C# types
|
||||
func SQLToCSharp(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToCSharp(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToPhp converts SQL types to PHP types
|
||||
func SQLToPhp(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToPhp(sqlType, nullable)
|
||||
}
|
||||
309
pkg/writers/template/writer.go
Normal file
309
pkg/writers/template/writer.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
)
|
||||
|
||||
// EntrypointMode defines how the template is executed
|
||||
type EntrypointMode string
|
||||
|
||||
const (
|
||||
// DatabaseMode executes the template once for the entire database (single output)
|
||||
DatabaseMode EntrypointMode = "database"
|
||||
// SchemaMode executes the template once per schema (multi-file output)
|
||||
SchemaMode EntrypointMode = "schema"
|
||||
// DomainMode executes the template once per domain (multi-file output)
|
||||
DomainMode EntrypointMode = "domain"
|
||||
// ScriptMode executes the template once per script (multi-file output)
|
||||
ScriptMode EntrypointMode = "script"
|
||||
// TableMode executes the template once per table (multi-file output)
|
||||
TableMode EntrypointMode = "table"
|
||||
)
|
||||
|
||||
// Writer implements the writers.Writer interface for template-based output
|
||||
type Writer struct {
|
||||
options *writers.WriterOptions
|
||||
templatePath string
|
||||
funcMap template.FuncMap
|
||||
tmpl *template.Template
|
||||
mode EntrypointMode
|
||||
filenamePattern string
|
||||
}
|
||||
|
||||
// NewWriter creates a new template writer with the given options
|
||||
func NewWriter(options *writers.WriterOptions) (*Writer, error) {
|
||||
w := &Writer{
|
||||
options: options,
|
||||
funcMap: BuildFuncMap(),
|
||||
mode: DatabaseMode, // default mode
|
||||
filenamePattern: "{{.Name}}.txt",
|
||||
}
|
||||
|
||||
// Extract template path from metadata
|
||||
if options.Metadata != nil {
|
||||
if path, ok := options.Metadata["template_path"].(string); ok {
|
||||
w.templatePath = path
|
||||
}
|
||||
|
||||
if mode, ok := options.Metadata["mode"].(string); ok {
|
||||
w.mode = EntrypointMode(mode)
|
||||
}
|
||||
|
||||
if pattern, ok := options.Metadata["filename_pattern"].(string); ok {
|
||||
w.filenamePattern = pattern
|
||||
}
|
||||
}
|
||||
|
||||
// Validate template path
|
||||
if w.templatePath == "" {
|
||||
return nil, NewTemplateLoadError("template path is required", nil)
|
||||
}
|
||||
|
||||
// Load and parse template
|
||||
if err := w.loadTemplate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// WriteDatabase writes a database using the template
|
||||
func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
switch w.mode {
|
||||
case DatabaseMode:
|
||||
return w.executeDatabaseMode(db)
|
||||
case SchemaMode:
|
||||
return w.executeSchemaMode(db)
|
||||
case DomainMode:
|
||||
return w.executeDomainMode(db)
|
||||
case ScriptMode:
|
||||
return w.executeScriptMode(db)
|
||||
case TableMode:
|
||||
return w.executeTableMode(db)
|
||||
default:
|
||||
return fmt.Errorf("unknown entrypoint mode: %s", w.mode)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSchema writes a schema using the template
|
||||
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
||||
// Create a temporary database with just this schema
|
||||
db := models.InitDatabase(schema.Name)
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
|
||||
return w.WriteDatabase(db)
|
||||
}
|
||||
|
||||
// WriteTable writes a single table using the template
|
||||
func (w *Writer) WriteTable(table *models.Table) error {
|
||||
// Create a temporary schema and database
|
||||
schema := models.InitSchema(table.Schema)
|
||||
schema.Tables = []*models.Table{table}
|
||||
|
||||
db := models.InitDatabase(schema.Name)
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
|
||||
return w.WriteDatabase(db)
|
||||
}
|
||||
|
||||
// executeDatabaseMode executes the template once for the entire database
|
||||
func (w *Writer) executeDatabaseMode(db *models.Database) error {
|
||||
data := NewDatabaseData(db, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.writeOutput(output, w.options.OutputPath)
|
||||
}
|
||||
|
||||
// executeSchemaMode executes the template once per schema
|
||||
func (w *Writer) executeSchemaMode(db *models.Database) error {
|
||||
for _, schema := range db.Schemas {
|
||||
data := NewSchemaData(schema, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute template for schema %s: %w", schema.Name, err)
|
||||
}
|
||||
|
||||
filename, err := w.generateFilename(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate filename for schema %s: %w", schema.Name, err)
|
||||
}
|
||||
|
||||
if err := w.writeOutput(output, filename); err != nil {
|
||||
return fmt.Errorf("failed to write output for schema %s: %w", schema.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeDomainMode executes the template once per domain
|
||||
func (w *Writer) executeDomainMode(db *models.Database) error {
|
||||
for _, domain := range db.Domains {
|
||||
data := NewDomainData(domain, db, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute template for domain %s: %w", domain.Name, err)
|
||||
}
|
||||
|
||||
filename, err := w.generateFilename(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate filename for domain %s: %w", domain.Name, err)
|
||||
}
|
||||
|
||||
if err := w.writeOutput(output, filename); err != nil {
|
||||
return fmt.Errorf("failed to write output for domain %s: %w", domain.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeScriptMode executes the template once per script
|
||||
func (w *Writer) executeScriptMode(db *models.Database) error {
|
||||
for _, schema := range db.Schemas {
|
||||
for _, script := range schema.Scripts {
|
||||
data := NewScriptData(script, schema, db, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute template for script %s: %w", script.Name, err)
|
||||
}
|
||||
|
||||
filename, err := w.generateFilename(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate filename for script %s: %w", script.Name, err)
|
||||
}
|
||||
|
||||
if err := w.writeOutput(output, filename); err != nil {
|
||||
return fmt.Errorf("failed to write output for script %s: %w", script.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeTableMode executes the template once per table
|
||||
func (w *Writer) executeTableMode(db *models.Database) error {
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
data := NewTableData(table, schema, db, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute template for table %s.%s: %w", schema.Name, table.Name, err)
|
||||
}
|
||||
|
||||
filename, err := w.generateFilename(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate filename for table %s.%s: %w", schema.Name, table.Name, err)
|
||||
}
|
||||
|
||||
if err := w.writeOutput(output, filename); err != nil {
|
||||
return fmt.Errorf("failed to write output for table %s.%s: %w", schema.Name, table.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadTemplate loads and parses the template file
|
||||
func (w *Writer) loadTemplate() error {
|
||||
// Read template file
|
||||
content, err := os.ReadFile(w.templatePath)
|
||||
if err != nil {
|
||||
return NewTemplateLoadError(fmt.Sprintf("failed to read template file: %s", w.templatePath), err)
|
||||
}
|
||||
|
||||
// Parse template with function map
|
||||
tmpl, err := template.New(filepath.Base(w.templatePath)).Funcs(w.funcMap).Parse(string(content))
|
||||
if err != nil {
|
||||
return NewTemplateParseError(fmt.Sprintf("failed to parse template: %s", w.templatePath), err)
|
||||
}
|
||||
|
||||
w.tmpl = tmpl
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeTemplate executes the template with the given data
|
||||
func (w *Writer) executeTemplate(data *TemplateData) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := w.tmpl.Execute(&buf, data); err != nil {
|
||||
return "", NewTemplateExecuteError("failed to execute template", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// generateFilename generates a filename from the filename pattern
|
||||
func (w *Writer) generateFilename(data *TemplateData) (string, error) {
|
||||
// Parse filename pattern as a template
|
||||
tmpl, err := template.New("filename").Funcs(w.funcMap).Parse(w.filenamePattern)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid filename pattern: %w", err)
|
||||
}
|
||||
|
||||
// Execute filename template
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("failed to generate filename: %w", err)
|
||||
}
|
||||
|
||||
filename := buf.String()
|
||||
|
||||
// If output path is a directory, join with generated filename
|
||||
if w.options.OutputPath != "" {
|
||||
// Check if output path is a directory
|
||||
info, err := os.Stat(w.options.OutputPath)
|
||||
if err == nil && info.IsDir() {
|
||||
filename = filepath.Join(w.options.OutputPath, filename)
|
||||
} else {
|
||||
// If it doesn't exist, check if it looks like a directory (ends with /)
|
||||
if strings.HasSuffix(w.options.OutputPath, string(filepath.Separator)) {
|
||||
filename = filepath.Join(w.options.OutputPath, filename)
|
||||
} else {
|
||||
// Use output path as base directory
|
||||
dir := filepath.Dir(w.options.OutputPath)
|
||||
if dir != "." {
|
||||
filename = filepath.Join(dir, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// writeOutput writes the output to a file or stdout
|
||||
func (w *Writer) writeOutput(content string, outputPath string) error {
|
||||
// If output path is empty, write to stdout
|
||||
if outputPath == "" {
|
||||
fmt.Print(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(outputPath)
|
||||
if dir != "." && dir != "" {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file
|
||||
if err := os.WriteFile(outputPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
13
vendor/github.com/gdamore/encoding/.appveyor.yml
generated
vendored
Normal file
13
vendor/github.com/gdamore/encoding/.appveyor.yml
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
version: 1.0.{build}
|
||||
clone_folder: c:\gopath\src\github.com\gdamore\encoding
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
build_script:
|
||||
- go version
|
||||
- go env
|
||||
- SET PATH=%LOCALAPPDATA%\atom\bin;%GOPATH%\bin;%PATH%
|
||||
- go get -t ./...
|
||||
- go build
|
||||
- go install ./...
|
||||
test_script:
|
||||
- go test ./...
|
||||
73
vendor/github.com/gdamore/encoding/CODE_OF_CONDUCT.md
generated
vendored
Normal file
73
vendor/github.com/gdamore/encoding/CODE_OF_CONDUCT.md
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at garrett@damore.org. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
202
vendor/github.com/gdamore/encoding/LICENSE
generated
vendored
Normal file
202
vendor/github.com/gdamore/encoding/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
20
vendor/github.com/gdamore/encoding/README.md
generated
vendored
Normal file
20
vendor/github.com/gdamore/encoding/README.md
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
## encoding
|
||||
|
||||
|
||||
[](https://github.com/gdamore/encoding/actions/workflows/linux.yml)
|
||||
[](https://github.com/gdamore/encoding/actions/workflows/windows.yml)
|
||||
[](https://github.com/gdamore/encoding/blob/master/LICENSE)
|
||||
[](https://codecov.io/gh/gdamore/encoding)
|
||||
[](https://godoc.org/github.com/gdamore/encoding)
|
||||
|
||||
Package encoding provides a number of encodings that are missing from the
|
||||
standard Go [encoding]("https://godoc.org/golang.org/x/text/encoding") package.
|
||||
|
||||
We hope that we can contribute these to the standard Go library someday. It
|
||||
turns out that some of these are useful for dealing with I/O streams coming
|
||||
from non-UTF friendly sources.
|
||||
|
||||
The UTF8 Encoder is also useful for situations where valid UTF-8 might be
|
||||
carried in streams that contain non-valid UTF; in particular I use it for
|
||||
helping me cope with terminals that embed escape sequences in otherwise
|
||||
valid UTF-8.
|
||||
12
vendor/github.com/gdamore/encoding/SECURITY.md
generated
vendored
Normal file
12
vendor/github.com/gdamore/encoding/SECURITY.md
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Security Policy
|
||||
|
||||
We take security very seriously in mangos, since you may be using it in
|
||||
Internet-facing applications.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, please contact us on our discord.
|
||||
You may also send an email to garrett@damore.org, or info@staysail.tech.
|
||||
|
||||
We will keep the reporter updated on any status updates on a regular basis,
|
||||
and will respond within two business days for any reported security issue.
|
||||
36
vendor/github.com/gdamore/encoding/ascii.go
generated
vendored
Normal file
36
vendor/github.com/gdamore/encoding/ascii.go
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2015 Garrett D'Amore
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"golang.org/x/text/encoding"
|
||||
)
|
||||
|
||||
// ASCII represents the 7-bit US-ASCII scheme. It decodes directly to
|
||||
// UTF-8 without change, as all ASCII values are legal UTF-8.
|
||||
// Unicode values less than 128 (i.e. 7 bits) map 1:1 with ASCII.
|
||||
// It encodes runes outside of that to 0x1A, the ASCII substitution character.
|
||||
var ASCII encoding.Encoding
|
||||
|
||||
func init() {
|
||||
amap := make(map[byte]rune)
|
||||
for i := 128; i <= 255; i++ {
|
||||
amap[byte(i)] = RuneError
|
||||
}
|
||||
|
||||
cm := &Charmap{Map: amap}
|
||||
cm.Init()
|
||||
ASCII = cm
|
||||
}
|
||||
195
vendor/github.com/gdamore/encoding/charmap.go
generated
vendored
Normal file
195
vendor/github.com/gdamore/encoding/charmap.go
generated
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright 2024 Garrett D'Amore
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
const (
|
||||
// RuneError is an alias for the UTF-8 replacement rune, '\uFFFD'.
|
||||
RuneError = '\uFFFD'
|
||||
|
||||
// RuneSelf is the rune below which UTF-8 and the Unicode values are
|
||||
// identical. Its also the limit for ASCII.
|
||||
RuneSelf = 0x80
|
||||
|
||||
// ASCIISub is the ASCII substitution character.
|
||||
ASCIISub = '\x1a'
|
||||
)
|
||||
|
||||
// Charmap is a structure for setting up encodings for 8-bit character sets,
|
||||
// for transforming between UTF8 and that other character set. It has some
|
||||
// ideas borrowed from golang.org/x/text/encoding/charmap, but it uses a
|
||||
// different implementation. This implementation uses maps, and supports
|
||||
// user-defined maps.
|
||||
//
|
||||
// We do assume that a character map has a reasonable substitution character,
|
||||
// and that valid encodings are stable (exactly a 1:1 map) and stateless
|
||||
// (that is there is no shift character or anything like that.) Hence this
|
||||
// approach will not work for many East Asian character sets.
|
||||
//
|
||||
// Measurement shows little or no measurable difference in the performance of
|
||||
// the two approaches. The difference was down to a couple of nsec/op, and
|
||||
// no consistent pattern as to which ran faster. With the conversion to
|
||||
// UTF-8 the code takes about 25 nsec/op. The conversion in the reverse
|
||||
// direction takes about 100 nsec/op. (The larger cost for conversion
|
||||
// from UTF-8 is most likely due to the need to convert the UTF-8 byte stream
|
||||
// to a rune before conversion.
|
||||
type Charmap struct {
|
||||
transform.NopResetter
|
||||
bytes map[rune]byte
|
||||
runes [256][]byte
|
||||
once sync.Once
|
||||
|
||||
// The map between bytes and runes. To indicate that a specific
|
||||
// byte value is invalid for a charcter set, use the rune
|
||||
// utf8.RuneError. Values that are absent from this map will
|
||||
// be assumed to have the identity mapping -- that is the default
|
||||
// is to assume ISO8859-1, where all 8-bit characters have the same
|
||||
// numeric value as their Unicode runes. (Not to be confused with
|
||||
// the UTF-8 values, which *will* be different for non-ASCII runes.)
|
||||
//
|
||||
// If no values less than RuneSelf are changed (or have non-identity
|
||||
// mappings), then the character set is assumed to be an ASCII
|
||||
// superset, and certain assumptions and optimizations become
|
||||
// available for ASCII bytes.
|
||||
Map map[byte]rune
|
||||
|
||||
// The ReplacementChar is the byte value to use for substitution.
|
||||
// It should normally be ASCIISub for ASCII encodings. This may be
|
||||
// unset (left to zero) for mappings that are strictly ASCII supersets.
|
||||
// In that case ASCIISub will be assumed instead.
|
||||
ReplacementChar byte
|
||||
}
|
||||
|
||||
type cmapDecoder struct {
|
||||
transform.NopResetter
|
||||
runes [256][]byte
|
||||
}
|
||||
|
||||
type cmapEncoder struct {
|
||||
transform.NopResetter
|
||||
bytes map[rune]byte
|
||||
replace byte
|
||||
}
|
||||
|
||||
// Init initializes internal values of a character map. This should
|
||||
// be done early, to minimize the cost of allocation of transforms
|
||||
// later. It is not strictly necessary however, as the allocation
|
||||
// functions will arrange to call it if it has not already been done.
|
||||
func (c *Charmap) Init() {
|
||||
c.once.Do(c.initialize)
|
||||
}
|
||||
|
||||
func (c *Charmap) initialize() {
|
||||
c.bytes = make(map[rune]byte)
|
||||
ascii := true
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
r, ok := c.Map[byte(i)]
|
||||
if !ok {
|
||||
r = rune(i)
|
||||
}
|
||||
if r < 128 && r != rune(i) {
|
||||
ascii = false
|
||||
}
|
||||
if r != RuneError {
|
||||
c.bytes[r] = byte(i)
|
||||
}
|
||||
utf := make([]byte, utf8.RuneLen(r))
|
||||
utf8.EncodeRune(utf, r)
|
||||
c.runes[i] = utf
|
||||
}
|
||||
if ascii && c.ReplacementChar == '\x00' {
|
||||
c.ReplacementChar = ASCIISub
|
||||
}
|
||||
}
|
||||
|
||||
// NewDecoder returns a Decoder the converts from the 8-bit
|
||||
// character set to UTF-8. Unknown mappings, if any, are mapped
|
||||
// to '\uFFFD'.
|
||||
func (c *Charmap) NewDecoder() *encoding.Decoder {
|
||||
c.Init()
|
||||
return &encoding.Decoder{Transformer: &cmapDecoder{runes: c.runes}}
|
||||
}
|
||||
|
||||
// NewEncoder returns a Transformer that converts from UTF8 to the
|
||||
// 8-bit character set. Unknown mappings are mapped to 0x1A.
|
||||
func (c *Charmap) NewEncoder() *encoding.Encoder {
|
||||
c.Init()
|
||||
return &encoding.Encoder{
|
||||
Transformer: &cmapEncoder{
|
||||
bytes: c.bytes,
|
||||
replace: c.ReplacementChar,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *cmapDecoder) Transform(dst, src []byte, atEOF bool) (int, int, error) {
|
||||
var e error
|
||||
var ndst, nsrc int
|
||||
|
||||
for _, c := range src {
|
||||
b := d.runes[c]
|
||||
l := len(b)
|
||||
|
||||
if ndst+l > len(dst) {
|
||||
e = transform.ErrShortDst
|
||||
break
|
||||
}
|
||||
for i := 0; i < l; i++ {
|
||||
dst[ndst] = b[i]
|
||||
ndst++
|
||||
}
|
||||
nsrc++
|
||||
}
|
||||
return ndst, nsrc, e
|
||||
}
|
||||
|
||||
func (d *cmapEncoder) Transform(dst, src []byte, atEOF bool) (int, int, error) {
|
||||
var e error
|
||||
var ndst, nsrc int
|
||||
for nsrc < len(src) {
|
||||
if ndst >= len(dst) {
|
||||
e = transform.ErrShortDst
|
||||
break
|
||||
}
|
||||
|
||||
r, sz := utf8.DecodeRune(src[nsrc:])
|
||||
if r == utf8.RuneError && sz == 1 {
|
||||
// If its inconclusive due to insufficient data in
|
||||
// in the source, report it
|
||||
if atEOF && !utf8.FullRune(src[nsrc:]) {
|
||||
e = transform.ErrShortSrc
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if c, ok := d.bytes[r]; ok {
|
||||
dst[ndst] = c
|
||||
} else {
|
||||
dst[ndst] = d.replace
|
||||
}
|
||||
nsrc += sz
|
||||
ndst++
|
||||
}
|
||||
|
||||
return ndst, nsrc, e
|
||||
}
|
||||
17
vendor/github.com/gdamore/encoding/doc.go
generated
vendored
Normal file
17
vendor/github.com/gdamore/encoding/doc.go
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2015 Garrett D'Amore
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package encoding provides a few of the encoding structures that are
|
||||
// missing from the Go x/text/encoding tree.
|
||||
package encoding
|
||||
273
vendor/github.com/gdamore/encoding/ebcdic.go
generated
vendored
Normal file
273
vendor/github.com/gdamore/encoding/ebcdic.go
generated
vendored
Normal file
@@ -0,0 +1,273 @@
|
||||
// Copyright 2015 Garrett D'Amore
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"golang.org/x/text/encoding"
|
||||
)
|
||||
|
||||
// EBCDIC represents the 8-bit EBCDIC scheme, found in some mainframe
|
||||
// environments. If you don't know what this is, consider yourself lucky.
|
||||
var EBCDIC encoding.Encoding
|
||||
|
||||
func init() {
|
||||
cm := &Charmap{
|
||||
ReplacementChar: '\x3f',
|
||||
Map: map[byte]rune{
|
||||
// 0x00-0x03 match
|
||||
0x04: RuneError,
|
||||
0x05: '\t',
|
||||
0x06: RuneError,
|
||||
0x07: '\x7f',
|
||||
0x08: RuneError,
|
||||
0x09: RuneError,
|
||||
0x0a: RuneError,
|
||||
// 0x0b-0x13 match
|
||||
0x14: RuneError,
|
||||
0x15: '\x85', // Not in any ISO code
|
||||
0x16: '\x08',
|
||||
0x17: RuneError,
|
||||
// 0x18-0x19 match
|
||||
0x1a: RuneError,
|
||||
0x1b: RuneError,
|
||||
// 0x1c-0x1f match
|
||||
0x20: RuneError,
|
||||
0x21: RuneError,
|
||||
0x22: RuneError,
|
||||
0x23: RuneError,
|
||||
0x24: RuneError,
|
||||
0x25: '\n',
|
||||
0x26: '\x17',
|
||||
0x27: '\x1b',
|
||||
0x28: RuneError,
|
||||
0x29: RuneError,
|
||||
0x2a: RuneError,
|
||||
0x2b: RuneError,
|
||||
0x2c: RuneError,
|
||||
0x2d: '\x05',
|
||||
0x2e: '\x06',
|
||||
0x2f: '\x07',
|
||||
0x30: RuneError,
|
||||
0x31: RuneError,
|
||||
0x32: '\x16',
|
||||
0x33: RuneError,
|
||||
0x34: RuneError,
|
||||
0x35: RuneError,
|
||||
0x36: RuneError,
|
||||
0x37: '\x04',
|
||||
0x38: RuneError,
|
||||
0x39: RuneError,
|
||||
0x3a: RuneError,
|
||||
0x3b: RuneError,
|
||||
0x3c: '\x14',
|
||||
0x3d: '\x15',
|
||||
0x3e: RuneError,
|
||||
0x3f: '\x1a', // also replacement char
|
||||
0x40: ' ',
|
||||
0x41: '\xa0',
|
||||
0x42: RuneError,
|
||||
0x43: RuneError,
|
||||
0x44: RuneError,
|
||||
0x45: RuneError,
|
||||
0x46: RuneError,
|
||||
0x47: RuneError,
|
||||
0x48: RuneError,
|
||||
0x49: RuneError,
|
||||
0x4a: RuneError,
|
||||
0x4b: '.',
|
||||
0x4c: '<',
|
||||
0x4d: '(',
|
||||
0x4e: '+',
|
||||
0x4f: '|',
|
||||
0x50: '&',
|
||||
0x51: RuneError,
|
||||
0x52: RuneError,
|
||||
0x53: RuneError,
|
||||
0x54: RuneError,
|
||||
0x55: RuneError,
|
||||
0x56: RuneError,
|
||||
0x57: RuneError,
|
||||
0x58: RuneError,
|
||||
0x59: RuneError,
|
||||
0x5a: '!',
|
||||
0x5b: '$',
|
||||
0x5c: '*',
|
||||
0x5d: ')',
|
||||
0x5e: ';',
|
||||
0x5f: '¬',
|
||||
0x60: '-',
|
||||
0x61: '/',
|
||||
0x62: RuneError,
|
||||
0x63: RuneError,
|
||||
0x64: RuneError,
|
||||
0x65: RuneError,
|
||||
0x66: RuneError,
|
||||
0x67: RuneError,
|
||||
0x68: RuneError,
|
||||
0x69: RuneError,
|
||||
0x6a: '¦',
|
||||
0x6b: ',',
|
||||
0x6c: '%',
|
||||
0x6d: '_',
|
||||
0x6e: '>',
|
||||
0x6f: '?',
|
||||
0x70: RuneError,
|
||||
0x71: RuneError,
|
||||
0x72: RuneError,
|
||||
0x73: RuneError,
|
||||
0x74: RuneError,
|
||||
0x75: RuneError,
|
||||
0x76: RuneError,
|
||||
0x77: RuneError,
|
||||
0x78: RuneError,
|
||||
0x79: '`',
|
||||
0x7a: ':',
|
||||
0x7b: '#',
|
||||
0x7c: '@',
|
||||
0x7d: '\'',
|
||||
0x7e: '=',
|
||||
0x7f: '"',
|
||||
0x80: RuneError,
|
||||
0x81: 'a',
|
||||
0x82: 'b',
|
||||
0x83: 'c',
|
||||
0x84: 'd',
|
||||
0x85: 'e',
|
||||
0x86: 'f',
|
||||
0x87: 'g',
|
||||
0x88: 'h',
|
||||
0x89: 'i',
|
||||
0x8a: RuneError,
|
||||
0x8b: RuneError,
|
||||
0x8c: RuneError,
|
||||
0x8d: RuneError,
|
||||
0x8e: RuneError,
|
||||
0x8f: '±',
|
||||
0x90: RuneError,
|
||||
0x91: 'j',
|
||||
0x92: 'k',
|
||||
0x93: 'l',
|
||||
0x94: 'm',
|
||||
0x95: 'n',
|
||||
0x96: 'o',
|
||||
0x97: 'p',
|
||||
0x98: 'q',
|
||||
0x99: 'r',
|
||||
0x9a: RuneError,
|
||||
0x9b: RuneError,
|
||||
0x9c: RuneError,
|
||||
0x9d: RuneError,
|
||||
0x9e: RuneError,
|
||||
0x9f: RuneError,
|
||||
0xa0: RuneError,
|
||||
0xa1: '~',
|
||||
0xa2: 's',
|
||||
0xa3: 't',
|
||||
0xa4: 'u',
|
||||
0xa5: 'v',
|
||||
0xa6: 'w',
|
||||
0xa7: 'x',
|
||||
0xa8: 'y',
|
||||
0xa9: 'z',
|
||||
0xaa: RuneError,
|
||||
0xab: RuneError,
|
||||
0xac: RuneError,
|
||||
0xad: RuneError,
|
||||
0xae: RuneError,
|
||||
0xaf: RuneError,
|
||||
0xb0: '^',
|
||||
0xb1: RuneError,
|
||||
0xb2: RuneError,
|
||||
0xb3: RuneError,
|
||||
0xb4: RuneError,
|
||||
0xb5: RuneError,
|
||||
0xb6: RuneError,
|
||||
0xb7: RuneError,
|
||||
0xb8: RuneError,
|
||||
0xb9: RuneError,
|
||||
0xba: '[',
|
||||
0xbb: ']',
|
||||
0xbc: RuneError,
|
||||
0xbd: RuneError,
|
||||
0xbe: RuneError,
|
||||
0xbf: RuneError,
|
||||
0xc0: '{',
|
||||
0xc1: 'A',
|
||||
0xc2: 'B',
|
||||
0xc3: 'C',
|
||||
0xc4: 'D',
|
||||
0xc5: 'E',
|
||||
0xc6: 'F',
|
||||
0xc7: 'G',
|
||||
0xc8: 'H',
|
||||
0xc9: 'I',
|
||||
0xca: '\xad', // NB: soft hyphen
|
||||
0xcb: RuneError,
|
||||
0xcc: RuneError,
|
||||
0xcd: RuneError,
|
||||
0xce: RuneError,
|
||||
0xcf: RuneError,
|
||||
0xd0: '}',
|
||||
0xd1: 'J',
|
||||
0xd2: 'K',
|
||||
0xd3: 'L',
|
||||
0xd4: 'M',
|
||||
0xd5: 'N',
|
||||
0xd6: 'O',
|
||||
0xd7: 'P',
|
||||
0xd8: 'Q',
|
||||
0xd9: 'R',
|
||||
0xda: RuneError,
|
||||
0xdb: RuneError,
|
||||
0xdc: RuneError,
|
||||
0xdd: RuneError,
|
||||
0xde: RuneError,
|
||||
0xdf: RuneError,
|
||||
0xe0: '\\',
|
||||
0xe1: '\u2007', // Non-breaking space
|
||||
0xe2: 'S',
|
||||
0xe3: 'T',
|
||||
0xe4: 'U',
|
||||
0xe5: 'V',
|
||||
0xe6: 'W',
|
||||
0xe7: 'X',
|
||||
0xe8: 'Y',
|
||||
0xe9: 'Z',
|
||||
0xea: RuneError,
|
||||
0xeb: RuneError,
|
||||
0xec: RuneError,
|
||||
0xed: RuneError,
|
||||
0xee: RuneError,
|
||||
0xef: RuneError,
|
||||
0xf0: '0',
|
||||
0xf1: '1',
|
||||
0xf2: '2',
|
||||
0xf3: '3',
|
||||
0xf4: '4',
|
||||
0xf5: '5',
|
||||
0xf6: '6',
|
||||
0xf7: '7',
|
||||
0xf8: '8',
|
||||
0xf9: '9',
|
||||
0xfa: RuneError,
|
||||
0xfb: RuneError,
|
||||
0xfc: RuneError,
|
||||
0xfd: RuneError,
|
||||
0xfe: RuneError,
|
||||
0xff: RuneError,
|
||||
}}
|
||||
cm.Init()
|
||||
EBCDIC = cm
|
||||
}
|
||||
33
vendor/github.com/gdamore/encoding/latin1.go
generated
vendored
Normal file
33
vendor/github.com/gdamore/encoding/latin1.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2015 Garrett D'Amore
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"golang.org/x/text/encoding"
|
||||
)
|
||||
|
||||
// ISO8859_1 represents the 8-bit ISO8859-1 scheme. It decodes directly to
|
||||
// UTF-8 without change, as all ISO8859-1 values are legal UTF-8.
|
||||
// Unicode values less than 256 (i.e. 8 bits) map 1:1 with 8859-1.
|
||||
// It encodes runes outside of that to 0x1A, the ASCII substitution character.
|
||||
var ISO8859_1 encoding.Encoding
|
||||
|
||||
func init() {
|
||||
cm := &Charmap{}
|
||||
cm.Init()
|
||||
|
||||
// 8859-1 is the 8-bit identity map for Unicode.
|
||||
ISO8859_1 = cm
|
||||
}
|
||||
35
vendor/github.com/gdamore/encoding/latin5.go
generated
vendored
Normal file
35
vendor/github.com/gdamore/encoding/latin5.go
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2015 Garrett D'Amore
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"golang.org/x/text/encoding"
|
||||
)
|
||||
|
||||
// ISO8859_9 represents the 8-bit ISO8859-9 scheme.
|
||||
var ISO8859_9 encoding.Encoding
|
||||
|
||||
func init() {
|
||||
cm := &Charmap{Map: map[byte]rune{
|
||||
0xD0: 'Ğ',
|
||||
0xDD: 'İ',
|
||||
0xDE: 'Ş',
|
||||
0xF0: 'ğ',
|
||||
0xFD: 'ı',
|
||||
0xFE: 'ş',
|
||||
}}
|
||||
cm.Init()
|
||||
ISO8859_9 = cm
|
||||
}
|
||||
35
vendor/github.com/gdamore/encoding/utf8.go
generated
vendored
Normal file
35
vendor/github.com/gdamore/encoding/utf8.go
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2015 Garrett D'Amore
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"golang.org/x/text/encoding"
|
||||
)
|
||||
|
||||
type validUtf8 struct{}
|
||||
|
||||
// UTF8 is an encoding for UTF-8. All it does is verify that the UTF-8
|
||||
// in is valid. The main reason for its existence is that it will detect
|
||||
// and report ErrSrcShort or ErrDstShort, whereas the Nop encoding just
|
||||
// passes every byte, blithely.
|
||||
var UTF8 encoding.Encoding = validUtf8{}
|
||||
|
||||
func (validUtf8) NewDecoder() *encoding.Decoder {
|
||||
return &encoding.Decoder{Transformer: encoding.UTF8Validator}
|
||||
}
|
||||
|
||||
func (validUtf8) NewEncoder() *encoding.Encoder {
|
||||
return &encoding.Encoder{Transformer: encoding.UTF8Validator}
|
||||
}
|
||||
13
vendor/github.com/gdamore/tcell/v2/.appveyor.yml
generated
vendored
Normal file
13
vendor/github.com/gdamore/tcell/v2/.appveyor.yml
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
version: 1.0.{build}
|
||||
clone_folder: c:\gopath\src\github.com\gdamore\tcell
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
build_script:
|
||||
- go version
|
||||
- go env
|
||||
- SET PATH=%LOCALAPPDATA%\atom\bin;%GOPATH%\bin;%PATH%
|
||||
- go get -t ./...
|
||||
- go build
|
||||
- go install ./...
|
||||
test_script:
|
||||
- go test ./...
|
||||
1
vendor/github.com/gdamore/tcell/v2/.gitignore
generated
vendored
Normal file
1
vendor/github.com/gdamore/tcell/v2/.gitignore
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
coverage.txt
|
||||
18
vendor/github.com/gdamore/tcell/v2/.travis.yml
generated
vendored
Normal file
18
vendor/github.com/gdamore/tcell/v2/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.15.x
|
||||
- master
|
||||
|
||||
arch:
|
||||
- amd64
|
||||
- ppc64le
|
||||
|
||||
before_install:
|
||||
- go get -t -v ./...
|
||||
|
||||
script:
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
4
vendor/github.com/gdamore/tcell/v2/AUTHORS
generated
vendored
Normal file
4
vendor/github.com/gdamore/tcell/v2/AUTHORS
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
Garrett D'Amore <garrett@damore.org>
|
||||
Zachary Yedidia <zyedidia@gmail.com>
|
||||
Junegunn Choi <junegunn.c@gmail.com>
|
||||
Staysail Systems, Inc. <info@staysail.tech>
|
||||
82
vendor/github.com/gdamore/tcell/v2/CHANGESv2.md
generated
vendored
Normal file
82
vendor/github.com/gdamore/tcell/v2/CHANGESv2.md
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
## Breaking Changes in _Tcell_ v2
|
||||
|
||||
A number of changes were made to _Tcell_ for version two, and some of these are breaking.
|
||||
|
||||
### Import Path
|
||||
|
||||
The import path for tcell has changed to `github.com/gdamore/tcell/v2` to reflect a new major version.
|
||||
|
||||
### Style Is Not Numeric
|
||||
|
||||
The type `Style` has changed to a structure, to allow us to add additional data such as flags for color setting,
|
||||
more attribute bits, and so forth.
|
||||
Applications that relied on this being a number will need to be updated to use the accessor methods.
|
||||
|
||||
### Mouse Event Changes
|
||||
|
||||
The middle mouse button was reported as button 2 on Linux, but as button 3 on Windows,
|
||||
and the right mouse button was reported the reverse way.
|
||||
_Tcell_ now always reports the right mouse button as button 2, and the middle button as button 3.
|
||||
To help make this clearer, new symbols `ButtonPrimary`, `ButtonSecondary`, and
|
||||
`ButtonMiddle` are provided.
|
||||
(Note that which button is right vs. left may be impacted by user preferences.
|
||||
Usually the left button will be considered the Primary, and the right will be the Secondary.)
|
||||
Applications may need to adjust their handling of mouse buttons 2 and 3 accordingly.
|
||||
|
||||
### Terminals Removed
|
||||
|
||||
A number of terminals have been removed.
|
||||
These are mostly ancient definitions unlikely to be used by anyone, such as `adm3a`.
|
||||
|
||||
### High Number Function Keys
|
||||
|
||||
Historically terminfo reported function keys with modifiers set as a different
|
||||
function key altogether. For example, Shift-F1 was reported as F13 on XTerm.
|
||||
_Tcell_ now prefers to report these using the base key (such as F1) with modifiers added.
|
||||
This works on XTerm and VTE based emulators, but some emulators may not support this.
|
||||
The new behavior more closely aligns with behavior on Windows platforms.
|
||||
|
||||
## New Features in _Tcell_ v2
|
||||
|
||||
These features are not breaking, but are introduced in version 2.
|
||||
|
||||
### Improved Modifier Support
|
||||
|
||||
For terminals that appear to behave like the venerable XTerm, _tcell_
|
||||
automatically adds modifier reporting for ALT, CTRL, SHIFT, and META keys
|
||||
when the terminal reports them.
|
||||
|
||||
### Better Support for Palettes (Themes)
|
||||
|
||||
When using a color by its name or palette entry, _Tcell_ now tries to
|
||||
use that palette entry as is; this should avoid some inconsistency and respect
|
||||
terminal themes correctly.
|
||||
|
||||
When true fidelity to RGB values is needed, the new `TrueColor()` API can be used
|
||||
to create a direct color, which bypasses the palette altogether.
|
||||
|
||||
### Automatic TrueColor Detection
|
||||
|
||||
For some terminals, if the `Tc` or `RGB` properties are present in terminfo,
|
||||
_Tcell_ will automatically assume the terminal supports 24-bit color.
|
||||
|
||||
### ColorReset
|
||||
|
||||
A new color value, `ColorReset` can be used on the foreground or background
|
||||
to reset the color the default used by the terminal.
|
||||
|
||||
### tmux Support
|
||||
|
||||
_Tcell_ now has improved support for tmux, when the `$TERM` variable is set to "tmux".
|
||||
|
||||
### Strikethrough Support
|
||||
|
||||
_Tcell_ has support for strikethrough when the terminal supports it, using the new `StrikeThrough()` API.
|
||||
|
||||
### Bracketed Paste Support
|
||||
|
||||
_Tcell_ provides the long requested capability to discriminate paste event by using the
|
||||
bracketed-paste capability present in some terminals. This is automatically available on
|
||||
terminals that support XTerm style mouse handling, but applications must opt-in to this
|
||||
by using the new `EnablePaste()` function. A new `EventPaste` type of event will be
|
||||
delivered when starting and finishing a paste operation.
|
||||
202
vendor/github.com/gdamore/tcell/v2/LICENSE
generated
vendored
Normal file
202
vendor/github.com/gdamore/tcell/v2/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
61
vendor/github.com/gdamore/tcell/v2/README-wasm.md
generated
vendored
Normal file
61
vendor/github.com/gdamore/tcell/v2/README-wasm.md
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# WASM for _Tcell_
|
||||
|
||||
You can build _Tcell_ project into a webpage by compiling it slightly differently. This will result in a _Tcell_ project you can embed into another html page, or use as a standalone page.
|
||||
|
||||
## Building your project
|
||||
|
||||
WASM needs special build flags in order to work. You can build it by executing
|
||||
```sh
|
||||
GOOS=js GOARCH=wasm go build -o yourfile.wasm
|
||||
```
|
||||
|
||||
## Additional files
|
||||
|
||||
You also need 5 other files in the same directory as the wasm. Four (`tcell.html`, `tcell.js`, `termstyle.css`, and `beep.wav`) are provided in the `webfiles` directory. The last one, `wasm_exec.js`, can be copied from GOROOT into the current directory by executing
|
||||
```sh
|
||||
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./
|
||||
```
|
||||
|
||||
In `tcell.js`, you also need to change the constant
|
||||
```js
|
||||
const wasmFilePath = "yourfile.wasm"
|
||||
```
|
||||
to the file you outputted to when building.
|
||||
|
||||
## Displaying your project
|
||||
|
||||
### Standalone
|
||||
|
||||
You can see the project (with an white background around the terminal) by serving the directory. You can do this using any framework, including another golang project:
|
||||
|
||||
```golang
|
||||
// server.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Fatal(http.ListenAndServe(":8080",
|
||||
http.FileServer(http.Dir("/path/to/dir/to/serve")),
|
||||
))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To see the webpage with this example, you can type in `localhost:8080/tcell.html` into your browser while `server.go` is running.
|
||||
|
||||
### Embedding
|
||||
It is recommended to use an iframe if you want to embed the app into a webpage:
|
||||
```html
|
||||
<iframe src="tcell.html" title="Tcell app"></iframe>
|
||||
```
|
||||
|
||||
## Other considerations
|
||||
|
||||
### Accessing files
|
||||
|
||||
`io.Open(filename)` and other related functions for reading file systems do not work; use `http.Get(filename)` instead.
|
||||
290
vendor/github.com/gdamore/tcell/v2/README.md
generated
vendored
Normal file
290
vendor/github.com/gdamore/tcell/v2/README.md
generated
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
<img src="logos/tcell.png" style="float: right"/>
|
||||
|
||||
# Tcell
|
||||
|
||||
_Tcell_ is a _Go_ package that provides a cell based view for text terminals, like _XTerm_.
|
||||
It was inspired by _termbox_, but includes many additional improvements.
|
||||
|
||||
[](https://stand-with-ukraine.pp.ua)
|
||||
[](https://github.com/gdamore/tcell/actions/workflows/linux.yml)
|
||||
[](https://github.com/gdamore/tcell/actions/workflows/windows.yml)
|
||||
[](https://github.com/gdamore/tcell/blob/master/LICENSE)
|
||||
[](https://pkg.go.dev/github.com/gdamore/tcell/v2)
|
||||
[](https://discord.gg/urTTxDN)
|
||||
[](https://codecov.io/gh/gdamore/tcell)
|
||||
[](https://goreportcard.com/report/github.com/gdamore/tcell/v2)
|
||||
|
||||
Please see [here](UKRAINE.md) for an important message for the people of Russia.
|
||||
|
||||
NOTE: This is version 2 of _Tcell_. There are breaking changes relative to version 1.
|
||||
Version 1.x remains available using the import `github.com/gdamore/tcell`.
|
||||
|
||||
## Tutorial
|
||||
|
||||
A brief, and still somewhat rough, [tutorial](TUTORIAL.md) is available.
|
||||
|
||||
## Examples
|
||||
|
||||
- [proxima5](https://github.com/gdamore/proxima5) - space shooter ([video](https://youtu.be/jNxKTCmY_bQ))
|
||||
- [govisor](https://github.com/gdamore/govisor) - service management UI ([screenshot](http://2.bp.blogspot.com/--OsvnfzSNow/Vf7aqMw3zXI/AAAAAAAAARo/uOMtOvw4Sbg/s1600/Screen%2BShot%2B2015-09-20%2Bat%2B9.08.41%2BAM.png))
|
||||
- mouse demo - included mouse test ([screenshot](http://2.bp.blogspot.com/-fWvW5opT0es/VhIdItdKqJI/AAAAAAAAATE/7Ojc0L1SpB0/s1600/Screen%2BShot%2B2015-10-04%2Bat%2B11.47.13%2BPM.png))
|
||||
- [gomatrix](https://github.com/gdamore/gomatrix) - converted from Termbox
|
||||
- [micro](https://github.com/zyedidia/micro/) - lightweight text editor with syntax-highlighting and themes
|
||||
- [godu](https://github.com/viktomas/godu) - utility to discover large files/folders
|
||||
- [tview](https://github.com/rivo/tview/) - rich interactive widgets
|
||||
- [cview](https://code.rocketnine.space/tslocum/cview) - user interface toolkit (fork of _tview_)
|
||||
- [awesome gocui](https://github.com/awesome-gocui/gocui) - Go Console User Interface
|
||||
- [gomandelbrot](https://github.com/rgm3/gomandelbrot) - Mandelbrot!
|
||||
- [WTF](https://github.com/senorprogrammer/wtf) - personal information dashboard
|
||||
- [browsh](https://github.com/browsh-org/browsh) - modern web browser ([video](https://www.youtube.com/watch?v=HZq86XfBoRo))
|
||||
- [go-life](https://github.com/sachaos/go-life) - Conway's Game of Life
|
||||
- [gowid](https://github.com/gcla/gowid) - compositional widgets for terminal UIs, inspired by _urwid_
|
||||
- [termshark](https://termshark.io) - interface for _tshark_, inspired by Wireshark, built on _gowid_
|
||||
- [go-tetris](https://github.com/MichaelS11/go-tetris) - Go Tetris with AI option
|
||||
- [fzf](https://github.com/junegunn/fzf) - command-line fuzzy finder
|
||||
- [ascii-fluid](https://github.com/esimov/ascii-fluid) - fluid simulation controlled by webcam
|
||||
- [cbind](https://code.rocketnine.space/tslocum/cbind) - key event encoding, decoding and handling
|
||||
- [tpong](https://github.com/spinzed/tpong) - old-school Pong
|
||||
- [aerc](https://git.sr.ht/~sircmpwn/aerc) - email client
|
||||
- [tblogs](https://github.com/ezeoleaf/tblogs) - development blogs reader
|
||||
- [spinc](https://github.com/lallassu/spinc) - _irssi_ inspired chat application for Cisco Spark/WebEx
|
||||
- [gorss](https://github.com/lallassu/gorss) - RSS/Atom feed reader
|
||||
- [memoryalike](https://github.com/Bios-Marcel/memoryalike) - memorization game
|
||||
- [lf](https://github.com/gokcehan/lf) - file manager
|
||||
- [goful](https://github.com/anmitsu/goful) - CUI file manager
|
||||
- [gokeybr](https://github.com/bunyk/gokeybr) - deliberately practice your typing
|
||||
- [gonano](https://github.com/jbaramidze/gonano) - editor, mimics _nano_
|
||||
- [uchess](https://github.com/tmountain/uchess) - UCI chess client
|
||||
- [min](https://github.com/a-h/min) - Gemini browser
|
||||
- [ov](https://github.com/noborus/ov) - file pager
|
||||
- [tmux-wormhole](https://github.com/gcla/tmux-wormhole) - _tmux_ plugin to transfer files
|
||||
- [gruid-tcell](https://github.com/anaseto/gruid-tcell) - driver for the grid based UI and game framework
|
||||
- [aretext](https://github.com/aretext/aretext) - minimalist text editor with _vim_ key bindings
|
||||
- [sync](https://github.com/kyprifog/sync) - GitHub repo synchronization tool
|
||||
- [statusbar](https://github.com/kyprifog/statusbar) - statusbar motivation tool for tracking periodic tasks/goals
|
||||
- [todo](https://github.com/kyprifog/todo) - simple todo app
|
||||
- [gosnakego](https://github.com/liweiyi88/gosnakego) - a snake game
|
||||
- [gbb](https://github.com/sdemingo/gbb) - A classical bulletin board app for tildes or public unix servers
|
||||
- [lil](https://github.com/andrievsky/lil) - A simple and flexible interface for any service by implementing only list and get operations
|
||||
- [hero.go](https://github.com/barisbll/hero.go) - 2d monster shooter ([video](https://user-images.githubusercontent.com/40062673/277157369-240d7606-b471-4aa1-8c54-4379a513122b.mp4))
|
||||
- [go-tetris](https://github.com/aaronriekenberg/go-tetris) - simple tetris game for native terminal and WASM using github actions+pages
|
||||
- [oddshub](https://github.com/dos-2/oddshub) - A TUI designed for analyzing sports betting odds
|
||||
|
||||
## Pure Go Terminfo Database
|
||||
|
||||
_Tcell_ includes a full parser and expander for terminfo capability strings,
|
||||
so that it can avoid hard coding escape strings for formatting. It also favors
|
||||
portability, and includes support for all POSIX systems.
|
||||
|
||||
The database is also flexible & extensible, and can be modified by either running
|
||||
a program to build the entire database, or an entry for just a single terminal.
|
||||
|
||||
## More Portable
|
||||
|
||||
_Tcell_ is portable to a wide variety of systems, and is pure Go, without
|
||||
any need for CGO.
|
||||
_Tcell_ is believed to work with mainstream systems officially supported by golang.
|
||||
|
||||
## No Async IO
|
||||
|
||||
_Tcell_ is able to operate without requiring `SIGIO` signals (unlike _termbox_),
|
||||
or asynchronous I/O, and can instead use standard Go file objects and Go routines.
|
||||
This means it should be safe, especially for
|
||||
use with programs that use exec, or otherwise need to manipulate the tty streams.
|
||||
This model is also much closer to idiomatic Go, leading to fewer surprises.
|
||||
|
||||
## Rich Unicode & non-Unicode support
|
||||
|
||||
_Tcell_ includes enhanced support for Unicode, including wide characters and
|
||||
combining characters, provided your terminal can support them.
|
||||
Note that
|
||||
Windows terminals generally don't support the full Unicode repertoire.
|
||||
|
||||
It will also convert to and from Unicode locales, so that the program
|
||||
can work with UTF-8 internally, and get reasonable output in other locales.
|
||||
_Tcell_ tries hard to convert to native characters on both input and output.
|
||||
On output _Tcell_ even makes use of the alternate character set to facilitate
|
||||
drawing certain characters.
|
||||
|
||||
## More Function Keys
|
||||
|
||||
_Tcell_ also has richer support for a larger number of special keys that some
|
||||
terminals can send.
|
||||
|
||||
## Better Color Handling
|
||||
|
||||
_Tcell_ will respect your terminal's color space as specified within your terminfo entries.
|
||||
For example attempts to emit color sequences on VT100 terminals
|
||||
won't result in unintended consequences.
|
||||
|
||||
In legacy Windows mode, _Tcell_ supports 16 colors, bold, dim, and reverse,
|
||||
instead of just termbox's 8 colors with reverse. (Note that there is some
|
||||
conflation with bold/dim and colors.)
|
||||
Modern Windows 10 can benefit from much richer colors however.
|
||||
|
||||
_Tcell_ maps 16 colors down to 8, for terminals that need it.
|
||||
(The upper 8 colors are just brighter versions of the lower 8.)
|
||||
|
||||
## Better Mouse Support
|
||||
|
||||
_Tcell_ supports enhanced mouse tracking mode, so your application can receive
|
||||
regular mouse motion events, and wheel events, if your terminal supports it.
|
||||
|
||||
(Note: The Windows 10 Terminal application suffers from a flaw in this regard,
|
||||
and does not support mouse interaction. The stock Windows 10 console host
|
||||
fired up with cmd.exe or PowerShell works fine however.)
|
||||
|
||||
## _Termbox_ Compatibility
|
||||
|
||||
A compatibility layer for _termbox_ is provided in the `compat` directory.
|
||||
To use it, try importing `github.com/gdamore/tcell/termbox` instead.
|
||||
Most _termbox-go_ programs will probably work without further modification.
|
||||
|
||||
## Working With Unicode
|
||||
|
||||
Internally _Tcell_ uses UTF-8, just like Go.
|
||||
However, _Tcell_ understands how to
|
||||
convert to and from other character sets, using the capabilities of
|
||||
the `golang.org/x/text/encoding` packages.
|
||||
Your application must supply
|
||||
them, as the full set of the most common ones bloats the program by about 2 MB.
|
||||
If you're lazy, and want them all anyway, see the `encoding` sub-directory.
|
||||
|
||||
## Wide & Combining Characters
|
||||
|
||||
The `SetContent()` API takes a primary rune, and an optional list of combining runes.
|
||||
If any of the runes is a wide (East Asian) rune occupying two cells,
|
||||
then the library will skip output from the following cell. Care must be
|
||||
taken in the application to avoid explicitly attempting to set content in the
|
||||
next cell, otherwise the results are undefined. (Normally the wide character
|
||||
is displayed, and the other character is not; do not depend on that behavior.)
|
||||
|
||||
Older terminal applications (especially on systems like Windows 8) lack support
|
||||
for advanced Unicode, and thus may not fare well.
|
||||
|
||||
## Colors
|
||||
|
||||
_Tcell_ assumes the ANSI/XTerm color model, including the 256 color map that
|
||||
XTerm uses when it supports 256 colors. The terminfo guidance will be
|
||||
honored, with respect to the number of colors supported. Also, only
|
||||
terminals which expose ANSI style `setaf` and `setab` will support color;
|
||||
if you have a color terminal that only has `setf` and `setb`, please submit
|
||||
a ticket.
|
||||
|
||||
## 24-bit Color
|
||||
|
||||
_Tcell_ _supports 24-bit color!_ (That is, if your terminal can support it.)
|
||||
|
||||
NOTE: Technically the approach of using 24-bit RGB values for color is more
|
||||
accurately described as "direct color", but most people use the term "true color".
|
||||
We follow the (inaccurate) common convention.
|
||||
|
||||
There are a few ways you can enable (or disable) true color.
|
||||
|
||||
- For many terminals, we can detect it automatically if your terminal
|
||||
includes the `RGB` or `Tc` capabilities (or rather it did when the database
|
||||
was updated.)
|
||||
|
||||
- You can force this one by setting the `COLORTERM` environment variable to
|
||||
`24-bit`, `truecolor` or `24bit`. This is the same method used
|
||||
by most other terminal applications that support 24-bit color.
|
||||
|
||||
- If you set your `TERM` environment variable to a value with the suffix `-truecolor`
|
||||
then 24-bit color compatible with XTerm and ECMA-48 will be assumed.
|
||||
(This feature is deprecated.
|
||||
It is recommended to use one of other methods listed above.)
|
||||
|
||||
- You can disable 24-bit color by setting `TCELL_TRUECOLOR=disable` in your
|
||||
environment.
|
||||
|
||||
When using TrueColor, programs will display the colors that the programmer
|
||||
intended, overriding any "`themes`" you may have set in your terminal
|
||||
emulator. (For some cases, accurate color fidelity is more important
|
||||
than respecting themes. For other cases, such as typical text apps that
|
||||
only use a few colors, its more desirable to respect the themes that
|
||||
the user has established.)
|
||||
|
||||
## Performance
|
||||
|
||||
Reasonable attempts have been made to minimize sending data to terminals,
|
||||
avoiding repeated sequences or drawing the same cell on refresh updates.
|
||||
|
||||
## Terminfo
|
||||
|
||||
(Not relevant for Windows users.)
|
||||
|
||||
The Terminfo implementation operates with a built-in database.
|
||||
This should satisfy most users. However, it can also (on systems
|
||||
with ncurses installed), dynamically parse the output from `infocmp`
|
||||
for terminals it does not already know about.
|
||||
|
||||
See the `terminfo/` directory for more information about generating
|
||||
new entries for the built-in database.
|
||||
|
||||
_Tcell_ requires that the terminal support the `cup` mode of cursor addressing.
|
||||
Ancient terminals without the ability to position the cursor directly
|
||||
are not supported.
|
||||
This is unlikely to be a problem; such terminals have not been mass-produced
|
||||
since the early 1970s.
|
||||
|
||||
## Mouse Support
|
||||
|
||||
Mouse support is detected via the `kmous` terminfo variable, however,
|
||||
enablement/disablement and decoding mouse events is done using hard coded
|
||||
sequences based on the XTerm X11 model. All popular
|
||||
terminals with mouse tracking support this model. (Full terminfo support
|
||||
is not possible as terminfo sequences are not defined.)
|
||||
|
||||
On Windows, the mouse works normally.
|
||||
|
||||
Mouse wheel buttons on various terminals are known to work, but the support
|
||||
in terminal emulators, as well as support for various buttons and
|
||||
live mouse tracking, varies widely.
|
||||
Modern _xterm_, macOS _Terminal_, and _iTerm_ all work well.
|
||||
|
||||
## Bracketed Paste
|
||||
|
||||
Terminals that appear to support the XTerm mouse model also can support
|
||||
bracketed paste, for applications that opt-in. See `EnablePaste()` for details.
|
||||
|
||||
## Testability
|
||||
|
||||
There is a `SimulationScreen`, that can be used to simulate a real screen
|
||||
for automated testing. The supplied tests do this. The simulation contains
|
||||
event delivery, screen resizing support, and capabilities to inject events
|
||||
and examine "`physical`" screen contents.
|
||||
|
||||
## Platforms
|
||||
|
||||
### POSIX (Linux, FreeBSD, macOS, Solaris, etc.)
|
||||
|
||||
Everything works using pure Go on mainstream platforms. Some more esoteric
|
||||
platforms (e.g., AIX) may need to be added. Pull requests are welcome!
|
||||
|
||||
### Windows
|
||||
|
||||
Windows console mode applications are supported.
|
||||
|
||||
Modern console applications like ConEmu and the Windows 10 terminal,
|
||||
support all the good features (resize, mouse tracking, etc.)
|
||||
|
||||
### WASM
|
||||
|
||||
WASM is supported, but needs additional setup detailed in [README-wasm](README-wasm.md).
|
||||
|
||||
### Plan9 and others
|
||||
|
||||
These platforms won't work, but compilation stubs are supplied
|
||||
for folks that want to include parts of this in software for those
|
||||
platforms. The Simulation screen works, but as _Tcell_ doesn't know how to
|
||||
allocate a real screen object on those platforms, `NewScreen()` will fail.
|
||||
|
||||
If anyone has wisdom about how to improve support for these,
|
||||
please let me know. PRs are especially welcome.
|
||||
|
||||
### Commercial Support
|
||||
|
||||
_Tcell_ is absolutely free, but if you want to obtain commercial, professional support, there are options.
|
||||
|
||||
- [TideLift](https://tidelift.com/) subscriptions include support for _Tcell_, as well as many other open source packages.
|
||||
- [Staysail Systems Inc.](mailto:info@staysail.tech) offers direct support, and custom development around _Tcell_ on an hourly basis.
|
||||
15
vendor/github.com/gdamore/tcell/v2/SECURITY.md
generated
vendored
Normal file
15
vendor/github.com/gdamore/tcell/v2/SECURITY.md
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# SECURITY
|
||||
|
||||
It's somewhat unlikely that tcell is in a security sensitive path,
|
||||
but we do take security seriously.
|
||||
|
||||
## Vulnerabilityu Response
|
||||
|
||||
If you report a vulnerability, we will respond within 2 business days.
|
||||
|
||||
## Report a Vulnerability
|
||||
|
||||
If you wish to report a vulnerability found in tcell, simply send a message
|
||||
to garrett@damore.org. You may also reach us on our discord channel -
|
||||
https://discord.gg/urTTxDN - a private message to `gdamore` on that channel
|
||||
may be submitted instead of mail.
|
||||
313
vendor/github.com/gdamore/tcell/v2/TUTORIAL.md
generated
vendored
Normal file
313
vendor/github.com/gdamore/tcell/v2/TUTORIAL.md
generated
vendored
Normal file
@@ -0,0 +1,313 @@
|
||||
# _Tcell_ Tutorial
|
||||
|
||||
_Tcell_ provides a low-level, portable API for building terminal-based programs.
|
||||
A [terminal emulator](https://en.wikipedia.org/wiki/Terminal_emulator)
|
||||
(or a real terminal such as a DEC VT-220) is used to interact with such a program.
|
||||
|
||||
_Tcell_'s interface is fairly low-level.
|
||||
While it provides a reasonably portable way of dealing with all the usual terminal
|
||||
features, it may be easier to utilize a higher level framework.
|
||||
A number of such frameworks are listed on the _Tcell_ main [README](README.md).
|
||||
|
||||
This tutorial provides the details of _Tcell_, and is appropriate for developers
|
||||
wishing to create their own application frameworks or needing more direct access
|
||||
to the terminal capabilities.
|
||||
|
||||
## Resize events
|
||||
|
||||
Applications receive an event of type `EventResize` when they are first initialized and each time the terminal is resized.
|
||||
The new size is available as `Size`.
|
||||
|
||||
```go
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
w, h := ev.Size()
|
||||
logMessage(fmt.Sprintf("Resized to %dx%d", w, h))
|
||||
}
|
||||
```
|
||||
|
||||
## Key events
|
||||
|
||||
When a key is pressed, applications receive an event of type `EventKey`.
|
||||
This event describes the modifier keys pressed (if any) and the pressed key or rune.
|
||||
|
||||
When a rune key is pressed, an event with its `Key` set to `KeyRune` is dispatched.
|
||||
|
||||
When a non-rune key is pressed, it is available as the `Key` of the event.
|
||||
|
||||
```go
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventKey:
|
||||
mod, key, ch := ev.Mod(), ev.Key(), ev.Rune()
|
||||
logMessage(fmt.Sprintf("EventKey Modifiers: %d Key: %d Rune: %d", mod, key, ch))
|
||||
}
|
||||
```
|
||||
|
||||
### Key event restrictions
|
||||
|
||||
Terminal-based programs have less visibility into keyboard activity than graphical applications.
|
||||
|
||||
When a key is pressed and held, additional key press events are sent by the terminal emulator.
|
||||
The rate of these repeated events depends on the emulator's configuration.
|
||||
Key release events are not available.
|
||||
|
||||
It is not possible to distinguish runes typed while holding shift and runes typed using caps lock.
|
||||
Capital letters are reported without the Shift modifier.
|
||||
|
||||
## Mouse events
|
||||
|
||||
Applications receive an event of type `EventMouse` when the mouse moves, or a mouse button is pressed or released.
|
||||
Mouse events are only delivered if
|
||||
`EnableMouse` has been called.
|
||||
|
||||
The mouse buttons being pressed (if any) are available as `Buttons`, and the position of the mouse is available as `Position`.
|
||||
|
||||
```go
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventMouse:
|
||||
mod := ev.Modifiers()
|
||||
btns := ev.Buttons()
|
||||
x, y := ev.Position()
|
||||
logMessage(fmt.Sprintf("EventMouse Modifiers: %d Buttons: %d Position: %d,%d", mod, btns, x, y))
|
||||
}
|
||||
```
|
||||
|
||||
### Mouse buttons
|
||||
|
||||
Identifier | Alias | Description
|
||||
-----------|-----------------|-----------
|
||||
Button1 | ButtonPrimary | Left button
|
||||
Button2 | ButtonSecondary | Right button
|
||||
Button3 | ButtonMiddle | Middle button
|
||||
Button4 | | Side button (thumb/next)
|
||||
Button5 | | Side button (thumb/prev)
|
||||
WheelUp | | Scroll wheel up
|
||||
WheelDown | | Scroll wheel down
|
||||
WheelLeft | | Horizontal wheel left
|
||||
WheelRight | | Horizontal wheel right
|
||||
|
||||
## Usage
|
||||
|
||||
To create a _Tcell_ application, first initialize a screen to hold it.
|
||||
|
||||
```go
|
||||
s, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
if err := s.Init(); err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
||||
// Set default text style
|
||||
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
|
||||
s.SetStyle(defStyle)
|
||||
|
||||
// Clear screen
|
||||
s.Clear()
|
||||
```
|
||||
|
||||
Text may be drawn on the screen using `SetContent`.
|
||||
|
||||
```go
|
||||
s.SetContent(0, 0, 'H', nil, defStyle)
|
||||
s.SetContent(1, 0, 'i', nil, defStyle)
|
||||
s.SetContent(2, 0, '!', nil, defStyle)
|
||||
```
|
||||
|
||||
To draw text more easily, define a render function.
|
||||
|
||||
```go
|
||||
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
|
||||
row := y1
|
||||
col := x1
|
||||
for _, r := range []rune(text) {
|
||||
s.SetContent(col, row, r, nil, style)
|
||||
col++
|
||||
if col >= x2 {
|
||||
row++
|
||||
col = x1
|
||||
}
|
||||
if row > y2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Lastly, define an event loop to handle user input and update application state.
|
||||
|
||||
```go
|
||||
quit := func() {
|
||||
s.Fini()
|
||||
os.Exit(0)
|
||||
}
|
||||
for {
|
||||
// Update screen
|
||||
s.Show()
|
||||
|
||||
// Poll event
|
||||
ev := s.PollEvent()
|
||||
|
||||
// Process event
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
s.Sync()
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
|
||||
quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Demo application
|
||||
|
||||
The following demonstrates how to initialize a screen, draw text/graphics and handle user input.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
|
||||
row := y1
|
||||
col := x1
|
||||
for _, r := range []rune(text) {
|
||||
s.SetContent(col, row, r, nil, style)
|
||||
col++
|
||||
if col >= x2 {
|
||||
row++
|
||||
col = x1
|
||||
}
|
||||
if row > y2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
|
||||
if y2 < y1 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
if x2 < x1 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
|
||||
// Fill background
|
||||
for row := y1; row <= y2; row++ {
|
||||
for col := x1; col <= x2; col++ {
|
||||
s.SetContent(col, row, ' ', nil, style)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw borders
|
||||
for col := x1; col <= x2; col++ {
|
||||
s.SetContent(col, y1, tcell.RuneHLine, nil, style)
|
||||
s.SetContent(col, y2, tcell.RuneHLine, nil, style)
|
||||
}
|
||||
for row := y1 + 1; row < y2; row++ {
|
||||
s.SetContent(x1, row, tcell.RuneVLine, nil, style)
|
||||
s.SetContent(x2, row, tcell.RuneVLine, nil, style)
|
||||
}
|
||||
|
||||
// Only draw corners if necessary
|
||||
if y1 != y2 && x1 != x2 {
|
||||
s.SetContent(x1, y1, tcell.RuneULCorner, nil, style)
|
||||
s.SetContent(x2, y1, tcell.RuneURCorner, nil, style)
|
||||
s.SetContent(x1, y2, tcell.RuneLLCorner, nil, style)
|
||||
s.SetContent(x2, y2, tcell.RuneLRCorner, nil, style)
|
||||
}
|
||||
|
||||
drawText(s, x1+1, y1+1, x2-1, y2-1, style, text)
|
||||
}
|
||||
|
||||
func main() {
|
||||
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
|
||||
boxStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorPurple)
|
||||
|
||||
// Initialize screen
|
||||
s, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
if err := s.Init(); err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
s.SetStyle(defStyle)
|
||||
s.EnableMouse()
|
||||
s.EnablePaste()
|
||||
s.Clear()
|
||||
|
||||
// Draw initial boxes
|
||||
drawBox(s, 1, 1, 42, 7, boxStyle, "Click and drag to draw a box")
|
||||
drawBox(s, 5, 9, 32, 14, boxStyle, "Press C to reset")
|
||||
|
||||
quit := func() {
|
||||
// You have to catch panics in a defer, clean up, and
|
||||
// re-raise them - otherwise your application can
|
||||
// die without leaving any diagnostic trace.
|
||||
maybePanic := recover()
|
||||
s.Fini()
|
||||
if maybePanic != nil {
|
||||
panic(maybePanic)
|
||||
}
|
||||
}
|
||||
defer quit()
|
||||
|
||||
// Here's how to get the screen size when you need it.
|
||||
// xmax, ymax := s.Size()
|
||||
|
||||
// Here's an example of how to inject a keystroke where it will
|
||||
// be picked up by the next PollEvent call. Note that the
|
||||
// queue is LIFO, it has a limited length, and PostEvent() can
|
||||
// return an error.
|
||||
// s.PostEvent(tcell.NewEventKey(tcell.KeyRune, rune('a'), 0))
|
||||
|
||||
// Event loop
|
||||
ox, oy := -1, -1
|
||||
for {
|
||||
// Update screen
|
||||
s.Show()
|
||||
|
||||
// Poll event
|
||||
ev := s.PollEvent()
|
||||
|
||||
// Process event
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
s.Sync()
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
|
||||
return
|
||||
} else if ev.Key() == tcell.KeyCtrlL {
|
||||
s.Sync()
|
||||
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
|
||||
s.Clear()
|
||||
}
|
||||
case *tcell.EventMouse:
|
||||
x, y := ev.Position()
|
||||
|
||||
switch ev.Buttons() {
|
||||
case tcell.Button1, tcell.Button2:
|
||||
if ox < 0 {
|
||||
ox, oy = x, y // record location when click started
|
||||
}
|
||||
|
||||
case tcell.ButtonNone:
|
||||
if ox >= 0 {
|
||||
label := fmt.Sprintf("%d,%d to %d,%d", ox, oy, x, y)
|
||||
drawBox(s, ox, oy, x, y, boxStyle, label)
|
||||
ox, oy = -1, -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
77
vendor/github.com/gdamore/tcell/v2/UKRAINE.md
generated
vendored
Normal file
77
vendor/github.com/gdamore/tcell/v2/UKRAINE.md
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
# Ukraine, Russia, and a World Tragedy
|
||||
|
||||
## A message to those inside Russia
|
||||
|
||||
### Written March 4, 2022.
|
||||
|
||||
It is with a very heavy heart that I write this. I am normally opposed to the use of open source
|
||||
projects to communicate political positions or advocate for things outside the immediate relevancy
|
||||
to that project.
|
||||
|
||||
However, the events occurring in Ukraine, and specifically the unprecedented invasion of Ukraine by
|
||||
Russian forces operating under orders from Russian President Vladimir Putin compel me to speak out.
|
||||
|
||||
Those who know me, know that I have family, friends, and colleagues in Russia, and Ukraine both. My closest friends
|
||||
have historically been Russian friends my wife's hometown of Chelyabinsk. I myself have in the past
|
||||
frequently traveled to Russia, and indeed operated a software development firm with offices in St. Petersburg.
|
||||
I had a special kinship with Russia and its people.
|
||||
|
||||
I say "had", because I fear that the actions of Putin, and the massive disinformation campaign that his regime
|
||||
has waged inside Russia, mean that it's likely that I won't see those friends again. At present, I'm not sure
|
||||
my wife will see her own mother again. We no longer feel it's safe for either of us to return Russia given
|
||||
actions taken by the regime to crack down on those who express disagreement.
|
||||
|
||||
Russian citizens are being led to believe it is acting purely defensively, and that only legitimate military
|
||||
targets are being targeted, and that all the information we have received in the West are fakes.
|
||||
|
||||
I am confident that nothing could be further from the truth.
|
||||
|
||||
This has caused many in Russia, including people whom I respect and believe to be smarter than this, to
|
||||
stand by Putin, and endorse his actions. The claim is that the entirety of NATO is operating at the behest
|
||||
of the USA, and that the entirety of Europe was poised to attack Russia. While this is clearly absurd to those
|
||||
of us with any understanding of western politics, Russian citizens are being fed this lie, and believing it.
|
||||
|
||||
If you're reading this from inside Russia -- YOU are the person that I hope this message reaches. Your
|
||||
government is LYING to you. Of course, all governments lie all the time. But consider this. Almost the
|
||||
entire world has condemned the invasion of Ukraine as criminal, and has applied sanctions. Even countries
|
||||
which have poor relations with the US sanctioning Russia, as well as nations which historically have remained
|
||||
neutral. (Famously neutral -- even during World War II, Switzerland has acted to apply sanctions in
|
||||
concert with the rest of the world.)
|
||||
|
||||
Ask yourself, why does Putin fear a free press so much, if what he says is true? Why the crack-downs on
|
||||
children expressing only a desire for peace with Ukraine? Why would the entire world unified against him,
|
||||
if Putin was in the right? Why would the only countries that stood with Russia against
|
||||
the UN resolution to condemn these acts as crimes be Belarus, North Korea, and Syria? Even countries normally
|
||||
allied to Russia could not bring themselves to do more than abstain from the vote to condemn it.
|
||||
|
||||
To be clear, I do not claim that the actions taken by the West or by the Ukrainian government were completely
|
||||
blameless. On the contrary, I understand that Western media is biased, and the truth is rarely exactly
|
||||
as reported. I believe that there is a kernel of truth in the claims of fascists and ultra-nationalist
|
||||
militias operating in Ukraine and specifically Donbas. However, I am also equally certain that Putin's
|
||||
response is out of proportion, and that concerns about such militias are principally just a pretext to justify
|
||||
an invasion.
|
||||
|
||||
Europe is at war, unlike we've seen in my lifetime. The world is more divided, and closer to nuclear holocaust
|
||||
than it has been since the Cold War. And that is 100% the fault of Putin.
|
||||
|
||||
While Putin remains in power, there cannot really be any way for Russian international relations to return
|
||||
to normal. Putin has set your country on a path to return to the Cold War, likely because he fancies himself
|
||||
to be a new Stalin. However, unlike the Soviet Union, the Russian economy does not have the wherewithal to
|
||||
stand on its own, and the invasion of Ukraine has fully ensured that Russia will not find any friends anywhere
|
||||
else in Europe, and probably few places in Asia.
|
||||
|
||||
The *only* paths forward for Russia are either a Russia without Putin (and those who would support his agenda),
|
||||
or a complete breakdown of Russian prosperity, likely followed by the increasing international conflict that will
|
||||
be the natural escalation from a country that is isolated and impoverished. Those of us observing from the West are
|
||||
gravely concerned, because we cannot see any end to this madness that does not result in nuclear conflict,
|
||||
unless from within.
|
||||
|
||||
In the meantime, the worst prices will be paid for by innocents in Ukraine, and by young Russian mean
|
||||
forced to carry out the orders of Putin's corrupt regime.
|
||||
|
||||
And *that* is why I write this -- to appeal to those within Russia to open your eyes, and think with
|
||||
your minds. It is right and proper to be proud of your country and its rich heritage. But it is also
|
||||
right and proper to look for ways to save it from the ruinous path that its current leadership has set it upon,
|
||||
and to recognize when that leadership is no longer acting in interest of the country or its people.
|
||||
|
||||
- Garrett D'Amore, March 4, 2022
|
||||
34
vendor/github.com/gdamore/tcell/v2/attr.go
generated
vendored
Normal file
34
vendor/github.com/gdamore/tcell/v2/attr.go
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2024 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
// AttrMask represents a mask of text attributes, apart from color.
|
||||
// Note that support for attributes may vary widely across terminals.
|
||||
type AttrMask uint
|
||||
|
||||
// Attributes are not colors, but affect the display of text. They can
|
||||
// be combined, in some cases, but not others. (E.g. you can have Dim Italic,
|
||||
// but only CurlyUnderline cannot be mixed with DottedUnderline.)
|
||||
const (
|
||||
AttrBold AttrMask = 1 << iota
|
||||
AttrBlink
|
||||
AttrReverse
|
||||
AttrUnderline // Deprecated: Use UnderlineStyle
|
||||
AttrDim
|
||||
AttrItalic
|
||||
AttrStrikeThrough
|
||||
AttrInvalid AttrMask = 1 << 31 // Mark the style or attributes invalid
|
||||
AttrNone AttrMask = 0 // Just normal text.
|
||||
)
|
||||
249
vendor/github.com/gdamore/tcell/v2/cell.go
generated
vendored
Normal file
249
vendor/github.com/gdamore/tcell/v2/cell.go
generated
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
// Copyright 2024 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
type cell struct {
|
||||
currMain rune
|
||||
currComb []rune
|
||||
currStyle Style
|
||||
lastMain rune
|
||||
lastStyle Style
|
||||
lastComb []rune
|
||||
width int
|
||||
lock bool
|
||||
}
|
||||
|
||||
// CellBuffer represents a two-dimensional array of character cells.
|
||||
// This is primarily intended for use by Screen implementors; it
|
||||
// contains much of the common code they need. To create one, just
|
||||
// declare a variable of its type; no explicit initialization is necessary.
|
||||
//
|
||||
// CellBuffer is not thread safe.
|
||||
type CellBuffer struct {
|
||||
w int
|
||||
h int
|
||||
cells []cell
|
||||
}
|
||||
|
||||
// SetContent sets the contents (primary rune, combining runes,
|
||||
// and style) for a cell at a given location. If the background or
|
||||
// foreground of the style is set to ColorNone, then the respective
|
||||
// color is left un changed.
|
||||
func (cb *CellBuffer) SetContent(x int, y int,
|
||||
mainc rune, combc []rune, style Style,
|
||||
) {
|
||||
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
|
||||
c := &cb.cells[(y*cb.w)+x]
|
||||
|
||||
// Wide characters: we want to mark the "wide" cells
|
||||
// dirty as well as the base cell, to make sure we consider
|
||||
// both cells as dirty together. We only need to do this
|
||||
// if we're changing content
|
||||
if (c.width > 0) && (mainc != c.currMain || len(combc) != len(c.currComb) || (len(combc) > 0 && !reflect.DeepEqual(combc, c.currComb))) {
|
||||
for i := 0; i < c.width; i++ {
|
||||
cb.SetDirty(x+i, y, true)
|
||||
}
|
||||
}
|
||||
|
||||
c.currComb = append([]rune{}, combc...)
|
||||
|
||||
if c.currMain != mainc {
|
||||
c.width = runewidth.RuneWidth(mainc)
|
||||
}
|
||||
c.currMain = mainc
|
||||
if style.fg == ColorNone {
|
||||
style.fg = c.currStyle.fg
|
||||
}
|
||||
if style.bg == ColorNone {
|
||||
style.bg = c.currStyle.bg
|
||||
}
|
||||
c.currStyle = style
|
||||
}
|
||||
}
|
||||
|
||||
// GetContent returns the contents of a character cell, including the
|
||||
// primary rune, any combining character runes (which will usually be
|
||||
// nil), the style, and the display width in cells. (The width can be
|
||||
// either 1, normally, or 2 for East Asian full-width characters.)
|
||||
func (cb *CellBuffer) GetContent(x, y int) (rune, []rune, Style, int) {
|
||||
var mainc rune
|
||||
var combc []rune
|
||||
var style Style
|
||||
var width int
|
||||
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
|
||||
c := &cb.cells[(y*cb.w)+x]
|
||||
mainc, combc, style = c.currMain, c.currComb, c.currStyle
|
||||
if width = c.width; width == 0 || mainc < ' ' {
|
||||
width = 1
|
||||
mainc = ' '
|
||||
}
|
||||
}
|
||||
return mainc, combc, style, width
|
||||
}
|
||||
|
||||
// Size returns the (width, height) in cells of the buffer.
|
||||
func (cb *CellBuffer) Size() (int, int) {
|
||||
return cb.w, cb.h
|
||||
}
|
||||
|
||||
// Invalidate marks all characters within the buffer as dirty.
|
||||
func (cb *CellBuffer) Invalidate() {
|
||||
for i := range cb.cells {
|
||||
cb.cells[i].lastMain = rune(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Dirty checks if a character at the given location needs to be
|
||||
// refreshed on the physical display. This returns true if the cell
|
||||
// content is different since the last time it was marked clean.
|
||||
func (cb *CellBuffer) Dirty(x, y int) bool {
|
||||
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
|
||||
c := &cb.cells[(y*cb.w)+x]
|
||||
if c.lock {
|
||||
return false
|
||||
}
|
||||
if c.lastMain == rune(0) {
|
||||
return true
|
||||
}
|
||||
if c.lastMain != c.currMain {
|
||||
return true
|
||||
}
|
||||
if c.lastStyle != c.currStyle {
|
||||
return true
|
||||
}
|
||||
if len(c.lastComb) != len(c.currComb) {
|
||||
return true
|
||||
}
|
||||
for i := range c.lastComb {
|
||||
if c.lastComb[i] != c.currComb[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDirty is normally used to indicate that a cell has
|
||||
// been displayed (in which case dirty is false), or to manually
|
||||
// force a cell to be marked dirty.
|
||||
func (cb *CellBuffer) SetDirty(x, y int, dirty bool) {
|
||||
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
|
||||
c := &cb.cells[(y*cb.w)+x]
|
||||
if dirty {
|
||||
c.lastMain = rune(0)
|
||||
} else {
|
||||
if c.currMain == rune(0) {
|
||||
c.currMain = ' '
|
||||
}
|
||||
c.lastMain = c.currMain
|
||||
c.lastComb = c.currComb
|
||||
c.lastStyle = c.currStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LockCell locks a cell from being drawn, effectively marking it "clean" until
|
||||
// the lock is removed. This can be used to prevent tcell from drawing a given
|
||||
// cell, even if the underlying content has changed. For example, when drawing a
|
||||
// sixel graphic directly to a TTY screen an implementer must lock the region
|
||||
// underneath the graphic to prevent tcell from drawing on top of the graphic.
|
||||
func (cb *CellBuffer) LockCell(x, y int) {
|
||||
if x < 0 || y < 0 {
|
||||
return
|
||||
}
|
||||
if x >= cb.w || y >= cb.h {
|
||||
return
|
||||
}
|
||||
c := &cb.cells[(y*cb.w)+x]
|
||||
c.lock = true
|
||||
}
|
||||
|
||||
// UnlockCell removes a lock from the cell and marks it as dirty
|
||||
func (cb *CellBuffer) UnlockCell(x, y int) {
|
||||
if x < 0 || y < 0 {
|
||||
return
|
||||
}
|
||||
if x >= cb.w || y >= cb.h {
|
||||
return
|
||||
}
|
||||
c := &cb.cells[(y*cb.w)+x]
|
||||
c.lock = false
|
||||
cb.SetDirty(x, y, true)
|
||||
}
|
||||
|
||||
// Resize is used to resize the cells array, with different dimensions,
|
||||
// while preserving the original contents. The cells will be invalidated
|
||||
// so that they can be redrawn.
|
||||
func (cb *CellBuffer) Resize(w, h int) {
|
||||
if cb.h == h && cb.w == w {
|
||||
return
|
||||
}
|
||||
|
||||
newc := make([]cell, w*h)
|
||||
for y := 0; y < h && y < cb.h; y++ {
|
||||
for x := 0; x < w && x < cb.w; x++ {
|
||||
oc := &cb.cells[(y*cb.w)+x]
|
||||
nc := &newc[(y*w)+x]
|
||||
nc.currMain = oc.currMain
|
||||
nc.currComb = oc.currComb
|
||||
nc.currStyle = oc.currStyle
|
||||
nc.width = oc.width
|
||||
nc.lastMain = rune(0)
|
||||
}
|
||||
}
|
||||
cb.cells = newc
|
||||
cb.h = h
|
||||
cb.w = w
|
||||
}
|
||||
|
||||
// Fill fills the entire cell buffer array with the specified character
|
||||
// and style. Normally choose ' ' to clear the screen. This API doesn't
|
||||
// support combining characters, or characters with a width larger than one.
|
||||
// If either the foreground or background are ColorNone, then the respective
|
||||
// color is unchanged.
|
||||
func (cb *CellBuffer) Fill(r rune, style Style) {
|
||||
for i := range cb.cells {
|
||||
c := &cb.cells[i]
|
||||
c.currMain = r
|
||||
c.currComb = nil
|
||||
cs := style
|
||||
if cs.fg == ColorNone {
|
||||
cs.fg = c.currStyle.fg
|
||||
}
|
||||
if cs.bg == ColorNone {
|
||||
cs.bg = c.currStyle.bg
|
||||
}
|
||||
c.currStyle = cs
|
||||
c.width = 1
|
||||
}
|
||||
}
|
||||
|
||||
var runeConfig *runewidth.Condition
|
||||
|
||||
func init() {
|
||||
// The defaults for the runewidth package are poorly chosen for terminal
|
||||
// applications. We however will honor the setting in the environment if
|
||||
// it is set.
|
||||
if os.Getenv("RUNEWIDTH_EASTASIAN") == "" {
|
||||
runewidth.DefaultCondition.EastAsianWidth = false
|
||||
}
|
||||
}
|
||||
22
vendor/github.com/gdamore/tcell/v2/charset_stub.go
generated
vendored
Normal file
22
vendor/github.com/gdamore/tcell/v2/charset_stub.go
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
//go:build plan9 || nacl
|
||||
// +build plan9 nacl
|
||||
|
||||
// Copyright 2015 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
func getCharset() string {
|
||||
return ""
|
||||
}
|
||||
50
vendor/github.com/gdamore/tcell/v2/charset_unix.go
generated
vendored
Normal file
50
vendor/github.com/gdamore/tcell/v2/charset_unix.go
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
//go:build !windows && !nacl && !plan9
|
||||
// +build !windows,!nacl,!plan9
|
||||
|
||||
// Copyright 2016 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getCharset() string {
|
||||
// Determine the character set. This can help us later.
|
||||
// Per POSIX, we search for LC_ALL first, then LC_CTYPE, and
|
||||
// finally LANG. First one set wins.
|
||||
locale := ""
|
||||
if locale = os.Getenv("LC_ALL"); locale == "" {
|
||||
if locale = os.Getenv("LC_CTYPE"); locale == "" {
|
||||
locale = os.Getenv("LANG")
|
||||
}
|
||||
}
|
||||
if locale == "POSIX" || locale == "C" {
|
||||
return "US-ASCII"
|
||||
}
|
||||
if i := strings.IndexRune(locale, '@'); i >= 0 {
|
||||
locale = locale[:i]
|
||||
}
|
||||
if i := strings.IndexRune(locale, '.'); i >= 0 {
|
||||
locale = locale[i+1:]
|
||||
} else {
|
||||
// Default assumption, and on Linux we can see LC_ALL
|
||||
// without a character set, which we assume implies UTF-8.
|
||||
return "UTF-8"
|
||||
}
|
||||
// XXX: add support for aliases
|
||||
return locale
|
||||
}
|
||||
22
vendor/github.com/gdamore/tcell/v2/charset_windows.go
generated
vendored
Normal file
22
vendor/github.com/gdamore/tcell/v2/charset_windows.go
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Copyright 2015 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
func getCharset() string {
|
||||
return "UTF-16"
|
||||
}
|
||||
1128
vendor/github.com/gdamore/tcell/v2/color.go
generated
vendored
Normal file
1128
vendor/github.com/gdamore/tcell/v2/color.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
53
vendor/github.com/gdamore/tcell/v2/colorfit.go
generated
vendored
Normal file
53
vendor/github.com/gdamore/tcell/v2/colorfit.go
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2016 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
// FindColor attempts to find a given color, or the best match possible for it,
|
||||
// from the palette given. This is an expensive operation, so results should
|
||||
// be cached by the caller.
|
||||
func FindColor(c Color, palette []Color) Color {
|
||||
match := ColorDefault
|
||||
dist := float64(0)
|
||||
r, g, b := c.RGB()
|
||||
c1 := colorful.Color{
|
||||
R: float64(r) / 255.0,
|
||||
G: float64(g) / 255.0,
|
||||
B: float64(b) / 255.0,
|
||||
}
|
||||
for _, d := range palette {
|
||||
r, g, b = d.RGB()
|
||||
c2 := colorful.Color{
|
||||
R: float64(r) / 255.0,
|
||||
G: float64(g) / 255.0,
|
||||
B: float64(b) / 255.0,
|
||||
}
|
||||
// CIE94 is more accurate, but really really expensive.
|
||||
nd := c1.DistanceCIE76(c2)
|
||||
if math.IsNaN(nd) {
|
||||
nd = math.Inf(1)
|
||||
}
|
||||
if match == ColorDefault || nd < dist {
|
||||
match = d
|
||||
dist = nd
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
24
vendor/github.com/gdamore/tcell/v2/console_stub.go
generated
vendored
Normal file
24
vendor/github.com/gdamore/tcell/v2/console_stub.go
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
// Copyright 2015 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
// NewConsoleScreen returns a console based screen. This platform
|
||||
// doesn't have support for any, so it returns nil and a suitable error.
|
||||
func NewConsoleScreen() (Screen, error) {
|
||||
return nil, ErrNoScreen
|
||||
}
|
||||
1398
vendor/github.com/gdamore/tcell/v2/console_win.go
generated
vendored
Normal file
1398
vendor/github.com/gdamore/tcell/v2/console_win.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
47
vendor/github.com/gdamore/tcell/v2/doc.go
generated
vendored
Normal file
47
vendor/github.com/gdamore/tcell/v2/doc.go
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2018 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package tcell provides a lower-level, portable API for building
|
||||
// programs that interact with terminals or consoles. It works with
|
||||
// both common (and many uncommon!) terminals or terminal emulators,
|
||||
// and Windows console implementations.
|
||||
//
|
||||
// It provides support for up to 256 colors, text attributes, and box drawing
|
||||
// elements. A database of terminals built from a real terminfo database
|
||||
// is provided, along with code to generate new database entries.
|
||||
//
|
||||
// Tcell offers very rich support for mice, dependent upon the terminal
|
||||
// of course. (Windows, XTerm, and iTerm 2 are known to work very well.)
|
||||
//
|
||||
// If the environment is not Unicode by default, such as an ISO8859 based
|
||||
// locale or GB18030, Tcell can convert input and output, so that your
|
||||
// terminal can operate in whatever locale is most convenient, while the
|
||||
// application program can just assume "everything is UTF-8". Reasonable
|
||||
// defaults are used for updating characters to something suitable for
|
||||
// display. Unicode box drawing characters will be converted to use the
|
||||
// alternate character set of your terminal, if native conversions are
|
||||
// not available. If no ACS is available, then some ASCII fallbacks will
|
||||
// be used.
|
||||
//
|
||||
// Note that support for non-UTF-8 locales (other than C) must be enabled
|
||||
// by the application using RegisterEncoding() -- we don't have them all
|
||||
// enabled by default to avoid bloating the application unnecessarily.
|
||||
// (These days UTF-8 is good enough for almost everyone, and nobody should
|
||||
// be using legacy locales anymore.) Also, actual glyphs for various code
|
||||
// point will only be displayed if your terminal or emulator (or the font
|
||||
// the emulator is using) supports them.
|
||||
//
|
||||
// A rich set of key codes is supported, with support for up to 65 function
|
||||
// keys, and various other special keys.
|
||||
package tcell
|
||||
140
vendor/github.com/gdamore/tcell/v2/encoding.go
generated
vendored
Normal file
140
vendor/github.com/gdamore/tcell/v2/encoding.go
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2022 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
|
||||
gencoding "github.com/gdamore/encoding"
|
||||
)
|
||||
|
||||
var encodings map[string]encoding.Encoding
|
||||
var encodingLk sync.Mutex
|
||||
var encodingFallback EncodingFallback = EncodingFallbackFail
|
||||
|
||||
// RegisterEncoding may be called by the application to register an encoding.
|
||||
// The presence of additional encodings will facilitate application usage with
|
||||
// terminal environments where the I/O subsystem does not support Unicode.
|
||||
//
|
||||
// Windows systems use Unicode natively, and do not need any of the encoding
|
||||
// subsystem when using Windows Console screens.
|
||||
//
|
||||
// Please see the Go documentation for golang.org/x/text/encoding -- most of
|
||||
// the common ones exist already as stock variables. For example, ISO8859-15
|
||||
// can be registered using the following code:
|
||||
//
|
||||
// import "golang.org/x/text/encoding/charmap"
|
||||
//
|
||||
// ...
|
||||
// RegisterEncoding("ISO8859-15", charmap.ISO8859_15)
|
||||
//
|
||||
// Aliases can be registered as well, for example "8859-15" could be an alias
|
||||
// for "ISO8859-15".
|
||||
//
|
||||
// For POSIX systems, this package will check the environment variables
|
||||
// LC_ALL, LC_CTYPE, and LANG (in that order) to determine the character set.
|
||||
// These are expected to have the following pattern:
|
||||
//
|
||||
// $language[.$codeset[@$variant]
|
||||
//
|
||||
// We extract only the $codeset part, which will usually be something like
|
||||
// UTF-8 or ISO8859-15 or KOI8-R. Note that if the locale is either "POSIX"
|
||||
// or "C", then we assume US-ASCII (the POSIX 'portable character set'
|
||||
// and assume all other characters are somehow invalid.)
|
||||
//
|
||||
// Modern POSIX systems and terminal emulators may use UTF-8, and for those
|
||||
// systems, this API is also unnecessary. For example, Darwin (MacOS X) and
|
||||
// modern Linux running modern xterm generally will out of the box without
|
||||
// any of this. Use of UTF-8 is recommended when possible, as it saves
|
||||
// quite a lot processing overhead.
|
||||
//
|
||||
// Note that some encodings are quite large (for example GB18030 which is a
|
||||
// superset of Unicode) and so the application size can be expected to
|
||||
// increase quite a bit as each encoding is added.
|
||||
|
||||
// The East Asian encodings have been seen to add 100-200K per encoding to the
|
||||
// size of the resulting binary.
|
||||
func RegisterEncoding(charset string, enc encoding.Encoding) {
|
||||
encodingLk.Lock()
|
||||
charset = strings.ToLower(charset)
|
||||
encodings[charset] = enc
|
||||
encodingLk.Unlock()
|
||||
}
|
||||
|
||||
// EncodingFallback describes how the system behaves when the locale
|
||||
// requires a character set that we do not support. The system always
|
||||
// supports UTF-8 and US-ASCII. On Windows consoles, UTF-16LE is also
|
||||
// supported automatically. Other character sets must be added using the
|
||||
// RegisterEncoding API. (A large group of nearly all of them can be
|
||||
// added using the RegisterAll function in the encoding sub package.)
|
||||
type EncodingFallback int
|
||||
|
||||
const (
|
||||
// EncodingFallbackFail behavior causes GetEncoding to fail
|
||||
// when it cannot find an encoding.
|
||||
EncodingFallbackFail = iota
|
||||
|
||||
// EncodingFallbackASCII behavior causes GetEncoding to fall back
|
||||
// to a 7-bit ASCII encoding, if no other encoding can be found.
|
||||
EncodingFallbackASCII
|
||||
|
||||
// EncodingFallbackUTF8 behavior causes GetEncoding to assume
|
||||
// UTF8 can pass unmodified upon failure. Note that this behavior
|
||||
// is not recommended, unless you are sure your terminal can cope
|
||||
// with real UTF8 sequences.
|
||||
EncodingFallbackUTF8
|
||||
)
|
||||
|
||||
// SetEncodingFallback changes the behavior of GetEncoding when a suitable
|
||||
// encoding is not found. The default is EncodingFallbackFail, which
|
||||
// causes GetEncoding to simply return nil.
|
||||
func SetEncodingFallback(fb EncodingFallback) {
|
||||
encodingLk.Lock()
|
||||
encodingFallback = fb
|
||||
encodingLk.Unlock()
|
||||
}
|
||||
|
||||
// GetEncoding is used by Screen implementors who want to locate an encoding
|
||||
// for the given character set name. Note that this will return nil for
|
||||
// either the Unicode (UTF-8) or ASCII encodings, since we don't use
|
||||
// encodings for them but instead have our own native methods.
|
||||
func GetEncoding(charset string) encoding.Encoding {
|
||||
charset = strings.ToLower(charset)
|
||||
encodingLk.Lock()
|
||||
defer encodingLk.Unlock()
|
||||
if enc, ok := encodings[charset]; ok {
|
||||
return enc
|
||||
}
|
||||
switch encodingFallback {
|
||||
case EncodingFallbackASCII:
|
||||
return gencoding.ASCII
|
||||
case EncodingFallbackUTF8:
|
||||
return encoding.Nop
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// We always support UTF-8 and ASCII.
|
||||
encodings = make(map[string]encoding.Encoding)
|
||||
encodings["utf-8"] = gencoding.UTF8
|
||||
encodings["utf8"] = gencoding.UTF8
|
||||
encodings["us-ascii"] = gencoding.ASCII
|
||||
encodings["ascii"] = gencoding.ASCII
|
||||
encodings["iso646"] = gencoding.ASCII
|
||||
}
|
||||
73
vendor/github.com/gdamore/tcell/v2/errors.go
generated
vendored
Normal file
73
vendor/github.com/gdamore/tcell/v2/errors.go
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2015 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2/terminfo"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrTermNotFound indicates that a suitable terminal entry could
|
||||
// not be found. This can result from either not having TERM set,
|
||||
// or from the TERM failing to support certain minimal functionality,
|
||||
// in particular absolute cursor addressability (the cup capability)
|
||||
// is required. For example, legacy "adm3" lacks this capability,
|
||||
// whereas the slightly newer "adm3a" supports it. This failure
|
||||
// occurs most often with "dumb".
|
||||
ErrTermNotFound = terminfo.ErrTermNotFound
|
||||
|
||||
// ErrNoScreen indicates that no suitable screen could be found.
|
||||
// This may result from attempting to run on a platform where there
|
||||
// is no support for either termios or console I/O (such as nacl),
|
||||
// or from running in an environment where there is no access to
|
||||
// a suitable console/terminal device. (For example, running on
|
||||
// without a controlling TTY or with no /dev/tty on POSIX platforms.)
|
||||
ErrNoScreen = errors.New("no suitable screen available")
|
||||
|
||||
// ErrNoCharset indicates that the locale environment the
|
||||
// program is not supported by the program, because no suitable
|
||||
// encoding was found for it. This problem never occurs if
|
||||
// the environment is UTF-8 or UTF-16.
|
||||
ErrNoCharset = errors.New("character set not supported")
|
||||
|
||||
// ErrEventQFull indicates that the event queue is full, and
|
||||
// cannot accept more events.
|
||||
ErrEventQFull = errors.New("event queue full")
|
||||
)
|
||||
|
||||
// An EventError is an event representing some sort of error, and carries
|
||||
// an error payload.
|
||||
type EventError struct {
|
||||
t time.Time
|
||||
err error
|
||||
}
|
||||
|
||||
// When returns the time when the event was created.
|
||||
func (ev *EventError) When() time.Time {
|
||||
return ev.t
|
||||
}
|
||||
|
||||
// Error implements the error.
|
||||
func (ev *EventError) Error() string {
|
||||
return ev.err.Error()
|
||||
}
|
||||
|
||||
// NewEventError creates an ErrorEvent with the given error payload.
|
||||
func NewEventError(err error) *EventError {
|
||||
return &EventError{t: time.Now(), err: err}
|
||||
}
|
||||
53
vendor/github.com/gdamore/tcell/v2/event.go
generated
vendored
Normal file
53
vendor/github.com/gdamore/tcell/v2/event.go
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2015 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Event is a generic interface used for passing around Events.
|
||||
// Concrete types follow.
|
||||
type Event interface {
|
||||
// When reports the time when the event was generated.
|
||||
When() time.Time
|
||||
}
|
||||
|
||||
// EventTime is a simple base event class, suitable for easy reuse.
|
||||
// It can be used to deliver actual timer events as well.
|
||||
type EventTime struct {
|
||||
when time.Time
|
||||
}
|
||||
|
||||
// When returns the time stamp when the event occurred.
|
||||
func (e *EventTime) When() time.Time {
|
||||
return e.when
|
||||
}
|
||||
|
||||
// SetEventTime sets the time of occurrence for the event.
|
||||
func (e *EventTime) SetEventTime(t time.Time) {
|
||||
e.when = t
|
||||
}
|
||||
|
||||
// SetEventNow sets the time of occurrence for the event to the current time.
|
||||
func (e *EventTime) SetEventNow() {
|
||||
e.SetEventTime(time.Now())
|
||||
}
|
||||
|
||||
// EventHandler is anything that handles events. If the handler has
|
||||
// consumed the event, it should return true. False otherwise.
|
||||
type EventHandler interface {
|
||||
HandleEvent(Event) bool
|
||||
}
|
||||
28
vendor/github.com/gdamore/tcell/v2/focus.go
generated
vendored
Normal file
28
vendor/github.com/gdamore/tcell/v2/focus.go
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright 2023 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
// EventFocus is a focus event. It is sent when the terminal window (or tab)
|
||||
// gets or loses focus.
|
||||
type EventFocus struct {
|
||||
*EventTime
|
||||
|
||||
// True if the window received focus, false if it lost focus
|
||||
Focused bool
|
||||
}
|
||||
|
||||
func NewEventFocus(focused bool) *EventFocus {
|
||||
return &EventFocus{Focused: focused}
|
||||
}
|
||||
41
vendor/github.com/gdamore/tcell/v2/interrupt.go
generated
vendored
Normal file
41
vendor/github.com/gdamore/tcell/v2/interrupt.go
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2015 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventInterrupt is a generic wakeup event. Its can be used to
|
||||
// to request a redraw. It can carry an arbitrary payload, as well.
|
||||
type EventInterrupt struct {
|
||||
t time.Time
|
||||
v interface{}
|
||||
}
|
||||
|
||||
// When returns the time when this event was created.
|
||||
func (ev *EventInterrupt) When() time.Time {
|
||||
return ev.t
|
||||
}
|
||||
|
||||
// Data is used to obtain the opaque event payload.
|
||||
func (ev *EventInterrupt) Data() interface{} {
|
||||
return ev.v
|
||||
}
|
||||
|
||||
// NewEventInterrupt creates an EventInterrupt with the given payload.
|
||||
func NewEventInterrupt(data interface{}) *EventInterrupt {
|
||||
return &EventInterrupt{t: time.Now(), v: data}
|
||||
}
|
||||
470
vendor/github.com/gdamore/tcell/v2/key.go
generated
vendored
Normal file
470
vendor/github.com/gdamore/tcell/v2/key.go
generated
vendored
Normal file
@@ -0,0 +1,470 @@
|
||||
// Copyright 2016 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventKey represents a key press. Usually this is a key press followed
|
||||
// by a key release, but since terminal programs don't have a way to report
|
||||
// key release events, we usually get just one event. If a key is held down
|
||||
// then the terminal may synthesize repeated key presses at some predefined
|
||||
// rate. We have no control over that, nor visibility into it.
|
||||
//
|
||||
// In some cases, we can have a modifier key, such as ModAlt, that can be
|
||||
// generated with a key press. (This usually is represented by having the
|
||||
// high bit set, or in some cases, by sending an ESC prior to the rune.)
|
||||
//
|
||||
// If the value of Key() is KeyRune, then the actual key value will be
|
||||
// available with the Rune() method. This will be the case for most keys.
|
||||
// In most situations, the modifiers will not be set. For example, if the
|
||||
// rune is 'A', this will be reported without the ModShift bit set, since
|
||||
// really can't tell if the Shift key was pressed (it might have been CAPSLOCK,
|
||||
// or a terminal that only can send capitals, or keyboard with separate
|
||||
// capital letters from lower case letters).
|
||||
//
|
||||
// Generally, terminal applications have far less visibility into keyboard
|
||||
// activity than graphical applications. Hence, they should avoid depending
|
||||
// overly much on availability of modifiers, or the availability of any
|
||||
// specific keys.
|
||||
type EventKey struct {
|
||||
t time.Time
|
||||
mod ModMask
|
||||
key Key
|
||||
ch rune
|
||||
}
|
||||
|
||||
// When returns the time when this Event was created, which should closely
|
||||
// match the time when the key was pressed.
|
||||
func (ev *EventKey) When() time.Time {
|
||||
return ev.t
|
||||
}
|
||||
|
||||
// Rune returns the rune corresponding to the key press, if it makes sense.
|
||||
// The result is only defined if the value of Key() is KeyRune.
|
||||
func (ev *EventKey) Rune() rune {
|
||||
return ev.ch
|
||||
}
|
||||
|
||||
// Key returns a virtual key code. We use this to identify specific key
|
||||
// codes, such as KeyEnter, etc. Most control and function keys are reported
|
||||
// with unique Key values. Normal alphanumeric and punctuation keys will
|
||||
// generally return KeyRune here; the specific key can be further decoded
|
||||
// using the Rune() function.
|
||||
func (ev *EventKey) Key() Key {
|
||||
return ev.key
|
||||
}
|
||||
|
||||
// Modifiers returns the modifiers that were present with the key press. Note
|
||||
// that not all platforms and terminals support this equally well, and some
|
||||
// cases we will not not know for sure. Hence, applications should avoid
|
||||
// using this in most circumstances.
|
||||
func (ev *EventKey) Modifiers() ModMask {
|
||||
return ev.mod
|
||||
}
|
||||
|
||||
// KeyNames holds the written names of special keys. Useful to echo back a key
|
||||
// name, or to look up a key from a string value.
|
||||
var KeyNames = map[Key]string{
|
||||
KeyEnter: "Enter",
|
||||
KeyBackspace: "Backspace",
|
||||
KeyTab: "Tab",
|
||||
KeyBacktab: "Backtab",
|
||||
KeyEsc: "Esc",
|
||||
KeyBackspace2: "Backspace2",
|
||||
KeyDelete: "Delete",
|
||||
KeyInsert: "Insert",
|
||||
KeyUp: "Up",
|
||||
KeyDown: "Down",
|
||||
KeyLeft: "Left",
|
||||
KeyRight: "Right",
|
||||
KeyHome: "Home",
|
||||
KeyEnd: "End",
|
||||
KeyUpLeft: "UpLeft",
|
||||
KeyUpRight: "UpRight",
|
||||
KeyDownLeft: "DownLeft",
|
||||
KeyDownRight: "DownRight",
|
||||
KeyCenter: "Center",
|
||||
KeyPgDn: "PgDn",
|
||||
KeyPgUp: "PgUp",
|
||||
KeyClear: "Clear",
|
||||
KeyExit: "Exit",
|
||||
KeyCancel: "Cancel",
|
||||
KeyPause: "Pause",
|
||||
KeyPrint: "Print",
|
||||
KeyF1: "F1",
|
||||
KeyF2: "F2",
|
||||
KeyF3: "F3",
|
||||
KeyF4: "F4",
|
||||
KeyF5: "F5",
|
||||
KeyF6: "F6",
|
||||
KeyF7: "F7",
|
||||
KeyF8: "F8",
|
||||
KeyF9: "F9",
|
||||
KeyF10: "F10",
|
||||
KeyF11: "F11",
|
||||
KeyF12: "F12",
|
||||
KeyF13: "F13",
|
||||
KeyF14: "F14",
|
||||
KeyF15: "F15",
|
||||
KeyF16: "F16",
|
||||
KeyF17: "F17",
|
||||
KeyF18: "F18",
|
||||
KeyF19: "F19",
|
||||
KeyF20: "F20",
|
||||
KeyF21: "F21",
|
||||
KeyF22: "F22",
|
||||
KeyF23: "F23",
|
||||
KeyF24: "F24",
|
||||
KeyF25: "F25",
|
||||
KeyF26: "F26",
|
||||
KeyF27: "F27",
|
||||
KeyF28: "F28",
|
||||
KeyF29: "F29",
|
||||
KeyF30: "F30",
|
||||
KeyF31: "F31",
|
||||
KeyF32: "F32",
|
||||
KeyF33: "F33",
|
||||
KeyF34: "F34",
|
||||
KeyF35: "F35",
|
||||
KeyF36: "F36",
|
||||
KeyF37: "F37",
|
||||
KeyF38: "F38",
|
||||
KeyF39: "F39",
|
||||
KeyF40: "F40",
|
||||
KeyF41: "F41",
|
||||
KeyF42: "F42",
|
||||
KeyF43: "F43",
|
||||
KeyF44: "F44",
|
||||
KeyF45: "F45",
|
||||
KeyF46: "F46",
|
||||
KeyF47: "F47",
|
||||
KeyF48: "F48",
|
||||
KeyF49: "F49",
|
||||
KeyF50: "F50",
|
||||
KeyF51: "F51",
|
||||
KeyF52: "F52",
|
||||
KeyF53: "F53",
|
||||
KeyF54: "F54",
|
||||
KeyF55: "F55",
|
||||
KeyF56: "F56",
|
||||
KeyF57: "F57",
|
||||
KeyF58: "F58",
|
||||
KeyF59: "F59",
|
||||
KeyF60: "F60",
|
||||
KeyF61: "F61",
|
||||
KeyF62: "F62",
|
||||
KeyF63: "F63",
|
||||
KeyF64: "F64",
|
||||
KeyCtrlA: "Ctrl-A",
|
||||
KeyCtrlB: "Ctrl-B",
|
||||
KeyCtrlC: "Ctrl-C",
|
||||
KeyCtrlD: "Ctrl-D",
|
||||
KeyCtrlE: "Ctrl-E",
|
||||
KeyCtrlF: "Ctrl-F",
|
||||
KeyCtrlG: "Ctrl-G",
|
||||
KeyCtrlJ: "Ctrl-J",
|
||||
KeyCtrlK: "Ctrl-K",
|
||||
KeyCtrlL: "Ctrl-L",
|
||||
KeyCtrlN: "Ctrl-N",
|
||||
KeyCtrlO: "Ctrl-O",
|
||||
KeyCtrlP: "Ctrl-P",
|
||||
KeyCtrlQ: "Ctrl-Q",
|
||||
KeyCtrlR: "Ctrl-R",
|
||||
KeyCtrlS: "Ctrl-S",
|
||||
KeyCtrlT: "Ctrl-T",
|
||||
KeyCtrlU: "Ctrl-U",
|
||||
KeyCtrlV: "Ctrl-V",
|
||||
KeyCtrlW: "Ctrl-W",
|
||||
KeyCtrlX: "Ctrl-X",
|
||||
KeyCtrlY: "Ctrl-Y",
|
||||
KeyCtrlZ: "Ctrl-Z",
|
||||
KeyCtrlSpace: "Ctrl-Space",
|
||||
KeyCtrlUnderscore: "Ctrl-_",
|
||||
KeyCtrlRightSq: "Ctrl-]",
|
||||
KeyCtrlBackslash: "Ctrl-\\",
|
||||
KeyCtrlCarat: "Ctrl-^",
|
||||
}
|
||||
|
||||
// Name returns a printable value or the key stroke. This can be used
|
||||
// when printing the event, for example.
|
||||
func (ev *EventKey) Name() string {
|
||||
s := ""
|
||||
m := []string{}
|
||||
if ev.mod&ModShift != 0 {
|
||||
m = append(m, "Shift")
|
||||
}
|
||||
if ev.mod&ModAlt != 0 {
|
||||
m = append(m, "Alt")
|
||||
}
|
||||
if ev.mod&ModMeta != 0 {
|
||||
m = append(m, "Meta")
|
||||
}
|
||||
if ev.mod&ModCtrl != 0 {
|
||||
m = append(m, "Ctrl")
|
||||
}
|
||||
|
||||
ok := false
|
||||
if s, ok = KeyNames[ev.key]; !ok {
|
||||
if ev.key == KeyRune {
|
||||
s = "Rune[" + string(ev.ch) + "]"
|
||||
} else {
|
||||
s = fmt.Sprintf("Key[%d,%d]", ev.key, int(ev.ch))
|
||||
}
|
||||
}
|
||||
if len(m) != 0 {
|
||||
if ev.mod&ModCtrl != 0 && strings.HasPrefix(s, "Ctrl-") {
|
||||
s = s[5:]
|
||||
}
|
||||
return fmt.Sprintf("%s+%s", strings.Join(m, "+"), s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// NewEventKey attempts to create a suitable event. It parses the various
|
||||
// ASCII control sequences if KeyRune is passed for Key, but if the caller
|
||||
// has more precise information it should set that specifically. Callers
|
||||
// that aren't sure about modifier state (most) should just pass ModNone.
|
||||
func NewEventKey(k Key, ch rune, mod ModMask) *EventKey {
|
||||
if k == KeyRune && (ch < ' ' || ch == 0x7f) {
|
||||
// Turn specials into proper key codes. This is for
|
||||
// control characters and the DEL.
|
||||
k = Key(ch)
|
||||
if mod == ModNone && ch < ' ' {
|
||||
switch Key(ch) {
|
||||
case KeyBackspace, KeyTab, KeyEsc, KeyEnter:
|
||||
// these keys are directly typeable without CTRL
|
||||
default:
|
||||
// most likely entered with a CTRL keypress
|
||||
mod = ModCtrl
|
||||
}
|
||||
}
|
||||
}
|
||||
return &EventKey{t: time.Now(), key: k, ch: ch, mod: mod}
|
||||
}
|
||||
|
||||
// ModMask is a mask of modifier keys. Note that it will not always be
|
||||
// possible to report modifier keys.
|
||||
type ModMask int16
|
||||
|
||||
// These are the modifiers keys that can be sent either with a key press,
|
||||
// or a mouse event. Note that as of now, due to the confusion associated
|
||||
// with Meta, and the lack of support for it on many/most platforms, the
|
||||
// current implementations never use it. Instead, they use ModAlt, even for
|
||||
// events that could possibly have been distinguished from ModAlt.
|
||||
const (
|
||||
ModShift ModMask = 1 << iota
|
||||
ModCtrl
|
||||
ModAlt
|
||||
ModMeta
|
||||
ModNone ModMask = 0
|
||||
)
|
||||
|
||||
// Key is a generic value for representing keys, and especially special
|
||||
// keys (function keys, cursor movement keys, etc.) For normal keys, like
|
||||
// ASCII letters, we use KeyRune, and then expect the application to
|
||||
// inspect the Rune() member of the EventKey.
|
||||
type Key int16
|
||||
|
||||
// This is the list of named keys. KeyRune is special however, in that it is
|
||||
// a place holder key indicating that a printable character was sent. The
|
||||
// actual value of the rune will be transported in the Rune of the associated
|
||||
// EventKey.
|
||||
const (
|
||||
KeyRune Key = iota + 256
|
||||
KeyUp
|
||||
KeyDown
|
||||
KeyRight
|
||||
KeyLeft
|
||||
KeyUpLeft
|
||||
KeyUpRight
|
||||
KeyDownLeft
|
||||
KeyDownRight
|
||||
KeyCenter
|
||||
KeyPgUp
|
||||
KeyPgDn
|
||||
KeyHome
|
||||
KeyEnd
|
||||
KeyInsert
|
||||
KeyDelete
|
||||
KeyHelp
|
||||
KeyExit
|
||||
KeyClear
|
||||
KeyCancel
|
||||
KeyPrint
|
||||
KeyPause
|
||||
KeyBacktab
|
||||
KeyF1
|
||||
KeyF2
|
||||
KeyF3
|
||||
KeyF4
|
||||
KeyF5
|
||||
KeyF6
|
||||
KeyF7
|
||||
KeyF8
|
||||
KeyF9
|
||||
KeyF10
|
||||
KeyF11
|
||||
KeyF12
|
||||
KeyF13
|
||||
KeyF14
|
||||
KeyF15
|
||||
KeyF16
|
||||
KeyF17
|
||||
KeyF18
|
||||
KeyF19
|
||||
KeyF20
|
||||
KeyF21
|
||||
KeyF22
|
||||
KeyF23
|
||||
KeyF24
|
||||
KeyF25
|
||||
KeyF26
|
||||
KeyF27
|
||||
KeyF28
|
||||
KeyF29
|
||||
KeyF30
|
||||
KeyF31
|
||||
KeyF32
|
||||
KeyF33
|
||||
KeyF34
|
||||
KeyF35
|
||||
KeyF36
|
||||
KeyF37
|
||||
KeyF38
|
||||
KeyF39
|
||||
KeyF40
|
||||
KeyF41
|
||||
KeyF42
|
||||
KeyF43
|
||||
KeyF44
|
||||
KeyF45
|
||||
KeyF46
|
||||
KeyF47
|
||||
KeyF48
|
||||
KeyF49
|
||||
KeyF50
|
||||
KeyF51
|
||||
KeyF52
|
||||
KeyF53
|
||||
KeyF54
|
||||
KeyF55
|
||||
KeyF56
|
||||
KeyF57
|
||||
KeyF58
|
||||
KeyF59
|
||||
KeyF60
|
||||
KeyF61
|
||||
KeyF62
|
||||
KeyF63
|
||||
KeyF64
|
||||
)
|
||||
|
||||
const (
|
||||
// These key codes are used internally, and will never appear to applications.
|
||||
keyPasteStart Key = iota + 16384
|
||||
keyPasteEnd
|
||||
)
|
||||
|
||||
// These are the control keys. Note that they overlap with other keys,
|
||||
// perhaps. For example, KeyCtrlH is the same as KeyBackspace.
|
||||
const (
|
||||
KeyCtrlSpace Key = iota
|
||||
KeyCtrlA
|
||||
KeyCtrlB
|
||||
KeyCtrlC
|
||||
KeyCtrlD
|
||||
KeyCtrlE
|
||||
KeyCtrlF
|
||||
KeyCtrlG
|
||||
KeyCtrlH
|
||||
KeyCtrlI
|
||||
KeyCtrlJ
|
||||
KeyCtrlK
|
||||
KeyCtrlL
|
||||
KeyCtrlM
|
||||
KeyCtrlN
|
||||
KeyCtrlO
|
||||
KeyCtrlP
|
||||
KeyCtrlQ
|
||||
KeyCtrlR
|
||||
KeyCtrlS
|
||||
KeyCtrlT
|
||||
KeyCtrlU
|
||||
KeyCtrlV
|
||||
KeyCtrlW
|
||||
KeyCtrlX
|
||||
KeyCtrlY
|
||||
KeyCtrlZ
|
||||
KeyCtrlLeftSq // Escape
|
||||
KeyCtrlBackslash
|
||||
KeyCtrlRightSq
|
||||
KeyCtrlCarat
|
||||
KeyCtrlUnderscore
|
||||
)
|
||||
|
||||
// Special values - these are fixed in an attempt to make it more likely
|
||||
// that aliases will encode the same way.
|
||||
|
||||
// These are the defined ASCII values for key codes. They generally match
|
||||
// with KeyCtrl values.
|
||||
const (
|
||||
KeyNUL Key = iota
|
||||
KeySOH
|
||||
KeySTX
|
||||
KeyETX
|
||||
KeyEOT
|
||||
KeyENQ
|
||||
KeyACK
|
||||
KeyBEL
|
||||
KeyBS
|
||||
KeyTAB
|
||||
KeyLF
|
||||
KeyVT
|
||||
KeyFF
|
||||
KeyCR
|
||||
KeySO
|
||||
KeySI
|
||||
KeyDLE
|
||||
KeyDC1
|
||||
KeyDC2
|
||||
KeyDC3
|
||||
KeyDC4
|
||||
KeyNAK
|
||||
KeySYN
|
||||
KeyETB
|
||||
KeyCAN
|
||||
KeyEM
|
||||
KeySUB
|
||||
KeyESC
|
||||
KeyFS
|
||||
KeyGS
|
||||
KeyRS
|
||||
KeyUS
|
||||
KeyDEL Key = 0x7F
|
||||
)
|
||||
|
||||
// These keys are aliases for other names.
|
||||
const (
|
||||
KeyBackspace = KeyBS
|
||||
KeyTab = KeyTAB
|
||||
KeyEsc = KeyESC
|
||||
KeyEscape = KeyESC
|
||||
KeyEnter = KeyCR
|
||||
KeyBackspace2 = KeyDEL
|
||||
)
|
||||
103
vendor/github.com/gdamore/tcell/v2/mouse.go
generated
vendored
Normal file
103
vendor/github.com/gdamore/tcell/v2/mouse.go
generated
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2020 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventMouse is a mouse event. It is sent on either mouse up or mouse down
|
||||
// events. It is also sent on mouse motion events - if the terminal supports
|
||||
// it. We make every effort to ensure that mouse release events are delivered.
|
||||
// Hence, click drag can be identified by a motion event with the mouse down,
|
||||
// without any intervening button release. On some terminals only the initiating
|
||||
// press and terminating release event will be delivered.
|
||||
//
|
||||
// Mouse wheel events, when reported, may appear on their own as individual
|
||||
// impulses; that is, there will normally not be a release event delivered
|
||||
// for mouse wheel movements.
|
||||
//
|
||||
// Most terminals cannot report the state of more than one button at a time --
|
||||
// and some cannot report motion events unless a button is pressed.
|
||||
//
|
||||
// Applications can inspect the time between events to resolve double or
|
||||
// triple clicks.
|
||||
type EventMouse struct {
|
||||
t time.Time
|
||||
btn ButtonMask
|
||||
mod ModMask
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
// When returns the time when this EventMouse was created.
|
||||
func (ev *EventMouse) When() time.Time {
|
||||
return ev.t
|
||||
}
|
||||
|
||||
// Buttons returns the list of buttons that were pressed or wheel motions.
|
||||
func (ev *EventMouse) Buttons() ButtonMask {
|
||||
return ev.btn
|
||||
}
|
||||
|
||||
// Modifiers returns a list of keyboard modifiers that were pressed
|
||||
// with the mouse button(s).
|
||||
func (ev *EventMouse) Modifiers() ModMask {
|
||||
return ev.mod
|
||||
}
|
||||
|
||||
// Position returns the mouse position in character cells. The origin
|
||||
// 0, 0 is at the upper left corner.
|
||||
func (ev *EventMouse) Position() (int, int) {
|
||||
return ev.x, ev.y
|
||||
}
|
||||
|
||||
// NewEventMouse is used to create a new mouse event. Applications
|
||||
// shouldn't need to use this; its mostly for screen implementors.
|
||||
func NewEventMouse(x, y int, btn ButtonMask, mod ModMask) *EventMouse {
|
||||
return &EventMouse{t: time.Now(), x: x, y: y, btn: btn, mod: mod}
|
||||
}
|
||||
|
||||
// ButtonMask is a mask of mouse buttons and wheel events. Mouse button presses
|
||||
// are normally delivered as both press and release events. Mouse wheel events
|
||||
// are normally just single impulse events. Windows supports up to eight
|
||||
// separate buttons plus all four wheel directions, but XTerm can only support
|
||||
// mouse buttons 1-3 and wheel up/down. Its not unheard of for terminals
|
||||
// to support only one or two buttons (think Macs). Old terminals, and true
|
||||
// emulations (such as vt100) won't support mice at all, of course.
|
||||
type ButtonMask int16
|
||||
|
||||
// These are the actual button values. Note that tcell version 1.x reversed buttons
|
||||
// two and three on *nix based terminals. We use button 1 as the primary, and
|
||||
// button 2 as the secondary, and button 3 (which is often missing) as the middle.
|
||||
const (
|
||||
Button1 ButtonMask = 1 << iota // Usually the left (primary) mouse button.
|
||||
Button2 // Usually the right (secondary) mouse button.
|
||||
Button3 // Usually the middle mouse button.
|
||||
Button4 // Often a side button (thumb/next).
|
||||
Button5 // Often a side button (thumb/prev).
|
||||
Button6
|
||||
Button7
|
||||
Button8
|
||||
WheelUp // Wheel motion up/away from user.
|
||||
WheelDown // Wheel motion down/towards user.
|
||||
WheelLeft // Wheel motion to left.
|
||||
WheelRight // Wheel motion to right.
|
||||
ButtonNone ButtonMask = 0 // No button or wheel events.
|
||||
|
||||
ButtonPrimary = Button1
|
||||
ButtonSecondary = Button2
|
||||
ButtonMiddle = Button3
|
||||
)
|
||||
43
vendor/github.com/gdamore/tcell/v2/nonblock_bsd.go
generated
vendored
Normal file
43
vendor/github.com/gdamore/tcell/v2/nonblock_bsd.go
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2021 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
|
||||
// +build darwin dragonfly freebsd netbsd openbsd
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// BSD systems use TIOC style ioctls.
|
||||
|
||||
// tcSetBufParams is used by the tty driver on UNIX systems to configure the
|
||||
// buffering parameters (minimum character count and minimum wait time in msec.)
|
||||
// This also waits for output to drain first.
|
||||
func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {
|
||||
_ = syscall.SetNonblock(fd, true)
|
||||
tio, err := unix.IoctlGetTermios(fd, unix.TIOCGETA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tio.Cc[unix.VMIN] = vMin
|
||||
tio.Cc[unix.VTIME] = vTime
|
||||
if err = unix.IoctlSetTermios(fd, unix.TIOCSETAW, tio); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
41
vendor/github.com/gdamore/tcell/v2/nonblock_unix.go
generated
vendored
Normal file
41
vendor/github.com/gdamore/tcell/v2/nonblock_unix.go
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2021 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build linux || aix || zos || solaris
|
||||
// +build linux aix zos solaris
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// tcSetBufParams is used by the tty driver on UNIX systems to configure the
|
||||
// buffering parameters (minimum character count and minimum wait time in msec.)
|
||||
// This also waits for output to drain first.
|
||||
func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {
|
||||
_ = syscall.SetNonblock(fd, true)
|
||||
tio, err := unix.IoctlGetTermios(fd, unix.TCGETS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tio.Cc[unix.VMIN] = vMin
|
||||
tio.Cc[unix.VTIME] = vTime
|
||||
if err = unix.IoctlSetTermios(fd, unix.TCSETSW, tio); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
72
vendor/github.com/gdamore/tcell/v2/paste.go
generated
vendored
Normal file
72
vendor/github.com/gdamore/tcell/v2/paste.go
generated
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright 2024 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventPaste is used to mark the start and end of a bracketed paste.
|
||||
//
|
||||
// An event with .Start() true will be sent to mark the start of a bracketed paste,
|
||||
// followed by a number of keys (string data) for the content, ending with the
|
||||
// an event with .End() true.
|
||||
type EventPaste struct {
|
||||
start bool
|
||||
t time.Time
|
||||
data []byte
|
||||
}
|
||||
|
||||
// When returns the time when this EventPaste was created.
|
||||
func (ev *EventPaste) When() time.Time {
|
||||
return ev.t
|
||||
}
|
||||
|
||||
// Start returns true if this is the start of a paste.
|
||||
func (ev *EventPaste) Start() bool {
|
||||
return ev.start
|
||||
}
|
||||
|
||||
// End returns true if this is the end of a paste.
|
||||
func (ev *EventPaste) End() bool {
|
||||
return !ev.start
|
||||
}
|
||||
|
||||
// NewEventPaste returns a new EventPaste.
|
||||
func NewEventPaste(start bool) *EventPaste {
|
||||
return &EventPaste{t: time.Now(), start: start}
|
||||
}
|
||||
|
||||
// NewEventClipboard returns a new NewEventClipboard with a data payload
|
||||
func NewEventClipboard(data []byte) *EventClipboard {
|
||||
return &EventClipboard{t: time.Now(), data: data}
|
||||
}
|
||||
|
||||
// EventClipboard represents data from the clipboard,
|
||||
// in response to a GetClipboard request.
|
||||
type EventClipboard struct {
|
||||
t time.Time
|
||||
data []byte
|
||||
}
|
||||
|
||||
// Data returns the attached binary data.
|
||||
func (ev *EventClipboard) Data() []byte {
|
||||
return ev.data
|
||||
}
|
||||
|
||||
// When returns the time when this event was created.
|
||||
func (ev *EventClipboard) When() time.Time {
|
||||
return ev.t
|
||||
}
|
||||
66
vendor/github.com/gdamore/tcell/v2/resize.go
generated
vendored
Normal file
66
vendor/github.com/gdamore/tcell/v2/resize.go
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2015 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventResize is sent when the window size changes.
|
||||
type EventResize struct {
|
||||
t time.Time
|
||||
ws WindowSize
|
||||
}
|
||||
|
||||
// NewEventResize creates an EventResize with the new updated window size,
|
||||
// which is given in character cells.
|
||||
func NewEventResize(width, height int) *EventResize {
|
||||
ws := WindowSize{
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
return &EventResize{t: time.Now(), ws: ws}
|
||||
}
|
||||
|
||||
// When returns the time when the Event was created.
|
||||
func (ev *EventResize) When() time.Time {
|
||||
return ev.t
|
||||
}
|
||||
|
||||
// Size returns the new window size as width, height in character cells.
|
||||
func (ev *EventResize) Size() (int, int) {
|
||||
return ev.ws.Width, ev.ws.Height
|
||||
}
|
||||
|
||||
// PixelSize returns the new window size as width, height in pixels. The size
|
||||
// will be 0,0 if the screen doesn't support this feature
|
||||
func (ev *EventResize) PixelSize() (int, int) {
|
||||
return ev.ws.PixelWidth, ev.ws.PixelHeight
|
||||
}
|
||||
|
||||
type WindowSize struct {
|
||||
Width int
|
||||
Height int
|
||||
PixelWidth int
|
||||
PixelHeight int
|
||||
}
|
||||
|
||||
// CellDimensions returns the dimensions of a single cell, in pixels
|
||||
func (ws WindowSize) CellDimensions() (int, int) {
|
||||
if ws.PixelWidth == 0 || ws.PixelHeight == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
return (ws.PixelWidth / ws.Width), (ws.PixelHeight / ws.Height)
|
||||
}
|
||||
111
vendor/github.com/gdamore/tcell/v2/runes.go
generated
vendored
Normal file
111
vendor/github.com/gdamore/tcell/v2/runes.go
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2015 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
// The names of these constants are chosen to match Terminfo names,
|
||||
// modulo case, and changing the prefix from ACS_ to Rune. These are
|
||||
// the runes we provide extra special handling for, with ASCII fallbacks
|
||||
// for terminals that lack them.
|
||||
const (
|
||||
RuneSterling = '£'
|
||||
RuneDArrow = '↓'
|
||||
RuneLArrow = '←'
|
||||
RuneRArrow = '→'
|
||||
RuneUArrow = '↑'
|
||||
RuneBullet = '·'
|
||||
RuneBoard = '░'
|
||||
RuneCkBoard = '▒'
|
||||
RuneDegree = '°'
|
||||
RuneDiamond = '◆'
|
||||
RuneGEqual = '≥'
|
||||
RunePi = 'π'
|
||||
RuneHLine = '─'
|
||||
RuneLantern = '§'
|
||||
RunePlus = '┼'
|
||||
RuneLEqual = '≤'
|
||||
RuneLLCorner = '└'
|
||||
RuneLRCorner = '┘'
|
||||
RuneNEqual = '≠'
|
||||
RunePlMinus = '±'
|
||||
RuneS1 = '⎺'
|
||||
RuneS3 = '⎻'
|
||||
RuneS7 = '⎼'
|
||||
RuneS9 = '⎽'
|
||||
RuneBlock = '█'
|
||||
RuneTTee = '┬'
|
||||
RuneRTee = '┤'
|
||||
RuneLTee = '├'
|
||||
RuneBTee = '┴'
|
||||
RuneULCorner = '┌'
|
||||
RuneURCorner = '┐'
|
||||
RuneVLine = '│'
|
||||
)
|
||||
|
||||
// RuneFallbacks is the default map of fallback strings that will be
|
||||
// used to replace a rune when no other more appropriate transformation
|
||||
// is available, and the rune cannot be displayed directly.
|
||||
//
|
||||
// New entries may be added to this map over time, as it becomes clear
|
||||
// that such is desirable. Characters that represent either letters or
|
||||
// numbers should not be added to this list unless it is certain that
|
||||
// the meaning will still convey unambiguously.
|
||||
//
|
||||
// As an example, it would be appropriate to add an ASCII mapping for
|
||||
// the full width form of the letter 'A', but it would not be appropriate
|
||||
// to do so a glyph representing the country China.
|
||||
//
|
||||
// Programs that desire richer fallbacks may register additional ones,
|
||||
// or change or even remove these mappings with Screen.RegisterRuneFallback
|
||||
// Screen.UnregisterRuneFallback methods.
|
||||
//
|
||||
// Note that Unicode is presumed to be able to display all glyphs.
|
||||
// This is a pretty poor assumption, but there is no easy way to
|
||||
// figure out which glyphs are supported in a given font. Hence,
|
||||
// some care in selecting the characters you support in your application
|
||||
// is still appropriate.
|
||||
var RuneFallbacks = map[rune]string{
|
||||
RuneSterling: "f",
|
||||
RuneDArrow: "v",
|
||||
RuneLArrow: "<",
|
||||
RuneRArrow: ">",
|
||||
RuneUArrow: "^",
|
||||
RuneBullet: "o",
|
||||
RuneBoard: "#",
|
||||
RuneCkBoard: ":",
|
||||
RuneDegree: "\\",
|
||||
RuneDiamond: "+",
|
||||
RuneGEqual: ">",
|
||||
RunePi: "*",
|
||||
RuneHLine: "-",
|
||||
RuneLantern: "#",
|
||||
RunePlus: "+",
|
||||
RuneLEqual: "<",
|
||||
RuneLLCorner: "+",
|
||||
RuneLRCorner: "+",
|
||||
RuneNEqual: "!",
|
||||
RunePlMinus: "#",
|
||||
RuneS1: "~",
|
||||
RuneS3: "-",
|
||||
RuneS7: "-",
|
||||
RuneS9: "_",
|
||||
RuneBlock: "#",
|
||||
RuneTTee: "+",
|
||||
RuneRTee: "+",
|
||||
RuneLTee: "+",
|
||||
RuneBTee: "+",
|
||||
RuneULCorner: "+",
|
||||
RuneURCorner: "+",
|
||||
RuneVLine: "|",
|
||||
}
|
||||
495
vendor/github.com/gdamore/tcell/v2/screen.go
generated
vendored
Normal file
495
vendor/github.com/gdamore/tcell/v2/screen.go
generated
vendored
Normal file
@@ -0,0 +1,495 @@
|
||||
// Copyright 2024 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import "sync"
|
||||
|
||||
// Screen represents the physical (or emulated) screen.
|
||||
// This can be a terminal window or a physical console. Platforms implement
|
||||
// this differently.
|
||||
type Screen interface {
|
||||
// Init initializes the screen for use.
|
||||
Init() error
|
||||
|
||||
// Fini finalizes the screen also releasing resources.
|
||||
Fini()
|
||||
|
||||
// Clear logically erases the screen.
|
||||
// This is effectively a short-cut for Fill(' ', StyleDefault).
|
||||
Clear()
|
||||
|
||||
// Fill fills the screen with the given character and style.
|
||||
// The effect of filling the screen is not visible until Show
|
||||
// is called (or Sync).
|
||||
Fill(rune, Style)
|
||||
|
||||
// SetCell is an older API, and will be removed. Please use
|
||||
// SetContent instead; SetCell is implemented in terms of SetContent.
|
||||
SetCell(x int, y int, style Style, ch ...rune)
|
||||
|
||||
// GetContent returns the contents at the given location. If the
|
||||
// coordinates are out of range, then the values will be 0, nil,
|
||||
// StyleDefault. Note that the contents returned are logical contents
|
||||
// and may not actually be what is displayed, but rather are what will
|
||||
// be displayed if Show() or Sync() is called. The width is the width
|
||||
// in screen cells; most often this will be 1, but some East Asian
|
||||
// characters and emoji require two cells.
|
||||
GetContent(x, y int) (primary rune, combining []rune, style Style, width int)
|
||||
|
||||
// SetContent sets the contents of the given cell location. If
|
||||
// the coordinates are out of range, then the operation is ignored.
|
||||
//
|
||||
// The first rune is the primary non-zero width rune. The array
|
||||
// that follows is a possible list of combining characters to append,
|
||||
// and will usually be nil (no combining characters.)
|
||||
//
|
||||
// The results are not displayed until Show() or Sync() is called.
|
||||
//
|
||||
// Note that wide (East Asian full width and emoji) runes occupy two cells,
|
||||
// and attempts to place character at next cell to the right will have
|
||||
// undefined effects. Wide runes that are printed in the
|
||||
// last column will be replaced with a single width space on output.
|
||||
SetContent(x int, y int, primary rune, combining []rune, style Style)
|
||||
|
||||
// SetStyle sets the default style to use when clearing the screen
|
||||
// or when StyleDefault is specified. If it is also StyleDefault,
|
||||
// then whatever system/terminal default is relevant will be used.
|
||||
SetStyle(style Style)
|
||||
|
||||
// ShowCursor is used to display the cursor at a given location.
|
||||
// If the coordinates -1, -1 are given or are otherwise outside the
|
||||
// dimensions of the screen, the cursor will be hidden.
|
||||
ShowCursor(x int, y int)
|
||||
|
||||
// HideCursor is used to hide the cursor. It's an alias for
|
||||
// ShowCursor(-1, -1).sim
|
||||
HideCursor()
|
||||
|
||||
// SetCursorStyle is used to set the cursor style. If the style
|
||||
// is not supported (or cursor styles are not supported at all),
|
||||
// then this will have no effect. Color will be changed if supplied,
|
||||
// and the terminal supports doing so.
|
||||
SetCursorStyle(CursorStyle, ...Color)
|
||||
|
||||
// Size returns the screen size as width, height. This changes in
|
||||
// response to a call to Clear or Flush.
|
||||
Size() (width, height int)
|
||||
|
||||
// ChannelEvents is an infinite loop that waits for an event and
|
||||
// channels it into the user provided channel ch. Closing the
|
||||
// quit channel and calling the Fini method are cancellation
|
||||
// signals. When a cancellation signal is received the method
|
||||
// returns after closing ch.
|
||||
//
|
||||
// This method should be used as a goroutine.
|
||||
//
|
||||
// NOTE: PollEvent should not be called while this method is running.
|
||||
ChannelEvents(ch chan<- Event, quit <-chan struct{})
|
||||
|
||||
// PollEvent waits for events to arrive. Main application loops
|
||||
// must spin on this to prevent the application from stalling.
|
||||
// Furthermore, this will return nil if the Screen is finalized.
|
||||
PollEvent() Event
|
||||
|
||||
// HasPendingEvent returns true if PollEvent would return an event
|
||||
// without blocking. If the screen is stopped and PollEvent would
|
||||
// return nil, then the return value from this function is unspecified.
|
||||
// The purpose of this function is to allow multiple events to be collected
|
||||
// at once, to minimize screen redraws.
|
||||
HasPendingEvent() bool
|
||||
|
||||
// PostEvent tries to post an event into the event stream. This
|
||||
// can fail if the event queue is full. In that case, the event
|
||||
// is dropped, and ErrEventQFull is returned.
|
||||
PostEvent(ev Event) error
|
||||
|
||||
// Deprecated: PostEventWait is unsafe, and will be removed
|
||||
// in the future.
|
||||
//
|
||||
// PostEventWait is like PostEvent, but if the queue is full, it
|
||||
// blocks until there is space in the queue, making delivery
|
||||
// reliable. However, it is VERY important that this function
|
||||
// never be called from within whatever event loop is polling
|
||||
// with PollEvent(), otherwise a deadlock may arise.
|
||||
//
|
||||
// For this reason, when using this function, the use of a
|
||||
// Goroutine is recommended to ensure no deadlock can occur.
|
||||
PostEventWait(ev Event)
|
||||
|
||||
// EnableMouse enables the mouse. (If your terminal supports it.)
|
||||
// If no flags are specified, then all events are reported, if the
|
||||
// terminal supports them.
|
||||
EnableMouse(...MouseFlags)
|
||||
|
||||
// DisableMouse disables the mouse.
|
||||
DisableMouse()
|
||||
|
||||
// EnablePaste enables bracketed paste mode, if supported.
|
||||
EnablePaste()
|
||||
|
||||
// DisablePaste disables bracketed paste mode.
|
||||
DisablePaste()
|
||||
|
||||
// EnableFocus enables reporting of focus events, if your terminal supports it.
|
||||
EnableFocus()
|
||||
|
||||
// DisableFocus disables reporting of focus events.
|
||||
DisableFocus()
|
||||
|
||||
// HasMouse returns true if the terminal (apparently) supports a
|
||||
// mouse. Note that the return value of true doesn't guarantee that
|
||||
// a mouse/pointing device is present; a false return definitely
|
||||
// indicates no mouse support is available.
|
||||
HasMouse() bool
|
||||
|
||||
// Colors returns the number of colors. All colors are assumed to
|
||||
// use the ANSI color map. If a terminal is monochrome, it will
|
||||
// return 0.
|
||||
Colors() int
|
||||
|
||||
// Show makes all the content changes made using SetContent() visible
|
||||
// on the display.
|
||||
//
|
||||
// It does so in the most efficient and least visually disruptive
|
||||
// manner possible.
|
||||
Show()
|
||||
|
||||
// Sync works like Show(), but it updates every visible cell on the
|
||||
// physical display, assuming that it is not synchronized with any
|
||||
// internal model. This may be both expensive and visually jarring,
|
||||
// so it should only be used when believed to actually be necessary.
|
||||
//
|
||||
// Typically, this is called as a result of a user-requested redraw
|
||||
// (e.g. to clear up on-screen corruption caused by some other program),
|
||||
// or during a resize event.
|
||||
Sync()
|
||||
|
||||
// CharacterSet returns information about the character set.
|
||||
// This isn't the full locale, but it does give us the input/output
|
||||
// character set. Note that this is just for diagnostic purposes,
|
||||
// we normally translate input/output to/from UTF-8, regardless of
|
||||
// what the user's environment is.
|
||||
CharacterSet() string
|
||||
|
||||
// RegisterRuneFallback adds a fallback for runes that are not
|
||||
// part of the character set -- for example one could register
|
||||
// o as a fallback for ø. This should be done cautiously for
|
||||
// characters that might be displayed ordinarily in language
|
||||
// specific text -- characters that could change the meaning of
|
||||
// written text would be dangerous. The intention here is to
|
||||
// facilitate fallback characters in pseudo-graphical applications.
|
||||
//
|
||||
// If the terminal has fallbacks already in place via an alternate
|
||||
// character set, those are used in preference. Also, standard
|
||||
// fallbacks for graphical characters in the alternate character set
|
||||
// terminfo string are registered implicitly.
|
||||
//
|
||||
// The display string should be the same width as original rune.
|
||||
// This makes it possible to register two character replacements
|
||||
// for full width East Asian characters, for example.
|
||||
//
|
||||
// It is recommended that replacement strings consist only of
|
||||
// 7-bit ASCII, since other characters may not display everywhere.
|
||||
RegisterRuneFallback(r rune, subst string)
|
||||
|
||||
// UnregisterRuneFallback unmaps a replacement. It will unmap
|
||||
// the implicit ASCII replacements for alternate characters as well.
|
||||
// When an unmapped char needs to be displayed, but no suitable
|
||||
// glyph is available, '?' is emitted instead. It is not possible
|
||||
// to "disable" the use of alternate characters that are supported
|
||||
// by your terminal except by changing the terminal database.
|
||||
UnregisterRuneFallback(r rune)
|
||||
|
||||
// CanDisplay returns true if the given rune can be displayed on
|
||||
// this screen. Note that this is a best-guess effort -- whether
|
||||
// your fonts support the character or not may be questionable.
|
||||
// Mostly this is for folks who work outside of Unicode.
|
||||
//
|
||||
// If checkFallbacks is true, then if any (possibly imperfect)
|
||||
// fallbacks are registered, this will return true. This will
|
||||
// also return true if the terminal can replace the glyph with
|
||||
// one that is visually indistinguishable from the one requested.
|
||||
CanDisplay(r rune, checkFallbacks bool) bool
|
||||
|
||||
// Resize does nothing, since it's generally not possible to
|
||||
// ask a screen to resize, but it allows the Screen to implement
|
||||
// the View interface.
|
||||
Resize(int, int, int, int)
|
||||
|
||||
// HasKey returns true if the keyboard is believed to have the
|
||||
// key. In some cases a keyboard may have keys with this name
|
||||
// but no support for them, while in others a key may be reported
|
||||
// as supported but not actually be usable (such as some emulators
|
||||
// that hijack certain keys). Its best not to depend to strictly
|
||||
// on this function, but it can be used for hinting when building
|
||||
// menus, displayed hot-keys, etc. Note that KeyRune (literal
|
||||
// runes) is always true.
|
||||
HasKey(Key) bool
|
||||
|
||||
// Suspend pauses input and output processing. It also restores the
|
||||
// terminal settings to what they were when the application started.
|
||||
// This can be used to, for example, run a sub-shell.
|
||||
Suspend() error
|
||||
|
||||
// Resume resumes after Suspend().
|
||||
Resume() error
|
||||
|
||||
// Beep attempts to sound an OS-dependent audible alert and returns an error
|
||||
// when unsuccessful.
|
||||
Beep() error
|
||||
|
||||
// SetSize attempts to resize the window. It also invalidates the cells and
|
||||
// calls the resize function. Note that if the window size is changed, it will
|
||||
// not be restored upon application exit.
|
||||
//
|
||||
// Many terminals cannot support this. Perversely, the "modern" Windows Terminal
|
||||
// does not support application-initiated resizing, whereas the legacy terminal does.
|
||||
// Also, some emulators can support this but may have it disabled by default.
|
||||
SetSize(int, int)
|
||||
|
||||
// LockRegion sets or unsets a lock on a region of cells. A lock on a
|
||||
// cell prevents the cell from being redrawn.
|
||||
LockRegion(x, y, width, height int, lock bool)
|
||||
|
||||
// Tty returns the underlying Tty. If the screen is not a terminal, the
|
||||
// returned bool will be false
|
||||
Tty() (Tty, bool)
|
||||
|
||||
// SetTitle sets a window title on the screen.
|
||||
// Terminals may be configured to ignore this, or unable to.
|
||||
// Tcell may attempt to save and restore the window title on entry and exit, but
|
||||
// the results may vary. Use of unicode characters may not be supported.
|
||||
SetTitle(string)
|
||||
|
||||
// SetClipboard is used to post arbitrary data to the system clipboard.
|
||||
// This need not be UTF-8 string data. It's up to the recipient to decode the
|
||||
// data meaningfully. Terminals may prevent this for security reasons.
|
||||
SetClipboard([]byte)
|
||||
|
||||
// GetClipboard is used to request the clipboard contents. It may be ignored.
|
||||
// If the terminal is willing, it will be post the clipboard contents using an
|
||||
// EventPaste with the clipboard content as the Data() field. Terminals may
|
||||
// prevent this for security reasons.
|
||||
GetClipboard()
|
||||
}
|
||||
|
||||
// NewScreen returns a default Screen suitable for the user's terminal
|
||||
// environment.
|
||||
func NewScreen() (Screen, error) {
|
||||
// Windows is happier if we try for a console screen first.
|
||||
if s, _ := NewConsoleScreen(); s != nil {
|
||||
return s, nil
|
||||
} else if s, e := NewTerminfoScreen(); s != nil {
|
||||
return s, nil
|
||||
} else {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
|
||||
// MouseFlags are options to modify the handling of mouse events.
|
||||
// Actual events can be ORed together.
|
||||
type MouseFlags int
|
||||
|
||||
const (
|
||||
MouseButtonEvents = MouseFlags(1) // Click events only
|
||||
MouseDragEvents = MouseFlags(2) // Click-drag events (includes button events)
|
||||
MouseMotionEvents = MouseFlags(4) // All mouse events (includes click and drag events)
|
||||
)
|
||||
|
||||
// CursorStyle represents a given cursor style, which can include the shape and
|
||||
// whether the cursor blinks or is solid. Support for changing this is not universal.
|
||||
type CursorStyle int
|
||||
|
||||
const (
|
||||
CursorStyleDefault = CursorStyle(iota) // The default
|
||||
CursorStyleBlinkingBlock
|
||||
CursorStyleSteadyBlock
|
||||
CursorStyleBlinkingUnderline
|
||||
CursorStyleSteadyUnderline
|
||||
CursorStyleBlinkingBar
|
||||
CursorStyleSteadyBar
|
||||
)
|
||||
|
||||
// screenImpl is a subset of Screen that can be used with baseScreen to formulate
|
||||
// a complete implementation of Screen. See Screen for doc comments about methods.
|
||||
type screenImpl interface {
|
||||
Init() error
|
||||
Fini()
|
||||
SetStyle(style Style)
|
||||
ShowCursor(x int, y int)
|
||||
HideCursor()
|
||||
SetCursor(CursorStyle, Color)
|
||||
Size() (width, height int)
|
||||
EnableMouse(...MouseFlags)
|
||||
DisableMouse()
|
||||
EnablePaste()
|
||||
DisablePaste()
|
||||
EnableFocus()
|
||||
DisableFocus()
|
||||
HasMouse() bool
|
||||
Colors() int
|
||||
Show()
|
||||
Sync()
|
||||
CharacterSet() string
|
||||
RegisterRuneFallback(r rune, subst string)
|
||||
UnregisterRuneFallback(r rune)
|
||||
CanDisplay(r rune, checkFallbacks bool) bool
|
||||
Resize(int, int, int, int)
|
||||
HasKey(Key) bool
|
||||
Suspend() error
|
||||
Resume() error
|
||||
Beep() error
|
||||
SetSize(int, int)
|
||||
SetTitle(string)
|
||||
Tty() (Tty, bool)
|
||||
SetClipboard([]byte)
|
||||
GetClipboard()
|
||||
|
||||
// Following methods are not part of the Screen api, but are used for interaction with
|
||||
// the common layer code.
|
||||
|
||||
// Locker locks the underlying data structures so that we can access them
|
||||
// in a thread-safe way.
|
||||
sync.Locker
|
||||
|
||||
// GetCells returns a pointer to the underlying CellBuffer that the implementation uses.
|
||||
// Various methods will write to these for performance, but will use the lock to do so.
|
||||
GetCells() *CellBuffer
|
||||
|
||||
// StopQ is closed when the screen is shut down via Fini. It remains open if the screen
|
||||
// is merely suspended.
|
||||
StopQ() <-chan struct{}
|
||||
|
||||
// EventQ delivers events. Events are posted to this by the screen in response to
|
||||
// key presses, resizes, etc. Application code receives events from this via the
|
||||
// Screen.PollEvent, Screen.ChannelEvents APIs.
|
||||
EventQ() chan Event
|
||||
}
|
||||
|
||||
type baseScreen struct {
|
||||
screenImpl
|
||||
}
|
||||
|
||||
func (b *baseScreen) SetCell(x int, y int, style Style, ch ...rune) {
|
||||
if len(ch) > 0 {
|
||||
b.SetContent(x, y, ch[0], ch[1:], style)
|
||||
} else {
|
||||
b.SetContent(x, y, ' ', nil, style)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *baseScreen) Clear() {
|
||||
b.Fill(' ', StyleDefault)
|
||||
}
|
||||
|
||||
func (b *baseScreen) Fill(r rune, style Style) {
|
||||
cb := b.GetCells()
|
||||
b.Lock()
|
||||
cb.Fill(r, style)
|
||||
b.Unlock()
|
||||
}
|
||||
|
||||
func (b *baseScreen) SetContent(x, y int, mainc rune, combc []rune, st Style) {
|
||||
|
||||
cells := b.GetCells()
|
||||
b.Lock()
|
||||
cells.SetContent(x, y, mainc, combc, st)
|
||||
b.Unlock()
|
||||
}
|
||||
|
||||
func (b *baseScreen) GetContent(x, y int) (rune, []rune, Style, int) {
|
||||
var primary rune
|
||||
var combining []rune
|
||||
var style Style
|
||||
var width int
|
||||
cells := b.GetCells()
|
||||
b.Lock()
|
||||
primary, combining, style, width = cells.GetContent(x, y)
|
||||
b.Unlock()
|
||||
return primary, combining, style, width
|
||||
}
|
||||
|
||||
func (b *baseScreen) LockRegion(x, y, width, height int, lock bool) {
|
||||
cells := b.GetCells()
|
||||
b.Lock()
|
||||
for j := y; j < (y + height); j += 1 {
|
||||
for i := x; i < (x + width); i += 1 {
|
||||
switch lock {
|
||||
case true:
|
||||
cells.LockCell(i, j)
|
||||
case false:
|
||||
cells.UnlockCell(i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.Unlock()
|
||||
}
|
||||
|
||||
func (b *baseScreen) ChannelEvents(ch chan<- Event, quit <-chan struct{}) {
|
||||
defer close(ch)
|
||||
for {
|
||||
select {
|
||||
case <-quit:
|
||||
return
|
||||
case <-b.StopQ():
|
||||
return
|
||||
case ev := <-b.EventQ():
|
||||
select {
|
||||
case <-quit:
|
||||
return
|
||||
case <-b.StopQ():
|
||||
return
|
||||
case ch <- ev:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *baseScreen) PollEvent() Event {
|
||||
select {
|
||||
case <-b.StopQ():
|
||||
return nil
|
||||
case ev := <-b.EventQ():
|
||||
return ev
|
||||
}
|
||||
}
|
||||
|
||||
func (b *baseScreen) HasPendingEvent() bool {
|
||||
return len(b.EventQ()) > 0
|
||||
}
|
||||
|
||||
func (b *baseScreen) PostEventWait(ev Event) {
|
||||
select {
|
||||
case b.EventQ() <- ev:
|
||||
case <-b.StopQ():
|
||||
}
|
||||
}
|
||||
|
||||
func (b *baseScreen) PostEvent(ev Event) error {
|
||||
select {
|
||||
case b.EventQ() <- ev:
|
||||
return nil
|
||||
default:
|
||||
return ErrEventQFull
|
||||
}
|
||||
}
|
||||
|
||||
func (b *baseScreen) SetCursorStyle(cs CursorStyle, ccs ...Color) {
|
||||
if len(ccs) > 0 {
|
||||
b.SetCursor(cs, ccs[0])
|
||||
} else {
|
||||
b.SetCursor(cs, ColorNone)
|
||||
}
|
||||
}
|
||||
528
vendor/github.com/gdamore/tcell/v2/simulation.go
generated
vendored
Normal file
528
vendor/github.com/gdamore/tcell/v2/simulation.go
generated
vendored
Normal file
@@ -0,0 +1,528 @@
|
||||
// Copyright 2024 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// NewSimulationScreen returns a SimulationScreen. Note that
|
||||
// SimulationScreen is also a Screen.
|
||||
func NewSimulationScreen(charset string) SimulationScreen {
|
||||
if charset == "" {
|
||||
charset = "UTF-8"
|
||||
}
|
||||
ss := &simscreen{charset: charset}
|
||||
ss.Screen = &baseScreen{screenImpl: ss}
|
||||
return ss
|
||||
}
|
||||
|
||||
// SimulationScreen represents a screen simulation. This is intended to
|
||||
// be a superset of normal Screens, but also adds some important interfaces
|
||||
// for testing.
|
||||
type SimulationScreen interface {
|
||||
Screen
|
||||
|
||||
// InjectKeyBytes injects a stream of bytes corresponding to
|
||||
// the native encoding (see charset). It turns true if the entire
|
||||
// set of bytes were processed and delivered as KeyEvents, false
|
||||
// if any bytes were not fully understood. Any bytes that are not
|
||||
// fully converted are discarded.
|
||||
InjectKeyBytes(buf []byte) bool
|
||||
|
||||
// InjectKey injects a key event. The rune is a UTF-8 rune, post
|
||||
// any translation.
|
||||
InjectKey(key Key, r rune, mod ModMask)
|
||||
|
||||
// InjectMouse injects a mouse event.
|
||||
InjectMouse(x, y int, buttons ButtonMask, mod ModMask)
|
||||
|
||||
// GetContents returns screen contents as an array of
|
||||
// cells, along with the physical width & height. Note that the
|
||||
// physical contents will be used until the next time SetSize()
|
||||
// is called.
|
||||
GetContents() (cells []SimCell, width int, height int)
|
||||
|
||||
// GetCursor returns the cursor details.
|
||||
GetCursor() (x int, y int, visible bool)
|
||||
|
||||
// GetTitle gets the previously set title.
|
||||
GetTitle() string
|
||||
|
||||
// GetClipboardData gets the actual data for the clipboard.
|
||||
GetClipboardData() []byte
|
||||
}
|
||||
|
||||
// SimCell represents a simulated screen cell. The purpose of this
|
||||
// is to track on screen content.
|
||||
type SimCell struct {
|
||||
// Bytes is the actual character bytes. Normally this is
|
||||
// rune data, but it could be be data in another encoding system.
|
||||
Bytes []byte
|
||||
|
||||
// Style is the style used to display the data.
|
||||
Style Style
|
||||
|
||||
// Runes is the list of runes, unadulterated, in UTF-8.
|
||||
Runes []rune
|
||||
}
|
||||
|
||||
type simscreen struct {
|
||||
physw int
|
||||
physh int
|
||||
fini bool
|
||||
style Style
|
||||
evch chan Event
|
||||
quit chan struct{}
|
||||
|
||||
front []SimCell
|
||||
back CellBuffer
|
||||
clear bool
|
||||
cursorx int
|
||||
cursory int
|
||||
cursorvis bool
|
||||
mouse bool
|
||||
paste bool
|
||||
charset string
|
||||
encoder transform.Transformer
|
||||
decoder transform.Transformer
|
||||
fillchar rune
|
||||
fillstyle Style
|
||||
fallback map[rune]string
|
||||
title string
|
||||
clipboard []byte
|
||||
|
||||
Screen
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (s *simscreen) Init() error {
|
||||
s.evch = make(chan Event, 10)
|
||||
s.quit = make(chan struct{})
|
||||
s.fillchar = 'X'
|
||||
s.fillstyle = StyleDefault
|
||||
s.mouse = false
|
||||
s.physw = 80
|
||||
s.physh = 25
|
||||
s.cursorx = -1
|
||||
s.cursory = -1
|
||||
s.style = StyleDefault
|
||||
|
||||
if enc := GetEncoding(s.charset); enc != nil {
|
||||
s.encoder = enc.NewEncoder()
|
||||
s.decoder = enc.NewDecoder()
|
||||
} else {
|
||||
return ErrNoCharset
|
||||
}
|
||||
|
||||
s.front = make([]SimCell, s.physw*s.physh)
|
||||
s.back.Resize(80, 25)
|
||||
|
||||
// default fallbacks
|
||||
s.fallback = make(map[rune]string)
|
||||
for k, v := range RuneFallbacks {
|
||||
s.fallback[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *simscreen) Fini() {
|
||||
s.Lock()
|
||||
s.fini = true
|
||||
s.back.Resize(0, 0)
|
||||
s.Unlock()
|
||||
if s.quit != nil {
|
||||
close(s.quit)
|
||||
}
|
||||
s.physw = 0
|
||||
s.physh = 0
|
||||
s.front = nil
|
||||
}
|
||||
|
||||
func (s *simscreen) SetStyle(style Style) {
|
||||
s.Lock()
|
||||
s.style = style
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
func (s *simscreen) drawCell(x, y int) int {
|
||||
|
||||
mainc, combc, style, width := s.back.GetContent(x, y)
|
||||
if !s.back.Dirty(x, y) {
|
||||
return width
|
||||
}
|
||||
if x >= s.physw || y >= s.physh || x < 0 || y < 0 {
|
||||
return width
|
||||
}
|
||||
simc := &s.front[(y*s.physw)+x]
|
||||
|
||||
if style == StyleDefault {
|
||||
style = s.style
|
||||
}
|
||||
simc.Style = style
|
||||
simc.Runes = append([]rune{mainc}, combc...)
|
||||
|
||||
// now emit runes - taking care to not overrun width with a
|
||||
// wide character, and to ensure that we emit exactly one regular
|
||||
// character followed up by any residual combing characters
|
||||
|
||||
simc.Bytes = nil
|
||||
|
||||
if x > s.physw-width {
|
||||
simc.Runes = []rune{' '}
|
||||
simc.Bytes = []byte{' '}
|
||||
return width
|
||||
}
|
||||
|
||||
lbuf := make([]byte, 12)
|
||||
ubuf := make([]byte, 12)
|
||||
nout := 0
|
||||
|
||||
for _, r := range simc.Runes {
|
||||
|
||||
l := utf8.EncodeRune(ubuf, r)
|
||||
|
||||
nout, _, _ = s.encoder.Transform(lbuf, ubuf[:l], true)
|
||||
|
||||
if nout == 0 || lbuf[0] == '\x1a' {
|
||||
|
||||
// skip combining
|
||||
|
||||
if subst, ok := s.fallback[r]; ok {
|
||||
simc.Bytes = append(simc.Bytes,
|
||||
[]byte(subst)...)
|
||||
|
||||
} else if r >= ' ' && r <= '~' {
|
||||
simc.Bytes = append(simc.Bytes, byte(r))
|
||||
|
||||
} else if simc.Bytes == nil {
|
||||
simc.Bytes = append(simc.Bytes, '?')
|
||||
}
|
||||
} else {
|
||||
simc.Bytes = append(simc.Bytes, lbuf[:nout]...)
|
||||
}
|
||||
}
|
||||
s.back.SetDirty(x, y, false)
|
||||
return width
|
||||
}
|
||||
|
||||
func (s *simscreen) ShowCursor(x, y int) {
|
||||
s.Lock()
|
||||
s.cursorx, s.cursory = x, y
|
||||
s.showCursor()
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
func (s *simscreen) HideCursor() {
|
||||
s.ShowCursor(-1, -1)
|
||||
}
|
||||
|
||||
func (s *simscreen) showCursor() {
|
||||
|
||||
x, y := s.cursorx, s.cursory
|
||||
if x < 0 || y < 0 || x >= s.physw || y >= s.physh {
|
||||
s.cursorvis = false
|
||||
} else {
|
||||
s.cursorvis = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *simscreen) hideCursor() {
|
||||
// does not update cursor position
|
||||
s.cursorvis = false
|
||||
}
|
||||
|
||||
func (s *simscreen) SetCursor(CursorStyle, Color) {}
|
||||
|
||||
func (s *simscreen) Show() {
|
||||
s.Lock()
|
||||
s.resize()
|
||||
s.draw()
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
func (s *simscreen) clearScreen() {
|
||||
// We emulate a hardware clear by filling with a specific pattern
|
||||
for i := range s.front {
|
||||
s.front[i].Style = s.fillstyle
|
||||
s.front[i].Runes = []rune{s.fillchar}
|
||||
s.front[i].Bytes = []byte{byte(s.fillchar)}
|
||||
}
|
||||
s.clear = false
|
||||
}
|
||||
|
||||
func (s *simscreen) draw() {
|
||||
s.hideCursor()
|
||||
if s.clear {
|
||||
s.clearScreen()
|
||||
}
|
||||
|
||||
w, h := s.back.Size()
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
width := s.drawCell(x, y)
|
||||
x += width - 1
|
||||
}
|
||||
}
|
||||
s.showCursor()
|
||||
}
|
||||
|
||||
func (s *simscreen) EnableMouse(...MouseFlags) {
|
||||
s.mouse = true
|
||||
}
|
||||
|
||||
func (s *simscreen) DisableMouse() {
|
||||
s.mouse = false
|
||||
}
|
||||
|
||||
func (s *simscreen) EnablePaste() {
|
||||
s.paste = true
|
||||
}
|
||||
|
||||
func (s *simscreen) DisablePaste() {
|
||||
s.paste = false
|
||||
}
|
||||
|
||||
func (s *simscreen) EnableFocus() {
|
||||
}
|
||||
|
||||
func (s *simscreen) DisableFocus() {
|
||||
}
|
||||
|
||||
func (s *simscreen) Size() (int, int) {
|
||||
s.Lock()
|
||||
w, h := s.back.Size()
|
||||
s.Unlock()
|
||||
return w, h
|
||||
}
|
||||
|
||||
func (s *simscreen) resize() {
|
||||
w, h := s.physw, s.physh
|
||||
ow, oh := s.back.Size()
|
||||
if w != ow || h != oh {
|
||||
s.back.Resize(w, h)
|
||||
ev := NewEventResize(w, h)
|
||||
s.postEvent(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *simscreen) Colors() int {
|
||||
return 256
|
||||
}
|
||||
|
||||
func (s *simscreen) postEvent(ev Event) {
|
||||
select {
|
||||
case s.evch <- ev:
|
||||
case <-s.quit:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *simscreen) InjectMouse(x, y int, buttons ButtonMask, mod ModMask) {
|
||||
ev := NewEventMouse(x, y, buttons, mod)
|
||||
s.postEvent(ev)
|
||||
}
|
||||
|
||||
func (s *simscreen) InjectKey(key Key, r rune, mod ModMask) {
|
||||
ev := NewEventKey(key, r, mod)
|
||||
s.postEvent(ev)
|
||||
}
|
||||
|
||||
func (s *simscreen) InjectKeyBytes(b []byte) bool {
|
||||
failed := false
|
||||
|
||||
outer:
|
||||
for len(b) > 0 {
|
||||
if b[0] >= ' ' && b[0] <= 0x7F {
|
||||
// printable ASCII easy to deal with -- no encodings
|
||||
ev := NewEventKey(KeyRune, rune(b[0]), ModNone)
|
||||
s.postEvent(ev)
|
||||
b = b[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
if b[0] < 0x80 {
|
||||
mod := ModNone
|
||||
// No encodings start with low numbered values
|
||||
if Key(b[0]) >= KeyCtrlA && Key(b[0]) <= KeyCtrlZ {
|
||||
mod = ModCtrl
|
||||
}
|
||||
ev := NewEventKey(Key(b[0]), 0, mod)
|
||||
s.postEvent(ev)
|
||||
b = b[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
utfb := make([]byte, len(b)*4) // worst case
|
||||
for l := 1; l < len(b); l++ {
|
||||
s.decoder.Reset()
|
||||
nout, nin, _ := s.decoder.Transform(utfb, b[:l], true)
|
||||
|
||||
if nout != 0 {
|
||||
r, _ := utf8.DecodeRune(utfb[:nout])
|
||||
if r != utf8.RuneError {
|
||||
ev := NewEventKey(KeyRune, r, ModNone)
|
||||
s.postEvent(ev)
|
||||
}
|
||||
b = b[nin:]
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
failed = true
|
||||
b = b[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
return !failed
|
||||
}
|
||||
|
||||
func (s *simscreen) Sync() {
|
||||
s.Lock()
|
||||
s.clear = true
|
||||
s.resize()
|
||||
s.back.Invalidate()
|
||||
s.draw()
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
func (s *simscreen) CharacterSet() string {
|
||||
return s.charset
|
||||
}
|
||||
|
||||
func (s *simscreen) SetSize(w, h int) {
|
||||
s.Lock()
|
||||
newc := make([]SimCell, w*h)
|
||||
for row := 0; row < h && row < s.physh; row++ {
|
||||
for col := 0; col < w && col < s.physw; col++ {
|
||||
newc[(row*w)+col] = s.front[(row*s.physw)+col]
|
||||
}
|
||||
}
|
||||
s.cursorx, s.cursory = -1, -1
|
||||
s.physw, s.physh = w, h
|
||||
s.front = newc
|
||||
s.back.Resize(w, h)
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
func (s *simscreen) GetContents() ([]SimCell, int, int) {
|
||||
s.Lock()
|
||||
cells, w, h := s.front, s.physw, s.physh
|
||||
s.Unlock()
|
||||
return cells, w, h
|
||||
}
|
||||
|
||||
func (s *simscreen) GetCursor() (int, int, bool) {
|
||||
s.Lock()
|
||||
x, y, vis := s.cursorx, s.cursory, s.cursorvis
|
||||
s.Unlock()
|
||||
return x, y, vis
|
||||
}
|
||||
|
||||
func (s *simscreen) RegisterRuneFallback(r rune, subst string) {
|
||||
s.Lock()
|
||||
s.fallback[r] = subst
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
func (s *simscreen) UnregisterRuneFallback(r rune) {
|
||||
s.Lock()
|
||||
delete(s.fallback, r)
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
func (s *simscreen) CanDisplay(r rune, checkFallbacks bool) bool {
|
||||
|
||||
if enc := s.encoder; enc != nil {
|
||||
nb := make([]byte, 6)
|
||||
ob := make([]byte, 6)
|
||||
num := utf8.EncodeRune(ob, r)
|
||||
|
||||
enc.Reset()
|
||||
dst, _, err := enc.Transform(nb, ob[:num], true)
|
||||
if dst != 0 && err == nil && nb[0] != '\x1A' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if !checkFallbacks {
|
||||
return false
|
||||
}
|
||||
if _, ok := s.fallback[r]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *simscreen) HasMouse() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *simscreen) Resize(int, int, int, int) {}
|
||||
|
||||
func (s *simscreen) HasKey(Key) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *simscreen) Beep() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *simscreen) Suspend() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *simscreen) Resume() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *simscreen) Tty() (Tty, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (s *simscreen) GetCells() *CellBuffer {
|
||||
return &s.back
|
||||
}
|
||||
|
||||
func (s *simscreen) EventQ() chan Event {
|
||||
return s.evch
|
||||
}
|
||||
|
||||
func (s *simscreen) StopQ() <-chan struct{} {
|
||||
return s.quit
|
||||
}
|
||||
|
||||
func (s *simscreen) SetTitle(title string) {
|
||||
s.title = title
|
||||
}
|
||||
|
||||
func (s *simscreen) GetTitle() string {
|
||||
return s.title
|
||||
}
|
||||
|
||||
func (s *simscreen) SetClipboard(data []byte) {
|
||||
s.clipboard = data
|
||||
}
|
||||
|
||||
func (s *simscreen) GetClipboard() {
|
||||
if s.clipboard != nil {
|
||||
ev := NewEventClipboard(s.clipboard)
|
||||
s.postEvent(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *simscreen) GetClipboardData() []byte {
|
||||
return s.clipboard
|
||||
}
|
||||
186
vendor/github.com/gdamore/tcell/v2/stdin_unix.go
generated
vendored
Normal file
186
vendor/github.com/gdamore/tcell/v2/stdin_unix.go
generated
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright 2021 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// stdIoTty is an implementation of the Tty API based upon stdin/stdout.
|
||||
type stdIoTty struct {
|
||||
fd int
|
||||
in *os.File
|
||||
out *os.File
|
||||
saved *term.State
|
||||
sig chan os.Signal
|
||||
cb func()
|
||||
stopQ chan struct{}
|
||||
dev string
|
||||
wg sync.WaitGroup
|
||||
l sync.Mutex
|
||||
}
|
||||
|
||||
func (tty *stdIoTty) Read(b []byte) (int, error) {
|
||||
return tty.in.Read(b)
|
||||
}
|
||||
|
||||
func (tty *stdIoTty) Write(b []byte) (int, error) {
|
||||
return tty.out.Write(b)
|
||||
}
|
||||
|
||||
func (tty *stdIoTty) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tty *stdIoTty) Start() error {
|
||||
tty.l.Lock()
|
||||
defer tty.l.Unlock()
|
||||
|
||||
// We open another copy of /dev/tty. This is a workaround for unusual behavior
|
||||
// observed in macOS, apparently caused when a sub-shell (for example) closes our
|
||||
// own tty device (when it exits for example). Getting a fresh new one seems to
|
||||
// resolve the problem. (We believe this is a bug in the macOS tty driver that
|
||||
// fails to account for dup() references to the same file before applying close()
|
||||
// related behaviors to the tty.) We're also holding the original copy we opened
|
||||
// since closing that might have deleterious effects as well. The upshot is that
|
||||
// we will have up to two separate file handles open on /dev/tty. (Note that when
|
||||
// using stdin/stdout instead of /dev/tty this problem is not observed.)
|
||||
var err error
|
||||
tty.in = os.Stdin
|
||||
tty.out = os.Stdout
|
||||
tty.fd = int(tty.in.Fd())
|
||||
|
||||
if !term.IsTerminal(tty.fd) {
|
||||
return errors.New("device is not a terminal")
|
||||
}
|
||||
|
||||
_ = tty.in.SetReadDeadline(time.Time{})
|
||||
saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tty.saved = saved
|
||||
|
||||
tty.stopQ = make(chan struct{})
|
||||
tty.wg.Add(1)
|
||||
go func(stopQ chan struct{}) {
|
||||
defer tty.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-tty.sig:
|
||||
tty.l.Lock()
|
||||
cb := tty.cb
|
||||
tty.l.Unlock()
|
||||
if cb != nil {
|
||||
cb()
|
||||
}
|
||||
case <-stopQ:
|
||||
return
|
||||
}
|
||||
}
|
||||
}(tty.stopQ)
|
||||
|
||||
signal.Notify(tty.sig, syscall.SIGWINCH)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tty *stdIoTty) Drain() error {
|
||||
_ = tty.in.SetReadDeadline(time.Now())
|
||||
if err := tcSetBufParams(tty.fd, 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tty *stdIoTty) Stop() error {
|
||||
tty.l.Lock()
|
||||
if err := term.Restore(tty.fd, tty.saved); err != nil {
|
||||
tty.l.Unlock()
|
||||
return err
|
||||
}
|
||||
_ = tty.in.SetReadDeadline(time.Now())
|
||||
|
||||
signal.Stop(tty.sig)
|
||||
close(tty.stopQ)
|
||||
tty.l.Unlock()
|
||||
|
||||
tty.wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tty *stdIoTty) WindowSize() (WindowSize, error) {
|
||||
size := WindowSize{}
|
||||
ws, err := unix.IoctlGetWinsize(tty.fd, unix.TIOCGWINSZ)
|
||||
if err != nil {
|
||||
return size, err
|
||||
}
|
||||
w := int(ws.Col)
|
||||
h := int(ws.Row)
|
||||
if w == 0 {
|
||||
w, _ = strconv.Atoi(os.Getenv("COLUMNS"))
|
||||
}
|
||||
if w == 0 {
|
||||
w = 80 // default
|
||||
}
|
||||
if h == 0 {
|
||||
h, _ = strconv.Atoi(os.Getenv("LINES"))
|
||||
}
|
||||
if h == 0 {
|
||||
h = 25 // default
|
||||
}
|
||||
size.Width = w
|
||||
size.Height = h
|
||||
size.PixelWidth = int(ws.Xpixel)
|
||||
size.PixelHeight = int(ws.Ypixel)
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (tty *stdIoTty) NotifyResize(cb func()) {
|
||||
tty.l.Lock()
|
||||
tty.cb = cb
|
||||
tty.l.Unlock()
|
||||
}
|
||||
|
||||
// NewStdioTty opens a tty using standard input/output.
|
||||
func NewStdIoTty() (Tty, error) {
|
||||
tty := &stdIoTty{
|
||||
sig: make(chan os.Signal),
|
||||
in: os.Stdin,
|
||||
out: os.Stdout,
|
||||
}
|
||||
var err error
|
||||
tty.fd = int(tty.in.Fd())
|
||||
if !term.IsTerminal(tty.fd) {
|
||||
return nil, errors.New("not a terminal")
|
||||
}
|
||||
if tty.saved, err = term.GetState(tty.fd); err != nil {
|
||||
return nil, fmt.Errorf("failed to get state: %w", err)
|
||||
}
|
||||
return tty, nil
|
||||
}
|
||||
192
vendor/github.com/gdamore/tcell/v2/style.go
generated
vendored
Normal file
192
vendor/github.com/gdamore/tcell/v2/style.go
generated
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright 2024 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
// Style represents a complete text style, including both foreground color,
|
||||
// background color, and additional attributes such as "bold" or "underline".
|
||||
//
|
||||
// Note that not all terminals can display all colors or attributes, and
|
||||
// many might have specific incompatibilities between specific attributes
|
||||
// and color combinations.
|
||||
//
|
||||
// To use Style, just declare a variable of its type.
|
||||
type Style struct {
|
||||
fg Color
|
||||
bg Color
|
||||
ulStyle UnderlineStyle
|
||||
ulColor Color
|
||||
attrs AttrMask
|
||||
url string
|
||||
urlId string
|
||||
}
|
||||
|
||||
// StyleDefault represents a default style, based upon the context.
|
||||
// It is the zero value.
|
||||
var StyleDefault Style
|
||||
|
||||
// styleInvalid is just an arbitrary invalid style used internally.
|
||||
var styleInvalid = Style{attrs: AttrInvalid}
|
||||
|
||||
// Foreground returns a new style based on s, with the foreground color set
|
||||
// as requested. ColorDefault can be used to select the global default.
|
||||
func (s Style) Foreground(c Color) Style {
|
||||
s2 := s
|
||||
s2.fg = c
|
||||
return s2
|
||||
}
|
||||
|
||||
// Background returns a new style based on s, with the background color set
|
||||
// as requested. ColorDefault can be used to select the global default.
|
||||
func (s Style) Background(c Color) Style {
|
||||
s2 := s
|
||||
s2.bg = c
|
||||
return s2
|
||||
}
|
||||
|
||||
// Decompose breaks a style up, returning the foreground, background,
|
||||
// and other attributes. The URL if set is not included.
|
||||
// Deprecated: Applications should not attempt to decompose style,
|
||||
// as this content is not sufficient to describe the actual style.
|
||||
func (s Style) Decompose() (fg Color, bg Color, attr AttrMask) {
|
||||
return s.fg, s.bg, s.attrs
|
||||
}
|
||||
|
||||
func (s Style) setAttrs(attrs AttrMask, on bool) Style {
|
||||
s2 := s
|
||||
if on {
|
||||
s2.attrs |= attrs
|
||||
} else {
|
||||
s2.attrs &^= attrs
|
||||
}
|
||||
return s2
|
||||
}
|
||||
|
||||
// Normal returns the style with all attributes disabled.
|
||||
func (s Style) Normal() Style {
|
||||
return Style{
|
||||
fg: s.fg,
|
||||
bg: s.bg,
|
||||
}
|
||||
}
|
||||
|
||||
// Bold returns a new style based on s, with the bold attribute set
|
||||
// as requested.
|
||||
func (s Style) Bold(on bool) Style {
|
||||
return s.setAttrs(AttrBold, on)
|
||||
}
|
||||
|
||||
// Blink returns a new style based on s, with the blink attribute set
|
||||
// as requested.
|
||||
func (s Style) Blink(on bool) Style {
|
||||
return s.setAttrs(AttrBlink, on)
|
||||
}
|
||||
|
||||
// Dim returns a new style based on s, with the dim attribute set
|
||||
// as requested.
|
||||
func (s Style) Dim(on bool) Style {
|
||||
return s.setAttrs(AttrDim, on)
|
||||
}
|
||||
|
||||
// Italic returns a new style based on s, with the italic attribute set
|
||||
// as requested.
|
||||
func (s Style) Italic(on bool) Style {
|
||||
return s.setAttrs(AttrItalic, on)
|
||||
}
|
||||
|
||||
// Reverse returns a new style based on s, with the reverse attribute set
|
||||
// as requested. (Reverse usually changes the foreground and background
|
||||
// colors.)
|
||||
func (s Style) Reverse(on bool) Style {
|
||||
return s.setAttrs(AttrReverse, on)
|
||||
}
|
||||
|
||||
// StrikeThrough sets strikethrough mode.
|
||||
func (s Style) StrikeThrough(on bool) Style {
|
||||
return s.setAttrs(AttrStrikeThrough, on)
|
||||
}
|
||||
|
||||
// Underline style. Modern terminals have the option of rendering the
|
||||
// underline using different styles, and even different colors.
|
||||
type UnderlineStyle int
|
||||
|
||||
const (
|
||||
UnderlineStyleNone = UnderlineStyle(iota)
|
||||
UnderlineStyleSolid
|
||||
UnderlineStyleDouble
|
||||
UnderlineStyleCurly
|
||||
UnderlineStyleDotted
|
||||
UnderlineStyleDashed
|
||||
)
|
||||
|
||||
// Underline returns a new style based on s, with the underline attribute set
|
||||
// as requested. The parameters can be:
|
||||
//
|
||||
// bool: on / off - enables just a simple underline
|
||||
// UnderlineStyle: sets a specific style (should not coexist with the bool)
|
||||
// Color: the color to use
|
||||
func (s Style) Underline(params ...interface{}) Style {
|
||||
s2 := s
|
||||
for _, param := range params {
|
||||
switch v := param.(type) {
|
||||
case bool:
|
||||
if v {
|
||||
s2.ulStyle = UnderlineStyleSolid
|
||||
s2.attrs |= AttrUnderline
|
||||
} else {
|
||||
s2.ulStyle = UnderlineStyleNone
|
||||
s2.attrs &^= AttrUnderline
|
||||
}
|
||||
case UnderlineStyle:
|
||||
if v == UnderlineStyleNone {
|
||||
s2.attrs &^= AttrUnderline
|
||||
} else {
|
||||
s2.attrs |= AttrUnderline
|
||||
}
|
||||
s2.ulStyle = v
|
||||
case Color:
|
||||
s2.ulColor = v
|
||||
default:
|
||||
panic("Bad type for underline")
|
||||
}
|
||||
}
|
||||
return s2
|
||||
}
|
||||
|
||||
// Attributes returns a new style based on s, with its attributes set as
|
||||
// specified.
|
||||
func (s Style) Attributes(attrs AttrMask) Style {
|
||||
s2 := s
|
||||
s2.attrs = attrs
|
||||
return s2
|
||||
}
|
||||
|
||||
// Url returns a style with the Url set. If the provided Url is not empty,
|
||||
// and the terminal supports it, text will typically be marked up as a clickable
|
||||
// link to that Url. If the Url is empty, then this mode is turned off.
|
||||
func (s Style) Url(url string) Style {
|
||||
s2 := s
|
||||
s2.url = url
|
||||
return s2
|
||||
}
|
||||
|
||||
// UrlId returns a style with the UrlId set. If the provided UrlId is not empty,
|
||||
// any marked up Url with this style will be given the UrlId also. If the
|
||||
// terminal supports it, any text with the same UrlId will be grouped as if it
|
||||
// were one Url, even if it spans multiple lines.
|
||||
func (s Style) UrlId(id string) Style {
|
||||
s2 := s
|
||||
s2.urlId = "id=" + id
|
||||
return s2
|
||||
}
|
||||
1
vendor/github.com/gdamore/tcell/v2/terminfo/.gitignore
generated
vendored
Normal file
1
vendor/github.com/gdamore/tcell/v2/terminfo/.gitignore
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
mkinfo
|
||||
25
vendor/github.com/gdamore/tcell/v2/terminfo/README.md
generated
vendored
Normal file
25
vendor/github.com/gdamore/tcell/v2/terminfo/README.md
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
This package represents the parent for all terminals.
|
||||
|
||||
In older versions of tcell we had (a couple of) different
|
||||
external file formats for the terminal database. Those are
|
||||
now removed. All terminal definitions are supplied by
|
||||
one of two methods:
|
||||
|
||||
1. Compiled Go code
|
||||
|
||||
2. For systems with terminfo and infocmp, dynamically
|
||||
generated at runtime.
|
||||
|
||||
The Go code can be generated using the mkinfo utility in
|
||||
this directory. The database entry should be generated
|
||||
into a package in a directory named as the first character
|
||||
of the package name. (This permits us to group them all
|
||||
without having a huge directory of little packages.)
|
||||
|
||||
It may be desirable to add new packages to the extended
|
||||
package, or -- rarely -- the base package.
|
||||
|
||||
Applications which want to have the large set of terminal
|
||||
descriptions built into the binary can simply import the
|
||||
extended package. Otherwise a smaller reasonable default
|
||||
set (the base package) will be included instead.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user