Compare commits

...

18 Commits

Author SHA1 Message Date
warkanum e29c7e31c1 chore(release): update package version to 1.0.59
Release / test (push) Failing after -35m9s
Release / release (push) Has been skipped
Release / pkg-aur (push) Has been skipped
Release / pkg-deb (push) Has been skipped
Release / pkg-rpm (push) Has been skipped
2026-05-20 22:52:29 +02:00
warkanum 43f4680176 chore: ⬆️ updated deps 2026-05-20 22:52:20 +02:00
warkanum d9f27c1775 feat(pgsql): enhance SQL statement execution logging
* add context information for executed SQL statements
* implement extractStatementContext function for context retrieval
2026-05-19 19:42:57 +02:00
warkanum bb7ceb37fe chore(release): update package version to 1.0.58
Release / test (push) Successful in -32m53s
Release / release (push) Successful in -20m53s
Release / pkg-deb (push) Successful in -31m34s
Release / pkg-rpm (push) Successful in -31m3s
Release / pkg-aur (push) Successful in -11m7s
2026-05-19 19:26:45 +02:00
warkanum 6a759ef3d1 fix(mssql): correct order of MSSQL type mappings 2026-05-19 19:26:30 +02:00
warkanum cb735f0754 feat(sqlite): add SQLite type mapping and conversion functions
* Implement SQLiteToCanonicalTypes for type mapping
* Add ConvertSQLiteToCanonical and ConvertCanonicalToSQLite functions
* Update mapDataType to utilize new conversion logic
2026-05-19 19:26:09 +02:00
warkanum 80fb49bc5e refactor(datatypes): remove redundant normalization function 2026-05-19 19:12:54 +02:00
warkanum 9190df81dd feat(merge): enhance type conflict detection for columns
* Introduced extractTypeParts function to handle embedded dimensions in type strings.
* Updated columnTypeConflict to utilize new type extraction logic.
* Improved PostgreSQL type normalization and handling in various components.
2026-05-19 19:12:27 +02:00
Hein 9235ef5e08 chore(release): update package version to 1.0.57
Release / test (push) Successful in -32m7s
Release / release (push) Successful in -27m58s
Release / pkg-deb (push) Successful in -30m52s
Release / pkg-aur (push) Successful in -29m5s
Release / pkg-rpm (push) Successful in -29m58s
2026-05-07 14:45:02 +02:00
Hein b91d6b33b5 feat(writer): add continue-on-error option for SQL writers
* Introduce ContinueOnError option to WriterOptions
* Update writer functions to support continue-on-error behavior
* Modify migration and database writing to handle continue-on-error
2026-05-07 14:44:36 +02:00
warkanum 30ef1db010 chore(release): update package version to 1.0.56
Release / test (push) Successful in -32m19s
Release / release (push) Successful in -31m39s
Release / pkg-deb (push) Successful in -31m57s
Release / pkg-aur (push) Successful in -31m46s
Release / pkg-rpm (push) Successful in -4m7s
2026-05-05 14:51:10 +02:00
warkanum 2d97a47ee1 feat: Enhance PostgreSQL type handling and migration scripts
- Introduced equivalent base types and variants for PostgreSQL types to normalize type comparisons.
- Added functions for normalizing SQL types and retrieving equivalent type variants.
- Updated migration writer to handle type alterations with checks for existing types.
- Implemented logic to create necessary extensions (e.g., pg_trgm) based on schema requirements.
- Enhanced tests to cover new functionality for type normalization and migration handling.
- Improved handling of GIN indexes to use appropriate operator classes based on column types.
2026-05-05 14:50:34 +02:00
warkanum 72200ea72e chore(release): update package version to 1.0.55
Release / test (push) Successful in -32m1s
Release / release (push) Successful in -31m13s
Release / pkg-aur (push) Successful in -32m13s
Release / pkg-deb (push) Successful in -31m12s
Release / pkg-rpm (push) Successful in -29m45s
2026-05-05 11:36:29 +02:00
warkanum 608893a3d6 feat(index): implement GIN index support for quoted text columns and enhance index column resolution 2026-05-05 11:32:15 +02:00
warkanum 53ff745d5d chore(release): update package version to 1.0.54
Release / test (push) Successful in -31m47s
Release / release (push) Successful in -31m9s
Release / pkg-aur (push) Successful in -31m57s
Release / pkg-deb (push) Successful in -31m1s
Release / pkg-rpm (push) Successful in -29m27s
2026-05-05 11:12:49 +02:00
warkanum 17bc8ed395 feat(migration): enhance primary key handling and add GIN index support in migration writer 2026-05-05 11:12:23 +02:00
warkanum a447b68b22 chore(release): update package version to 1.0.53
Release / test (push) Successful in -31m55s
Release / release (push) Successful in -31m19s
Release / pkg-aur (push) Successful in -32m3s
Release / pkg-deb (push) Successful in -31m21s
Release / pkg-rpm (push) Successful in -28m4s
2026-05-05 10:48:27 +02:00
warkanum 4303dcf59b Support typed primary key helpers in gorm and bun writers 2026-05-05 10:32:33 +02:00
414 changed files with 298095 additions and 301891 deletions
+5 -3
View File
@@ -53,6 +53,7 @@ var (
convertSchemaFilter string convertSchemaFilter string
convertFlattenSchema bool convertFlattenSchema bool
convertNullableTypes string convertNullableTypes string
convertContinueOnError bool
) )
var convertCmd = &cobra.Command{ var convertCmd = &cobra.Command{
@@ -177,6 +178,7 @@ func init() {
convertCmd.Flags().StringVar(&convertSchemaFilter, "schema", "", "Filter to a specific schema by name (required for formats like dctx that only support single schemas)") convertCmd.Flags().StringVar(&convertSchemaFilter, "schema", "", "Filter to a specific schema by name (required for formats like dctx that only support single schemas)")
convertCmd.Flags().BoolVar(&convertFlattenSchema, "flatten-schema", false, "Flatten schema.table names to schema_table (useful for databases like SQLite that do not support schemas)") convertCmd.Flags().BoolVar(&convertFlattenSchema, "flatten-schema", false, "Flatten schema.table names to schema_table (useful for databases like SQLite that do not support schemas)")
convertCmd.Flags().StringVar(&convertNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'resolvespec' (default) or 'stdlib' (database/sql)") convertCmd.Flags().StringVar(&convertNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'resolvespec' (default) or 'stdlib' (database/sql)")
convertCmd.Flags().BoolVar(&convertContinueOnError, "continue-on-error", false, "Prepend \\set ON_ERROR_STOP off to generated SQL so psql continues past errors (pgsql output only)")
err := convertCmd.MarkFlagRequired("from") err := convertCmd.MarkFlagRequired("from")
if err != nil { if err != nil {
@@ -243,7 +245,7 @@ func runConvert(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, " Schema: %s\n", convertSchemaFilter) fmt.Fprintf(os.Stderr, " Schema: %s\n", convertSchemaFilter)
} }
if err := writeDatabase(db, convertTargetType, convertTargetPath, convertPackageName, convertSchemaFilter, convertFlattenSchema, convertNullableTypes); err != nil { if err := writeDatabase(db, convertTargetType, convertTargetPath, convertPackageName, convertSchemaFilter, convertFlattenSchema, convertNullableTypes, convertContinueOnError); err != nil {
return fmt.Errorf("failed to write target: %w", err) return fmt.Errorf("failed to write target: %w", err)
} }
@@ -383,10 +385,10 @@ func readDatabaseForConvert(dbType, filePath, connString string) (*models.Databa
return db, nil return db, nil
} }
func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaFilter string, flattenSchema bool, nullableTypes string) error { func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaFilter string, flattenSchema bool, nullableTypes string, continueOnError bool) error {
var writer writers.Writer var writer writers.Writer
writerOpts := newWriterOptions(outputPath, packageName, flattenSchema, nullableTypes) writerOpts := newWriterOptions(outputPath, packageName, flattenSchema, nullableTypes, continueOnError)
switch strings.ToLower(dbType) { switch strings.ToLower(dbType) {
case "dbml": case "dbml":
+13 -13
View File
@@ -323,31 +323,31 @@ func writeDatabaseForEdit(dbType, filePath, connString string, db *models.Databa
switch strings.ToLower(dbType) { switch strings.ToLower(dbType) {
case "dbml": case "dbml":
writer = wdbml.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wdbml.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "dctx": case "dctx":
writer = wdctx.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wdctx.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "drawdb": case "drawdb":
writer = wdrawdb.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wdrawdb.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "graphql": case "graphql":
writer = wgraphql.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wgraphql.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "json": case "json":
writer = wjson.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wjson.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "yaml": case "yaml":
writer = wyaml.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wyaml.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "gorm": case "gorm":
writer = wgorm.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wgorm.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "bun": case "bun":
writer = wbun.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wbun.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "drizzle": case "drizzle":
writer = wdrizzle.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wdrizzle.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "prisma": case "prisma":
writer = wprisma.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wprisma.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "typeorm": case "typeorm":
writer = wtypeorm.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wtypeorm.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "sqlite", "sqlite3": case "sqlite", "sqlite3":
writer = wsqlite.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wsqlite.NewWriter(newWriterOptions(filePath, "", false, "", false))
case "pgsql": case "pgsql":
writer = wpgsql.NewWriter(newWriterOptions(filePath, "", false, "")) writer = wpgsql.NewWriter(newWriterOptions(filePath, "", false, "", false))
default: default:
return fmt.Errorf("%s: unsupported format: %s", label, dbType) return fmt.Errorf("%s: unsupported format: %s", label, dbType)
} }
+18 -13
View File
@@ -258,6 +258,11 @@ func runMerge(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, " ✓ Merge complete\n\n") fmt.Fprintf(os.Stderr, " ✓ Merge complete\n\n")
fmt.Fprintf(os.Stderr, "%s\n", merge.GetMergeSummary(result)) fmt.Fprintf(os.Stderr, "%s\n", merge.GetMergeSummary(result))
if strings.EqualFold(mergeOutputType, "pgsql") && len(result.TypeConflicts) > 0 {
return fmt.Errorf("merge detected conflicting existing column types and cannot safely continue with pgsql output\n%s",
merge.GetColumnTypeConflictSummary(result, 10))
}
// Step 4: Write output // Step 4: Write output
fmt.Fprintf(os.Stderr, "\n[4/4] Writing output...\n") fmt.Fprintf(os.Stderr, "\n[4/4] Writing output...\n")
fmt.Fprintf(os.Stderr, " Format: %s\n", mergeOutputType) fmt.Fprintf(os.Stderr, " Format: %s\n", mergeOutputType)
@@ -370,61 +375,61 @@ func writeDatabaseForMerge(dbType, filePath, connString string, db *models.Datab
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for DBML format", label) return fmt.Errorf("%s: file path is required for DBML format", label)
} }
writer = wdbml.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wdbml.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "dctx": case "dctx":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for DCTX format", label) return fmt.Errorf("%s: file path is required for DCTX format", label)
} }
writer = wdctx.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wdctx.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "drawdb": case "drawdb":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for DrawDB format", label) return fmt.Errorf("%s: file path is required for DrawDB format", label)
} }
writer = wdrawdb.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wdrawdb.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "graphql": case "graphql":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for GraphQL format", label) return fmt.Errorf("%s: file path is required for GraphQL format", label)
} }
writer = wgraphql.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wgraphql.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "json": case "json":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for JSON format", label) return fmt.Errorf("%s: file path is required for JSON format", label)
} }
writer = wjson.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wjson.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "yaml": case "yaml":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for YAML format", label) return fmt.Errorf("%s: file path is required for YAML format", label)
} }
writer = wyaml.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wyaml.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "gorm": case "gorm":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for GORM format", label) return fmt.Errorf("%s: file path is required for GORM format", label)
} }
writer = wgorm.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wgorm.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "bun": case "bun":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for Bun format", label) return fmt.Errorf("%s: file path is required for Bun format", label)
} }
writer = wbun.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wbun.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "drizzle": case "drizzle":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for Drizzle format", label) return fmt.Errorf("%s: file path is required for Drizzle format", label)
} }
writer = wdrizzle.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wdrizzle.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "prisma": case "prisma":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for Prisma format", label) return fmt.Errorf("%s: file path is required for Prisma format", label)
} }
writer = wprisma.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wprisma.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "typeorm": case "typeorm":
if filePath == "" { if filePath == "" {
return fmt.Errorf("%s: file path is required for TypeORM format", label) return fmt.Errorf("%s: file path is required for TypeORM format", label)
} }
writer = wtypeorm.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wtypeorm.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "sqlite", "sqlite3": case "sqlite", "sqlite3":
writer = wsqlite.NewWriter(newWriterOptions(filePath, "", flattenSchema, "")) writer = wsqlite.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
case "pgsql": case "pgsql":
writerOpts := newWriterOptions(filePath, "", flattenSchema, "") writerOpts := newWriterOptions(filePath, "", flattenSchema, "", false)
if connString != "" { if connString != "" {
writerOpts.Metadata = map[string]interface{}{ writerOpts.Metadata = map[string]interface{}{
"connection_string": connString, "connection_string": connString,
+36
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
) )
@@ -160,3 +161,38 @@ func TestRunMerge_FromListMissingSourceType(t *testing.T) {
t.Error("expected error when neither --source-path nor --from-list is provided") t.Error("expected error when neither --source-path nor --from-list is provided")
} }
} }
func TestRunMerge_PgsqlOutputRejectsColumnTypeConflict(t *testing.T) {
saved := saveMergeState()
defer restoreMergeState(saved)
dir := t.TempDir()
targetFile := filepath.Join(dir, "target.json")
sourceFile := filepath.Join(dir, "source.json")
writeTestJSONWithSingleColumnType(t, targetFile, "users", "integer")
writeTestJSONWithSingleColumnType(t, sourceFile, "users", "uuid")
mergeTargetType = "json"
mergeTargetPath = targetFile
mergeTargetConn = ""
mergeSourceType = "json"
mergeSourcePath = sourceFile
mergeSourceConn = ""
mergeFromList = nil
mergeOutputType = "pgsql"
mergeOutputPath = ""
mergeOutputConn = "postgres://relspec:secret@localhost/testdb"
mergeSkipTables = ""
mergeReportPath = ""
err := runMerge(nil, nil)
if err == nil {
t.Fatal("expected pgsql output merge to fail on column type conflict")
}
if !strings.Contains(err.Error(), "column type conflicts detected") {
t.Fatalf("expected conflict summary in error, got: %v", err)
}
if !strings.Contains(err.Error(), "public.users.id") {
t.Fatalf("expected conflicting column path in error, got: %v", err)
}
}
+2 -1
View File
@@ -13,12 +13,13 @@ func newReaderOptions(filePath, connString string) *readers.ReaderOptions {
} }
} }
func newWriterOptions(outputPath, packageName string, flattenSchema bool, nullableTypes string) *writers.WriterOptions { func newWriterOptions(outputPath, packageName string, flattenSchema bool, nullableTypes string, continueOnError bool) *writers.WriterOptions {
return &writers.WriterOptions{ return &writers.WriterOptions{
OutputPath: outputPath, OutputPath: outputPath,
PackageName: packageName, PackageName: packageName,
FlattenSchema: flattenSchema, FlattenSchema: flattenSchema,
NullableTypes: nullableTypes, NullableTypes: nullableTypes,
Prisma7: prisma7, Prisma7: prisma7,
ContinueOnError: continueOnError,
} }
} }
+1
View File
@@ -188,6 +188,7 @@ func runSplit(cmd *cobra.Command, args []string) error {
"", // no schema filter for split "", // no schema filter for split
false, // no flatten-schema for split false, // no flatten-schema for split
splitNullableTypes, splitNullableTypes,
false, // no continue-on-error for split
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to write output: %w", err) return fmt.Errorf("failed to write output: %w", err)
+34
View File
@@ -71,6 +71,40 @@ func writeTestJSON(t *testing.T, path string, tableNames []string) {
} }
} }
func writeTestJSONWithSingleColumnType(t *testing.T, path, tableName, columnType string) {
t.Helper()
db := minimalDatabase{
Name: "test_db",
Schemas: []minimalSchema{{
Name: "public",
Tables: []minimalTable{{
Name: tableName,
Schema: "public",
Columns: map[string]minimalColumn{
"id": {
Name: "id",
Table: tableName,
Schema: "public",
Type: columnType,
NotNull: true,
IsPrimaryKey: true,
AutoIncrement: true,
},
},
}},
}},
}
data, err := json.Marshal(db)
if err != nil {
t.Fatalf("failed to marshal test JSON: %v", err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatalf("failed to write test file %s: %v", path, err)
}
}
// convertState captures and restores all convert global vars. // convertState captures and restores all convert global vars.
type convertState struct { type convertState struct {
sourceType string sourceType string
+14 -15
View File
@@ -1,19 +1,19 @@
module git.warky.dev/wdevs/relspecgo module git.warky.dev/wdevs/relspecgo
go 1.24.0 go 1.25.7
require ( require (
github.com/gdamore/tcell/v2 v2.8.1 github.com/gdamore/tcell/v2 v2.13.9
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.6 github.com/jackc/pgx/v5 v5.9.2
github.com/microsoft/go-mssqldb v1.9.6 github.com/microsoft/go-mssqldb v1.10.0
github.com/rivo/tview v0.42.0 github.com/rivo/tview v0.42.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/uptrace/bun v1.2.16 github.com/uptrace/bun v1.2.18
golang.org/x/text v0.31.0 golang.org/x/text v0.37.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.44.3 modernc.org/sqlite v1.50.1
) )
require ( require (
@@ -27,9 +27,8 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
@@ -41,11 +40,11 @@ require (
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/crypto v0.45.0 // indirect golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/sys v0.44.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.43.0 // indirect
golang.org/x/term v0.37.0 // indirect golang.org/x/tools v0.45.0 // indirect
modernc.org/libc v1.67.6 // indirect modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
) )
+56 -95
View File
@@ -1,15 +1,15 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -19,15 +19,14 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= 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/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.13.9 h1:uI5l3DYPcFvHINKlGft+en23evOKL+dwtD21QR8ejVA=
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= github.com/gdamore/tcell/v2 v2.13.9/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -40,8 +39,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -52,14 +51,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg=
github.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw=
github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -73,8 +70,6 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= 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/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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@@ -95,8 +90,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.2.16 h1:QlObi6ZIK5Ao7kAALnh91HWYNZUBbVwye52fmlQM9kc= github.com/uptrace/bun v1.2.18 h1:3HnRcMfS6OBPMG1eSOzlbFJ/X/AyMEJb7rMxE6VQvDU=
github.com/uptrace/bun v1.2.16/go.mod h1:jMoNg2n56ckaawi/O/J92BHaECmrz6IRjuMWqlMaMTM= github.com/uptrace/bun v1.2.18/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 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 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
@@ -105,83 +100,49 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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-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.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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-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.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.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.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
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/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.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.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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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-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.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.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.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
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.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -189,30 +150,30 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: Hein (Warky Devs) <hein@warky.dev> # Maintainer: Hein (Warky Devs) <hein@warky.dev>
pkgname=relspec pkgname=relspec
pkgver=1.0.52 pkgver=1.0.59
pkgrel=1 pkgrel=1
pkgdesc="RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs." pkgdesc="RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs."
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
+1 -1
View File
@@ -1,5 +1,5 @@
Name: relspec Name: relspec
Version: 1.0.52 Version: 1.0.59
Release: 1%{?dist} Release: 1%{?dist}
Summary: RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs. Summary: RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs.
+156
View File
@@ -0,0 +1,156 @@
package mariadb
import "strings"
// MariaDBToCanonicalTypes maps MariaDB/MySQL type names to canonical types.
var MariaDBToCanonicalTypes = map[string]string{
// Integer types
"tinyint": "int8",
"smallint": "int16",
"mediumint": "int",
"int": "int",
"integer": "int",
"int2": "int16",
"int4": "int",
"int8": "int64",
"bigint": "int64",
// Boolean (TINYINT(1) alias)
"boolean": "bool",
"bool": "bool",
"bit": "bool",
// Float types
"float": "float32",
"double": "float64",
"real": "float64",
"double precision": "float64",
// Decimal types
"decimal": "decimal",
"numeric": "decimal",
"dec": "decimal",
"fixed": "decimal",
// String types
"char": "string",
"character": "string",
"varchar": "string",
"nchar": "string",
"nvarchar": "string",
"tinytext": "text",
"text": "text",
"mediumtext": "text",
"longtext": "text",
// Binary/blob types
"binary": "bytea",
"varbinary": "bytea",
"tinyblob": "bytea",
"blob": "bytea",
"mediumblob": "bytea",
"longblob": "bytea",
// Date/time types
"date": "date",
"time": "time",
"datetime": "timestamp",
"timestamp": "timestamp",
"year": "int",
// Other types
"json": "json",
"enum": "string",
"set": "string",
"uuid": "uuid",
}
// CanonicalToMariaDBTypes maps canonical types to MariaDB/MySQL types.
var CanonicalToMariaDBTypes = map[string]string{
"bool": "TINYINT(1)",
"int8": "TINYINT",
"int16": "SMALLINT",
"int": "INT",
"int32": "INT",
"int64": "BIGINT",
"uint": "INT UNSIGNED",
"uint8": "TINYINT UNSIGNED",
"uint16": "SMALLINT UNSIGNED",
"uint32": "INT UNSIGNED",
"uint64": "BIGINT UNSIGNED",
"float32": "FLOAT",
"float64": "DOUBLE",
"decimal": "DECIMAL",
"string": "VARCHAR(255)",
"text": "TEXT",
"date": "DATE",
"time": "TIME",
"timestamp": "DATETIME",
"timestamptz": "DATETIME",
"uuid": "CHAR(36)",
"json": "JSON",
"jsonb": "JSON",
"bytea": "BLOB",
}
// MariaDBTypeSynonyms maps MariaDB/MySQL type aliases to their canonical MariaDB name.
var MariaDBTypeSynonyms = map[string]string{
"integer": "int",
"int2": "smallint",
"int4": "int",
"int8": "bigint",
"double precision": "double",
"character": "char",
"dec": "decimal",
"fixed": "decimal",
"numeric": "decimal",
"boolean": "tinyint",
"bool": "tinyint",
}
// NormalizeMariaDBType maps a MariaDB/MySQL base type (no dimension parameters)
// to its canonical MariaDB form. Unknown types are returned as-is (lowercased).
func NormalizeMariaDBType(baseType string) string {
lower := strings.ToLower(strings.TrimSpace(baseType))
if canonical, ok := MariaDBTypeSynonyms[lower]; ok {
return canonical
}
return lower
}
// ConvertMariaDBToCanonical converts a MariaDB/MySQL type name to the canonical type.
// Strips dimension parameters and normalizes aliases. Defaults to "string".
func ConvertMariaDBToCanonical(mariadbType string) string {
base := strings.ToLower(strings.TrimSpace(mariadbType))
if idx := strings.Index(base, "("); idx >= 0 {
base = strings.TrimSpace(base[:idx])
}
if canonical, ok := MariaDBToCanonicalTypes[base]; ok {
return canonical
}
// Prefix match for composite types (e.g., "unsigned bigint")
for key, canonical := range MariaDBToCanonicalTypes {
if strings.HasPrefix(base, key) {
return canonical
}
}
return "string"
}
// ConvertCanonicalToMariaDB converts a canonical type to a MariaDB/MySQL type.
// Defaults to VARCHAR(255) for unrecognised types.
func ConvertCanonicalToMariaDB(canonicalType string) string {
lower := strings.ToLower(strings.TrimSpace(canonicalType))
if idx := strings.Index(lower, "("); idx >= 0 {
lower = strings.TrimSpace(lower[:idx])
}
if mariadbType, ok := CanonicalToMariaDBTypes[lower]; ok {
return mariadbType
}
// Prefix fallback
for canonical, mariadb := range CanonicalToMariaDBTypes {
if strings.HasPrefix(lower, canonical) {
return mariadb
}
}
return "VARCHAR(255)"
}
+126 -1
View File
@@ -5,9 +5,11 @@ package merge
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/pgsql"
) )
// MergeResult represents the result of a merge operation // MergeResult represents the result of a merge operation
@@ -22,6 +24,16 @@ type MergeResult struct {
EnumsAdded int EnumsAdded int
ViewsAdded int ViewsAdded int
SequencesAdded int SequencesAdded int
TypeConflicts []ColumnTypeConflict
}
// ColumnTypeConflict describes a column that exists in both schemas but with incompatible types.
type ColumnTypeConflict struct {
Schema string
Table string
Column string
TargetType string
SourceType string
} }
// MergeOptions contains options for merge operations // MergeOptions contains options for merge operations
@@ -146,11 +158,19 @@ func (r *MergeResult) mergeColumns(table *models.Table, srcTable *models.Table)
// Merge columns // Merge columns
for colName, srcCol := range srcTable.Columns { for colName, srcCol := range srcTable.Columns {
if _, exists := existingColumns[colName]; !exists { if tgtCol, exists := existingColumns[colName]; !exists {
// Column doesn't exist, add it // Column doesn't exist, add it
newCol := cloneColumn(srcCol) newCol := cloneColumn(srcCol)
table.Columns[colName] = newCol table.Columns[colName] = newCol
r.ColumnsAdded++ r.ColumnsAdded++
} else if columnTypeConflict(tgtCol, srcCol) {
r.TypeConflicts = append(r.TypeConflicts, ColumnTypeConflict{
Schema: firstNonEmpty(table.Schema, srcTable.Schema, srcCol.Schema),
Table: firstNonEmpty(table.Name, srcTable.Name, srcCol.Table),
Column: firstNonEmpty(tgtCol.Name, srcCol.Name, colName),
TargetType: describeColumnType(tgtCol),
SourceType: describeColumnType(srcCol),
})
} }
} }
} }
@@ -426,6 +446,78 @@ func cloneColumn(col *models.Column) *models.Column {
return newCol return newCol
} }
func columnTypeConflict(target, source *models.Column) bool {
if target == nil || source == nil {
return false
}
tType, tLen, tPrec, tScale := extractTypeParts(target)
sType, sLen, sPrec, sScale := extractTypeParts(source)
return tType != sType || tLen != sLen || tPrec != sPrec || tScale != sScale
}
// extractTypeParts returns the canonical base type and dimensions for a column,
// handling the case where dimensions are embedded in the type string (e.g. "char(2)")
// rather than stored in the separate Length/Precision/Scale fields.
func extractTypeParts(col *models.Column) (baseType string, length, precision, scale int) {
typeName := strings.ToLower(strings.TrimSpace(col.Type))
length, precision, scale = col.Length, col.Precision, col.Scale
if idx := strings.Index(typeName, "("); idx >= 0 {
inner := strings.TrimRight(strings.TrimSpace(typeName[idx+1:]), ")")
typeName = strings.TrimSpace(typeName[:idx])
parts := strings.Split(inner, ",")
if len(parts) == 2 {
if p, err := strconv.Atoi(strings.TrimSpace(parts[0])); err == nil && p > 0 && precision == 0 {
precision = p
}
if s, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil && s > 0 && scale == 0 {
scale = s
}
} else if len(parts) == 1 {
if l, err := strconv.Atoi(strings.TrimSpace(parts[0])); err == nil && l > 0 && length == 0 && precision == 0 {
length = l
}
}
}
typeName = pgsql.NormalizePGType(typeName)
return typeName, length, precision, scale
}
func describeColumnType(col *models.Column) string {
if col == nil {
return ""
}
typeName := strings.TrimSpace(col.Type)
if typeName == "" {
return ""
}
switch {
case col.Precision > 0 && col.Scale > 0:
return fmt.Sprintf("%s(%d,%d)", typeName, col.Precision, col.Scale)
case col.Precision > 0:
return fmt.Sprintf("%s(%d)", typeName, col.Precision)
case col.Length > 0:
return fmt.Sprintf("%s(%d)", typeName, col.Length)
default:
return typeName
}
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func cloneConstraint(constraint *models.Constraint) *models.Constraint { func cloneConstraint(constraint *models.Constraint) *models.Constraint {
if constraint == nil { if constraint == nil {
return nil return nil
@@ -609,6 +701,7 @@ func GetMergeSummary(result *MergeResult) string {
fmt.Sprintf("Enums added: %d", result.EnumsAdded), fmt.Sprintf("Enums added: %d", result.EnumsAdded),
fmt.Sprintf("Relations added: %d", result.RelationsAdded), fmt.Sprintf("Relations added: %d", result.RelationsAdded),
fmt.Sprintf("Domains added: %d", result.DomainsAdded), fmt.Sprintf("Domains added: %d", result.DomainsAdded),
fmt.Sprintf("Type conflicts: %d", len(result.TypeConflicts)),
} }
totalAdded := result.SchemasAdded + result.TablesAdded + result.ColumnsAdded + totalAdded := result.SchemasAdded + result.TablesAdded + result.ColumnsAdded +
@@ -625,3 +718,35 @@ func GetMergeSummary(result *MergeResult) string {
return summary return summary
} }
// GetColumnTypeConflictSummary returns a short, human-readable conflict summary.
func GetColumnTypeConflictSummary(result *MergeResult, limit int) string {
if result == nil || len(result.TypeConflicts) == 0 {
return ""
}
if limit <= 0 {
limit = len(result.TypeConflicts)
}
lines := make([]string, 0, min(limit, len(result.TypeConflicts))+1)
lines = append(lines, "column type conflicts detected:")
for i, conflict := range result.TypeConflicts {
if i >= limit {
break
}
lines = append(lines, fmt.Sprintf(" - %s.%s.%s: target=%s source=%s",
conflict.Schema, conflict.Table, conflict.Column, conflict.TargetType, conflict.SourceType))
}
if len(result.TypeConflicts) > limit {
lines = append(lines, fmt.Sprintf(" ... and %d more", len(result.TypeConflicts)-limit))
}
return strings.Join(lines, "\n")
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
+62
View File
@@ -1,6 +1,7 @@
package merge package merge
import ( import (
"strings"
"testing" "testing"
"git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/models"
@@ -140,6 +141,61 @@ func TestMergeColumns_NewColumn(t *testing.T) {
} }
} }
func TestMergeColumns_TypeConflictIsDetected(t *testing.T) {
target := &models.Database{
Schemas: []*models.Schema{
{
Name: "public",
Tables: []*models.Table{
{
Name: "users",
Schema: "public",
Columns: map[string]*models.Column{
"email": {Name: "email", Type: "varchar", Length: 255},
},
},
},
},
},
}
source := &models.Database{
Schemas: []*models.Schema{
{
Name: "public",
Tables: []*models.Table{
{
Name: "users",
Schema: "public",
Columns: map[string]*models.Column{
"email": {Name: "email", Type: "text"},
},
},
},
},
},
}
result := MergeDatabases(target, source, nil)
if len(result.TypeConflicts) != 1 {
t.Fatalf("Expected 1 type conflict, got %d", len(result.TypeConflicts))
}
conflict := result.TypeConflicts[0]
if conflict.Schema != "public" || conflict.Table != "users" || conflict.Column != "email" {
t.Fatalf("Unexpected conflict location: %+v", conflict)
}
if conflict.TargetType != "varchar(255)" {
t.Fatalf("Expected target type varchar(255), got %q", conflict.TargetType)
}
if conflict.SourceType != "text" {
t.Fatalf("Expected source type text, got %q", conflict.SourceType)
}
if got := target.Schemas[0].Tables[0].Columns["email"].Type; got != "varchar" {
t.Fatalf("Expected target column type to remain unchanged, got %q", got)
}
}
func TestMergeConstraints_NewConstraint(t *testing.T) { func TestMergeConstraints_NewConstraint(t *testing.T) {
target := &models.Database{ target := &models.Database{
Schemas: []*models.Schema{ Schemas: []*models.Schema{
@@ -509,6 +565,9 @@ func TestGetMergeSummary(t *testing.T) {
ConstraintsAdded: 3, ConstraintsAdded: 3,
IndexesAdded: 2, IndexesAdded: 2,
ViewsAdded: 1, ViewsAdded: 1,
TypeConflicts: []ColumnTypeConflict{
{Schema: "public", Table: "users", Column: "email", TargetType: "varchar(255)", SourceType: "text"},
},
} }
summary := GetMergeSummary(result) summary := GetMergeSummary(result)
@@ -518,6 +577,9 @@ func TestGetMergeSummary(t *testing.T) {
if len(summary) < 50 { if len(summary) < 50 {
t.Errorf("Summary seems too short: %s", summary) t.Errorf("Summary seems too short: %s", summary)
} }
if !strings.Contains(summary, "Type conflicts: 1") {
t.Errorf("Expected type conflict count in summary, got: %s", summary)
}
} }
func TestGetMergeSummary_Nil(t *testing.T) { func TestGetMergeSummary_Nil(t *testing.T) {
+76 -32
View File
@@ -2,32 +2,73 @@ package mssql
import "strings" import "strings"
// CanonicalToMSSQLTypes maps canonical types to MSSQL types // CanonicalToMSSQLTypes maps canonical types to MSSQL types.
// Accepts both Go canonical names ("int", "string") and SQL canonical names
// ("integer", "varchar") so the writer handles input from any reader.
var CanonicalToMSSQLTypes = map[string]string{ var CanonicalToMSSQLTypes = map[string]string{
// Boolean — Go and SQL canonical
"bool": "BIT", "bool": "BIT",
"boolean": "BIT",
// Integer — Go canonical
"int8": "TINYINT", "int8": "TINYINT",
"int16": "SMALLINT", "int16": "SMALLINT",
"int": "INT", "int": "INT",
"int32": "INT", "int32": "INT",
"int64": "BIGINT", "int64": "BIGINT",
"uint": "BIGINT", "uint": "BIGINT",
"uint8": "SMALLINT", "uint8": "TINYINT",
"uint16": "INT", "uint16": "SMALLINT",
"uint32": "BIGINT", "uint32": "BIGINT",
"uint64": "BIGINT", "uint64": "BIGINT",
// Integer — SQL canonical (serial types map to base integer; IDENTITY is set via AutoIncrement)
"integer": "INT",
"smallint": "SMALLINT",
"bigint": "BIGINT",
"tinyint": "TINYINT",
"serial": "INT",
"smallserial": "SMALLINT",
"bigserial": "BIGINT",
// Float — Go canonical
"float32": "REAL", "float32": "REAL",
"float64": "FLOAT", "float64": "FLOAT",
// Float — SQL canonical
"real": "REAL",
"double precision": "FLOAT",
"double": "FLOAT",
// Decimal/numeric
"decimal": "NUMERIC", "decimal": "NUMERIC",
"numeric": "NUMERIC",
"money": "MONEY",
// String — Go canonical
"string": "NVARCHAR(255)", "string": "NVARCHAR(255)",
"text": "NVARCHAR(MAX)", "text": "NVARCHAR(MAX)",
// String — SQL canonical
"varchar": "NVARCHAR(255)",
"char": "NCHAR",
"nvarchar": "NVARCHAR(255)",
"nchar": "NCHAR",
"citext": "NVARCHAR(MAX)",
// Date/time
"date": "DATE", "date": "DATE",
"time": "TIME", "time": "TIME",
"timetz": "DATETIMEOFFSET",
"timestamp": "DATETIME2", "timestamp": "DATETIME2",
"timestamptz": "DATETIMEOFFSET", "timestamptz": "DATETIMEOFFSET",
"datetime": "DATETIME2",
"interval": "NVARCHAR(50)",
// UUID
"uuid": "UNIQUEIDENTIFIER", "uuid": "UNIQUEIDENTIFIER",
// JSON — MSSQL has no native JSON type; stored as NVARCHAR(MAX)
"json": "NVARCHAR(MAX)", "json": "NVARCHAR(MAX)",
"jsonb": "NVARCHAR(MAX)", "jsonb": "NVARCHAR(MAX)",
// Binary
"bytea": "VARBINARY(MAX)", "bytea": "VARBINARY(MAX)",
"blob": "VARBINARY(MAX)",
// Network/geo types — no MSSQL native equivalent
"xml": "XML",
"inet": "NVARCHAR(45)",
"cidr": "NVARCHAR(43)",
"macaddr": "NVARCHAR(17)",
} }
// MSSQLToCanonicalTypes maps MSSQL types to canonical types // MSSQLToCanonicalTypes maps MSSQL types to canonical types
@@ -68,47 +109,50 @@ var MSSQLToCanonicalTypes = map[string]string{
"geometry": "string", "geometry": "string",
} }
// ConvertCanonicalToMSSQL converts a canonical type to MSSQL type // MSSQLTypeSynonyms maps MSSQL type aliases to their canonical MSSQL name.
var MSSQLTypeSynonyms = map[string]string{
"integer": "int",
"dec": "decimal",
"float(n)": "float",
}
// NormalizeMSSQLType maps an MSSQL base type (no dimension parameters) to its
// canonical MSSQL form. Unknown types are returned as-is (lowercased).
func NormalizeMSSQLType(baseType string) string {
lower := strings.ToLower(strings.TrimSpace(baseType))
if canonical, ok := MSSQLTypeSynonyms[lower]; ok {
return canonical
}
return lower
}
// ConvertCanonicalToMSSQL converts a canonical type (Go or SQL) to an MSSQL type.
// Strips dimension parameters before lookup. Defaults to NVARCHAR(255) for unknown types.
func ConvertCanonicalToMSSQL(canonicalType string) string { func ConvertCanonicalToMSSQL(canonicalType string) string {
// Check direct mapping base := strings.ToLower(strings.TrimSpace(canonicalType))
if mssqlType, exists := CanonicalToMSSQLTypes[strings.ToLower(canonicalType)]; exists { if idx := strings.Index(base, "("); idx >= 0 {
base = strings.TrimSpace(base[:idx])
}
base = strings.TrimSuffix(base, "[]")
if mssqlType, exists := CanonicalToMSSQLTypes[base]; exists {
return mssqlType return mssqlType
} }
// Try to find by prefix
lowerType := strings.ToLower(canonicalType)
for canonical, mssql := range CanonicalToMSSQLTypes {
if strings.HasPrefix(lowerType, canonical) {
return mssql
}
}
// Default to NVARCHAR
return "NVARCHAR(255)" return "NVARCHAR(255)"
} }
// ConvertMSSQLToCanonical converts an MSSQL type to canonical type // ConvertMSSQLToCanonical converts an MSSQL type to the canonical type.
// Strips dimension parameters before lookup. Defaults to "string" for unknown types.
func ConvertMSSQLToCanonical(mssqlType string) string { func ConvertMSSQLToCanonical(mssqlType string) string {
// Extract base type (remove parentheses and parameters) base := strings.ToLower(strings.TrimSpace(mssqlType))
baseType := mssqlType if idx := strings.Index(base, "("); idx >= 0 {
if idx := strings.Index(baseType, "("); idx != -1 { base = strings.TrimSpace(base[:idx])
baseType = baseType[:idx]
} }
baseType = strings.TrimSpace(baseType)
// Check direct mapping if canonicalType, exists := MSSQLToCanonicalTypes[base]; exists {
if canonicalType, exists := MSSQLToCanonicalTypes[strings.ToLower(baseType)]; exists {
return canonicalType return canonicalType
} }
// Try to find by prefix
lowerType := strings.ToLower(baseType)
for mssql, canonical := range MSSQLToCanonicalTypes {
if strings.HasPrefix(lowerType, mssql) {
return canonical
}
}
// Default to string
return "string" return "string"
} }
+58
View File
@@ -45,6 +45,7 @@ var GoToStdTypes = map[string]string{
"sqldate": "date", "sqldate": "date",
"sqltime": "time", "sqltime": "time",
"sqltimestamp": "timestamp", "sqltimestamp": "timestamp",
"time.Time": "timestamp",
} }
var GoToPGSQLTypes = map[string]string{ var GoToPGSQLTypes = map[string]string{
@@ -90,6 +91,7 @@ var GoToPGSQLTypes = map[string]string{
"sqldate": "date", "sqldate": "date",
"sqltime": "time", "sqltime": "time",
"sqltimestamp": "timestamp", "sqltimestamp": "timestamp",
"time.Time": "timestamp",
"citext": "citext", "citext": "citext",
} }
@@ -135,6 +137,62 @@ func ConvertSQLType(anytype string) string {
return anytype return anytype
} }
// PGTypeCanonical maps PostgreSQL type aliases and synonyms to their canonical base name.
// Input should be a base type (no dimension parameters, lowercase).
var PGTypeCanonical = map[string]string{
// integer aliases
"int": "integer",
"int4": "integer",
"int2": "smallint",
"int8": "bigint",
// float aliases
"float4": "real",
"float8": "double precision",
// bool alias
"bool": "boolean",
// char aliases
"character": "char",
"character varying": "varchar",
"bpchar": "char",
// timestamp aliases
"timestamp without time zone": "timestamp",
"timestamp with time zone": "timestamptz",
// time aliases
"time without time zone": "time",
"time with time zone": "timetz",
// decimal alias
"decimal": "numeric",
}
// knownPGBaseTypes is the set of canonical PostgreSQL base types (no aliases).
var knownPGBaseTypes = map[string]struct{}{
"integer": {}, "bigint": {}, "smallint": {},
"serial": {}, "bigserial": {}, "smallserial": {},
"numeric": {}, "real": {}, "double precision": {}, "money": {},
"varchar": {}, "char": {}, "text": {}, "citext": {},
"boolean": {},
"date": {}, "time": {}, "timetz": {}, "timestamp": {}, "timestamptz": {}, "interval": {},
"uuid": {}, "json": {}, "jsonb": {}, "bytea": {},
"inet": {}, "cidr": {}, "macaddr": {}, "xml": {},
}
// NormalizePGType maps a PostgreSQL base type (no dimension parameters) to its
// canonical form. Unknown types are returned as-is (lowercased).
func NormalizePGType(baseType string) string {
lower := strings.ToLower(strings.TrimSpace(baseType))
if canonical, ok := PGTypeCanonical[lower]; ok {
return canonical
}
return lower
}
// IsKnownPGBaseType reports whether the given name (after NormalizePGType) is a
// recognized built-in PostgreSQL type. Custom types (e.g. vector, postgis) return false.
func IsKnownPGBaseType(baseType string) bool {
_, ok := knownPGBaseTypes[strings.ToLower(strings.TrimSpace(baseType))]
return ok
}
func IsGoType(pTypeName string) bool { func IsGoType(pTypeName string) bool {
for k := range GoToStdTypes { for k := range GoToStdTypes {
if strings.EqualFold(pTypeName, k) { if strings.EqualFold(pTypeName, k) {
+98
View File
@@ -145,6 +145,24 @@ var postgresTypeAliases = map[string]string{
"bool": "boolean", "bool": "boolean",
} }
var postgresEquivalentBaseTypes = map[string]string{
"character varying": "varchar",
"character": "char",
"timestamp without time zone": "timestamp",
"timestamp with time zone": "timestamptz",
"time without time zone": "time",
"time with time zone": "timetz",
}
var postgresEquivalentBaseTypeVariants = map[string][]string{
"varchar": {"varchar", "character varying"},
"char": {"char", "character"},
"timestamp": {"timestamp", "timestamp without time zone"},
"timestamptz": {"timestamptz", "timestamp with time zone"},
"time": {"time", "time without time zone"},
"timetz": {"timetz", "time with time zone"},
}
// GetPostgresBaseTypes returns a sorted-ish stable list of registered base type names. // GetPostgresBaseTypes returns a sorted-ish stable list of registered base type names.
func GetPostgresBaseTypes() []string { func GetPostgresBaseTypes() []string {
result := make([]string, 0, len(postgresBaseTypes)) result := make([]string, 0, len(postgresBaseTypes))
@@ -212,6 +230,86 @@ func CanonicalizeBaseType(baseType string) string {
return base return base
} }
// EquivalentBaseType resolves broader SQL-equivalent spellings to a common comparable form.
func EquivalentBaseType(baseType string) string {
base := CanonicalizeBaseType(baseType)
if equivalent, ok := postgresEquivalentBaseTypes[base]; ok {
return equivalent
}
return base
}
// NormalizeEquivalentSQLType returns a normalized SQL type string suitable for equality checks.
// Equivalent spellings such as "character varying(255)" and "varchar(255)" normalize identically.
func NormalizeEquivalentSQLType(sqlType string) string {
t := normalizeTypeToken(sqlType)
if t == "" {
return ""
}
arrayDepth := 0
for strings.HasSuffix(t, "[]") {
arrayDepth++
t = strings.TrimSpace(strings.TrimSuffix(t, "[]"))
}
modifier := ""
if idx := strings.Index(t, "("); idx >= 0 {
modifier = strings.TrimSpace(t[idx:])
t = strings.TrimSpace(t[:idx])
}
base := EquivalentBaseType(t)
normalized := base + modifier
for i := 0; i < arrayDepth; i++ {
normalized += "[]"
}
return normalized
}
// EquivalentSQLTypeVariants returns equivalent PostgreSQL spellings for a SQL type.
// Examples:
// - varchar(255) -> ["varchar(255)", "character varying(255)"]
// - timestamptz -> ["timestamptz", "timestamp with time zone"]
func EquivalentSQLTypeVariants(sqlType string) []string {
t := normalizeTypeToken(sqlType)
if t == "" {
return nil
}
arrayDepth := 0
for strings.HasSuffix(t, "[]") {
arrayDepth++
t = strings.TrimSpace(strings.TrimSuffix(t, "[]"))
}
modifier := ""
if idx := strings.Index(t, "("); idx >= 0 {
modifier = strings.TrimSpace(t[idx:])
t = strings.TrimSpace(t[:idx])
}
base := EquivalentBaseType(t)
bases := postgresEquivalentBaseTypeVariants[base]
if len(bases) == 0 {
bases = []string{base}
}
seen := make(map[string]bool, len(bases))
result := make([]string, 0, len(bases))
for _, variantBase := range bases {
variant := variantBase + modifier
for i := 0; i < arrayDepth; i++ {
variant += "[]"
}
if !seen[variant] {
seen[variant] = true
result = append(result, variant)
}
}
return result
}
// IsKnownPostgresType reports whether a type (including array forms) exists in the registry. // IsKnownPostgresType reports whether a type (including array forms) exists in the registry.
func IsKnownPostgresType(sqlType string) bool { func IsKnownPostgresType(sqlType string) bool {
base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType)) base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType))
+48
View File
@@ -97,3 +97,51 @@ func TestPostgresTypeRegistry_TypeParsingAndCapabilities(t *testing.T) {
}) })
} }
} }
func TestNormalizeEquivalentSQLType(t *testing.T) {
tests := []struct {
input string
want string
}{
{input: "character varying(255)", want: "varchar(255)"},
{input: "varchar(255)", want: "varchar(255)"},
{input: "timestamp with time zone", want: "timestamptz"},
{input: "timestamptz", want: "timestamptz"},
{input: "time without time zone", want: "time"},
{input: "character varying(255)[]", want: "varchar(255)[]"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := NormalizeEquivalentSQLType(tt.input)
if got != tt.want {
t.Fatalf("NormalizeEquivalentSQLType(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestEquivalentSQLTypeVariants(t *testing.T) {
tests := []struct {
input string
want []string
}{
{input: "character varying(255)", want: []string{"varchar(255)", "character varying(255)"}},
{input: "timestamptz", want: []string{"timestamptz", "timestamp with time zone"}},
{input: "text[]", want: []string{"text[]"}},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := EquivalentSQLTypeVariants(tt.input)
if len(got) != len(tt.want) {
t.Fatalf("EquivalentSQLTypeVariants(%q) len = %d, want %d (%v)", tt.input, len(got), len(tt.want), got)
}
for i := range tt.want {
if got[i] != tt.want[i] {
t.Fatalf("EquivalentSQLTypeVariants(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
}
}
})
}
}
+8
View File
@@ -270,8 +270,16 @@ func (r *Reader) queryColumns(schemaName string) (map[string]map[string]*models.
} }
if numPrecision != nil { if numPrecision != nil {
// For integer and serial types, numeric_precision is a bit-width (32, 64, 16)
// not a user-visible column parameter. Only store precision for types where
// it represents actual decimal/scale precision (numeric, decimal, float).
switch column.Type {
case "integer", "bigint", "smallint", "serial", "bigserial", "smallserial":
// skip — bit-width, not a column parameter
default:
column.Precision = *numPrecision column.Precision = *numPrecision
} }
}
if numScale != nil { if numScale != nil {
column.Scale = *numScale column.Scale = *numScale
+28 -62
View File
@@ -259,12 +259,14 @@ func (r *Reader) close() {
} }
} }
// mapDataType maps PostgreSQL data types while preserving exact type text when available. // mapDataType maps a PostgreSQL data type to its canonical RelSpec name.
// For known built-in types, dimensions are stripped from the type string (they are
// stored separately in column.Length/Precision/Scale). For custom types (e.g.
// vector(1536), postgis geometries), the full formatted type is preserved.
func (r *Reader) mapDataType(pgType, udtName, formattedType string, hasNextval bool) string { func (r *Reader) mapDataType(pgType, udtName, formattedType string, hasNextval bool) string {
normalizedPGType := strings.ToLower(strings.TrimSpace(pgType)) normalizedPGType := strings.ToLower(strings.TrimSpace(pgType))
// If the column has a nextval default, it's likely a serial type // Detect serial types from nextval defaults before anything else.
// Map to the appropriate serial type instead of the base integer type
if hasNextval { if hasNextval {
switch normalizedPGType { switch normalizedPGType {
case "integer", "int", "int4": case "integer", "int", "int4":
@@ -276,73 +278,38 @@ func (r *Reader) mapDataType(pgType, udtName, formattedType string, hasNextval b
} }
} }
// Prefer the database-provided formatted type; this preserves arrays/custom
// types/modifiers like text[], vector(1536), numeric(10,2), etc.
if strings.TrimSpace(formattedType) != "" {
return formattedType
}
// information_schema reports arrays generically as "ARRAY" with udt_name like "_text". // information_schema reports arrays generically as "ARRAY" with udt_name like "_text".
if strings.EqualFold(pgType, "ARRAY") && strings.HasPrefix(udtName, "_") && len(udtName) > 1 { if strings.EqualFold(pgType, "ARRAY") && strings.HasPrefix(udtName, "_") && len(udtName) > 1 {
return udtName[1:] + "[]" return udtName[1:] + "[]"
} }
// Map common PostgreSQL types // Use the database-formatted type when available. For known built-in types, strip
typeMap := map[string]string{ // embedded dimensions (they are stored in column.Length/Precision/Scale separately).
"integer": "integer", // For unknown/custom types, keep the full formatted string (e.g. vector(1536)).
"bigint": "bigint", if strings.TrimSpace(formattedType) != "" {
"smallint": "smallint", lower := strings.ToLower(strings.TrimSpace(formattedType))
"int": "integer", isArray := strings.HasSuffix(lower, "[]")
"int2": "smallint", base := strings.TrimSuffix(lower, "[]")
"int4": "integer", if idx := strings.Index(base, "("); idx >= 0 {
"int8": "bigint", base = strings.TrimSpace(base[:idx])
"serial": "serial", }
"bigserial": "bigserial", canonical := pgsql.NormalizePGType(base)
"smallserial": "smallserial", if pgsql.IsKnownPGBaseType(canonical) {
"numeric": "numeric", if isArray {
"decimal": "decimal", return canonical + "[]"
"real": "real", }
"double precision": "double precision", return canonical
"float4": "real", }
"float8": "double precision", return formattedType
"money": "money",
"character varying": "varchar",
"varchar": "varchar",
"character": "char",
"char": "char",
"text": "text",
"boolean": "boolean",
"bool": "boolean",
"date": "date",
"time": "time",
"time without time zone": "time",
"time with time zone": "timetz",
"timestamp": "timestamp",
"timestamp without time zone": "timestamp",
"timestamp with time zone": "timestamptz",
"timestamptz": "timestamptz",
"interval": "interval",
"uuid": "uuid",
"json": "json",
"jsonb": "jsonb",
"bytea": "bytea",
"inet": "inet",
"cidr": "cidr",
"macaddr": "macaddr",
"xml": "xml",
} }
// Try mapped type first // Fall back to normalizing the information_schema type name directly.
if mapped, exists := typeMap[normalizedPGType]; exists { canonical := pgsql.NormalizePGType(normalizedPGType)
return mapped if pgsql.IsKnownPGBaseType(canonical) {
return canonical
} }
// Use pgsql utilities if available // Return UDT name for custom types.
if pgsql.ValidSQLType(pgType) {
return pgsql.GetSQLType(pgType)
}
// Return UDT name for custom types (including array fallback when needed)
if udtName != "" { if udtName != "" {
if strings.HasPrefix(udtName, "_") && len(udtName) > 1 { if strings.HasPrefix(udtName, "_") && len(udtName) > 1 {
return udtName[1:] + "[]" return udtName[1:] + "[]"
@@ -350,7 +317,6 @@ func (r *Reader) mapDataType(pgType, udtName, formattedType string, hasNextval b
return udtName return udtName
} }
// Default to the original type
return pgType return pgType
} }
+1 -1
View File
@@ -198,7 +198,7 @@ func TestMapDataType(t *testing.T) {
{"unknown_type", "custom", "", "custom"}, // Should return UDT name {"unknown_type", "custom", "", "custom"}, // Should return UDT name
{"ARRAY", "_text", "", "text[]"}, {"ARRAY", "_text", "", "text[]"},
{"USER-DEFINED", "vector", "vector(1536)", "vector(1536)"}, {"USER-DEFINED", "vector", "vector(1536)", "vector(1536)"},
{"character varying", "varchar", "character varying(255)", "character varying(255)"}, {"character varying", "varchar", "character varying(255)", "varchar"},
} }
for _, tt := range tests { for _, tt := range tests {
+3 -52
View File
@@ -10,6 +10,7 @@ import (
"git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/readers" "git.warky.dev/wdevs/relspecgo/pkg/readers"
sqlitepkg "git.warky.dev/wdevs/relspecgo/pkg/sqlite"
) )
// Reader implements the readers.Reader interface for SQLite databases // Reader implements the readers.Reader interface for SQLite databases
@@ -183,59 +184,9 @@ func (r *Reader) close() {
} }
} }
// mapDataType maps SQLite data types to canonical types // mapDataType maps SQLite data types to canonical types.
func (r *Reader) mapDataType(sqliteType string) string { func (r *Reader) mapDataType(sqliteType string) string {
// SQLite has a flexible type system, but we map common types return sqlitepkg.ConvertSQLiteToCanonical(sqliteType)
typeMap := map[string]string{
"INTEGER": "int",
"INT": "int",
"TINYINT": "int8",
"SMALLINT": "int16",
"MEDIUMINT": "int",
"BIGINT": "int64",
"UNSIGNED BIG INT": "uint64",
"INT2": "int16",
"INT8": "int64",
"REAL": "float64",
"DOUBLE": "float64",
"DOUBLE PRECISION": "float64",
"FLOAT": "float32",
"NUMERIC": "decimal",
"DECIMAL": "decimal",
"BOOLEAN": "bool",
"BOOL": "bool",
"DATE": "date",
"DATETIME": "timestamp",
"TIMESTAMP": "timestamp",
"TEXT": "string",
"VARCHAR": "string",
"CHAR": "string",
"CHARACTER": "string",
"VARYING CHARACTER": "string",
"NCHAR": "string",
"NVARCHAR": "string",
"CLOB": "text",
"BLOB": "bytea",
}
// Try exact match first
if mapped, exists := typeMap[sqliteType]; exists {
return mapped
}
// Try case-insensitive match for common types
sqliteTypeUpper := sqliteType
if len(sqliteType) > 0 {
// Extract base type (e.g., "VARCHAR(255)" -> "VARCHAR")
for baseType := range typeMap {
if len(sqliteTypeUpper) >= len(baseType) && sqliteTypeUpper[:len(baseType)] == baseType {
return typeMap[baseType]
}
}
}
// Default to string for unknown types
return "string"
} }
// deriveRelationship creates a relationship from a foreign key constraint // deriveRelationship creates a relationship from a foreign key constraint
+152
View File
@@ -0,0 +1,152 @@
package sqlite
import "strings"
// SQLiteToCanonicalTypes maps SQLite type names to canonical types.
// SQLite has type affinity rules; this maps common type names including
// MySQL/PostgreSQL types that users write in SQLite schemas.
var SQLiteToCanonicalTypes = map[string]string{
// Integer affinity
"integer": "int",
"int": "int",
"tinyint": "int8",
"smallint": "int16",
"mediumint": "int",
"bigint": "int64",
"unsigned big int": "uint64",
"int2": "int16",
"int8": "int64",
// Real affinity
"real": "float64",
"double": "float64",
"double precision": "float64",
"float": "float32",
// Numeric affinity
"numeric": "decimal",
"decimal": "decimal",
// Boolean (stored as integer in SQLite)
"boolean": "bool",
"bool": "bool",
// Date/time (stored as text in SQLite)
"date": "date",
"datetime": "timestamp",
"timestamp": "timestamp",
// Text affinity
"text": "string",
"varchar": "string",
"char": "string",
"character": "string",
"varying character": "string",
"nchar": "string",
"nvarchar": "string",
"clob": "text",
// Blob affinity
"blob": "bytea",
}
// CanonicalToSQLiteAffinity maps type names to SQLite type affinity names.
// Accepts both Go canonical names ("int", "string") and SQL canonical names
// ("integer", "varchar") so the writer handles input from any reader.
// The five SQLite type affinities are TEXT, INTEGER, REAL, NUMERIC, BLOB.
var CanonicalToSQLiteAffinity = map[string]string{
// INTEGER affinity — Go canonical
"int": "INTEGER",
"int8": "INTEGER",
"int16": "INTEGER",
"int32": "INTEGER",
"int64": "INTEGER",
"uint": "INTEGER",
"uint8": "INTEGER",
"uint16": "INTEGER",
"uint32": "INTEGER",
"uint64": "INTEGER",
"bool": "INTEGER",
// INTEGER affinity — SQL canonical
"integer": "INTEGER",
"smallint": "INTEGER",
"bigint": "INTEGER",
"serial": "INTEGER",
"smallserial": "INTEGER",
"bigserial": "INTEGER",
"boolean": "INTEGER",
"tinyint": "INTEGER",
"mediumint": "INTEGER",
// REAL affinity — Go canonical
"float32": "REAL",
"float64": "REAL",
// REAL affinity — SQL canonical
"real": "REAL",
"float": "REAL",
"double": "REAL",
"double precision": "REAL",
// NUMERIC affinity
"decimal": "NUMERIC",
"numeric": "NUMERIC",
"money": "NUMERIC",
"smallmoney": "NUMERIC",
// BLOB affinity
"bytea": "BLOB",
"blob": "BLOB",
// TEXT affinity — Go canonical
"string": "TEXT",
"text": "TEXT",
// TEXT affinity — SQL canonical
"varchar": "TEXT",
"char": "TEXT",
"nvarchar": "TEXT",
"nchar": "TEXT",
"citext": "TEXT",
"date": "TEXT",
"time": "TEXT",
"timetz": "TEXT",
"timestamp": "TEXT",
"timestamptz": "TEXT",
"datetime": "TEXT",
"uuid": "TEXT",
"json": "TEXT",
"jsonb": "TEXT",
"xml": "TEXT",
"inet": "TEXT",
"cidr": "TEXT",
"macaddr": "TEXT",
}
// ConvertSQLiteToCanonical converts a SQLite type name to the canonical type.
// Strips dimension parameters (e.g. VARCHAR(255) → string) and handles
// SQLite's flexible affinity rules. Defaults to "string" for unknown types.
func ConvertSQLiteToCanonical(sqliteType string) string {
base := strings.ToUpper(strings.TrimSpace(sqliteType))
if idx := strings.Index(base, "("); idx >= 0 {
base = strings.TrimSpace(base[:idx])
}
lower := strings.ToLower(base)
if canonical, ok := SQLiteToCanonicalTypes[lower]; ok {
return canonical
}
// Prefix match for types like "VARYING CHARACTER(255)"
for key, canonical := range SQLiteToCanonicalTypes {
if strings.HasPrefix(lower, key) {
return canonical
}
}
return "string"
}
// ConvertCanonicalToSQLite converts a canonical type (or any SQL type) to its
// SQLite type affinity. Defaults to TEXT for unrecognised types.
func ConvertCanonicalToSQLite(canonicalType string) string {
normalized := strings.ToLower(strings.TrimSpace(canonicalType))
if idx := strings.Index(normalized, "("); idx >= 0 {
normalized = strings.TrimSpace(normalized[:idx])
}
normalized = strings.TrimSuffix(normalized, "[]")
if affinity, ok := CanonicalToSQLiteAffinity[normalized]; ok {
return affinity
}
return "TEXT"
}
+18
View File
@@ -26,7 +26,10 @@ type ModelData struct {
Fields []*FieldData Fields []*FieldData
Config *MethodConfig Config *MethodConfig
PrimaryKeyField string // Name of the primary key field PrimaryKeyField string // Name of the primary key field
PrimaryKeyType string // Go type of the primary key field
PrimaryKeyIsSQL bool // Whether PK uses SQL type (needs .Int64() call) PrimaryKeyIsSQL bool // Whether PK uses SQL type (needs .Int64() call)
PrimaryKeyIsStr bool // Whether helper methods should use string IDs
PrimaryKeyIDType string // Helper method GetID/SetID/UpdateID type
IDColumnName string // Name of the ID column in database IDColumnName string // Name of the ID column in database
Prefix string // 3-letter prefix Prefix string // 3-letter prefix
} }
@@ -140,7 +143,13 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper, fl
model.IDColumnName = safeName model.IDColumnName = safeName
// Check if PK type is a SQL type (contains resolvespec_common or sql_types) // Check if PK type is a SQL type (contains resolvespec_common or sql_types)
goType := typeMapper.SQLTypeToGoType(col.Type, col.NotNull) goType := typeMapper.SQLTypeToGoType(col.Type, col.NotNull)
model.PrimaryKeyType = goType
model.PrimaryKeyIsSQL = strings.Contains(goType, "resolvespec_common") || strings.Contains(goType, "sql_types") model.PrimaryKeyIsSQL = strings.Contains(goType, "resolvespec_common") || strings.Contains(goType, "sql_types")
model.PrimaryKeyIsStr = isStringLikePrimaryKeyType(goType)
model.PrimaryKeyIDType = "int64"
if model.PrimaryKeyIsStr {
model.PrimaryKeyIDType = "string"
}
break break
} }
} }
@@ -192,6 +201,15 @@ func formatComment(description, comment string) string {
return comment return comment
} }
func isStringLikePrimaryKeyType(goType string) bool {
switch goType {
case "string", "sql.NullString", "resolvespec_common.SqlString", "resolvespec_common.SqlUUID":
return true
default:
return false
}
}
// resolveFieldNameCollision checks if a field name conflicts with generated method names // resolveFieldNameCollision checks if a field name conflicts with generated method names
// and adds an underscore suffix if there's a collision // and adds an underscore suffix if there's a collision
func resolveFieldNameCollision(fieldName string) string { func resolveFieldNameCollision(fieldName string) string {
+27 -5
View File
@@ -44,33 +44,55 @@ func (m {{.Name}}) SchemaName() string {
{{end}} {{end}}
{{if and .Config.GenerateGetID .PrimaryKeyField}} {{if and .Config.GenerateGetID .PrimaryKeyField}}
// GetID returns the primary key value // GetID returns the primary key value
func (m {{.Name}}) GetID() int64 { func (m {{.Name}}) GetID() {{.PrimaryKeyIDType}} {
{{if .PrimaryKeyIsSQL -}} {{if .PrimaryKeyIsSQL -}}
{{if .PrimaryKeyIsStr -}}
return m.{{.PrimaryKeyField}}.String()
{{- else -}}
return m.{{.PrimaryKeyField}}.Int64() return m.{{.PrimaryKeyField}}.Int64()
{{- end}}
{{- else -}}
{{if .PrimaryKeyIsStr -}}
return m.{{.PrimaryKeyField}}
{{- else -}} {{- else -}}
return int64(m.{{.PrimaryKeyField}}) return int64(m.{{.PrimaryKeyField}})
{{- end}} {{- end}}
{{- end}}
} }
{{end}} {{end}}
{{if and .Config.GenerateGetIDStr .PrimaryKeyField}} {{if and .Config.GenerateGetIDStr .PrimaryKeyField}}
// GetIDStr returns the primary key as a string // GetIDStr returns the primary key as a string
func (m {{.Name}}) GetIDStr() string { func (m {{.Name}}) GetIDStr() string {
{{if .PrimaryKeyIsSQL -}}
return m.{{.PrimaryKeyField}}.String()
{{- else if .PrimaryKeyIsStr -}}
return m.{{.PrimaryKeyField}}
{{- else -}}
return fmt.Sprintf("%d", m.{{.PrimaryKeyField}}) return fmt.Sprintf("%d", m.{{.PrimaryKeyField}})
{{- end}}
} }
{{end}} {{end}}
{{if and .Config.GenerateSetID .PrimaryKeyField}} {{if and .Config.GenerateSetID .PrimaryKeyField}}
// SetID sets the primary key value // SetID sets the primary key value
func (m {{.Name}}) SetID(newid int64) { func (m {{.Name}}) SetID(newid {{.PrimaryKeyIDType}}) {
m.UpdateID(newid) m.UpdateID(newid)
} }
{{end}} {{end}}
{{if and .Config.GenerateUpdateID .PrimaryKeyField}} {{if and .Config.GenerateUpdateID .PrimaryKeyField}}
// UpdateID updates the primary key value // UpdateID updates the primary key value
func (m *{{.Name}}) UpdateID(newid int64) { func (m *{{.Name}}) UpdateID(newid {{.PrimaryKeyIDType}}) {
{{if .PrimaryKeyIsSQL -}} {{if .PrimaryKeyIsSQL -}}
m.{{.PrimaryKeyField}}.FromString(fmt.Sprintf("%d", newid)) {{if .PrimaryKeyIsStr -}}
m.{{.PrimaryKeyField}}.FromString(newid)
{{- else -}} {{- else -}}
m.{{.PrimaryKeyField}} = int32(newid) m.{{.PrimaryKeyField}}.FromString(fmt.Sprintf("%d", newid))
{{- end}}
{{- else -}}
{{if .PrimaryKeyIsStr -}}
m.{{.PrimaryKeyField}} = newid
{{- else -}}
m.{{.PrimaryKeyField}} = {{.PrimaryKeyType}}(newid)
{{- end}}
{{- end}} {{- end}}
} }
{{end}} {{end}}
+24 -4
View File
@@ -102,8 +102,8 @@ func (w *Writer) writeSingleFile(db *models.Database) error {
} }
} }
// Add fmt import if GetIDStr is enabled // Add fmt import when generated helper methods need string formatting.
if w.config.GenerateGetIDStr { if w.needsFmtImport(templateData.Models) {
templateData.AddImport("\"fmt\"") templateData.AddImport("\"fmt\"")
} }
@@ -195,8 +195,8 @@ func (w *Writer) writeMultiFile(db *models.Database) error {
} }
} }
// Add fmt import if GetIDStr is enabled // Add fmt import when generated helper methods need string formatting.
if w.config.GenerateGetIDStr { if w.needsFmtImport(templateData.Models) {
templateData.AddImport("\"fmt\"") templateData.AddImport("\"fmt\"")
} }
@@ -301,6 +301,26 @@ func (w *Writer) addRelationshipFields(modelData *ModelData, table *models.Table
} }
} }
func (w *Writer) needsFmtImport(models []*ModelData) bool {
if w.config.GenerateGetIDStr {
for _, model := range models {
if model.PrimaryKeyField != "" && !model.PrimaryKeyIsSQL && !model.PrimaryKeyIsStr {
return true
}
}
}
if w.config.GenerateUpdateID {
for _, model := range models {
if model.PrimaryKeyField != "" && model.PrimaryKeyIsSQL && !model.PrimaryKeyIsStr {
return true
}
}
}
return false
}
// findTable finds a table by schema and name in the database // findTable finds a table by schema and name in the database
func (w *Writer) findTable(schemaName, tableName string, db *models.Database) *models.Table { func (w *Writer) findTable(schemaName, tableName string, db *models.Database) *models.Table {
for _, schema := range db.Schemas { for _, schema := range db.Schemas {
+110
View File
@@ -590,6 +590,116 @@ func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) {
} }
} }
func TestWriter_UpdateIDTypeSafety_Bun(t *testing.T) {
tests := []struct {
name string
pkType string
expectedPK string
expectedLine string
forbidInt32 bool
}{
{"int32_pk", "int", "int32", "m.ID = int32(newid)", false},
{"sql_int16_pk", "smallint", "resolvespec_common.SqlInt16", "m.ID.FromString(fmt.Sprintf(\"%d\", newid))", true},
{"int64_pk", "bigint", "int64", "m.ID = int64(newid)", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
table := models.InitTable("test_table", "public")
table.Columns["id"] = &models.Column{
Name: "id",
Type: tt.pkType,
NotNull: true,
IsPrimaryKey: true,
}
tmpDir := t.TempDir()
opts := &writers.WriterOptions{
PackageName: "models",
OutputPath: filepath.Join(tmpDir, "test.go"),
}
writer := NewWriter(opts)
err := writer.WriteTable(table)
if err != nil {
t.Fatalf("WriteTable failed: %v", err)
}
content, err := os.ReadFile(opts.OutputPath)
if err != nil {
t.Fatalf("Failed to read generated file: %v", err)
}
generated := string(content)
if !strings.Contains(generated, tt.expectedLine) {
t.Errorf("Expected UpdateID to include %s\nGenerated:\n%s", tt.expectedLine, generated)
}
if !strings.Contains(generated, "ID "+tt.expectedPK) {
t.Errorf("Expected generated primary key field type %s\nGenerated:\n%s", tt.expectedPK, generated)
}
if tt.forbidInt32 && strings.Contains(generated, "int32(newid)") {
t.Errorf("UpdateID should not cast to int32 for %s type\nGenerated:\n%s", tt.pkType, generated)
}
if !strings.Contains(generated, "UpdateID(newid int64)") {
t.Errorf("UpdateID should accept int64 parameter\nGenerated:\n%s", generated)
}
})
}
}
func TestWriter_StringPrimaryKeyHelpers_Bun(t *testing.T) {
table := models.InitTable("accounts", "public")
table.Columns["id"] = &models.Column{
Name: "id",
Type: "uuid",
NotNull: true,
IsPrimaryKey: true,
}
tmpDir := t.TempDir()
opts := &writers.WriterOptions{
PackageName: "models",
OutputPath: filepath.Join(tmpDir, "test.go"),
}
writer := NewWriter(opts)
err := writer.WriteTable(table)
if err != nil {
t.Fatalf("WriteTable failed: %v", err)
}
content, err := os.ReadFile(opts.OutputPath)
if err != nil {
t.Fatalf("Failed to read generated file: %v", err)
}
generated := string(content)
expectations := []string{
"resolvespec_common.SqlUUID",
"func (m ModelPublicAccounts) GetID() string",
"return m.ID.String()",
"func (m ModelPublicAccounts) GetIDStr() string",
"func (m ModelPublicAccounts) SetID(newid string)",
"func (m *ModelPublicAccounts) UpdateID(newid string)",
"m.ID.FromString(newid)",
}
for _, expected := range expectations {
if !strings.Contains(generated, expected) {
t.Errorf("Generated code missing expected content: %q\nGenerated:\n%s", expected, generated)
}
}
if strings.Contains(generated, "GetID() int64") || strings.Contains(generated, "UpdateID(newid int64)") {
t.Errorf("String primary keys should not use int64 helper signatures\nGenerated:\n%s", generated)
}
}
func TestTypeMapper_BuildBunTag(t *testing.T) { func TestTypeMapper_BuildBunTag(t *testing.T) {
mapper := NewTypeMapper("") mapper := NewTypeMapper("")
+21 -1
View File
@@ -2,6 +2,7 @@ package gorm
import ( import (
"sort" "sort"
"strings"
"git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers" "git.warky.dev/wdevs/relspecgo/pkg/writers"
@@ -26,6 +27,9 @@ type ModelData struct {
Config *MethodConfig Config *MethodConfig
PrimaryKeyField string // Name of the primary key field PrimaryKeyField string // Name of the primary key field
PrimaryKeyType string // Go type of the primary key field PrimaryKeyType string // Go type of the primary key field
PrimaryKeyIsSQL bool // Whether PK uses a SQL wrapper type
PrimaryKeyIsStr bool // Whether helper methods should use string IDs
PrimaryKeyIDType string // Helper method GetID/SetID/UpdateID type
IDColumnName string // Name of the ID column in database IDColumnName string // Name of the ID column in database
Prefix string // 3-letter prefix Prefix string // 3-letter prefix
} }
@@ -136,7 +140,14 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper, fl
// Sanitize column name to remove backticks // Sanitize column name to remove backticks
safeName := writers.SanitizeStructTagValue(col.Name) safeName := writers.SanitizeStructTagValue(col.Name)
model.PrimaryKeyField = SnakeCaseToPascalCase(safeName) model.PrimaryKeyField = SnakeCaseToPascalCase(safeName)
model.PrimaryKeyType = typeMapper.SQLTypeToGoType(col.Type, col.NotNull) goType := typeMapper.SQLTypeToGoType(col.Type, col.NotNull)
model.PrimaryKeyType = goType
model.PrimaryKeyIsSQL = strings.Contains(goType, "sql_types.") || strings.Contains(goType, "sql.")
model.PrimaryKeyIsStr = isStringLikePrimaryKeyType(goType)
model.PrimaryKeyIDType = "int64"
if model.PrimaryKeyIsStr {
model.PrimaryKeyIDType = "string"
}
model.IDColumnName = safeName model.IDColumnName = safeName
break break
} }
@@ -189,6 +200,15 @@ func formatComment(description, comment string) string {
return comment return comment
} }
func isStringLikePrimaryKeyType(goType string) bool {
switch goType {
case "string", "sql.NullString", "sql_types.SqlString", "sql_types.SqlUUID":
return true
default:
return false
}
}
// resolveFieldNameCollision checks if a field name conflicts with generated method names // resolveFieldNameCollision checks if a field name conflicts with generated method names
// and adds an underscore suffix if there's a collision // and adds an underscore suffix if there's a collision
func resolveFieldNameCollision(fieldName string) string { func resolveFieldNameCollision(fieldName string) string {
+33 -3
View File
@@ -43,26 +43,56 @@ func (m {{.Name}}) SchemaName() string {
{{end}} {{end}}
{{if and .Config.GenerateGetID .PrimaryKeyField}} {{if and .Config.GenerateGetID .PrimaryKeyField}}
// GetID returns the primary key value // GetID returns the primary key value
func (m {{.Name}}) GetID() int64 { func (m {{.Name}}) GetID() {{.PrimaryKeyIDType}} {
{{if .PrimaryKeyIsSQL -}}
{{if .PrimaryKeyIsStr -}}
return m.{{.PrimaryKeyField}}.String()
{{- else -}}
return m.{{.PrimaryKeyField}}.Int64()
{{- end}}
{{- else -}}
{{if .PrimaryKeyIsStr -}}
return m.{{.PrimaryKeyField}}
{{- else -}}
return int64(m.{{.PrimaryKeyField}}) return int64(m.{{.PrimaryKeyField}})
{{- end}}
{{- end}}
} }
{{end}} {{end}}
{{if and .Config.GenerateGetIDStr .PrimaryKeyField}} {{if and .Config.GenerateGetIDStr .PrimaryKeyField}}
// GetIDStr returns the primary key as a string // GetIDStr returns the primary key as a string
func (m {{.Name}}) GetIDStr() string { func (m {{.Name}}) GetIDStr() string {
{{if .PrimaryKeyIsSQL -}}
return m.{{.PrimaryKeyField}}.String()
{{- else if .PrimaryKeyIsStr -}}
return m.{{.PrimaryKeyField}}
{{- else -}}
return fmt.Sprintf("%d", m.{{.PrimaryKeyField}}) return fmt.Sprintf("%d", m.{{.PrimaryKeyField}})
{{- end}}
} }
{{end}} {{end}}
{{if and .Config.GenerateSetID .PrimaryKeyField}} {{if and .Config.GenerateSetID .PrimaryKeyField}}
// SetID sets the primary key value // SetID sets the primary key value
func (m {{.Name}}) SetID(newid int64) { func (m {{.Name}}) SetID(newid {{.PrimaryKeyIDType}}) {
m.UpdateID(newid) m.UpdateID(newid)
} }
{{end}} {{end}}
{{if and .Config.GenerateUpdateID .PrimaryKeyField}} {{if and .Config.GenerateUpdateID .PrimaryKeyField}}
// UpdateID updates the primary key value // UpdateID updates the primary key value
func (m *{{.Name}}) UpdateID(newid int64) { func (m *{{.Name}}) UpdateID(newid {{.PrimaryKeyIDType}}) {
{{if .PrimaryKeyIsSQL -}}
{{if .PrimaryKeyIsStr -}}
m.{{.PrimaryKeyField}}.FromString(newid)
{{- else -}}
m.{{.PrimaryKeyField}}.FromString(fmt.Sprintf("%d", newid))
{{- end}}
{{- else -}}
{{if .PrimaryKeyIsStr -}}
m.{{.PrimaryKeyField}} = newid
{{- else -}}
m.{{.PrimaryKeyField}} = {{.PrimaryKeyType}}(newid) m.{{.PrimaryKeyField}} = {{.PrimaryKeyType}}(newid)
{{- end}}
{{- end}}
} }
{{end}} {{end}}
{{if and .Config.GenerateGetIDName .IDColumnName}} {{if and .Config.GenerateGetIDName .IDColumnName}}
+24 -4
View File
@@ -99,8 +99,8 @@ func (w *Writer) writeSingleFile(db *models.Database) error {
} }
} }
// Add fmt import if GetIDStr is enabled // Add fmt import when generated helper methods need string formatting.
if w.config.GenerateGetIDStr { if w.needsFmtImport(templateData.Models) {
templateData.AddImport("\"fmt\"") templateData.AddImport("\"fmt\"")
} }
@@ -189,8 +189,8 @@ func (w *Writer) writeMultiFile(db *models.Database) error {
} }
} }
// Add fmt import if GetIDStr is enabled // Add fmt import when generated helper methods need string formatting.
if w.config.GenerateGetIDStr { if w.needsFmtImport(templateData.Models) {
templateData.AddImport("\"fmt\"") templateData.AddImport("\"fmt\"")
} }
@@ -295,6 +295,26 @@ func (w *Writer) addRelationshipFields(modelData *ModelData, table *models.Table
} }
} }
func (w *Writer) needsFmtImport(models []*ModelData) bool {
if w.config.GenerateGetIDStr {
for _, model := range models {
if model.PrimaryKeyField != "" && !model.PrimaryKeyIsSQL && !model.PrimaryKeyIsStr {
return true
}
}
}
if w.config.GenerateUpdateID {
for _, model := range models {
if model.PrimaryKeyField != "" && model.PrimaryKeyIsSQL && !model.PrimaryKeyIsStr {
return true
}
}
}
return false
}
// findTable finds a table by schema and name in the database // findTable finds a table by schema and name in the database
func (w *Writer) findTable(schemaName, tableName string, db *models.Database) *models.Table { func (w *Writer) findTable(schemaName, tableName string, db *models.Database) *models.Table {
for _, schema := range db.Schemas { for _, schema := range db.Schemas {
+49
View File
@@ -598,6 +598,55 @@ func TestWriter_UpdateIDTypeSafety(t *testing.T) {
} }
} }
func TestWriter_StringPrimaryKeyHelpers_Gorm(t *testing.T) {
table := models.InitTable("accounts", "public")
table.Columns["id"] = &models.Column{
Name: "id",
Type: "uuid",
NotNull: true,
IsPrimaryKey: true,
}
tmpDir := t.TempDir()
opts := &writers.WriterOptions{
PackageName: "models",
OutputPath: filepath.Join(tmpDir, "test.go"),
}
writer := NewWriter(opts)
err := writer.WriteTable(table)
if err != nil {
t.Fatalf("WriteTable failed: %v", err)
}
content, err := os.ReadFile(opts.OutputPath)
if err != nil {
t.Fatalf("Failed to read generated file: %v", err)
}
generated := string(content)
expectations := []string{
"ID string",
"func (m ModelPublicAccounts) GetID() string",
"return m.ID",
"func (m ModelPublicAccounts) GetIDStr() string",
"func (m ModelPublicAccounts) SetID(newid string)",
"func (m *ModelPublicAccounts) UpdateID(newid string)",
"m.ID = newid",
}
for _, expected := range expectations {
if !strings.Contains(generated, expected) {
t.Errorf("Generated code missing expected content: %q\nGenerated:\n%s", expected, generated)
}
}
if strings.Contains(generated, "GetID() int64") || strings.Contains(generated, "UpdateID(newid int64)") {
t.Errorf("String primary keys should not use int64 helper signatures\nGenerated:\n%s", generated)
}
}
func TestNameConverter_SnakeCaseToPascalCase(t *testing.T) { func TestNameConverter_SnakeCaseToPascalCase(t *testing.T) {
tests := []struct { tests := []struct {
input string input string
+142 -22
View File
@@ -31,6 +31,10 @@ type MigrationWriter struct {
// NewMigrationWriter creates a new templated migration writer // NewMigrationWriter creates a new templated migration writer
func NewMigrationWriter(options *writers.WriterOptions) (*MigrationWriter, error) { func NewMigrationWriter(options *writers.WriterOptions) (*MigrationWriter, error) {
if options == nil {
options = &writers.WriterOptions{}
}
executor, err := NewTemplateExecutor(options.FlattenSchema) executor, err := NewTemplateExecutor(options.FlattenSchema)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create template executor: %w", err) return nil, fmt.Errorf("failed to create template executor: %w", err)
@@ -44,6 +48,16 @@ func NewMigrationWriter(options *writers.WriterOptions) (*MigrationWriter, error
// WriteMigration generates migration scripts using templates // WriteMigration generates migration scripts using templates
func (w *MigrationWriter) WriteMigration(model *models.Database, current *models.Database) error { func (w *MigrationWriter) WriteMigration(model *models.Database, current *models.Database) error {
if model == nil {
return fmt.Errorf("model database is required")
}
if w.options == nil {
w.options = &writers.WriterOptions{}
}
if current == nil {
current = models.InitDatabase(model.Name)
}
var writer io.Writer var writer io.Writer
var file *os.File var file *os.File
var err error var err error
@@ -86,9 +100,16 @@ func (w *MigrationWriter) WriteMigration(model *models.Database, current *models
// Process each schema in the model // Process each schema in the model
for _, modelSchema := range model.Schemas { for _, modelSchema := range model.Schemas {
if modelSchema == nil {
continue
}
// Find corresponding schema in current database // Find corresponding schema in current database
var currentSchema *models.Schema var currentSchema *models.Schema
for _, cs := range current.Schemas { for _, cs := range current.Schemas {
if cs == nil {
continue
}
if strings.EqualFold(cs.Name, modelSchema.Name) { if strings.EqualFold(cs.Name, modelSchema.Name) {
currentSchema = cs currentSchema = cs
break break
@@ -123,7 +144,11 @@ func (w *MigrationWriter) WriteMigration(model *models.Database, current *models
// Write header // Write header
fmt.Fprintf(w.writer, "-- PostgreSQL Migration Script\n") fmt.Fprintf(w.writer, "-- PostgreSQL Migration Script\n")
fmt.Fprintf(w.writer, "-- Generated by RelSpec\n") fmt.Fprintf(w.writer, "-- Generated by RelSpec\n")
fmt.Fprintf(w.writer, "-- Source: %s -> %s\n\n", current.Name, model.Name) fmt.Fprintf(w.writer, "-- Source: %s -> %s\n", current.Name, model.Name)
if w.options.ContinueOnError {
fmt.Fprintf(w.writer, "\\set ON_ERROR_STOP off\n")
}
fmt.Fprintf(w.writer, "\n")
// Write scripts // Write scripts
for _, script := range scripts { for _, script := range scripts {
@@ -139,13 +164,26 @@ func (w *MigrationWriter) WriteMigration(model *models.Database, current *models
func (w *MigrationWriter) generateSchemaScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) { func (w *MigrationWriter) generateSchemaScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0) scripts := make([]MigrationScript, 0)
// Phase 1: Drop constraints and indexes that changed (Priority 11-50) if schemaRequiresPGTrgm(model) {
scripts = append(scripts, MigrationScript{
ObjectName: "extension.pg_trgm",
ObjectType: "create extension",
Schema: model.Name,
Priority: 80,
Sequence: len(scripts),
Body: "CREATE EXTENSION IF NOT EXISTS pg_trgm;",
})
}
// Phase 1: Drop constraints and indexes that changed (Priority 5-50)
var droppedFKs map[string]bool
if current != nil { if current != nil {
dropScripts, err := w.generateDropScripts(model, current) dropScripts, dropped, err := w.generateDropScripts(model, current)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate drop scripts: %w", err) return nil, fmt.Errorf("failed to generate drop scripts: %w", err)
} }
scripts = append(scripts, dropScripts...) scripts = append(scripts, dropScripts...)
droppedFKs = dropped
} }
// Phase 3: Create/Alter tables and columns (Priority 100-145) // Phase 3: Create/Alter tables and columns (Priority 100-145)
@@ -163,7 +201,7 @@ func (w *MigrationWriter) generateSchemaScripts(model *models.Schema, current *m
scripts = append(scripts, indexScripts...) scripts = append(scripts, indexScripts...)
// Phase 5: Create foreign keys (Priority 195) // Phase 5: Create foreign keys (Priority 195)
fkScripts, err := w.generateForeignKeyScripts(model, current) fkScripts, err := w.generateForeignKeyScripts(model, current, droppedFKs)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate foreign key scripts: %w", err) return nil, fmt.Errorf("failed to generate foreign key scripts: %w", err)
} }
@@ -179,9 +217,12 @@ func (w *MigrationWriter) generateSchemaScripts(model *models.Schema, current *m
return scripts, nil return scripts, nil
} }
// generateDropScripts generates DROP scripts using templates // generateDropScripts generates DROP scripts using templates.
func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) { // Returns the scripts and a set of FK constraint keys (schema.table.name) that were
// explicitly dropped because their referenced PK was being dropped, so they can be force-recreated.
func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, map[string]bool, error) {
scripts := make([]MigrationScript, 0) scripts := make([]MigrationScript, 0)
droppedFKs := make(map[string]bool)
// Build map of model tables for quick lookup // Build map of model tables for quick lookup
modelTables := make(map[string]*models.Table) modelTables := make(map[string]*models.Table)
@@ -208,6 +249,44 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
shouldDrop = true shouldDrop = true
} }
if shouldDrop && currentConstraint.Type == models.PrimaryKeyConstraint {
// Drop FK constraints that depend on this PK before dropping the PK itself.
for _, otherTable := range current.Tables {
for fkName, fkConstraint := range otherTable.Constraints {
if fkConstraint.Type != models.ForeignKeyConstraint {
continue
}
refTable := fkConstraint.ReferencedTable
refSchema := fkConstraint.ReferencedSchema
if refSchema == "" {
refSchema = current.Name
}
if strings.EqualFold(refTable, currentTable.Name) && strings.EqualFold(refSchema, current.Name) {
fkKey := fmt.Sprintf("%s.%s.%s", current.Name, otherTable.Name, fkName)
if !droppedFKs[fkKey] {
droppedFKs[fkKey] = true
sql, err := w.executor.ExecuteDropConstraint(DropConstraintData{
SchemaName: current.Name,
TableName: otherTable.Name,
ConstraintName: fkName,
})
if err != nil {
return nil, nil, err
}
scripts = append(scripts, MigrationScript{
ObjectName: fkKey,
ObjectType: "drop constraint",
Schema: current.Name,
Priority: 5,
Sequence: len(scripts),
Body: sql,
})
}
}
}
}
}
if shouldDrop { if shouldDrop {
sql, err := w.executor.ExecuteDropConstraint(DropConstraintData{ sql, err := w.executor.ExecuteDropConstraint(DropConstraintData{
SchemaName: current.Name, SchemaName: current.Name,
@@ -215,7 +294,7 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
ConstraintName: constraintName, ConstraintName: constraintName,
}) })
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
script := MigrationScript{ script := MigrationScript{
@@ -247,7 +326,7 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
IndexName: indexName, IndexName: indexName,
}) })
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
script := MigrationScript{ script := MigrationScript{
@@ -263,7 +342,7 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
} }
} }
return scripts, nil return scripts, droppedFKs, nil
} }
// generateTableScripts generates CREATE/ALTER TABLE scripts using templates // generateTableScripts generates CREATE/ALTER TABLE scripts using templates
@@ -340,7 +419,7 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
SchemaName: schema.Name, SchemaName: schema.Name,
TableName: modelTable.Name, TableName: modelTable.Name,
ColumnName: modelCol.Name, ColumnName: modelCol.Name,
ColumnType: pgsql.ConvertSQLType(modelCol.Type), ColumnType: effectiveColumnSQLType(modelCol),
Default: defaultVal, Default: defaultVal,
NotNull: modelCol.NotNull, NotNull: modelCol.NotNull,
}) })
@@ -359,12 +438,13 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
scripts = append(scripts, script) scripts = append(scripts, script)
} else if !columnsEqual(modelCol, currentCol) { } else if !columnsEqual(modelCol, currentCol) {
// Column exists but properties changed // Column exists but properties changed
if modelCol.Type != currentCol.Type { if !columnTypesEqual(modelCol, currentCol) {
sql, err := w.executor.ExecuteAlterColumnType(AlterColumnTypeData{ sql, err := w.executor.ExecuteAlterColumnType(AlterColumnTypeData{
SchemaName: schema.Name, SchemaName: schema.Name,
TableName: modelTable.Name, TableName: modelTable.Name,
ColumnName: modelCol.Name, ColumnName: modelCol.Name,
NewType: pgsql.ConvertSQLType(modelCol.Type), NewType: effectiveAlterColumnSQLType(modelCol),
UsingExpr: buildAlterColumnUsingExpression(modelCol.Name, effectiveAlterColumnSQLType(modelCol)),
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -545,12 +625,17 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo
indexType = modelIndex.Type indexType = modelIndex.Type
} }
columnExprs := buildIndexColumnExpressions(modelTable, modelIndex, indexType)
if len(columnExprs) == 0 {
continue
}
sql, err := w.executor.ExecuteCreateIndex(CreateIndexData{ sql, err := w.executor.ExecuteCreateIndex(CreateIndexData{
SchemaName: model.Name, SchemaName: model.Name,
TableName: modelTable.Name, TableName: modelTable.Name,
IndexName: indexName, IndexName: indexName,
IndexType: indexType, IndexType: indexType,
Columns: strings.Join(modelIndex.Columns, ", "), Columns: strings.Join(columnExprs, ", "),
Unique: modelIndex.Unique, Unique: modelIndex.Unique,
}) })
if err != nil { if err != nil {
@@ -573,8 +658,30 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo
return scripts, nil return scripts, nil
} }
// generateForeignKeyScripts generates ADD CONSTRAINT FOREIGN KEY scripts using templates func buildIndexColumnExpressions(table *models.Table, index *models.Index, indexType string) []string {
func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) { columnExprs := make([]string, 0, len(index.Columns))
for _, colName := range index.Columns {
colExpr := colName
if table != nil {
if col, ok := resolveIndexColumn(table, colName); ok && col != nil {
colExpr = col.SQLName()
if strings.EqualFold(indexType, "gin") {
opClass := ginOperatorClassForColumn(col, index.Comment)
if opClass != "" {
colExpr = fmt.Sprintf("%s %s", col.SQLName(), opClass)
}
}
}
}
columnExprs = append(columnExprs, colExpr)
}
return columnExprs
}
// generateForeignKeyScripts generates ADD CONSTRAINT FOREIGN KEY scripts using templates.
// forceRecreate is a set of FK constraint keys (schema.table.name) that must be recreated
// even if unchanged, because their referenced PK was dropped and recreated.
func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, current *models.Schema, forceRecreate map[string]bool) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0) scripts := make([]MigrationScript, 0)
// Build map of current tables // Build map of current tables
@@ -595,13 +702,16 @@ func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, curren
continue continue
} }
shouldCreate := true fkKey := fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, constraintName)
shouldCreate := forceRecreate[fkKey]
if currentTable != nil { if !shouldCreate {
if currentConstraint, exists := currentTable.Constraints[constraintName]; exists { if currentTable == nil {
if constraintsEqual(constraint, currentConstraint) { shouldCreate = true
shouldCreate = false } else if currentConstraint, exists := currentTable.Constraints[constraintName]; !exists {
} shouldCreate = true
} else if !constraintsEqual(constraint, currentConstraint) {
shouldCreate = true
} }
} }
@@ -828,11 +938,21 @@ func columnsEqual(col1, col2 *models.Column) bool {
if col1 == nil || col2 == nil { if col1 == nil || col2 == nil {
return false return false
} }
return strings.EqualFold(col1.Type, col2.Type) && return columnTypesEqual(col1, col2) &&
col1.NotNull == col2.NotNull && col1.NotNull == col2.NotNull &&
fmt.Sprintf("%v", col1.Default) == fmt.Sprintf("%v", col2.Default) fmt.Sprintf("%v", col1.Default) == fmt.Sprintf("%v", col2.Default)
} }
func columnTypesEqual(col1, col2 *models.Column) bool {
if col1 == nil || col2 == nil {
return false
}
return strings.EqualFold(
pgsql.NormalizeEquivalentSQLType(effectiveColumnSQLType(col1)),
pgsql.NormalizeEquivalentSQLType(effectiveColumnSQLType(col2)),
)
}
// constraintsEqual checks if two constraints are equal // constraintsEqual checks if two constraints are equal
func constraintsEqual(c1, c2 *models.Constraint) bool { func constraintsEqual(c1, c2 *models.Constraint) bool {
if c1 == nil || c2 == nil { if c1 == nil || c2 == nil {
+407
View File
@@ -97,6 +97,370 @@ func TestWriteMigration_ArrayDefault(t *testing.T) {
} }
} }
func TestWriteMigration_AltersColumnTypeWhenActualTypeDiffers(t *testing.T) {
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
currentTable := models.InitTable("learnings", "public")
currentDetails := models.InitColumn("details", "learnings", "public")
currentDetails.Type = "jsonb"
currentTable.Columns["details"] = currentDetails
currentSchema.Tables = append(currentSchema.Tables, currentTable)
current.Schemas = append(current.Schemas, currentSchema)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
modelTable := models.InitTable("learnings", "public")
modelDetails := models.InitColumn("details", "learnings", "public")
modelDetails.Type = "text"
modelTable.Columns["details"] = modelDetails
modelSchema.Tables = append(modelSchema.Tables, modelTable)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, current); err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "ALTER TABLE public.learnings") || !strings.Contains(output, "ALTER COLUMN details TYPE text") {
t.Fatalf("expected migration to alter mismatched column type, got:\n%s", output)
}
if !strings.Contains(output, `ALTER COLUMN details TYPE text USING details::text;`) {
t.Fatalf("expected migration type alter to include USING cast, got:\n%s", output)
}
}
func TestWriteMigration_UsesStorageTypeForSerialAlterStatements(t *testing.T) {
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
currentTable := models.InitTable("learnings", "public")
currentID := models.InitColumn("id", "learnings", "public")
currentID.Type = "uuid"
currentTable.Columns["id"] = currentID
currentSchema.Tables = append(currentSchema.Tables, currentTable)
current.Schemas = append(current.Schemas, currentSchema)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
modelTable := models.InitTable("learnings", "public")
modelID := models.InitColumn("id", "learnings", "public")
modelID.Type = "bigserial"
modelTable.Columns["id"] = modelID
modelSchema.Tables = append(modelSchema.Tables, modelTable)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, current); err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "ALTER COLUMN id TYPE bigint") {
t.Fatalf("expected serial alter to use bigint storage type, got:\n%s", output)
}
if strings.Contains(output, "ALTER COLUMN id TYPE bigserial;") {
t.Fatalf("did not expect invalid bigserial alter statement, got:\n%s", output)
}
if !strings.Contains(output, `ALTER COLUMN id TYPE bigint USING id::bigint;`) {
t.Fatalf("expected serial alter to include USING cast, got:\n%s", output)
}
}
func TestWriteMigration_ArrayAlterIncludesUsingCast(t *testing.T) {
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
currentTable := models.InitTable("learnings", "public")
currentTags := models.InitColumn("tags", "learnings", "public")
currentTags.Type = "text"
currentTable.Columns["tags"] = currentTags
currentSchema.Tables = append(currentSchema.Tables, currentTable)
current.Schemas = append(current.Schemas, currentSchema)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
modelTable := models.InitTable("learnings", "public")
modelTags := models.InitColumn("tags", "learnings", "public")
modelTags.Type = "text[]"
modelTable.Columns["tags"] = modelTags
modelSchema.Tables = append(modelSchema.Tables, modelTable)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, current); err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, `ALTER COLUMN tags TYPE text[] USING tags::text[];`) {
t.Fatalf("expected array alter to include USING cast, got:\n%s", output)
}
}
func TestWriteMigration_DoesNotAlterEquivalentNormalizedColumnType(t *testing.T) {
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
currentTable := models.InitTable("users", "public")
currentEmail := models.InitColumn("email", "users", "public")
currentEmail.Type = "character varying"
currentEmail.Length = 255
currentTable.Columns["email"] = currentEmail
currentSchema.Tables = append(currentSchema.Tables, currentTable)
current.Schemas = append(current.Schemas, currentSchema)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
modelTable := models.InitTable("users", "public")
modelEmail := models.InitColumn("email", "users", "public")
modelEmail.Type = "varchar(255)"
modelTable.Columns["email"] = modelEmail
modelSchema.Tables = append(modelSchema.Tables, modelTable)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, current); err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
if strings.Contains(output, "ALTER COLUMN email TYPE") {
t.Fatalf("did not expect alter type for equivalent normalized types, got:\n%s", output)
}
}
func TestWriteMigration_GinIndexOnTextUsesTrigramOperatorClass(t *testing.T) {
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
current.Schemas = append(current.Schemas, currentSchema)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
table := models.InitTable("articles", "public")
titleCol := models.InitColumn("title", "articles", "public")
titleCol.Type = "text"
table.Columns["title"] = titleCol
index := &models.Index{
Name: "idx_articles_title_gin",
Type: "gin",
Columns: []string{"title"},
}
table.Indexes[index.Name] = index
modelSchema.Tables = append(modelSchema.Tables, table)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, current); err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "CREATE EXTENSION IF NOT EXISTS pg_trgm;") {
t.Fatalf("expected trigram extension for text GIN migration index, got:\n%s", output)
}
if !strings.Contains(output, "USING gin (title gin_trgm_ops)") {
t.Fatalf("expected GIN text index to include gin_trgm_ops, got:\n%s", output)
}
}
func TestWriteMigration_GinIndexOnQuotedTextColumnUsesTrigramOperatorClass(t *testing.T) {
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
current.Schemas = append(current.Schemas, currentSchema)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
table := models.InitTable("agent_personas", "public")
nameCol := models.InitColumn("name", "agent_personas", "public")
nameCol.Type = "text"
table.Columns["name"] = nameCol
index := &models.Index{
Name: "idx_agent_personas_name_gin",
Type: "gin",
Columns: []string{`"name"`},
}
table.Indexes[index.Name] = index
modelSchema.Tables = append(modelSchema.Tables, table)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, current); err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "USING gin (name gin_trgm_ops)") {
t.Fatalf("expected quoted text column GIN index to include gin_trgm_ops, got:\n%s", output)
}
}
func TestWriteMigration_GinIndexOnTextArrayDoesNotUseTrigramOperatorClass(t *testing.T) {
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
current.Schemas = append(current.Schemas, currentSchema)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
table := models.InitTable("plans", "public")
tagsCol := models.InitColumn("tags", "plans", "public")
tagsCol.Type = "text[]"
table.Columns["tags"] = tagsCol
index := &models.Index{
Name: "idx_plans_tags",
Type: "gin",
Columns: []string{"tags"},
}
table.Indexes[index.Name] = index
modelSchema.Tables = append(modelSchema.Tables, table)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, current); err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "USING gin (tags array_ops)") {
t.Fatalf("expected GIN array index with array_ops, got:\n%s", output)
}
if strings.Contains(output, "gin_trgm_ops") {
t.Fatalf("did not expect gin_trgm_ops for text[] migration index, got:\n%s", output)
}
}
func TestWriteMigration_GinIndexOnJSONBUsesJSONBOperatorClass(t *testing.T) {
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
current.Schemas = append(current.Schemas, currentSchema)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
table := models.InitTable("learnings", "public")
detailsCol := models.InitColumn("details", "learnings", "public")
detailsCol.Type = "jsonb"
table.Columns["details"] = detailsCol
index := &models.Index{
Name: "idx_learnings_details",
Type: "gin",
Columns: []string{"details"},
}
table.Indexes[index.Name] = index
modelSchema.Tables = append(modelSchema.Tables, table)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, current); err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "USING gin (details jsonb_ops)") {
t.Fatalf("expected GIN jsonb index to include jsonb_ops, got:\n%s", output)
}
if strings.Contains(output, "gin_trgm_ops") {
t.Fatalf("did not expect gin_trgm_ops for jsonb migration index, got:\n%s", output)
}
}
func TestWriteMigration_GinIndexOnJSONBIgnoresIncompatibleTrigramOperatorClass(t *testing.T) {
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
current.Schemas = append(current.Schemas, currentSchema)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
table := models.InitTable("learnings", "public")
detailsCol := models.InitColumn("details", "learnings", "public")
detailsCol.Type = "jsonb"
table.Columns["details"] = detailsCol
index := &models.Index{
Name: "idx_learnings_details",
Type: "gin",
Columns: []string{"details"},
Comment: "gin_trgm_ops",
}
table.Indexes[index.Name] = index
modelSchema.Tables = append(modelSchema.Tables, table)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, current); err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "USING gin (details jsonb_ops)") {
t.Fatalf("expected incompatible trigram hint on jsonb to fall back to jsonb_ops, got:\n%s", output)
}
}
func TestWriteMigration_WithAudit(t *testing.T) { func TestWriteMigration_WithAudit(t *testing.T) {
// Current database (empty) // Current database (empty)
current := models.InitDatabase("testdb") current := models.InitDatabase("testdb")
@@ -322,3 +686,46 @@ func TestWriteMigration_NumericConstraintNames(t *testing.T) {
t.Error("Migration missing FOREIGN KEY") t.Error("Migration missing FOREIGN KEY")
} }
} }
func TestNewMigrationWriter_NilOptions(t *testing.T) {
writer, err := NewMigrationWriter(nil)
if err != nil {
t.Fatalf("NewMigrationWriter(nil) returned error: %v", err)
}
if writer == nil {
t.Fatal("expected writer instance")
}
if writer.options == nil {
t.Fatal("expected default writer options to be initialized")
}
}
func TestWriteMigration_NilCurrentTreatsDatabaseAsEmpty(t *testing.T) {
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
table := models.InitTable("users", "public")
idCol := models.InitColumn("id", "users", "public")
idCol.Type = "integer"
idCol.NotNull = true
table.Columns["id"] = idCol
modelSchema.Tables = append(modelSchema.Tables, table)
model.Schemas = append(model.Schemas, modelSchema)
var buf bytes.Buffer
writer, err := NewMigrationWriter(nil)
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
if err := writer.WriteMigration(model, nil); err != nil {
t.Fatalf("WriteMigration with nil current failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "CREATE TABLE") {
t.Fatalf("expected CREATE TABLE in migration output, got:\n%s", output)
}
}
+5 -15
View File
@@ -5,6 +5,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"unicode" "unicode"
"git.warky.dev/wdevs/relspecgo/pkg/pgsql"
) )
// TemplateFunctions returns a map of custom template functions // TemplateFunctions returns a map of custom template functions
@@ -162,24 +164,12 @@ func quoteIdent(s string) string {
// Type conversion functions // Type conversion functions
// goTypeToSQL converts Go type to PostgreSQL type // goTypeToSQL converts Go type to PostgreSQL type using the shared pgsql type map.
func goTypeToSQL(goType string) string { func goTypeToSQL(goType string) string {
typeMap := map[string]string{ if sqlType, ok := pgsql.GoToPGSQLTypes[goType]; ok {
"string": "text",
"int": "integer",
"int32": "integer",
"int64": "bigint",
"float32": "real",
"float64": "double precision",
"bool": "boolean",
"time.Time": "timestamp",
"[]byte": "bytea",
}
if sqlType, ok := typeMap[goType]; ok {
return sqlType return sqlType
} }
return "text" // Default return "text"
} }
// sqlTypeToGo converts PostgreSQL type to Go type // sqlTypeToGo converts PostgreSQL type to Go type
+20
View File
@@ -95,6 +95,16 @@ type AlterColumnTypeData struct {
TableName string TableName string
ColumnName string ColumnName string
NewType string NewType string
UsingExpr string
}
type AlterColumnTypeWithCheckData struct {
SchemaName string
TableName string
ColumnName string
NewType string
EquivalentTypes string
UsingExpr string
} }
// AlterColumnDefaultData contains data for alter column default template // AlterColumnDefaultData contains data for alter column default template
@@ -267,6 +277,7 @@ type CreatePrimaryKeyWithAutoGenCheckData struct {
ConstraintName string ConstraintName string
AutoGenNames string // Comma-separated list of names like "'name1', 'name2'" AutoGenNames string // Comma-separated list of names like "'name1', 'name2'"
Columns string Columns string
ColumnNames string // Comma-separated list of quoted column names like "'id', 'tenant_id'"
} }
// Execute methods for each template // Execute methods for each template
@@ -301,6 +312,15 @@ func (te *TemplateExecutor) ExecuteAlterColumnType(data AlterColumnTypeData) (st
return buf.String(), nil return buf.String(), nil
} }
func (te *TemplateExecutor) ExecuteAlterColumnTypeWithCheck(data AlterColumnTypeWithCheckData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "alter_column_type_with_check.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute alter_column_type_with_check template: %w", err)
}
return buf.String(), nil
}
// ExecuteAlterColumnDefault executes the alter column default template // ExecuteAlterColumnDefault executes the alter column default template
func (te *TemplateExecutor) ExecuteAlterColumnDefault(data AlterColumnDefaultData) (string, error) { func (te *TemplateExecutor) ExecuteAlterColumnDefault(data AlterColumnDefaultData) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
@@ -1,2 +1,2 @@
ALTER TABLE {{qual_table .SchemaName .TableName}} ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} TYPE {{.NewType}}; ALTER COLUMN {{quote_ident .ColumnName}} TYPE {{.NewType}}{{if .UsingExpr}} USING {{.UsingExpr}}{{end}};
@@ -0,0 +1,22 @@
DO $$
DECLARE
current_type text;
BEGIN
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod)
INTO current_type
FROM pg_attribute a
JOIN pg_class t ON t.oid = a.attrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = '{{.SchemaName}}'
AND t.relname = '{{.TableName}}'
AND a.attname = '{{.ColumnName}}'
AND a.attnum > 0
AND NOT a.attisdropped;
IF current_type IS NOT NULL
AND current_type <> ALL(ARRAY[{{.EquivalentTypes}}]) THEN
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} TYPE {{.NewType}}{{if .UsingExpr}} USING {{.UsingExpr}}{{end}};
END IF;
END;
$$;
@@ -1,26 +1,42 @@
DO $$ DO $$
DECLARE DECLARE
auto_pk_name text; current_pk_name text;
current_pk_matches boolean := false;
BEGIN BEGIN
-- Drop auto-generated primary key if it exists SELECT tc.constraint_name,
SELECT constraint_name INTO auto_pk_name COALESCE(
FROM information_schema.table_constraints ARRAY(
WHERE table_schema = '{{.SchemaName}}' SELECT a.attname::text
AND table_name = '{{.TableName}}' FROM pg_constraint c
AND constraint_type = 'PRIMARY KEY' JOIN pg_class t ON t.oid = c.conrelid
AND constraint_name IN ({{.AutoGenNames}}); JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN unnest(c.conkey) WITH ORDINALITY AS cols(attnum, ord)
ON TRUE
JOIN pg_attribute a
ON a.attrelid = t.oid
AND a.attnum = cols.attnum
WHERE c.contype = 'p'
AND n.nspname = '{{.SchemaName}}'
AND t.relname = '{{.TableName}}'
ORDER BY cols.ord
),
ARRAY[]::text[]
) = ARRAY[{{.ColumnNames}}]
INTO current_pk_name, current_pk_matches
FROM information_schema.table_constraints tc
WHERE tc.table_schema = '{{.SchemaName}}'
AND tc.table_name = '{{.TableName}}'
AND tc.constraint_type = 'PRIMARY KEY';
IF auto_pk_name IS NOT NULL THEN IF current_pk_name IS NOT NULL
EXECUTE 'ALTER TABLE {{qual_table .SchemaName .TableName}} DROP CONSTRAINT ' || quote_ident(auto_pk_name); AND NOT current_pk_matches
AND current_pk_name IN ({{.AutoGenNames}}) THEN
EXECUTE 'ALTER TABLE {{qual_table .SchemaName .TableName}} DROP CONSTRAINT ' || quote_ident(current_pk_name) || ' CASCADE';
END IF; END IF;
-- Add named primary key if it doesn't exist -- Add the desired primary key only when no matching primary key already exists.
IF NOT EXISTS ( IF current_pk_name IS NULL
SELECT 1 FROM information_schema.table_constraints OR (NOT current_pk_matches AND current_pk_name IN ({{.AutoGenNames}})) THEN
WHERE table_schema = '{{.SchemaName}}'
AND table_name = '{{.TableName}}'
AND constraint_name = '{{.ConstraintName}}'
) THEN
ALTER TABLE {{qual_table .SchemaName .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} PRIMARY KEY ({{.Columns}}); ALTER TABLE {{qual_table .SchemaName .TableName}} ADD CONSTRAINT {{quote_ident .ConstraintName}} PRIMARY KEY ({{.Columns}});
END IF; END IF;
END; END;
+426 -49
View File
@@ -99,7 +99,11 @@ func (w *Writer) WriteDatabase(db *models.Database) error {
// Write header comment // Write header comment
fmt.Fprintf(w.writer, "-- PostgreSQL Database Schema\n") fmt.Fprintf(w.writer, "-- PostgreSQL Database Schema\n")
fmt.Fprintf(w.writer, "-- Database: %s\n", db.Name) fmt.Fprintf(w.writer, "-- Database: %s\n", db.Name)
fmt.Fprintf(w.writer, "-- Generated by RelSpec\n\n") fmt.Fprintf(w.writer, "-- Generated by RelSpec\n")
if w.options.ContinueOnError {
fmt.Fprintf(w.writer, "\\set ON_ERROR_STOP off\n")
}
fmt.Fprintf(w.writer, "\n")
// Process each schema in the database // Process each schema in the database
for _, schema := range db.Schemas { for _, schema := range db.Schemas {
@@ -143,6 +147,10 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
statements = append(statements, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schema.SQLName())) statements = append(statements, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schema.SQLName()))
} }
if schemaRequiresPGTrgm(schema) {
statements = append(statements, `CREATE EXTENSION IF NOT EXISTS pg_trgm`)
}
// Phase 2: Create sequences // Phase 2: Create sequences
for _, table := range schema.Tables { for _, table := range schema.Tables {
pk := table.GetPrimaryKey() pk := table.GetPrimaryKey()
@@ -181,6 +189,12 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
} }
statements = append(statements, addColStmts...) statements = append(statements, addColStmts...)
alterTypeStmts, err := w.GenerateAlterColumnTypeStatements(schema)
if err != nil {
return nil, fmt.Errorf("failed to generate alter column type statements: %w", err)
}
statements = append(statements, alterTypeStmts...)
// Phase 4: Primary keys // Phase 4: Primary keys
for _, table := range schema.Tables { for _, table := range schema.Tables {
// First check for explicit PrimaryKeyConstraint // First check for explicit PrimaryKeyConstraint
@@ -228,6 +242,7 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
ConstraintName: pkName, ConstraintName: pkName,
AutoGenNames: formatStringList(autoGenPKNames), AutoGenNames: formatStringList(autoGenPKNames),
Columns: strings.Join(pkColumns, ", "), Columns: strings.Join(pkColumns, ", "),
ColumnNames: formatStringList(pkColumns),
} }
stmt, err := w.executor.ExecuteCreatePrimaryKeyWithAutoGenCheck(data) stmt, err := w.executor.ExecuteCreatePrimaryKeyWithAutoGenCheck(data)
@@ -260,16 +275,13 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
columnExprs := make([]string, 0, len(index.Columns)) columnExprs := make([]string, 0, len(index.Columns))
for _, colName := range index.Columns { for _, colName := range index.Columns {
colExpr := colName colExpr := colName
if col, ok := table.Columns[colName]; ok { if col, ok := resolveIndexColumn(table, colName); ok {
// For GIN indexes on text columns, add operator class if strings.EqualFold(indexType, "gin") {
if strings.EqualFold(indexType, "gin") && isTextType(col.Type) { if opClass := ginOperatorClassForColumn(col, index.Comment); opClass != "" {
opClass := extractOperatorClass(index.Comment)
if opClass == "" {
opClass = "gin_trgm_ops"
}
colExpr = fmt.Sprintf("%s %s", colName, opClass) colExpr = fmt.Sprintf("%s %s", colName, opClass)
} }
} }
}
columnExprs = append(columnExprs, colExpr) columnExprs = append(columnExprs, colExpr)
} }
@@ -436,6 +448,33 @@ func (w *Writer) GenerateAddColumnStatements(schema *models.Schema) ([]string, e
return statements, nil return statements, nil
} }
func (w *Writer) GenerateAlterColumnTypeStatements(schema *models.Schema) ([]string, error) {
statements := []string{}
statements = append(statements, fmt.Sprintf("-- Alter column types for schema: %s", schema.Name))
for _, table := range schema.Tables {
columns := getSortedColumns(table.Columns)
for _, col := range columns {
targetType := effectiveAlterColumnSQLType(col)
stmt, err := w.executor.ExecuteAlterColumnTypeWithCheck(AlterColumnTypeWithCheckData{
SchemaName: schema.Name,
TableName: table.Name,
ColumnName: col.Name,
NewType: targetType,
EquivalentTypes: equivalentTypeListSQL(targetType),
UsingExpr: buildAlterColumnUsingExpression(col.Name, targetType),
})
if err != nil {
return nil, fmt.Errorf("failed to generate alter column type for %s.%s.%s: %w", schema.Name, table.Name, col.Name, err)
}
statements = append(statements, stmt)
}
}
return statements, nil
}
// GenerateAddColumnsForDatabase generates ALTER TABLE ADD COLUMN statements for the entire database // GenerateAddColumnsForDatabase generates ALTER TABLE ADD COLUMN statements for the entire database
func (w *Writer) GenerateAddColumnsForDatabase(db *models.Database) ([]string, error) { func (w *Writer) GenerateAddColumnsForDatabase(db *models.Database) ([]string, error) {
statements := []string{} statements := []string{}
@@ -488,31 +527,7 @@ func (w *Writer) generateCreateTableStatement(schema *models.Schema, table *mode
func (w *Writer) generateColumnDefinition(col *models.Column) string { func (w *Writer) generateColumnDefinition(col *models.Column) string {
parts := []string{col.SQLName()} parts := []string{col.SQLName()}
// Type with length/precision - convert to valid PostgreSQL type parts = append(parts, effectiveColumnSQLType(col))
baseType := pgsql.ConvertSQLType(col.Type)
typeStr := baseType
hasExplicitTypeModifier := pgsql.HasExplicitTypeModifier(baseType)
// Only add size specifiers for types that support them
if !hasExplicitTypeModifier && col.Length > 0 && col.Precision == 0 {
if pgsql.SupportsLength(baseType) {
typeStr = fmt.Sprintf("%s(%d)", baseType, col.Length)
} else if isTextTypeWithoutLength(baseType) {
// Convert text with length to varchar
typeStr = fmt.Sprintf("varchar(%d)", col.Length)
}
// For types that don't support length (integer, bigint, etc.), ignore the length
} else if !hasExplicitTypeModifier && col.Precision > 0 {
if pgsql.SupportsPrecision(baseType) {
if col.Scale > 0 {
typeStr = fmt.Sprintf("%s(%d,%d)", baseType, col.Precision, col.Scale)
} else {
typeStr = fmt.Sprintf("%s(%d)", baseType, col.Precision)
}
}
// For types that don't support precision, ignore it
}
parts = append(parts, typeStr)
// NOT NULL // NOT NULL
if col.NotNull { if col.NotNull {
@@ -534,6 +549,64 @@ func (w *Writer) generateColumnDefinition(col *models.Column) string {
return strings.Join(parts, " ") return strings.Join(parts, " ")
} }
func effectiveColumnSQLType(col *models.Column) string {
if col == nil {
return ""
}
baseType := pgsql.ConvertSQLType(col.Type)
typeStr := baseType
hasExplicitTypeModifier := pgsql.HasExplicitTypeModifier(baseType)
if !hasExplicitTypeModifier && col.Length > 0 && col.Precision == 0 {
if pgsql.SupportsLength(baseType) {
typeStr = fmt.Sprintf("%s(%d)", baseType, col.Length)
} else if isTextTypeWithoutLength(baseType) {
typeStr = fmt.Sprintf("varchar(%d)", col.Length)
}
} else if !hasExplicitTypeModifier && col.Precision > 0 {
if pgsql.SupportsPrecision(baseType) {
if col.Scale > 0 {
typeStr = fmt.Sprintf("%s(%d,%d)", baseType, col.Precision, col.Scale)
} else {
typeStr = fmt.Sprintf("%s(%d)", baseType, col.Precision)
}
}
}
return typeStr
}
func effectiveAlterColumnSQLType(col *models.Column) string {
typeStr := effectiveColumnSQLType(col)
switch strings.ToLower(strings.TrimSpace(typeStr)) {
case "smallserial":
return "smallint"
case "serial":
return "integer"
case "bigserial":
return "bigint"
default:
return typeStr
}
}
func buildAlterColumnUsingExpression(columnName, targetType string) string {
if strings.TrimSpace(columnName) == "" || strings.TrimSpace(targetType) == "" {
return ""
}
return fmt.Sprintf("%s::%s", quoteIdent(columnName), targetType)
}
func equivalentTypeListSQL(sqlType string) string {
variants := pgsql.EquivalentSQLTypeVariants(sqlType)
quoted := make([]string, 0, len(variants))
for _, variant := range variants {
quoted = append(quoted, fmt.Sprintf("'%s'", escapeQuote(variant)))
}
return strings.Join(quoted, ", ")
}
// WriteSchema writes a single schema and all its tables // WriteSchema writes a single schema and all its tables
func (w *Writer) WriteSchema(schema *models.Schema) error { func (w *Writer) WriteSchema(schema *models.Schema) error {
if w.writer == nil { if w.writer == nil {
@@ -545,6 +618,10 @@ func (w *Writer) WriteSchema(schema *models.Schema) error {
return err return err
} }
if err := w.writeRequiredExtensions(schema); err != nil {
return err
}
// Phase 2: Create sequences (priority 80) // Phase 2: Create sequences (priority 80)
if err := w.writeSequences(schema); err != nil { if err := w.writeSequences(schema); err != nil {
return err return err
@@ -560,6 +637,10 @@ func (w *Writer) WriteSchema(schema *models.Schema) error {
return err return err
} }
if err := w.writeAlterColumnTypes(schema); err != nil {
return err
}
// Phase 4: Create primary keys (priority 160) // Phase 4: Create primary keys (priority 160)
if err := w.writePrimaryKeys(schema); err != nil { if err := w.writePrimaryKeys(schema); err != nil {
return err return err
@@ -660,6 +741,16 @@ func (w *Writer) writeCreateSchema(schema *models.Schema) error {
return nil return nil
} }
func (w *Writer) writeRequiredExtensions(schema *models.Schema) error {
if !schemaRequiresPGTrgm(schema) {
return nil
}
fmt.Fprintln(w.writer, "CREATE EXTENSION IF NOT EXISTS pg_trgm;")
fmt.Fprintln(w.writer)
return nil
}
// writeSequences generates CREATE SEQUENCE statements for identity columns // writeSequences generates CREATE SEQUENCE statements for identity columns
func (w *Writer) writeSequences(schema *models.Schema) error { func (w *Writer) writeSequences(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Sequences for schema: %s\n", schema.Name) fmt.Fprintf(w.writer, "-- Sequences for schema: %s\n", schema.Name)
@@ -753,6 +844,21 @@ func (w *Writer) writeAddColumns(schema *models.Schema) error {
return nil return nil
} }
func (w *Writer) writeAlterColumnTypes(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Alter column types for schema: %s\n", schema.Name)
statements, err := w.GenerateAlterColumnTypeStatements(schema)
if err != nil {
return err
}
for _, stmt := range statements[1:] {
fmt.Fprint(w.writer, stmt)
fmt.Fprint(w.writer, "\n")
}
return nil
}
// writePrimaryKeys generates ALTER TABLE statements for primary keys // writePrimaryKeys generates ALTER TABLE statements for primary keys
func (w *Writer) writePrimaryKeys(schema *models.Schema) error { func (w *Writer) writePrimaryKeys(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Primary keys for schema: %s\n", schema.Name) fmt.Fprintf(w.writer, "-- Primary keys for schema: %s\n", schema.Name)
@@ -806,6 +912,7 @@ func (w *Writer) writePrimaryKeys(schema *models.Schema) error {
ConstraintName: pkName, ConstraintName: pkName,
AutoGenNames: formatStringList(autoGenPKNames), AutoGenNames: formatStringList(autoGenPKNames),
Columns: strings.Join(columnNames, ", "), Columns: strings.Join(columnNames, ", "),
ColumnNames: formatStringList(columnNames),
} }
sql, err := w.executor.ExecuteCreatePrimaryKeyWithAutoGenCheck(data) sql, err := w.executor.ExecuteCreatePrimaryKeyWithAutoGenCheck(data)
@@ -853,16 +960,14 @@ func (w *Writer) writeIndexes(schema *models.Schema) error {
// Build column list with operator class support for GIN indexes // Build column list with operator class support for GIN indexes
columnExprs := make([]string, 0, len(index.Columns)) columnExprs := make([]string, 0, len(index.Columns))
for _, colName := range index.Columns { for _, colName := range index.Columns {
if col, ok := table.Columns[colName]; ok { if col, ok := resolveIndexColumn(table, colName); ok {
colExpr := col.SQLName() colExpr := col.SQLName()
// For GIN indexes on text columns, add operator class if strings.EqualFold(index.Type, "gin") {
if strings.EqualFold(index.Type, "gin") && isTextType(col.Type) { opClass := ginOperatorClassForColumn(col, index.Comment)
opClass := extractOperatorClass(index.Comment) if opClass != "" {
if opClass == "" {
opClass = "gin_trgm_ops"
}
colExpr = fmt.Sprintf("%s %s", col.SQLName(), opClass) colExpr = fmt.Sprintf("%s %s", col.SQLName(), opClass)
} }
}
columnExprs = append(columnExprs, colExpr) columnExprs = append(columnExprs, colExpr)
} }
} }
@@ -1248,23 +1353,126 @@ func isIntegerType(colType string) bool {
} }
// isTextType checks if a column type is a text type (for GIN index operator class) // isTextType checks if a column type is a text type (for GIN index operator class)
func isTextType(colType string) bool { // func isTextType(colType string) bool {
textTypes := []string{"text", "varchar", "character varying", "char", "character", "string"} // textTypes := []string{"text", "varchar", "character varying", "char", "character", "string"}
lowerType := strings.ToLower(colType) // lowerType := strings.ToLower(colType)
if strings.HasSuffix(lowerType, "[]") { // if strings.HasSuffix(lowerType, "[]") {
// return false
// }
// for _, t := range textTypes {
// if strings.HasPrefix(lowerType, t) {
// return true
// }
// }
// return false
// }
// isTextTypeWithoutLength checks if type is text (which should convert to varchar when length is specified)
func isTextTypeWithoutLength(colType string) bool {
return strings.EqualFold(colType, "text")
}
func ginOperatorClassForColumn(col *models.Column, comment string) string {
if col == nil {
return ""
}
sqlType := effectiveColumnSQLType(col)
baseType := pgsql.CanonicalizeBaseType(pgsql.ExtractBaseTypeLower(sqlType))
isArray := pgsql.IsArrayType(sqlType)
requested := extractOperatorClass(comment)
if requested != "" && ginOperatorClassCompatible(baseType, isArray, requested) {
return requested
}
if isArray {
return "array_ops"
}
switch {
case isTextGinBaseType(baseType):
return "gin_trgm_ops"
case baseType == "jsonb":
return "jsonb_ops"
default:
return requested
}
}
func ginOperatorClassCompatible(baseType string, isArray bool, opClass string) bool {
switch opClass {
case "gin_trgm_ops", "gin_bigm_ops":
return !isArray && isTextGinBaseType(baseType)
case "jsonb_ops", "jsonb_path_ops":
return !isArray && baseType == "jsonb"
case "array_ops":
return isArray
default:
return true
}
}
func isTextGinBaseType(baseType string) bool {
switch baseType {
case "text", "varchar", "character varying", "char", "character", "string", "citext", "bpchar":
return true
default:
return false return false
} }
for _, t := range textTypes { }
if strings.HasPrefix(lowerType, t) {
func schemaRequiresPGTrgm(schema *models.Schema) bool {
if schema == nil {
return false
}
for _, table := range schema.Tables {
if table == nil {
continue
}
for _, index := range table.Indexes {
if index == nil || !strings.EqualFold(index.Type, "gin") {
continue
}
for _, colName := range index.Columns {
col, ok := resolveIndexColumn(table, colName)
if !ok || col == nil {
continue
}
if ginOperatorClassForColumn(col, index.Comment) == "gin_trgm_ops" {
return true return true
} }
} }
}
}
return false return false
} }
// isTextTypeWithoutLength checks if type is text (which should convert to varchar when length is specified) func resolveIndexColumn(table *models.Table, colName string) (*models.Column, bool) {
func isTextTypeWithoutLength(colType string) bool { if table == nil {
return strings.EqualFold(colType, "text") return nil, false
}
if col, ok := table.Columns[colName]; ok && col != nil {
return col, true
}
normalized := strings.ToLower(strings.Trim(colName, `"`))
for key, col := range table.Columns {
if col == nil {
continue
}
if strings.ToLower(strings.Trim(key, `"`)) == normalized {
return col, true
}
if strings.ToLower(strings.Trim(col.Name, `"`)) == normalized {
return col, true
}
if strings.ToLower(strings.Trim(col.SQLName(), `"`)) == normalized {
return col, true
}
}
return nil, false
} }
// formatStringList formats a list of strings as a SQL-safe comma-separated quoted list // formatStringList formats a list of strings as a SQL-safe comma-separated quoted list
@@ -1385,7 +1593,12 @@ func (w *Writer) executeDatabaseSQL(db *models.Database, connString string) erro
} }
stmtType := detectStatementType(stmtTrimmed) stmtType := detectStatementType(stmtTrimmed)
stmtCtx := extractStatementContext(stmtTrimmed)
if stmtCtx != "" {
fmt.Fprintf(os.Stderr, "Executing statement %d/%d [%s] %s...\n", i+1, len(statements), stmtType, stmtCtx)
} else {
fmt.Fprintf(os.Stderr, "Executing statement %d/%d [%s]...\n", i+1, len(statements), stmtType) fmt.Fprintf(os.Stderr, "Executing statement %d/%d [%s]...\n", i+1, len(statements), stmtType)
}
_, execErr := conn.Exec(ctx, stmt) _, execErr := conn.Exec(ctx, stmt)
if execErr != nil { if execErr != nil {
@@ -1520,6 +1733,170 @@ func getCurrentTimestamp() string {
return time.Now().Format("2006-01-02 15:04:05") return time.Now().Format("2006-01-02 15:04:05")
} }
// extractStatementContext returns a human-readable schema/table/column context string for a SQL statement.
func extractStatementContext(stmt string) string {
upper := strings.ToUpper(stmt)
// DO $$ blocks: extract identifiers from information_schema WHERE clauses
if strings.HasPrefix(upper, "DO $$") || strings.HasPrefix(upper, "DO $") {
schema := extractSQLStringValue(stmt, "table_schema")
table := extractSQLStringValue(stmt, "table_name")
column := extractSQLStringValue(stmt, "column_name")
constraint := extractSQLStringValue(stmt, "constraint_name")
return buildStmtContext(schema, table, column, constraint)
}
// ALTER TABLE [schema.]table ...
if strings.HasPrefix(upper, "ALTER TABLE") {
schema, table := parseQualifiedIdent(strings.TrimSpace(stmt[11:]))
if strings.Contains(upper, "ADD COLUMN") {
col := firstIdentAfterKeyword(stmt, upper, "ADD COLUMN")
return buildStmtContext(schema, table, col, "")
}
if strings.Contains(upper, "ALTER COLUMN") {
col := firstIdentAfterKeyword(stmt, upper, "ALTER COLUMN")
return buildStmtContext(schema, table, col, "")
}
if strings.Contains(upper, "ADD CONSTRAINT") {
con := firstIdentAfterKeyword(stmt, upper, "ADD CONSTRAINT")
return buildStmtContext(schema, table, "", con)
}
if strings.Contains(upper, "DROP CONSTRAINT") {
con := firstIdentAfterKeyword(stmt, upper, "DROP CONSTRAINT")
return buildStmtContext(schema, table, "", con)
}
return buildStmtContext(schema, table, "", "")
}
// CREATE TABLE [IF NOT EXISTS] [schema.]table
if strings.HasPrefix(upper, "CREATE TABLE") {
rest := strings.TrimSpace(stmt[12:])
if strings.HasPrefix(strings.ToUpper(rest), "IF NOT EXISTS") {
rest = strings.TrimSpace(rest[13:])
}
schema, table := parseQualifiedIdent(rest)
return buildStmtContext(schema, table, "", "")
}
// CREATE SCHEMA name
if strings.HasPrefix(upper, "CREATE SCHEMA") {
name := firstBareIdent(strings.TrimSpace(stmt[13:]))
return name
}
// CREATE [UNIQUE] INDEX ... ON [schema.]table
if strings.HasPrefix(upper, "CREATE INDEX") || strings.HasPrefix(upper, "CREATE UNIQUE INDEX") {
onIdx := strings.Index(upper, " ON ")
if onIdx != -1 {
schema, table := parseQualifiedIdent(strings.TrimSpace(stmt[onIdx+4:]))
return buildStmtContext(schema, table, "", "")
}
}
// COMMENT ON TABLE [schema.]table
if strings.HasPrefix(upper, "COMMENT ON TABLE") {
schema, table := parseQualifiedIdent(strings.TrimSpace(stmt[16:]))
return buildStmtContext(schema, table, "", "")
}
// COMMENT ON COLUMN [schema.]table.col
if strings.HasPrefix(upper, "COMMENT ON COLUMN") {
return firstBareIdent(strings.TrimSpace(stmt[17:]))
}
return ""
}
// extractSQLStringValue extracts 'value' from patterns like: key = 'value' (case-insensitive key match).
func extractSQLStringValue(stmt, key string) string {
lower := strings.ToLower(stmt)
idx := strings.Index(lower, strings.ToLower(key))
if idx == -1 {
return ""
}
rest := strings.TrimSpace(stmt[idx+len(key):])
eqIdx := strings.IndexByte(rest, '=')
if eqIdx == -1 || eqIdx > 5 {
return ""
}
rest = strings.TrimSpace(rest[eqIdx+1:])
if len(rest) == 0 || rest[0] != '\'' {
return ""
}
rest = rest[1:]
end := strings.IndexByte(rest, '\'')
if end == -1 {
return ""
}
return rest[:end]
}
// parseQualifiedIdent extracts (schema, name) from the start of s, handling optional "schema"."table" quoting.
func parseQualifiedIdent(s string) (schema, name string) {
token := firstBareIdent(s)
parts := strings.SplitN(token, ".", 2)
if len(parts) == 2 {
return stripQuotes(parts[0]), stripQuotes(parts[1])
}
return "", stripQuotes(token)
}
// firstBareIdent returns the first whitespace/delimiter-terminated token, stripping outer quotes.
func firstBareIdent(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
end := strings.IndexAny(s, " \t\n(,;")
if end == -1 {
return s
}
return s[:end]
}
// firstIdentAfterKeyword returns the first identifier token after keyword (matched case-insensitively via upperStmt).
func firstIdentAfterKeyword(stmt, upperStmt, keyword string) string {
idx := strings.Index(upperStmt, keyword)
if idx == -1 {
return ""
}
return stripQuotes(firstBareIdent(strings.TrimSpace(stmt[idx+len(keyword):])))
}
// stripQuotes removes surrounding double-quotes from an identifier.
func stripQuotes(s string) string {
return strings.Trim(s, "\"")
}
// buildStmtContext assembles a display string from available identifiers.
func buildStmtContext(schema, table, column, constraint string) string {
var b strings.Builder
if schema != "" && table != "" {
b.WriteString(schema)
b.WriteByte('.')
b.WriteString(table)
} else if table != "" {
b.WriteString(table)
}
if column != "" {
if b.Len() > 0 {
b.WriteByte(' ')
}
b.WriteByte('(')
b.WriteString(column)
b.WriteByte(')')
}
if constraint != "" {
if b.Len() > 0 {
b.WriteByte(' ')
}
b.WriteByte('[')
b.WriteString(constraint)
b.WriteByte(']')
}
return b.String()
}
// detectStatementType detects the type of SQL statement for logging // detectStatementType detects the type of SQL statement for logging
func detectStatementType(stmt string) string { func detectStatementType(stmt string) string {
upperStmt := strings.ToUpper(stmt) upperStmt := strings.ToUpper(stmt)
+200 -5
View File
@@ -116,14 +116,88 @@ func TestWriteDatabase_GinIndexOnTextArrayDoesNotUseTrigramOperatorClass(t *test
} }
output := buf.String() output := buf.String()
if !strings.Contains(output, `USING gin (tags)`) { if !strings.Contains(output, `USING gin (tags array_ops)`) {
t.Fatalf("expected GIN index on array column without explicit trigram opclass, got:\n%s", output) t.Fatalf("expected GIN index on array column with array_ops, got:\n%s", output)
} }
if strings.Contains(output, "gin_trgm_ops") { if strings.Contains(output, "gin_trgm_ops") {
t.Fatalf("did not expect gin_trgm_ops for text[] column, got:\n%s", output) t.Fatalf("did not expect gin_trgm_ops for text[] column, got:\n%s", output)
} }
} }
func TestWriteDatabase_GinIndexOnQuotedTextColumnUsesTrigramOperatorClass(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("agent_personas", "public")
nameCol := models.InitColumn("name", "agent_personas", "public")
nameCol.Type = "text"
table.Columns["name"] = nameCol
index := &models.Index{
Name: "idx_agent_personas_name_gin",
Type: "gin",
Columns: []string{`"name"`},
}
table.Indexes[index.Name] = index
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, `CREATE EXTENSION IF NOT EXISTS pg_trgm`) {
t.Fatalf("expected trigram extension for text GIN index, got:\n%s", output)
}
if !strings.Contains(output, `USING gin (name gin_trgm_ops)`) {
t.Fatalf("expected quoted text GIN index to include gin_trgm_ops, got:\n%s", output)
}
}
func TestWriteDatabase_GinIndexOnJSONBUsesJSONBOperatorClass(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("learnings", "public")
detailsCol := models.InitColumn("details", "learnings", "public")
detailsCol.Type = "jsonb"
table.Columns["details"] = detailsCol
index := &models.Index{
Name: "idx_learnings_details",
Type: "gin",
Columns: []string{"details"},
}
table.Indexes[index.Name] = index
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, `USING gin (details jsonb_ops)`) {
t.Fatalf("expected GIN jsonb index to include jsonb_ops, got:\n%s", output)
}
if strings.Contains(output, "gin_trgm_ops") {
t.Fatalf("did not expect gin_trgm_ops for jsonb column, got:\n%s", output)
}
}
func TestWriteForeignKeys(t *testing.T) { func TestWriteForeignKeys(t *testing.T) {
// Create a test database with two related tables // Create a test database with two related tables
db := models.InitDatabase("testdb") db := models.InitDatabase("testdb")
@@ -673,9 +747,14 @@ func TestPrimaryKeyExistenceCheck(t *testing.T) {
t.Errorf("Output missing logic to drop auto-generated primary key\nFull output:\n%s", output) t.Errorf("Output missing logic to drop auto-generated primary key\nFull output:\n%s", output)
} }
// Verify it checks for our specific named constraint before adding it // Verify it compares the current primary key columns before dropping/recreating
if !strings.Contains(output, "constraint_name = 'pk_public_products'") { if !strings.Contains(output, "current_pk_matches") || !strings.Contains(output, "ARRAY['id']") {
t.Errorf("Output missing check for our named primary key constraint\nFull output:\n%s", output) t.Errorf("Output missing safe primary key comparison logic\nFull output:\n%s", output)
}
// Verify it only adds the desired key when no PK exists or an auto-generated mismatch was dropped
if !strings.Contains(output, "current_pk_name IS NULL") || !strings.Contains(output, "current_pk_name IN ('products_pkey', 'public_products_pkey')") {
t.Errorf("Output missing guarded primary key creation logic\nFull output:\n%s", output)
} }
} }
@@ -766,6 +845,43 @@ func TestColumnSizeSpecifiers(t *testing.T) {
} }
} }
func TestWriteDatabase_PrimaryKeyTemplateDoesNotDropMatchingAutoPrimaryKey(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("learnings", "public")
idCol := models.InitColumn("id", "learnings", "public")
idCol.Type = "bigint"
idCol.IsPrimaryKey = true
table.Columns["id"] = idCol
parentCol := models.InitColumn("duplicate_of_learning_id", "learnings", "public")
parentCol.Type = "bigint"
table.Columns["duplicate_of_learning_id"] = parentCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "current_pk_matches") {
t.Fatalf("expected generated SQL to compare current PK columns, got:\n%s", output)
}
if !strings.Contains(output, "ARRAY['id']") {
t.Fatalf("expected generated SQL to compare against desired PK columns, got:\n%s", output)
}
if !strings.Contains(output, "NOT current_pk_matches") {
t.Fatalf("expected generated SQL to avoid dropping matching PKs, got:\n%s", output)
}
}
func TestGenerateColumnDefinition_PreservesExplicitTypeModifiers(t *testing.T) { func TestGenerateColumnDefinition_PreservesExplicitTypeModifiers(t *testing.T) {
writer := NewWriter(&writers.WriterOptions{}) writer := NewWriter(&writers.WriterOptions{})
@@ -942,3 +1058,82 @@ func TestWriteAddColumnStatements(t *testing.T) {
t.Errorf("Output missing DO block\nFull output:\n%s", output) t.Errorf("Output missing DO block\nFull output:\n%s", output)
} }
} }
func TestWriteSchema_EmitsGuardedAlterColumnTypeStatements(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("agent_skills", "public")
nameCol := models.InitColumn("name", "agent_skills", "public")
nameCol.Type = "character varying"
nameCol.Length = 255
table.Columns["name"] = nameCol
tagsCol := models.InitColumn("tags", "agent_skills", "public")
tagsCol.Type = "text[]"
table.Columns["tags"] = tagsCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "-- Alter column types for schema: public") {
t.Fatalf("expected alter column type section, got:\n%s", output)
}
if !strings.Contains(output, "pg_catalog.format_type") {
t.Fatalf("expected guarded live-type check, got:\n%s", output)
}
if !strings.Contains(output, "ALTER COLUMN name TYPE character varying(255)") {
t.Fatalf("expected guarded alter for character varying(255), got:\n%s", output)
}
if !strings.Contains(output, "ARRAY['varchar(255)', 'character varying(255)']") {
t.Fatalf("expected equivalent type spellings for varchar guard, got:\n%s", output)
}
if !strings.Contains(output, "ALTER COLUMN tags TYPE text[]") {
t.Fatalf("expected guarded alter for array type, got:\n%s", output)
}
if !strings.Contains(output, `ALTER COLUMN tags TYPE text[] USING tags::text[];`) {
t.Fatalf("expected guarded alter for array type to include USING cast, got:\n%s", output)
}
}
func TestWriteSchema_UsesStorageTypeForSerialAlterStatements(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("learnings", "public")
idCol := models.InitColumn("id", "learnings", "public")
idCol.Type = "bigserial"
table.Columns["id"] = idCol
schema.Tables = append(schema.Tables, table)
db.Schemas = append(db.Schemas, schema)
var buf bytes.Buffer
writer := NewWriter(&writers.WriterOptions{})
writer.writer = &buf
if err := writer.WriteDatabase(db); err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "ALTER COLUMN id TYPE bigint") {
t.Fatalf("expected serial alter to use bigint storage type, got:\n%s", output)
}
if strings.Contains(output, "ALTER COLUMN id TYPE bigserial;") {
t.Fatalf("did not expect invalid bigserial alter statement, got:\n%s", output)
}
if !strings.Contains(output, `ALTER COLUMN id TYPE bigint USING id::bigint;`) {
t.Fatalf("expected serial alter to include USING cast, got:\n%s", output)
}
}
+17 -60
View File
@@ -2,9 +2,11 @@ package sqlite
import ( import (
"strings" "strings"
sqlitepkg "git.warky.dev/wdevs/relspecgo/pkg/sqlite"
) )
// SQLite type affinities // SQLite type affinity constants
const ( const (
TypeText = "TEXT" TypeText = "TEXT"
TypeInteger = "INTEGER" TypeInteger = "INTEGER"
@@ -13,72 +15,27 @@ const (
TypeBlob = "BLOB" TypeBlob = "BLOB"
) )
// MapPostgreSQLType maps PostgreSQL data types to SQLite type affinities // MapTypeToSQLite maps any SQL or Go canonical type to a SQLite type affinity.
// Handles input from any reader (PostgreSQL, MSSQL, SQLite, Go canonical).
func MapTypeToSQLite(colType string) string {
return sqlitepkg.ConvertCanonicalToSQLite(colType)
}
// MapPostgreSQLType is an alias for MapTypeToSQLite kept for compatibility.
//
// Deprecated: use MapTypeToSQLite.
func MapPostgreSQLType(pgType string) string { func MapPostgreSQLType(pgType string) string {
// Normalize the type return MapTypeToSQLite(pgType)
normalized := strings.ToLower(strings.TrimSpace(pgType))
// Remove array notation if present
normalized = strings.TrimSuffix(normalized, "[]")
// Remove precision/scale if present
if idx := strings.Index(normalized, "("); idx != -1 {
normalized = normalized[:idx]
}
// Map to SQLite type affinity
switch normalized {
// TEXT affinity
case "varchar", "character varying", "text", "char", "character",
"citext", "uuid", "timestamp", "timestamptz", "timestamp with time zone",
"timestamp without time zone", "date", "time", "timetz", "time with time zone",
"time without time zone", "json", "jsonb", "xml", "inet", "cidr", "macaddr":
return TypeText
// INTEGER affinity
case "int", "int2", "int4", "int8", "integer", "smallint", "bigint",
"serial", "smallserial", "bigserial", "boolean", "bool":
return TypeInteger
// REAL affinity
case "real", "float", "float4", "float8", "double precision":
return TypeReal
// NUMERIC affinity
case "numeric", "decimal", "money":
return TypeNumeric
// BLOB affinity
case "bytea", "blob":
return TypeBlob
default:
// Default to TEXT for unknown types
return TypeText
}
} }
// IsIntegerType checks if a column type should be treated as integer // IsIntegerType reports whether a column type maps to SQLite INTEGER affinity.
func IsIntegerType(colType string) bool { func IsIntegerType(colType string) bool {
normalized := strings.ToLower(strings.TrimSpace(colType)) return MapTypeToSQLite(colType) == TypeInteger
normalized = strings.TrimSuffix(normalized, "[]")
if idx := strings.Index(normalized, "("); idx != -1 {
normalized = normalized[:idx]
}
switch normalized {
case "int", "int2", "int4", "int8", "integer", "smallint", "bigint",
"serial", "smallserial", "bigserial":
return true
default:
return false
}
} }
// MapBooleanValue converts PostgreSQL boolean literals to SQLite (0/1) // MapBooleanValue converts common boolean literals to SQLite integers (1/0).
func MapBooleanValue(value string) string { func MapBooleanValue(value string) string {
normalized := strings.ToLower(strings.TrimSpace(value)) switch strings.ToLower(strings.TrimSpace(value)) {
switch normalized {
case "true", "t", "yes", "y", "1": case "true", "t", "yes", "y", "1":
return "1" return "1"
case "false", "f", "no", "n", "0": case "false", "f", "no", "n", "0":
+4
View File
@@ -54,6 +54,10 @@ type WriterOptions struct {
// Prisma7 enables Prisma 7-specific output for Prisma writers. // Prisma7 enables Prisma 7-specific output for Prisma writers.
Prisma7 bool Prisma7 bool
// ContinueOnError instructs SQL writers to prepend `\set ON_ERROR_STOP off`
// to their output so that psql continues past errors instead of stopping.
ContinueOnError bool
// Additional options can be added here as needed // Additional options can be added here as needed
Metadata map[string]interface{} Metadata map[string]interface{}
} }
-13
View File
@@ -1,13 +0,0 @@
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 ./...
+2
View File
@@ -1 +1,3 @@
coverage.txt coverage.txt
.zed
.idea
-18
View File
@@ -1,18 +0,0 @@
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)
+1 -1
View File
@@ -13,7 +13,7 @@ GOOS=js GOARCH=wasm go build -o yourfile.wasm
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 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 ```sh
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./ cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" ./
``` ```
In `tcell.js`, you also need to change the constant In `tcell.js`, you also need to change the constant
+25 -134
View File
@@ -7,14 +7,14 @@ It was inspired by _termbox_, but includes many additional improvements.
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
[![Linux](https://img.shields.io/github/actions/workflow/status/gdamore/tcell/linux.yml?branch=main&logoColor=grey&logo=linux&label=)](https://github.com/gdamore/tcell/actions/workflows/linux.yml) [![Linux](https://img.shields.io/github/actions/workflow/status/gdamore/tcell/linux.yml?branch=main&logoColor=grey&logo=linux&label=)](https://github.com/gdamore/tcell/actions/workflows/linux.yml)
[![Windows](https://img.shields.io/github/actions/workflow/status/gdamore/tcell/windows.yml?branch=main&logoColor=grey&logo=windows&label=)](https://github.com/gdamore/tcell/actions/workflows/windows.yml) [![Windows](https://img.shields.io/github/actions/workflow/status/gdamore/tcell/windows.yml?branch=main&logoColor=grey&label=Windows)](https://github.com/gdamore/tcell/actions/workflows/windows.yml)
[![Web Assembly](https://img.shields.io/github/actions/workflow/status/gdamore/tcell/webasm.yml?branch=main&logoColor=grey&logo=webassembly&label=)](https://github.com/gdamore/tcell/actions/workflows/webasm.yml)
[![Apache License](https://img.shields.io/github/license/gdamore/tcell.svg?logoColor=silver&logo=opensourceinitiative&color=blue&label=)](https://github.com/gdamore/tcell/blob/master/LICENSE) [![Apache License](https://img.shields.io/github/license/gdamore/tcell.svg?logoColor=silver&logo=opensourceinitiative&color=blue&label=)](https://github.com/gdamore/tcell/blob/master/LICENSE)
[![Docs](https://img.shields.io/badge/godoc-reference-blue.svg?label=&logo=go)](https://pkg.go.dev/github.com/gdamore/tcell/v2) [![Docs](https://img.shields.io/badge/godoc-reference-blue.svg?label=&logo=go)](https://pkg.go.dev/github.com/gdamore/tcell/v2)
[![Discord](https://img.shields.io/discord/639503822733180969?label=&logo=discord)](https://discord.gg/urTTxDN) [![Discord](https://img.shields.io/discord/639503822733180969?label=&logo=discord)](https://discord.gg/urTTxDN)
[![Coverage](https://img.shields.io/codecov/c/github/gdamore/tcell?logoColor=grey&logo=codecov&label=)](https://codecov.io/gh/gdamore/tcell) [![Coverage](https://img.shields.io/codecov/c/github/gdamore/tcell?logoColor=grey&logo=codecov&label=)](https://codecov.io/gh/gdamore/tcell)
[![Go Report Card](https://goreportcard.com/badge/github.com/gdamore/tcell/v2)](https://goreportcard.com/report/github.com/gdamore/tcell/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/gdamore/tcell/v2)](https://goreportcard.com/report/github.com/gdamore/tcell/v2)
[![Latest Release](https://img.shields.io/github/v/release/gdamore/tcell.svg?logo=github&label=)](https://github.com/gdamore/tcell/releases)
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. 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`. Version 1.x remains available using the import `github.com/gdamore/tcell`.
@@ -25,59 +25,9 @@ A brief, and still somewhat rough, [tutorial](TUTORIAL.md) is available.
## Examples ## Examples
- [proxima5](https://github.com/gdamore/proxima5) - space shooter ([video](https://youtu.be/jNxKTCmY_bQ)) A number of example are posted up on our [Gallery](https://github.com/gdamore/tcell/wikis/Gallery/).
- [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 Let us know if you want to add your masterpiece to the list!
_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 ## More Portable
@@ -85,13 +35,10 @@ _Tcell_ is portable to a wide variety of systems, and is pure Go, without
any need for CGO. any need for CGO.
_Tcell_ is believed to work with mainstream systems officially supported by golang. _Tcell_ is believed to work with mainstream systems officially supported by golang.
## No Async IO Following the Go support policy, _Tcell_ officially only supports the current ("stable") version of go,
and the version immediately prior ("oldstable"). This policy is necessary to make sure that we can
_Tcell_ is able to operate without requiring `SIGIO` signals (unlike _termbox_), update dependencies to pick up security fixes and new features, and it allows us to adopt changes
or asynchronous I/O, and can instead use standard Go file objects and Go routines. (such as library and language features) that are only supported in newer versions of Go.
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 ## Rich Unicode & non-Unicode support
@@ -111,29 +58,11 @@ drawing certain characters.
_Tcell_ also has richer support for a larger number of special keys that some _Tcell_ also has richer support for a larger number of special keys that some
terminals can send. 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 ## Better Mouse Support
_Tcell_ supports enhanced mouse tracking mode, so your application can receive _Tcell_ supports enhanced mouse tracking mode, so your application can receive
regular mouse motion events, and wheel events, if your terminal supports it. 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 ## _Termbox_ Compatibility
A compatibility layer for _termbox_ is provided in the `compat` directory. A compatibility layer for _termbox_ is provided in the `compat` directory.
@@ -152,15 +81,15 @@ If you're lazy, and want them all anyway, see the `encoding` sub-directory.
## Wide & Combining Characters ## Wide & Combining Characters
The `SetContent()` API takes a primary rune, and an optional list of combining runes. The `Put()` API takes a string, which should be legal UTF-8, and displays
If any of the runes is a wide (East Asian) rune occupying two cells, the first grapheme (which may composed of multiple runes). It returns the
then the library will skip output from the following cell. Care must be actual width displayed, which can be used to advance the column positiion
taken in the application to avoid explicitly attempting to set content in the for the next display grapheme. Alternatively, `PutStr()` or `PutStrStyled()`
next cell, otherwise the results are undefined. (Normally the wide character can be used to display a single line of text (which will be clipped at the
is displayed, and the other character is not; do not depend on that behavior.) edge of the screen).
Older terminal applications (especially on systems like Windows 8) lack support If a second character is displayed immediately in the cell adjacent to a
for advanced Unicode, and thus may not fare well. wide character (offset by one instead of by two), then the results are undefined.
## Colors ## Colors
@@ -175,11 +104,7 @@ a ticket.
_Tcell_ _supports 24-bit color!_ (That is, if your terminal can support it.) _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 There are a few ways you can enable (or disable) 24-bit color.
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 - For many terminals, we can detect it automatically if your terminal
includes the `RGB` or `Tc` capabilities (or rather it did when the database includes the `RGB` or `Tc` capabilities (or rather it did when the database
@@ -197,8 +122,8 @@ There are a few ways you can enable (or disable) true color.
- You can disable 24-bit color by setting `TCELL_TRUECOLOR=disable` in your - You can disable 24-bit color by setting `TCELL_TRUECOLOR=disable` in your
environment. environment.
When using TrueColor, programs will display the colors that the programmer When using 24-bit color, programs will display the colors that the programmer
intended, overriding any "`themes`" you may have set in your terminal intended, overriding any "`themes`" the user may have set in their terminal
emulator. (For some cases, accurate color fidelity is more important emulator. (For some cases, accurate color fidelity is more important
than respecting themes. For other cases, such as typical text apps that 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 only use a few colors, its more desirable to respect the themes that
@@ -209,38 +134,10 @@ the user has established.)
Reasonable attempts have been made to minimize sending data to terminals, Reasonable attempts have been made to minimize sending data to terminals,
avoiding repeated sequences or drawing the same cell on refresh updates. 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
Mouse support is detected via the `kmous` terminfo variable, however, Mouse tracking, buttons, and even wheel mice works fine on most terminal
enablement/disablement and decoding mouse events is done using hard coded emulators, as well as Windows.
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 ## Bracketed Paste
@@ -265,22 +162,16 @@ platforms (e.g., AIX) may need to be added. Pull requests are welcome!
Windows console mode applications are supported. Windows console mode applications are supported.
Modern console applications like ConEmu and the Windows 10 terminal, Modern console applications like ConEmu and the Windows Terminal,
support all the good features (resize, mouse tracking, etc.) support all the good features (resize, mouse tracking, etc.)
### WASM ### WASM
WASM is supported, but needs additional setup detailed in [README-wasm](README-wasm.md). WASM is supported, but needs additional setup detailed in [README-wasm](README-wasm.md).
### Plan9 and others ### Plan9 and its variants
These platforms won't work, but compilation stubs are supplied Plan 9 is supported on a limited basis. The Plan 9 backend opens `/dev/cons` for I/O, enables raw mode by writing `rawon`/`rawoff` to `/dev/consctl`, watches `/dev/wctl` for resize notifications, and then constructs a **terminfo-backed** `Screen` (so `NewScreen` works as on other platforms). Typical usage is inside `vt(1)` with `TERM=vt100`. Expect **monochrome text** and **no mouse reporting** under stock `vt(1)` (it generally does not emit ANSI color or xterm mouse sequences). If a Plan 9 terminal supplies ANSI color escape sequences and xterm-style mouse reporting, color can be picked up via **terminfo** and mouse support could be added by wiring those sequences into the Plan 9 TTY path; contributions that improve terminal detection and broaden feature support are welcome.
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 ### Commercial Support
+36 -21
View File
@@ -107,23 +107,30 @@ s.SetStyle(defStyle)
s.Clear() s.Clear()
``` ```
Text may be drawn on the screen using `SetContent`. Text may be drawn on the screen using `Put`, `PutStr`, or `PutStrStyled`.
```go ```go
s.SetContent(0, 0, 'H', nil, defStyle) s.Put(0, 0, 'H', defStyle)
s.SetContent(1, 0, 'i', nil, defStyle) s.Put(1, 0, 'i', defStyle)
s.SetContent(2, 0, '!', nil, defStyle) s.Put(2, 0, '!', defStyle)
``` ```
To draw text more easily, define a render function. which is equivalent to
```go
s.PutStrStyled(0, 0, "Hi!", defStyle)
````
To draw text more easily with wrapping, define a render function.
```go ```go
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) { func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1 row := y1
col := x1 col := x1
for _, r := range []rune(text) { var width int
s.SetContent(col, row, r, nil, style) for text != "" {
col++ text, width = s.Put(col, row, text, style)
col += width
if col >= x2 { if col >= x2 {
row++ row++
col = x1 col = x1
@@ -131,6 +138,10 @@ func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string
if row > y2 { if row > y2 {
break break
} }
if width == 0 {
// incomplete grapheme at end of string
break
}
} }
} }
``` ```
@@ -178,9 +189,10 @@ import (
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) { func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1 row := y1
col := x1 col := x1
for _, r := range []rune(text) { var width int
s.SetContent(col, row, r, nil, style) for text != "" {
col++ text, width = s.Put(col, row, text, style)
col += width
if col >= x2 { if col >= x2 {
row++ row++
col = x1 col = x1
@@ -188,6 +200,10 @@ func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string
if row > y2 { if row > y2 {
break break
} }
if width == 0 {
// incomplete grapheme at end of string
break
}
} }
} }
@@ -202,26 +218,26 @@ func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string)
// Fill background // Fill background
for row := y1; row <= y2; row++ { for row := y1; row <= y2; row++ {
for col := x1; col <= x2; col++ { for col := x1; col <= x2; col++ {
s.SetContent(col, row, ' ', nil, style) s.Put(col, row, " ", style)
} }
} }
// Draw borders // Draw borders
for col := x1; col <= x2; col++ { for col := x1; col <= x2; col++ {
s.SetContent(col, y1, tcell.RuneHLine, nil, style) s.Put(col, y1, string(tcell.RuneHLine), style)
s.SetContent(col, y2, tcell.RuneHLine, nil, style) s.Put(col, y2, string(tcell.RuneHLine), style)
} }
for row := y1 + 1; row < y2; row++ { for row := y1 + 1; row < y2; row++ {
s.SetContent(x1, row, tcell.RuneVLine, nil, style) s.Put(x1, row, string(tcell.RuneVLine), style)
s.SetContent(x2, row, tcell.RuneVLine, nil, style) s.Put(x2, row, string(tcell.RuneVLine), style)
} }
// Only draw corners if necessary // Only draw corners if necessary
if y1 != y2 && x1 != x2 { if y1 != y2 && x1 != x2 {
s.SetContent(x1, y1, tcell.RuneULCorner, nil, style) s.Put(x1, y1, string(tcell.RuneULCorner), style)
s.SetContent(x2, y1, tcell.RuneURCorner, nil, style) s.Put(x2, y1, string(tcell.RuneURCorner), style)
s.SetContent(x1, y2, tcell.RuneLLCorner, nil, style) s.Put(x1, y2, string(tcell.RuneLLCorner), style)
s.SetContent(x2, y2, tcell.RuneLRCorner, nil, style) s.Put(x2, y2, string(tcell.RuneLRCorner), style)
} }
drawText(s, x1+1, y1+1, x2-1, y2-1, style, text) drawText(s, x1+1, y1+1, x2-1, y2-1, style, text)
@@ -310,4 +326,3 @@ func main() {
} }
} }
``` ```
-77
View File
@@ -1,77 +0,0 @@
# 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
+81 -66
View File
@@ -1,4 +1,4 @@
// Copyright 2024 The TCell Authors // Copyright 2025 The TCell Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License. // you may not use file except in compliance with the License.
@@ -15,23 +15,30 @@
package tcell package tcell
import ( import (
"os" "github.com/rivo/uniseg"
"reflect"
runewidth "github.com/mattn/go-runewidth"
) )
type cell struct { type cell struct {
currMain rune currStr string
currComb []rune lastStr string
currStyle Style currStyle Style
lastMain rune
lastStyle Style lastStyle Style
lastComb []rune
width int width int
lock bool lock bool
} }
func (c *cell) setDirty(dirty bool) {
if dirty {
c.lastStr = ""
} else {
if c.currStr == "" {
c.currStr = " "
}
c.lastStr = c.currStr
c.lastStyle = c.currStyle
}
}
// CellBuffer represents a two-dimensional array of character cells. // CellBuffer represents a two-dimensional array of character cells.
// This is primarily intended for use by Screen implementors; it // This is primarily intended for use by Screen implementors; it
// contains much of the common code they need. To create one, just // contains much of the common code they need. To create one, just
@@ -48,28 +55,47 @@ type CellBuffer struct {
// and style) for a cell at a given location. If the background or // and style) for a cell at a given location. If the background or
// foreground of the style is set to ColorNone, then the respective // foreground of the style is set to ColorNone, then the respective
// color is left un changed. // color is left un changed.
func (cb *CellBuffer) SetContent(x int, y int, //
mainc rune, combc []rune, style Style, // Deprecated: Use Put instead, which this is implemented in terms of.
) { func (cb *CellBuffer) SetContent(x int, y int, mainc rune, combc []rune, style Style) {
cb.Put(x, y, string(append([]rune{mainc}, combc...)), style)
}
// Put a single styled grapheme using the given string and style
// at the same location. Note that only the first grapheme in the string
// will bre displayed, using only the 1 or 2 (depending on width) cells
// located at x, y. It returns the rest of the string, and the width used.
func (cb *CellBuffer) Put(x int, y int, str string, style Style) (string, int) {
var width int = 0
if x >= 0 && y >= 0 && x < cb.w && y < cb.h { if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
var cl string
c := &cb.cells[(y*cb.w)+x] c := &cb.cells[(y*cb.w)+x]
state := -1
for width == 0 && str != "" {
var g string
g, str, width, state = uniseg.FirstGraphemeClusterInString(str, state)
cl += g
if g == "" {
break
}
}
// Wide characters: we want to mark the "wide" cells // Wide characters: we want to mark the "wide" cells
// dirty as well as the base cell, to make sure we consider // dirty as well as the base cell, to make sure we consider
// both cells as dirty together. We only need to do this // both cells as dirty together. We only need to do this
// if we're changing content // 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))) { if width > 0 && cl != c.currStr {
for i := 0; i < c.width; i++ { // Prevent unnecessary boundchecks for first cell, since we already
// received that one.
c.setDirty(true)
for i := 1; i < width; i++ {
cb.SetDirty(x+i, y, true) cb.SetDirty(x+i, y, true)
} }
} }
c.currComb = append([]rune{}, combc...) c.currStr = cl
c.width = width
if c.currMain != mainc {
c.width = runewidth.RuneWidth(mainc)
}
c.currMain = mainc
if style.fg == ColorNone { if style.fg == ColorNone {
style.fg = c.currStyle.fg style.fg = c.currStyle.fg
} }
@@ -78,23 +104,45 @@ func (cb *CellBuffer) SetContent(x int, y int,
} }
c.currStyle = style c.currStyle = style
} }
return str, width
}
// Get the contents of a character cell (or two adjacent cells), including the
// the style and the display width in cells. (The width can be either 1, normally,
// or 2 for East Asian full-width characters. If the width is 0, then the cell is
// is empty.)
func (cb *CellBuffer) Get(x, y int) (string, Style, int) {
var style Style
var width int
var str string
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
c := &cb.cells[(y*cb.w)+x]
str, style = c.currStr, c.currStyle
if width = c.width; width == 0 || str == "" {
width = 1
str = " "
}
}
return str, style, width
} }
// GetContent returns the contents of a character cell, including the // GetContent returns the contents of a character cell, including the
// primary rune, any combining character runes (which will usually be // primary rune, any combining character runes (which will usually be
// nil), the style, and the display width in cells. (The width can 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.) // either 1, normally, or 2 for East Asian full-width characters.)
//
// Deprecated: Use Get, which this implemented in terms of.
func (cb *CellBuffer) GetContent(x, y int) (rune, []rune, Style, int) { func (cb *CellBuffer) GetContent(x, y int) (rune, []rune, Style, int) {
var mainc rune
var combc []rune
var style Style var style Style
var width int var width int
if x >= 0 && y >= 0 && x < cb.w && y < cb.h { var mainc rune
c := &cb.cells[(y*cb.w)+x] var combc []rune
mainc, combc, style = c.currMain, c.currComb, c.currStyle str, style, width := cb.Get(x, y)
if width = c.width; width == 0 || mainc < ' ' { for i, r := range str {
width = 1 if i == 0 {
mainc = ' ' mainc = r
} else {
combc = append(combc, r)
} }
} }
return mainc, combc, style, width return mainc, combc, style, width
@@ -108,7 +156,7 @@ func (cb *CellBuffer) Size() (int, int) {
// Invalidate marks all characters within the buffer as dirty. // Invalidate marks all characters within the buffer as dirty.
func (cb *CellBuffer) Invalidate() { func (cb *CellBuffer) Invalidate() {
for i := range cb.cells { for i := range cb.cells {
cb.cells[i].lastMain = rune(0) cb.cells[i].lastStr = ""
} }
} }
@@ -121,23 +169,12 @@ func (cb *CellBuffer) Dirty(x, y int) bool {
if c.lock { if c.lock {
return false return false
} }
if c.lastMain == rune(0) {
return true
}
if c.lastMain != c.currMain {
return true
}
if c.lastStyle != c.currStyle { if c.lastStyle != c.currStyle {
return true return true
} }
if len(c.lastComb) != len(c.currComb) { if c.lastStr != c.currStr {
return true return true
} }
for i := range c.lastComb {
if c.lastComb[i] != c.currComb[i] {
return true
}
}
} }
return false return false
} }
@@ -148,16 +185,7 @@ func (cb *CellBuffer) Dirty(x, y int) bool {
func (cb *CellBuffer) SetDirty(x, y int, dirty bool) { func (cb *CellBuffer) SetDirty(x, y int, dirty bool) {
if x >= 0 && y >= 0 && x < cb.w && y < cb.h { if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
c := &cb.cells[(y*cb.w)+x] c := &cb.cells[(y*cb.w)+x]
if dirty { c.setDirty(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
}
} }
} }
@@ -203,11 +231,10 @@ func (cb *CellBuffer) Resize(w, h int) {
for x := 0; x < w && x < cb.w; x++ { for x := 0; x < w && x < cb.w; x++ {
oc := &cb.cells[(y*cb.w)+x] oc := &cb.cells[(y*cb.w)+x]
nc := &newc[(y*w)+x] nc := &newc[(y*w)+x]
nc.currMain = oc.currMain nc.currStr = oc.currStr
nc.currComb = oc.currComb
nc.currStyle = oc.currStyle nc.currStyle = oc.currStyle
nc.width = oc.width nc.width = oc.width
nc.lastMain = rune(0) nc.lastStr = ""
} }
} }
cb.cells = newc cb.cells = newc
@@ -223,8 +250,7 @@ func (cb *CellBuffer) Resize(w, h int) {
func (cb *CellBuffer) Fill(r rune, style Style) { func (cb *CellBuffer) Fill(r rune, style Style) {
for i := range cb.cells { for i := range cb.cells {
c := &cb.cells[i] c := &cb.cells[i]
c.currMain = r c.currStr = string(r)
c.currComb = nil
cs := style cs := style
if cs.fg == ColorNone { if cs.fg == ColorNone {
cs.fg = c.currStyle.fg cs.fg = c.currStyle.fg
@@ -236,14 +262,3 @@ func (cb *CellBuffer) Fill(r rune, style Style) {
c.width = 1 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
}
}
+23
View File
@@ -0,0 +1,23 @@
//go:build plan9
// +build plan9
// Copyright 2025 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
// Plan 9 uses UTF-8 system-wide, so we return "UTF-8" unconditionally.
func getCharset() string {
return "UTF-8"
}
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build plan9 || nacl //go:build nacl
// +build plan9 nacl // +build nacl
// Copyright 2015 The TCell Authors // Copyright 2015 The TCell Authors
// //
+1 -1
View File
@@ -18,5 +18,5 @@
package tcell package tcell
func getCharset() string { func getCharset() string {
return "UTF-16" return "UTF-8"
} }
+58 -181
View File
@@ -1,7 +1,7 @@
//go:build windows //go:build windows
// +build windows // +build windows
// Copyright 2024 The TCell Authors // Copyright 2025 The TCell Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License. // you may not use file except in compliance with the License.
@@ -38,7 +38,6 @@ type cScreen struct {
cury int cury int
style Style style Style
fini bool fini bool
vten bool
truecolor bool truecolor bool
running bool running bool
disableAlt bool // disable the alternate screen disableAlt bool // disable the alternate screen
@@ -106,7 +105,6 @@ var winColors = map[Color]Color{
} }
var ( var (
k32 = syscall.NewLazyDLL("kernel32.dll")
u32 = syscall.NewLazyDLL("user32.dll") u32 = syscall.NewLazyDLL("user32.dll")
) )
@@ -117,18 +115,8 @@ var (
// characters (Unicode) are in use. The documentation refers to them // characters (Unicode) are in use. The documentation refers to them
// without this suffix, as the resolution is made via preprocessor. // without this suffix, as the resolution is made via preprocessor.
var ( var (
procReadConsoleInput = k32.NewProc("ReadConsoleInputW")
procWaitForMultipleObjects = k32.NewProc("WaitForMultipleObjects")
procCreateEvent = k32.NewProc("CreateEventW")
procSetEvent = k32.NewProc("SetEvent")
procGetConsoleCursorInfo = k32.NewProc("GetConsoleCursorInfo") procGetConsoleCursorInfo = k32.NewProc("GetConsoleCursorInfo")
procSetConsoleCursorInfo = k32.NewProc("SetConsoleCursorInfo") procSetConsoleCursorInfo = k32.NewProc("SetConsoleCursorInfo")
procSetConsoleCursorPosition = k32.NewProc("SetConsoleCursorPosition")
procSetConsoleMode = k32.NewProc("SetConsoleMode")
procGetConsoleMode = k32.NewProc("GetConsoleMode")
procGetConsoleScreenBufferInfo = k32.NewProc("GetConsoleScreenBufferInfo")
procFillConsoleOutputAttribute = k32.NewProc("FillConsoleOutputAttribute")
procFillConsoleOutputCharacter = k32.NewProc("FillConsoleOutputCharacterW")
procSetConsoleWindowInfo = k32.NewProc("SetConsoleWindowInfo") procSetConsoleWindowInfo = k32.NewProc("SetConsoleWindowInfo")
procSetConsoleScreenBufferSize = k32.NewProc("SetConsoleScreenBufferSize") procSetConsoleScreenBufferSize = k32.NewProc("SetConsoleScreenBufferSize")
procSetConsoleTextAttribute = k32.NewProc("SetConsoleTextAttribute") procSetConsoleTextAttribute = k32.NewProc("SetConsoleTextAttribute")
@@ -195,6 +183,10 @@ var vtCursorStyles = map[CursorStyle]string{
// NewConsoleScreen returns a Screen for the Windows console associated // NewConsoleScreen returns a Screen for the Windows console associated
// with the current process. The Screen makes use of the Windows Console // with the current process. The Screen makes use of the Windows Console
// API to display content and read events. // API to display content and read events.
//
// Deprecated: The console API based implementation will be fully replaced
// with the VT based model. Use NewScreen() to get a reasonable screen
// by default.
func NewConsoleScreen() (Screen, error) { func NewConsoleScreen() (Screen, error) {
return &baseScreen{screenImpl: &cScreen{}}, nil return &baseScreen{screenImpl: &cScreen{}}, nil
} }
@@ -217,22 +209,11 @@ func (s *cScreen) Init() error {
s.truecolor = true s.truecolor = true
// ConEmu handling of colors and scrolling when in VT output mode is extremely poor.
// The color palette will scroll even though characters do not, when
// emitting stuff for the last character. In the future we might change this to
// look at specific versions of ConEmu if they fix the bug.
// We can also try disabling auto margin mode.
tryVt := true
if os.Getenv("ConEmuPID") != "" {
s.truecolor = false
tryVt = false
}
switch os.Getenv("TCELL_TRUECOLOR") { switch os.Getenv("TCELL_TRUECOLOR") {
case "disable": case "disable":
s.truecolor = false s.truecolor = false
case "enable": case "enable":
s.truecolor = true s.truecolor = true
tryVt = true
} }
s.Lock() s.Lock()
@@ -249,33 +230,17 @@ func (s *cScreen) Init() error {
s.fini = false s.fini = false
s.setInMode(modeResizeEn | modeExtendFlg) s.setInMode(modeResizeEn | modeExtendFlg)
// If a user needs to force old style console, they may do so
// by setting TCELL_VTMODE to disable. This is an undocumented safety net for now.
// It may be removed in the future. (This mostly exists because of ConEmu.)
switch os.Getenv("TCELL_VTMODE") {
case "disable":
tryVt = false
case "enable":
tryVt = true
}
switch os.Getenv("TCELL_ALTSCREEN") { switch os.Getenv("TCELL_ALTSCREEN") {
case "enable": case "enable":
s.disableAlt = false // also the default s.disableAlt = false // also the default
case "disable": case "disable":
s.disableAlt = true s.disableAlt = true
} }
if tryVt {
s.setOutMode(modeVtOutput | modeNoAutoNL | modeCookedOut | modeUnderline) s.setOutMode(modeVtOutput | modeNoAutoNL | modeCookedOut | modeUnderline)
var om uint32 var om uint32
s.getOutMode(&om) s.getOutMode(&om)
if om&modeVtOutput == modeVtOutput { if om&modeVtOutput != modeVtOutput {
s.vten = true return errors.New("failed to initialize: VT output not supported?")
} else {
s.truecolor = false
s.setOutMode(0)
}
} else {
s.setOutMode(0)
} }
s.Unlock() s.Unlock()
@@ -349,7 +314,6 @@ func (s *cScreen) disengage() {
s.wg.Wait() s.wg.Wait()
if s.vten {
s.emitVtString(vtCursorStyles[CursorStyleDefault]) s.emitVtString(vtCursorStyles[CursorStyleDefault])
s.emitVtString(vtCursorColorReset) s.emitVtString(vtCursorColorReset)
s.emitVtString(vtEnableAm) s.emitVtString(vtEnableAm)
@@ -357,10 +321,6 @@ func (s *cScreen) disengage() {
s.emitVtString(vtRestoreTitle) s.emitVtString(vtRestoreTitle)
s.emitVtString(vtExitCA) s.emitVtString(vtExitCA)
} }
} else if !s.disableAlt {
s.clearScreen(StyleDefault, s.vten)
s.setCursorPos(0, 0, false)
}
s.setCursorInfo(&s.ocursor) s.setCursorInfo(&s.ocursor)
s.setBufferSize(int(s.oscreen.size.x), int(s.oscreen.size.y)) s.setBufferSize(int(s.oscreen.size.x), int(s.oscreen.size.y))
s.setInMode(s.oimode) s.setInMode(s.oimode)
@@ -388,8 +348,7 @@ func (s *cScreen) engage() error {
s.running = true s.running = true
s.cancelflag = syscall.Handle(cf) s.cancelflag = syscall.Handle(cf)
s.enableMouse(s.mouseEnabled) s.enableMouse(s.mouseEnabled)
s.setInMode(modeVtInput | modeResizeEn | modeExtendFlg)
if s.vten {
s.setOutMode(modeVtOutput | modeNoAutoNL | modeCookedOut | modeUnderline) s.setOutMode(modeVtOutput | modeNoAutoNL | modeCookedOut | modeUnderline)
if !s.disableAlt { if !s.disableAlt {
s.emitVtString(vtSaveTitle) s.emitVtString(vtSaveTitle)
@@ -399,11 +358,8 @@ func (s *cScreen) engage() error {
if s.title != "" { if s.title != "" {
s.emitVtString(fmt.Sprintf(vtSetTitle, s.title)) s.emitVtString(fmt.Sprintf(vtSetTitle, s.title))
} }
} else {
s.setOutMode(0)
}
s.clearScreen(s.style, s.vten) s.clearScreen(s.style)
s.hideCursor() s.hideCursor()
s.cells.Invalidate() s.cells.Invalidate()
@@ -445,7 +401,6 @@ func (s *cScreen) emitVtString(vs string) {
} }
func (s *cScreen) showCursor() { func (s *cScreen) showCursor() {
if s.vten {
s.emitVtString(vtShowCursor) s.emitVtString(vtShowCursor)
s.emitVtString(vtCursorStyles[s.cursorStyle]) s.emitVtString(vtCursorStyles[s.cursorStyle])
if s.cursorColor == ColorReset { if s.cursorColor == ColorReset {
@@ -454,17 +409,10 @@ func (s *cScreen) showCursor() {
r, g, b := s.cursorColor.RGB() r, g, b := s.cursorColor.RGB()
s.emitVtString(fmt.Sprintf(vtCursorColorRGB, r, g, b)) s.emitVtString(fmt.Sprintf(vtCursorColorRGB, r, g, b))
} }
} else {
s.setCursorInfo(&cursorInfo{size: 100, visible: 1})
}
} }
func (s *cScreen) hideCursor() { func (s *cScreen) hideCursor() {
if s.vten {
s.emitVtString(vtHideCursor) s.emitVtString(vtHideCursor)
} else {
s.setCursorInfo(&cursorInfo{size: 1, visible: 0})
}
} }
func (s *cScreen) ShowCursor(x, y int) { func (s *cScreen) ShowCursor(x, y int) {
@@ -495,7 +443,7 @@ func (s *cScreen) doCursor() {
if x < 0 || y < 0 || x >= s.w || y >= s.h { if x < 0 || y < 0 || x >= s.w || y >= s.h {
s.hideCursor() s.hideCursor()
} else { } else {
s.setCursorPos(x, y, s.vten) s.setCursorPos(x, y)
s.showCursor() s.showCursor()
} }
} }
@@ -504,20 +452,6 @@ func (s *cScreen) HideCursor() {
s.ShowCursor(-1, -1) s.ShowCursor(-1, -1)
} }
type inputRecord struct {
typ uint16
_ uint16
data [16]byte
}
const (
keyEvent uint16 = 1
mouseEvent uint16 = 2
resizeEvent uint16 = 4
menuEvent uint16 = 8 // don't use
focusEvent uint16 = 16
)
type mouseRecord struct { type mouseRecord struct {
x int16 x int16
y int16 y int16
@@ -655,25 +589,28 @@ var vkKeys = map[uint16]Key{
func getu32(v []byte) uint32 { func getu32(v []byte) uint32 {
return uint32(v[0]) + (uint32(v[1]) << 8) + (uint32(v[2]) << 16) + (uint32(v[3]) << 24) return uint32(v[0]) + (uint32(v[1]) << 8) + (uint32(v[2]) << 16) + (uint32(v[3]) << 24)
} }
func geti32(v []byte) int32 { func geti32(v []byte) int32 {
return int32(getu32(v)) return int32(getu32(v))
} }
func getu16(v []byte) uint16 { func getu16(v []byte) uint16 {
return uint16(v[0]) + (uint16(v[1]) << 8) return uint16(v[0]) + (uint16(v[1]) << 8)
} }
func geti16(v []byte) int16 { func geti16(v []byte) int16 {
return int16(getu16(v)) return int16(getu16(v))
} }
// Convert windows dwControlKeyState to modifier mask // Convert windows dwControlKeyState to modifier mask
func mod2mask(cks uint32) ModMask { func mod2mask(cks uint32, filter_ctrl_alt bool) ModMask {
mm := ModNone mm := ModNone
// Left or right control // Left or right control
ctrl := (cks & (0x0008 | 0x0004)) != 0 ctrl := (cks & (0x0008 | 0x0004)) != 0
// Left or right alt // Left or right alt
alt := (cks & (0x0002 | 0x0001)) != 0 alt := (cks & (0x0002 | 0x0001)) != 0
// Filter out ctrl+alt (it means AltGr) // Filter out ctrl+alt (it means AltGr)
if !(ctrl && alt) { if !filter_ctrl_alt || !(ctrl && alt) {
if ctrl { if ctrl {
mm |= ModCtrl mm |= ModCtrl
} }
@@ -787,11 +724,15 @@ func (s *cScreen) getConsoleInput() error {
if krec.ch != 0 { if krec.ch != 0 {
// synthesized key code // synthesized key code
for krec.repeat > 0 { for krec.repeat > 0 {
if krec.ch < ' ' && mod2mask(krec.mod, false) == ModCtrl {
krec.ch += '\x60'
}
// convert shift+tab to backtab // convert shift+tab to backtab
if mod2mask(krec.mod) == ModShift && krec.ch == vkTab { if mod2mask(krec.mod, false) == ModShift && krec.ch == vkTab {
s.postEvent(NewEventKey(KeyBacktab, 0, ModNone)) s.postEvent(NewEventKey(KeyBacktab, 0, ModNone))
} else { } else {
s.postEvent(NewEventKey(KeyRune, rune(krec.ch), mod2mask(krec.mod))) s.postEvent(NewEventKey(KeyRune, rune(krec.ch), mod2mask(krec.mod, true)))
} }
krec.repeat-- krec.repeat--
} }
@@ -803,7 +744,7 @@ func (s *cScreen) getConsoleInput() error {
return nil return nil
} }
for krec.repeat > 0 { for krec.repeat > 0 {
s.postEvent(NewEventKey(key, rune(krec.ch), mod2mask(krec.mod))) s.postEvent(NewEventKey(key, rune(krec.ch), mod2mask(krec.mod, false)))
krec.repeat-- krec.repeat--
} }
@@ -816,7 +757,7 @@ func (s *cScreen) getConsoleInput() error {
mrec.flags = getu32(rec.data[12:]) mrec.flags = getu32(rec.data[12:])
btns := mrec2btns(mrec.btns, mrec.flags) btns := mrec2btns(mrec.btns, mrec.flags)
// we ignore double click, events are delivered normally // we ignore double click, events are delivered normally
s.postEvent(NewEventMouse(int(mrec.x), int(mrec.y), btns, mod2mask(mrec.mod))) s.postEvent(NewEventMouse(int(mrec.x), int(mrec.y), btns, mod2mask(mrec.mod, false)))
case resizeEvent: case resizeEvent:
var rrec resizeRecord var rrec resizeRecord
@@ -858,11 +799,10 @@ func (s *cScreen) scanInput(stopQ chan struct{}) {
} }
func (s *cScreen) Colors() int { func (s *cScreen) Colors() int {
if s.vten { if !s.truecolor {
return 1 << 24
}
// Windows console can display 8 colors, in either low or high intensity
return 16 return 16
}
return 1 << 24
} }
var vgaColors = map[Color]uint16{ var vgaColors = map[Color]uint16{
@@ -938,7 +878,7 @@ func (s *cScreen) mapStyle(style Style) uint16 {
return attr return attr
} }
func (s *cScreen) sendVtStyle(style Style) { func (s *cScreen) makeVtStyle(style Style) string {
esc := &strings.Builder{} esc := &strings.Builder{}
fg, bg, attrs := style.fg, style.bg, style.attrs fg, bg, attrs := style.fg, style.bg, style.attrs
@@ -998,30 +938,32 @@ func (s *cScreen) sendVtStyle(style Style) {
esc.WriteString(vtExitUrl) esc.WriteString(vtExitUrl)
} }
s.emitVtString(esc.String()) return esc.String()
} }
func (s *cScreen) writeString(x, y int, style Style, ch []uint16) { func (s *cScreen) sendVtStyle(style Style) {
s.emitVtString(s.makeVtStyle(style))
}
func (s *cScreen) writeString(x, y int, style Style, vtBuf, ch []uint16) {
// we assume the caller has hidden the cursor // we assume the caller has hidden the cursor
if len(ch) == 0 { if len(ch) == 0 {
return return
} }
s.setCursorPos(x, y, s.vten)
if s.vten { vtBuf = append(vtBuf, utf16.Encode([]rune(fmt.Sprintf(vtCursorPos, y+1, x+1)))...)
s.sendVtStyle(style) styleStr := s.makeVtStyle(style)
} else { vtBuf = append(vtBuf, utf16.Encode([]rune(styleStr))...)
_, _, _ = procSetConsoleTextAttribute.Call( vtBuf = append(vtBuf, ch...)
uintptr(s.out), _ = syscall.WriteConsole(s.out, &vtBuf[0], uint32(len(vtBuf)), nil, nil)
uintptr(s.mapStyle(style))) vtBuf = vtBuf[:0]
}
_ = syscall.WriteConsole(s.out, &ch[0], uint32(len(ch)), nil, nil)
} }
func (s *cScreen) draw() { func (s *cScreen) draw() {
// allocate a scratch line bit enough for no combining chars. // allocate a scratch line bit enough for no combining chars.
// if you have combining characters, you may pay for extra allocations. // if you have combining characters, you may pay for extra allocations.
buf := make([]uint16, 0, s.w) buf := make([]uint16, 0, s.w)
var vtBuf []uint16
wcs := buf[:] wcs := buf[:]
lstyle := styleInvalid lstyle := styleInvalid
@@ -1040,7 +982,7 @@ func (s *cScreen) draw() {
// write out any data queued thus far // write out any data queued thus far
// because we are going to skip over some // because we are going to skip over some
// cells, or because we need to change styles // cells, or because we need to change styles
s.writeString(lx, ly, lstyle, wcs) s.writeString(lx, ly, lstyle, vtBuf, wcs)
wcs = buf[0:0] wcs = buf[0:0]
lstyle = StyleDefault lstyle = StyleDefault
if !dirty { if !dirty {
@@ -1067,7 +1009,7 @@ func (s *cScreen) draw() {
} }
x += width - 1 x += width - 1
} }
s.writeString(lx, ly, lstyle, wcs) s.writeString(lx, ly, lstyle, vtBuf, wcs)
wcs = buf[0:0] wcs = buf[0:0]
lstyle = styleInvalid lstyle = styleInvalid
} }
@@ -1122,15 +1064,9 @@ func (s *cScreen) setCursorInfo(info *cursorInfo) {
uintptr(unsafe.Pointer(info))) uintptr(unsafe.Pointer(info)))
} }
func (s *cScreen) setCursorPos(x, y int, vtEnable bool) { func (s *cScreen) setCursorPos(x, y int) {
if vtEnable {
// Note that the string is Y first. Origin is 1,1. // Note that the string is Y first. Origin is 1,1.
s.emitVtString(fmt.Sprintf(vtCursorPos, y+1, x+1)) s.emitVtString(fmt.Sprintf(vtCursorPos, y+1, x+1))
} else {
_, _, _ = procSetConsoleCursorPosition.Call(
uintptr(s.out),
coord{int16(x), int16(y)}.uintptr())
}
} }
func (s *cScreen) setBufferSize(x, y int) { func (s *cScreen) setBufferSize(x, y int) {
@@ -1206,52 +1142,30 @@ func (s *cScreen) resize() {
} }
} }
func (s *cScreen) clearScreen(style Style, vtEnable bool) { func (s *cScreen) clearScreen(style Style) {
if vtEnable {
s.sendVtStyle(style) s.sendVtStyle(style)
row := strings.Repeat(" ", s.w) row := strings.Repeat(" ", s.w)
for y := 0; y < s.h; y++ { for y := 0; y < s.h; y++ {
s.setCursorPos(0, y, vtEnable) s.setCursorPos(0, y)
s.emitVtString(row) s.emitVtString(row)
} }
s.setCursorPos(0, 0, vtEnable) s.setCursorPos(0, 0)
} else {
pos := coord{0, 0}
attr := s.mapStyle(style)
x, y := s.w, s.h
scratch := uint32(0)
count := uint32(x * y)
_, _, _ = procFillConsoleOutputAttribute.Call(
uintptr(s.out),
uintptr(attr),
uintptr(count),
pos.uintptr(),
uintptr(unsafe.Pointer(&scratch)))
_, _, _ = procFillConsoleOutputCharacter.Call(
uintptr(s.out),
uintptr(' '),
uintptr(count),
pos.uintptr(),
uintptr(unsafe.Pointer(&scratch)))
}
} }
const ( const (
// Input modes // Input modes
modeExtendFlg uint32 = 0x0080 modeExtendFlg = uint32(0x0080)
modeMouseEn = 0x0010 modeMouseEn = uint32(0x0010)
modeResizeEn = 0x0008 modeResizeEn = uint32(0x0008)
// modeCooked = 0x0001 modeVtInput = uint32(0x0200)
// modeVtInput = 0x0200 // modeCooked = uint32(0x0001)
// Output modes // Output modes
modeCookedOut uint32 = 0x0001 modeCookedOut = uint32(0x0001)
modeVtOutput = 0x0004 modeVtOutput = uint32(0x0004)
modeNoAutoNL = 0x0008 modeNoAutoNL = uint32(0x0008)
modeUnderline = 0x0010 // ENABLE_LVB_GRID_WORLDWIDE, needed for underlines modeUnderline = uint32(0x0010) // ENABLE_LVB_GRID_WORLDWIDE, needed for underlines
// modeWrapEOL = 0x0002 // modeWrapEOL = uint32(0x0002)
) )
func (s *cScreen) setInMode(mode uint32) { func (s *cScreen) setInMode(mode uint32) {
@@ -1287,9 +1201,7 @@ func (s *cScreen) SetStyle(style Style) {
func (s *cScreen) SetTitle(title string) { func (s *cScreen) SetTitle(title string) {
s.Lock() s.Lock()
s.title = title s.title = title
if s.vten {
s.emitVtString(fmt.Sprintf(vtSetTitle, title)) s.emitVtString(fmt.Sprintf(vtSetTitle, title))
}
s.Unlock() s.Unlock()
} }
@@ -1320,43 +1232,8 @@ func (s *cScreen) GetClipboard() {
func (s *cScreen) Resize(int, int, int, int) {} func (s *cScreen) Resize(int, int, int, int) {}
func (s *cScreen) HasKey(k Key) bool { func (s *cScreen) HasKey(_ Key) bool {
// Microsoft has codes for some keys, but they are unusual, return true
// so we don't include them. We include all the typical
// 101, 105 key layout keys.
valid := map[Key]bool{
KeyBackspace: true,
KeyTab: true,
KeyEscape: true,
KeyPause: true,
KeyPrint: true,
KeyPgUp: true,
KeyPgDn: true,
KeyEnter: true,
KeyEnd: true,
KeyHome: true,
KeyLeft: true,
KeyUp: true,
KeyRight: true,
KeyDown: true,
KeyInsert: true,
KeyDelete: true,
KeyF1: true,
KeyF2: true,
KeyF3: true,
KeyF4: true,
KeyF5: true,
KeyF6: true,
KeyF7: true,
KeyF8: true,
KeyF9: true,
KeyF10: true,
KeyF11: true,
KeyF12: true,
KeyRune: true,
}
return valid[k]
} }
func (s *cScreen) Beep() error { func (s *cScreen) Beep() error {
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2025 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"
"github.com/rivo/uniseg"
)
func init() {
if rw := strings.ToLower(os.Getenv("RUNEWIDTH_EASTASIAN")); rw == "1" || rw == "true" || rw == "yes" {
uniseg.EastAsianAmbiguousWidth = 2
} else {
uniseg.EastAsianAmbiguousWidth = 1
}
}
+944
View File
@@ -0,0 +1,944 @@
// Copyright 2025 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.
// This file describes a generic VT input processor. It parses key sequences,
// (input bytes) and loads them into events. It expects UTF-8 or UTF-16 as the input
// feed, along with ECMA-48 sequences. The assumption here is that all potential
// key sequences are unambiguous between terminal variants (analysis of extant terminfo
// data appears to support this conjecture). This allows us to implement this once,
// in the most efficient and terminal-agnostic way possible.
//
// There is unfortunately *one* conflict, with aixterm, for CSI-P - which is KeyDelete
// in aixterm, but F1 in others.
package tcell
import (
"encoding/base64"
"os"
"strconv"
"strings"
"sync"
"time"
"unicode/utf16"
"unicode/utf8"
)
type inpState int
const (
inpStateInit = inpState(iota)
inpStateUtf
inpStateEsc
inpStateCsi // control sequence introducer
inpStateOsc // operating system command
inpStateDcs // device control string
inpStateSos // start of string (unused)
inpStatePm // privacy message (unused)
inpStateApc // application program command
inpStateSt // string terminator
inpStateSs2 // single shift 2
inpStateSs3 // single shift 3
inpStateLFK // linux F-key (not ECMA-48 compliant - bogus CSI)
)
type InputProcessor interface {
ScanUTF8([]byte)
ScanUTF16([]uint16)
SetSize(rows, cols int)
}
func NewInputProcessor(eq chan<- Event) InputProcessor {
return &inputProcessor{
evch: eq,
buf: make([]rune, 0, 128),
}
}
type inputProcessor struct {
ut8 []byte
ut16 []uint16
buf []rune
scratch []byte
csiParams []byte
csiInterm []byte
escaped bool
btnDown bool // mouse button tracking for broken terms
state inpState
strState inpState // saved str state (needed for ST)
timer *time.Timer
expire time.Time
l sync.Mutex
encBuf []rune
evch chan<- Event
rows int // used for clipping mouse coordinates
cols int // used for clipping mouse coordinates
surrogate rune
nested *inputProcessor
}
func (ip *inputProcessor) SetSize(w, h int) {
if ip.nested != nil {
ip.nested.SetSize(w, h)
return
}
go func() {
ip.l.Lock()
ip.rows = h
ip.cols = w
ip.post(NewEventResize(w, h))
ip.l.Unlock()
}()
}
func (ip *inputProcessor) post(ev Event) {
if ip.escaped {
ip.escaped = false
if ke, ok := ev.(*EventKey); ok {
ev = NewEventKey(ke.Key(), ke.Rune(), ke.Modifiers()|ModAlt)
}
} else if ke, ok := ev.(*EventKey); ok {
switch ke.Key() {
case keyPasteStart:
ev = NewEventPaste(true)
case keyPasteEnd:
ev = NewEventPaste(false)
}
}
ip.evch <- ev
}
func (ip *inputProcessor) escTimeout() {
ip.l.Lock()
defer ip.l.Unlock()
if ip.state == inpStateEsc && ip.expire.Before(time.Now()) {
// post it
ip.state = inpStateInit
ip.escaped = false
ip.post(NewEventKey(KeyEsc, 0, ModNone))
}
}
type csiParamMode struct {
M rune // Mode
P int // Parameter (first)
}
type keyMap struct {
Key Key
Mod ModMask
Rune rune
}
var csiAllKeys = map[csiParamMode]keyMap{
{M: 'A'}: {Key: KeyUp},
{M: 'B'}: {Key: KeyDown},
{M: 'C'}: {Key: KeyRight},
{M: 'D'}: {Key: KeyLeft},
{M: 'F'}: {Key: KeyEnd},
{M: 'H'}: {Key: KeyHome},
{M: 'L'}: {Key: KeyInsert},
{M: 'P'}: {Key: KeyF1}, // except for aixterm, where this is Delete
{M: 'Q'}: {Key: KeyF2},
{M: 'S'}: {Key: KeyF4},
{M: 'Z'}: {Key: KeyBacktab},
{M: 'a'}: {Key: KeyUp, Mod: ModShift},
{M: 'b'}: {Key: KeyDown, Mod: ModShift},
{M: 'c'}: {Key: KeyRight, Mod: ModShift},
{M: 'd'}: {Key: KeyLeft, Mod: ModShift},
{M: 'q', P: 1}: {Key: KeyF1}, // all these 'q' are for aixterm
{M: 'q', P: 2}: {Key: KeyF2},
{M: 'q', P: 3}: {Key: KeyF3},
{M: 'q', P: 4}: {Key: KeyF4},
{M: 'q', P: 5}: {Key: KeyF5},
{M: 'q', P: 6}: {Key: KeyF6},
{M: 'q', P: 7}: {Key: KeyF7},
{M: 'q', P: 8}: {Key: KeyF8},
{M: 'q', P: 9}: {Key: KeyF9},
{M: 'q', P: 10}: {Key: KeyF10},
{M: 'q', P: 11}: {Key: KeyF11},
{M: 'q', P: 12}: {Key: KeyF12},
{M: 'q', P: 13}: {Key: KeyF13},
{M: 'q', P: 14}: {Key: KeyF14},
{M: 'q', P: 15}: {Key: KeyF15},
{M: 'q', P: 16}: {Key: KeyF16},
{M: 'q', P: 17}: {Key: KeyF17},
{M: 'q', P: 18}: {Key: KeyF18},
{M: 'q', P: 19}: {Key: KeyF19},
{M: 'q', P: 20}: {Key: KeyF20},
{M: 'q', P: 21}: {Key: KeyF21},
{M: 'q', P: 22}: {Key: KeyF22},
{M: 'q', P: 23}: {Key: KeyF23},
{M: 'q', P: 24}: {Key: KeyF24},
{M: 'q', P: 25}: {Key: KeyF25},
{M: 'q', P: 26}: {Key: KeyF26},
{M: 'q', P: 27}: {Key: KeyF27},
{M: 'q', P: 28}: {Key: KeyF28},
{M: 'q', P: 29}: {Key: KeyF29},
{M: 'q', P: 30}: {Key: KeyF30},
{M: 'q', P: 31}: {Key: KeyF31},
{M: 'q', P: 32}: {Key: KeyF32},
{M: 'q', P: 33}: {Key: KeyF33},
{M: 'q', P: 34}: {Key: KeyF34},
{M: 'q', P: 35}: {Key: KeyF35},
{M: 'q', P: 36}: {Key: KeyF36},
{M: 'q', P: 144}: {Key: KeyClear},
{M: 'q', P: 146}: {Key: KeyEnd},
{M: 'q', P: 150}: {Key: KeyPgUp},
{M: 'q', P: 154}: {Key: KeyPgDn},
{M: 'z', P: 214}: {Key: KeyHome},
{M: 'z', P: 216}: {Key: KeyPgUp},
{M: 'z', P: 220}: {Key: KeyEnd},
{M: 'z', P: 222}: {Key: KeyPgDn},
{M: 'z', P: 224}: {Key: KeyF1},
{M: 'z', P: 225}: {Key: KeyF2},
{M: 'z', P: 226}: {Key: KeyF3},
{M: 'z', P: 227}: {Key: KeyF4},
{M: 'z', P: 228}: {Key: KeyF5},
{M: 'z', P: 229}: {Key: KeyF6},
{M: 'z', P: 230}: {Key: KeyF7},
{M: 'z', P: 231}: {Key: KeyF8},
{M: 'z', P: 232}: {Key: KeyF9},
{M: 'z', P: 233}: {Key: KeyF10},
{M: 'z', P: 234}: {Key: KeyF11},
{M: 'z', P: 235}: {Key: KeyF12},
{M: 'z', P: 247}: {Key: KeyInsert},
{M: '^', P: 7}: {Key: KeyHome, Mod: ModCtrl},
{M: '^', P: 8}: {Key: KeyEnd, Mod: ModCtrl},
{M: '^', P: 11}: {Key: KeyF23},
{M: '^', P: 12}: {Key: KeyF24},
{M: '^', P: 13}: {Key: KeyF25},
{M: '^', P: 14}: {Key: KeyF26},
{M: '^', P: 15}: {Key: KeyF27},
{M: '^', P: 17}: {Key: KeyF28}, // 16 is a gap
{M: '^', P: 18}: {Key: KeyF29},
{M: '^', P: 19}: {Key: KeyF30},
{M: '^', P: 20}: {Key: KeyF31},
{M: '^', P: 21}: {Key: KeyF32},
{M: '^', P: 23}: {Key: KeyF33}, // 22 is a gap
{M: '^', P: 24}: {Key: KeyF34},
{M: '^', P: 25}: {Key: KeyF35},
{M: '^', P: 26}: {Key: KeyF36}, // 27 is a gap
{M: '^', P: 28}: {Key: KeyF37},
{M: '^', P: 29}: {Key: KeyF38}, // 30 is a gap
{M: '^', P: 31}: {Key: KeyF39},
{M: '^', P: 32}: {Key: KeyF40},
{M: '^', P: 33}: {Key: KeyF41},
{M: '^', P: 34}: {Key: KeyF42},
{M: '@', P: 23}: {Key: KeyF43},
{M: '@', P: 24}: {Key: KeyF44},
{M: '$', P: 2}: {Key: KeyInsert, Mod: ModShift},
{M: '$', P: 3}: {Key: KeyDelete, Mod: ModShift},
{M: '$', P: 7}: {Key: KeyHome, Mod: ModShift},
{M: '$', P: 8}: {Key: KeyEnd, Mod: ModShift},
{M: '$', P: 23}: {Key: KeyF21},
{M: '$', P: 24}: {Key: KeyF22},
{M: '~', P: 1}: {Key: KeyHome},
{M: '~', P: 2}: {Key: KeyInsert},
{M: '~', P: 3}: {Key: KeyDelete},
{M: '~', P: 4}: {Key: KeyEnd},
{M: '~', P: 5}: {Key: KeyPgUp},
{M: '~', P: 6}: {Key: KeyPgDn},
{M: '~', P: 7}: {Key: KeyHome},
{M: '~', P: 8}: {Key: KeyEnd},
{M: '~', P: 11}: {Key: KeyF1},
{M: '~', P: 12}: {Key: KeyF2},
{M: '~', P: 13}: {Key: KeyF3},
{M: '~', P: 14}: {Key: KeyF4},
{M: '~', P: 15}: {Key: KeyF5},
{M: '~', P: 17}: {Key: KeyF6},
{M: '~', P: 18}: {Key: KeyF7},
{M: '~', P: 19}: {Key: KeyF8},
{M: '~', P: 20}: {Key: KeyF9},
{M: '~', P: 21}: {Key: KeyF10},
{M: '~', P: 23}: {Key: KeyF11},
{M: '~', P: 24}: {Key: KeyF12},
{M: '~', P: 25}: {Key: KeyF13},
{M: '~', P: 26}: {Key: KeyF14},
{M: '~', P: 28}: {Key: KeyF15}, // aka KeyHelp
{M: '~', P: 29}: {Key: KeyF16},
{M: '~', P: 31}: {Key: KeyF17},
{M: '~', P: 32}: {Key: KeyF18},
{M: '~', P: 33}: {Key: KeyF19},
{M: '~', P: 34}: {Key: KeyF20},
{M: '~', P: 200}: {Key: keyPasteStart},
{M: '~', P: 201}: {Key: keyPasteEnd},
}
// keys reported using Kitty csi-u protocol
var csiUKeys = map[int]keyMap{
27: {Key: KeyESC},
9: {Key: KeyTAB},
13: {Key: KeyEnter},
127: {Key: KeyBS},
57358: {Key: KeyCapsLock},
57359: {Key: KeyScrollLock},
57360: {Key: KeyNumLock},
57361: {Key: KeyPrint},
57362: {Key: KeyPause},
57363: {Key: KeyMenu},
57376: {Key: KeyF13},
57377: {Key: KeyF14},
57378: {Key: KeyF15},
57379: {Key: KeyF16},
57380: {Key: KeyF17},
57381: {Key: KeyF18},
57382: {Key: KeyF19},
57383: {Key: KeyF20},
57384: {Key: KeyF21},
57385: {Key: KeyF22},
57386: {Key: KeyF23},
57387: {Key: KeyF24},
57388: {Key: KeyF25},
57389: {Key: KeyF26},
57390: {Key: KeyF27},
57391: {Key: KeyF28},
57392: {Key: KeyF29},
57393: {Key: KeyF30},
57394: {Key: KeyF31},
57395: {Key: KeyF32},
57396: {Key: KeyF33},
57397: {Key: KeyF34},
57398: {Key: KeyF35},
57399: {Key: KeyRune, Rune: '0'}, // KP 0
57400: {Key: KeyRune, Rune: '1'}, // KP 1
57401: {Key: KeyRune, Rune: '2'}, // KP 2
57402: {Key: KeyRune, Rune: '3'}, // KP 3
57403: {Key: KeyRune, Rune: '4'}, // KP 4
57404: {Key: KeyRune, Rune: '5'}, // KP 5
57405: {Key: KeyRune, Rune: '6'}, // KP 6
57406: {Key: KeyRune, Rune: '7'}, // KP 7
57407: {Key: KeyRune, Rune: '8'}, // KP 8
57408: {Key: KeyRune, Rune: '9'}, // KP 9
57409: {Key: KeyRune, Rune: '.'}, // KP_DECIMAL
57410: {Key: KeyRune, Rune: '/'}, // KP_DIVIDE
57411: {Key: KeyRune, Rune: '*'}, // KP_MULTIPLY
57412: {Key: KeyRune, Rune: '-'}, // KP_SUBTRACT
57413: {Key: KeyRune, Rune: '+'}, // KP_ADD
57414: {Key: KeyEnter}, // KP_ENTER
57415: {Key: KeyRune, Rune: '='}, // KP_EQUAL
57416: {Key: KeyClear}, // KP_SEPARATOR
57417: {Key: KeyLeft}, // KP_LEFT
57418: {Key: KeyRight}, // KP_RIGHT
57419: {Key: KeyUp}, // KP_UP
57420: {Key: KeyDown}, // KP_DOWN
57421: {Key: KeyPgUp}, // KP_PG_UP
57422: {Key: KeyPgDn}, // KP_PG_DN
57423: {Key: KeyHome}, // KP_HOME
57424: {Key: KeyEnd}, // KP_END
57425: {Key: KeyInsert}, // KP_INSERT
57426: {Key: KeyDelete}, // KP_DELETE
// 57427: {Key: KeyBegin}, // KP_BEGIN
// TODO: Media keys
}
// windows virtual key codes per microsoft
var winKeys = map[int]Key{
0x03: KeyCancel, // vkCancel
0x08: KeyBackspace, // vkBackspace
0x09: KeyTab, // vkTab
0x0c: KeyClear, // vClear
0x0d: KeyEnter, // vkReturn
0x13: KeyPause, // vkPause
0x1b: KeyEscape, // vkEscape
0x21: KeyPgUp, // vkPrior
0x22: KeyPgDn, // vkNext
0x23: KeyEnd, // vkEnd
0x24: KeyHome, // vkHome
0x25: KeyLeft, // vkLeft
0x26: KeyUp, // vkUp
0x27: KeyRight, // vkRight
0x28: KeyDown, // vkDown
0x2a: KeyPrint, // vkPrint
0x2c: KeyPrint, // vkPrtScr
0x2d: KeyInsert, // vkInsert
0x2e: KeyDelete, // vkDelete
0x2f: KeyHelp, // vkHelp
0x70: KeyF1, // vkF1
0x71: KeyF2, // vkF2
0x72: KeyF3, // vkF3
0x73: KeyF4, // vkF4
0x74: KeyF5, // vkF5
0x75: KeyF6, // vkF6
0x76: KeyF7, // vkF7
0x77: KeyF8, // vkF8
0x78: KeyF9, // vkF9
0x79: KeyF10, // vkF10
0x7a: KeyF11, // vkF11
0x7b: KeyF12, // vkF12
0x7c: KeyF13, // vkF13
0x7d: KeyF14, // vkF14
0x7e: KeyF15, // vkF15
0x7f: KeyF16, // vkF16
0x80: KeyF17, // vkF17
0x81: KeyF18, // vkF18
0x82: KeyF19, // vkF19
0x83: KeyF20, // vkF20
0x84: KeyF21, // vkF21
0x85: KeyF22, // vkF22
0x86: KeyF23, // vkF23
0x87: KeyF24, // vkF24
}
// keys by their SS3 - used in application mode usually (legacy VT-style)
var ss3Keys = map[rune]Key{
'A': KeyUp,
'B': KeyDown,
'C': KeyRight,
'D': KeyLeft,
'F': KeyEnd,
'H': KeyHome,
'P': KeyF1,
'Q': KeyF2,
'R': KeyF3,
'S': KeyF4,
't': KeyF5,
'u': KeyF6,
'v': KeyF7,
'l': KeyF8,
'w': KeyF9,
'x': KeyF10,
}
// linux terminal uses these non ECMA keys prefixed by CSI-[
var linuxFKeys = map[rune]Key{
'A': KeyF1,
'B': KeyF2,
'C': KeyF3,
'D': KeyF4,
'E': KeyF5,
}
func (ip *inputProcessor) scan() {
for _, r := range ip.buf {
ip.buf = ip.buf[1:]
if r > 0x7F {
// 8-bit extended Unicode we just treat as such - this will swallow anything else queued up
ip.state = inpStateInit
ip.post(NewEventKey(KeyRune, r, ModNone))
continue
}
switch ip.state {
case inpStateInit:
switch r {
case '\x1b':
// escape.. pending
ip.state = inpStateEsc
if len(ip.buf) == 0 && ip.nested == nil {
ip.expire = time.Now().Add(time.Millisecond * 50)
ip.timer = time.AfterFunc(time.Millisecond*60, ip.escTimeout)
}
case '\t':
ip.post(NewEventKey(KeyTab, 0, ModNone))
case '\b', '\x7F':
ip.post(NewEventKey(KeyBackspace, 0, ModNone))
case '\r':
ip.post(NewEventKey(KeyEnter, 0, ModNone))
default:
// Control keys - legacy handling
if r < ' ' {
ip.post(NewEventKey(KeyCtrlSpace+Key(r), 0, ModCtrl))
} else {
ip.post(NewEventKey(KeyRune, r, ModNone))
}
}
case inpStateEsc:
switch r {
case '[':
ip.state = inpStateCsi
ip.csiInterm = nil
ip.csiParams = nil
case ']':
ip.state = inpStateOsc
ip.scratch = nil
case 'N':
ip.state = inpStateSs2 // no known uses
ip.scratch = nil
case 'O':
ip.state = inpStateSs3
ip.scratch = nil
case 'X':
ip.state = inpStateSos
ip.scratch = nil
case '^':
ip.state = inpStatePm
ip.scratch = nil
case '_':
ip.state = inpStateApc
ip.scratch = nil
case '\\':
// string terminator reached, (orphaned?)
ip.state = inpStateInit
case '\t':
// Linux console only, does not conform to ECMA
ip.state = inpStateInit
ip.post(NewEventKey(KeyBacktab, 0, ModNone))
default:
if r == '\x1b' {
// leading ESC to capture alt
ip.escaped = true
} else {
// treat as alt-key ... legacy emulators only (no CSI-u or other)
ip.state = inpStateInit
mod := ModAlt
if r < ' ' {
mod |= ModCtrl
r += 0x60
}
ip.post(NewEventKey(KeyRune, r, mod))
}
}
case inpStateCsi:
// usual case for incoming keys
if r == '\x1b' {
// Per ECMA-48 §5.3.1, ESC restarts the escape
// sequence machine from any intermediate state.
ip.state = inpStateEsc
if len(ip.buf) == 0 && ip.nested == nil {
ip.expire = time.Now().Add(time.Millisecond * 50)
ip.timer = time.AfterFunc(time.Millisecond*60, ip.escTimeout)
}
} else if r >= 0x30 && r <= 0x3F { // parameter bytes
ip.csiParams = append(ip.csiParams, byte(r))
} else if r >= 0x20 && r <= 0x2F { // intermediate bytes, rarely used
ip.csiInterm = append(ip.csiInterm, byte(r))
} else if r >= 0x40 && r <= 0x7F { // final byte
ip.handleCsi(r, ip.csiParams, ip.csiInterm)
} else {
// bad parse, just swallow it all
ip.state = inpStateInit
}
case inpStateSs2:
// No known uses for SS2
ip.state = inpStateInit
case inpStateSs3: // typically application mode keys or older terminals
ip.state = inpStateInit
if r == '\x1b' {
// Per ECMA-48 §5.3.1, ESC restarts the escape
// sequence machine from any intermediate state.
ip.state = inpStateEsc
if len(ip.buf) == 0 && ip.nested == nil {
ip.expire = time.Now().Add(time.Millisecond * 50)
ip.timer = time.AfterFunc(time.Millisecond*60, ip.escTimeout)
}
} else if k, ok := ss3Keys[r]; ok {
ip.post(NewEventKey(k, 0, ModNone))
}
case inpStatePm, inpStateApc, inpStateSos, inpStateDcs: // these we just eat
switch r {
case '\x1b':
ip.strState = ip.state
ip.state = inpStateSt
case '\x07': // bell - some send this instead of ST
ip.state = inpStateInit
}
case inpStateOsc: // not sure if used
switch r {
case '\x1b':
ip.strState = ip.state
ip.state = inpStateSt
case '\x07':
ip.handleOsc(string(ip.scratch))
default:
ip.scratch = append(ip.scratch, byte(r&0x7f))
}
case inpStateSt:
if r == '\\' || r == '\x07' {
ip.state = inpStateInit
switch ip.strState {
case inpStateOsc:
ip.handleOsc(string(ip.scratch))
case inpStatePm, inpStateApc, inpStateSos, inpStateDcs:
ip.state = inpStateInit
}
} else {
ip.scratch = append(ip.scratch, '\x1b', byte(r))
ip.state = ip.strState
}
case inpStateLFK:
// linux console does not follow ECMA
if k, ok := linuxFKeys[r]; ok {
ip.post(NewEventKey(k, 0, ModNone))
}
ip.state = inpStateInit
}
}
}
func (ip *inputProcessor) handleOsc(str string) {
ip.state = inpStateInit
if content, ok := strings.CutPrefix(str, "52;c;"); ok {
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(content)))
if count, err := base64.StdEncoding.Decode(decoded, []byte(content)); err == nil {
ip.post(NewEventClipboard(decoded[:count]))
return
}
}
}
func calcModifier(n int) ModMask {
n--
m := ModNone
if n&1 != 0 {
m |= ModShift
}
if n&2 != 0 {
m |= ModAlt
}
if n&4 != 0 {
m |= ModCtrl
}
if n&8 != 0 {
m |= ModMeta // kitty calls this Super
}
if n&16 != 0 {
m |= ModHyper
}
if n&32 != 0 {
m |= ModMeta // for now not separating from Super
}
// Not doing (kitty only):
// caps_lock 0b1000000 (64)
// num_lock 0b10000000 (128)
return m
}
// func (ip *inputProcessor) handleMouse(x, y, btn int, down bool) *EventMouse {
func (ip *inputProcessor) handleMouse(mode rune, params []int) {
// XTerm mouse events only report at most one button at a time,
// which may include a wheel button. Wheel motion events are
// reported as single impulses, while other button events are reported
// as separate press & release events.
if len(params) < 3 {
return
}
btn := params[0]
// Some terminals will report mouse coordinates outside the
// screen, especially with click-drag events. Clip the coordinates
// to the screen in that case.
x := max(min(params[1]-1, ip.cols-1), 0)
y := max(min(params[2]-1, ip.rows-1), 0)
motion := (btn & 0x20) != 0
scroll := (btn & 0x42) == 0x40
btn &^= 0x20
if mode == 'm' {
// mouse release, clear all buttons
btn |= 3
btn &^= 0x40
ip.btnDown = false
} else if motion {
/*
* Some broken terminals appear to send
* mouse button one motion events, instead of
* encoding 35 (no buttons) into these events.
* We resolve these by looking for a non-motion
* event first.
*/
if !ip.btnDown {
btn |= 3
btn &^= 0x40
}
} else if !scroll {
ip.btnDown = true
}
button := ButtonNone
mod := ModNone
// Mouse wheel has bit 6 set, no release events. It should be noted
// that wheel events are sometimes misdelivered as mouse button events
// during a click-drag, so we debounce these, considering them to be
// button press events unless we see an intervening release event.
switch btn & 0x43 {
case 0:
button = Button1
case 1:
button = Button3 // Note we prefer to treat right as button 2
case 2:
button = Button2 // And the middle button as button 3
case 3:
button = ButtonNone
case 0x40:
button = WheelUp
case 0x41:
button = WheelDown
case 0x42:
button = WheelLeft
case 0x43:
button = WheelRight
}
if btn&0x4 != 0 {
mod |= ModShift
}
if btn&0x8 != 0 {
mod |= ModAlt
}
if btn&0x10 != 0 {
mod |= ModCtrl
}
ip.post(NewEventMouse(x, y, button, mod))
}
func (ip *inputProcessor) handleWinKey(P []int) {
// win32-input-mode
// ^[ [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _
// Vk: the value of wVirtualKeyCode - any number. If omitted, defaults to '0'.
// Sc: the value of wVirtualScanCode - any number. If omitted, defaults to '0'.
// Uc: the decimal value of UnicodeChar - for example, NUL is "0", LF is
// "10", the character 'A' is "65". If omitted, defaults to '0'.
// Kd: the value of bKeyDown - either a '0' or '1'. If omitted, defaults to '0'.
// Cs: the value of dwControlKeyState - any number. If omitted, defaults to '0'.
// Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'.
//
// Note that some 3rd party terminal emulators (not Terminal) suffer from a bug
// where other events, such as mouse events, are doubly encoded, using Vk 0
// for each character. (So a CSI-M sequence is encoded as a series of CSI-_
// sequences.) We consider this a bug in those terminal emulators -- Windows 11
// Terminal does not suffer this brain damage. (We've observed this with both Alacritty
// and WezTerm.)
for len(P) < 6 {
P = append(P, 0) // ensure sufficient length
}
if P[3] == 0 {
// key up event ignore ignore
return
}
if P[0] == 0 && P[1] == 0 && P[2] > 0 && P[2] < 0x80 { // only ASCII in win32-input-mode
if ip.nested == nil {
ip.nested = &inputProcessor{
evch: ip.evch,
rows: ip.rows,
cols: ip.cols,
}
}
ip.nested.ScanUTF8([]byte{byte(P[2])})
return
}
key := KeyRune
chr := rune(P[2])
mod := ModNone
rpt := max(1, P[5])
if k1, ok := winKeys[P[0]]; ok {
chr = 0
key = k1
} else if chr == 0 && P[0] >= 0x30 && P[0] <= 0x39 {
chr = rune(P[0])
} else if chr < ' ' && P[0] >= 0x41 && P[0] <= 0x5a {
key = Key(P[0])
chr = 0
} else if chr >= 0xD800 && chr <= 0xDBFF {
// high surrogate pair
ip.surrogate = chr
return
} else if chr >= 0xDC00 && chr <= 0xDFFF {
// low surrogate pair
chr = utf16.DecodeRune(ip.surrogate, chr)
} else if P[0] == 0x10 || P[0] == 0x11 || P[0] == 0x12 || P[0] == 0x14 {
// lone modifiers
ip.surrogate = 0
return
}
ip.surrogate = 0
// Modifiers
if P[4]&0x010 != 0 {
mod |= ModShift
}
if P[4]&0x000c != 0 {
mod |= ModCtrl
}
if P[4]&0x0003 != 0 {
mod |= ModAlt
}
if key == KeyRune && chr > ' ' && mod == ModShift {
// filter out lone shift for printable chars
mod = ModNone
}
if chr != 0 && mod&(ModCtrl|ModAlt) == ModCtrl|ModAlt {
// Filter out ctrl+alt (it means AltGr)
mod = ModNone
}
for range rpt {
if key != KeyRune || chr != 0 {
ip.post(NewEventKey(key, chr, mod))
}
}
}
func (ip *inputProcessor) handleCsi(mode rune, params []byte, intermediate []byte) {
// reset state
ip.state = inpStateInit
if len(intermediate) != 0 {
// we don't know what to do with these for now
return
}
var parts []string
var P []int
hasLT := false
pstr := string(params)
// extract numeric parameters
if strings.HasPrefix(pstr, "<") {
hasLT = true
pstr = pstr[1:]
}
if pstr != "" && pstr[0] >= '0' && pstr[0] <= '9' {
parts = strings.Split(pstr, ";")
for i := range parts {
if parts[i] != "" {
if n, e := strconv.ParseInt(parts[i], 10, 32); e == nil {
P = append(P, int(n))
}
}
}
}
var P0 int
if len(P) > 0 {
P0 = P[0]
}
if hasLT {
switch mode {
case 'm', 'M': // mouse event, we only do SGR tracking
ip.handleMouse(mode, P)
}
}
switch mode {
case 'I': // focus in
ip.post(NewEventFocus(true))
return
case 'O': // focus out
ip.post(NewEventFocus(false))
return
case '[':
// linux console F-key - CSI-[ modifies next key
ip.state = inpStateLFK
return
case 'u':
// CSI-u kitty keyboard protocol
if len(P) > 0 && !hasLT {
mod := ModNone
key := KeyRune
chr := rune(0)
if k1, ok := csiUKeys[P0]; ok {
key = k1.Key
chr = k1.Rune
} else {
chr = rune(P0)
}
if len(P) > 1 {
mod = calcModifier(P[1])
}
ip.post(NewEventKey(key, chr, mod))
}
return
case '_':
if len(intermediate) == 0 && len(P) > 0 {
ip.handleWinKey(P)
return
}
case '~':
if len(intermediate) == 0 && len(P) >= 2 {
mod := calcModifier(P[1])
if ks, ok := csiAllKeys[csiParamMode{M: mode, P: P0}]; ok {
ip.post(NewEventKey(ks.Key, 0, mod))
return
}
if P0 == 27 && len(P) > 2 && P[2] > 0 && P[2] <= 0xff {
if P[2] < ' ' || P[2] == 0x7F {
ip.post(NewEventKey(Key(P[2]), 0, mod))
} else {
ip.post(NewEventKey(KeyRune, rune(P[2]), mod))
}
return
}
}
}
if ks, ok := csiAllKeys[csiParamMode{M: mode, P: P0}]; ok && !hasLT {
if mode == '~' && len(P) > 1 && ks.Mod == ModNone {
// apply modifiers if present
ks.Mod = calcModifier(P[1])
} else if mode == 'P' && os.Getenv("TERM") == "aixterm" {
ks.Key = KeyDelete // aixterm hack - conflicts with kitty protocol
}
ip.post(NewEventKey(ks.Key, 0, ks.Mod))
return
}
// this might have been an SS3 style key with modifiers applied
if k, ok := ss3Keys[mode]; ok && P0 == 1 && len(P) > 1 {
ip.post(NewEventKey(k, 0, calcModifier(P[1])))
return
}
// if we got here we just swallow the unknown sequence
}
func (ip *inputProcessor) ScanUTF8(b []byte) {
ip.l.Lock()
defer ip.l.Unlock()
ip.ut8 = append(ip.ut8, b...)
for len(ip.ut8) > 0 {
// fast path, basic ascii
if ip.ut8[0] < 0x7F {
ip.buf = append(ip.buf, rune(ip.ut8[0]))
ip.ut8 = ip.ut8[1:]
} else {
r, len := utf8.DecodeRune(ip.ut8)
if r == utf8.RuneError {
r = rune(ip.ut8[0])
len = 1
}
ip.buf = append(ip.buf, r)
ip.ut8 = ip.ut8[len:]
}
}
ip.scan()
}
func (ip *inputProcessor) ScanUTF16(u []uint16) {
ip.l.Lock()
defer ip.l.Unlock()
ip.ut16 = append(ip.ut16, u...)
for len(ip.ut16) > 0 {
if !utf16.IsSurrogate(rune(ip.ut16[0])) {
ip.buf = append(ip.buf, rune(ip.ut16[0]))
ip.ut16 = ip.ut16[1:]
} else if len(ip.ut16) > 1 {
ip.buf = append(ip.buf, utf16.DecodeRune(rune(ip.ut16[0]), rune(ip.ut16[1])))
ip.ut16 = ip.ut16[2:]
} else {
break
}
}
}
+64 -7
View File
@@ -1,4 +1,4 @@
// Copyright 2016 The TCell Authors // Copyright 2025 The TCell Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License. // you may not use file except in compliance with the License.
@@ -171,6 +171,11 @@ var KeyNames = map[Key]string{
KeyF62: "F62", KeyF62: "F62",
KeyF63: "F63", KeyF63: "F63",
KeyF64: "F64", KeyF64: "F64",
KeyMenu: "Menu",
KeyCapsLock: "CapsLock",
KeyScrollLock: "ScrollLock",
KeyNumLock: "NumLock",
KeyCtrlSpace: "Ctrl-Space",
KeyCtrlA: "Ctrl-A", KeyCtrlA: "Ctrl-A",
KeyCtrlB: "Ctrl-B", KeyCtrlB: "Ctrl-B",
KeyCtrlC: "Ctrl-C", KeyCtrlC: "Ctrl-C",
@@ -178,9 +183,12 @@ var KeyNames = map[Key]string{
KeyCtrlE: "Ctrl-E", KeyCtrlE: "Ctrl-E",
KeyCtrlF: "Ctrl-F", KeyCtrlF: "Ctrl-F",
KeyCtrlG: "Ctrl-G", KeyCtrlG: "Ctrl-G",
KeyCtrlH: "Ctrl-H",
KeyCtrlI: "Ctrl-I",
KeyCtrlJ: "Ctrl-J", KeyCtrlJ: "Ctrl-J",
KeyCtrlK: "Ctrl-K", KeyCtrlK: "Ctrl-K",
KeyCtrlL: "Ctrl-L", KeyCtrlL: "Ctrl-L",
KeyCtrlM: "Ctrl-M",
KeyCtrlN: "Ctrl-N", KeyCtrlN: "Ctrl-N",
KeyCtrlO: "Ctrl-O", KeyCtrlO: "Ctrl-O",
KeyCtrlP: "Ctrl-P", KeyCtrlP: "Ctrl-P",
@@ -194,11 +202,11 @@ var KeyNames = map[Key]string{
KeyCtrlX: "Ctrl-X", KeyCtrlX: "Ctrl-X",
KeyCtrlY: "Ctrl-Y", KeyCtrlY: "Ctrl-Y",
KeyCtrlZ: "Ctrl-Z", KeyCtrlZ: "Ctrl-Z",
KeyCtrlSpace: "Ctrl-Space", KeyCtrlLeftSq: "Ctrl-[",
KeyCtrlUnderscore: "Ctrl-_",
KeyCtrlRightSq: "Ctrl-]", KeyCtrlRightSq: "Ctrl-]",
KeyCtrlBackslash: "Ctrl-\\", KeyCtrlBackslash: "Ctrl-\\",
KeyCtrlCarat: "Ctrl-^", KeyCtrlCarat: "Ctrl-^",
KeyCtrlUnderscore: "Ctrl-_",
} }
// Name returns a printable value or the key stroke. This can be used // Name returns a printable value or the key stroke. This can be used
@@ -218,6 +226,9 @@ func (ev *EventKey) Name() string {
if ev.mod&ModCtrl != 0 { if ev.mod&ModCtrl != 0 {
m = append(m, "Ctrl") m = append(m, "Ctrl")
} }
if ev.mod&ModHyper != 0 {
m = append(m, "Hyper")
}
ok := false ok := false
if s, ok = KeyNames[ev.key]; !ok { if s, ok = KeyNames[ev.key]; !ok {
@@ -246,15 +257,52 @@ func NewEventKey(k Key, ch rune, mod ModMask) *EventKey {
// control characters and the DEL. // control characters and the DEL.
k = Key(ch) k = Key(ch)
if mod == ModNone && ch < ' ' { if mod == ModNone && ch < ' ' {
switch Key(ch) { switch k {
case KeyBackspace, KeyTab, KeyEsc, KeyEnter: case KeyBackspace, KeyTab, KeyEsc, KeyEnter:
// these keys are directly typeable without CTRL // these keys are directly typeable without CTRL
default: default:
// most likely entered with a CTRL keypress // most likely entered with a CTRL keypress
mod = ModCtrl mod = ModCtrl
} }
ch = ch + '\x60'
} }
} }
if k == KeyRune && ch >= 'A' && ch <= 'Z' && mod == ModCtrl {
// We don't do Ctrl-[ or backslash or those specially.
k = KeyCtrlA + Key(ch-'A')
}
// Might be lower case
if k == KeyRune && ch >= 'a' && ch <= 'z' && mod == ModCtrl {
// We don't do Ctrl-[ or backslash or those specially.
k = KeyCtrlA + Key(ch-'a')
}
// Windows reports ModShift for shifted keys. This is inconsistent
// with UNIX, lets harmonize this.
if k == KeyRune && mod == ModShift && ch != 0 {
mod = ModNone
}
if k >= KeyCtrlA && k <= KeyCtrlZ {
if mod&ModShift != 0 {
ch = rune((k - KeyCtrlA) + 'A')
} else {
ch = rune((k - KeyCtrlA) + 'a')
}
}
// Backspace2 is just another name for backspace.
if k == KeyBackspace2 {
k = KeyBackspace
}
// Shift-Tab should be Backtab.
if k == KeyTab && (mod&ModShift) != 0 {
k = KeyBacktab
mod &^= ModShift
}
return &EventKey{t: time.Now(), key: k, ch: ch, mod: mod} return &EventKey{t: time.Now(), key: k, ch: ch, mod: mod}
} }
@@ -272,6 +320,7 @@ const (
ModCtrl ModCtrl
ModAlt ModAlt
ModMeta ModMeta
ModHyper
ModNone ModMask = 0 ModNone ModMask = 0
) )
@@ -373,6 +422,10 @@ const (
KeyF62 KeyF62
KeyF63 KeyF63
KeyF64 KeyF64
KeyMenu
KeyCapsLock
KeyScrollLock
KeyNumLock
) )
const ( const (
@@ -381,10 +434,12 @@ const (
keyPasteEnd keyPasteEnd
) )
// These are the control keys. Note that they overlap with other keys, // These are the control keys, they will also be reported with the
// perhaps. For example, KeyCtrlH is the same as KeyBackspace. // rune (lower case) and control modifier. If the shift key
// or other modifiers are present then these will *NOT* be reported,
// but reported instead as KeyRune.
const ( const (
KeyCtrlSpace Key = iota KeyCtrlSpace Key = iota + 64
KeyCtrlA KeyCtrlA
KeyCtrlB KeyCtrlB
KeyCtrlC KeyCtrlC
@@ -466,5 +521,7 @@ const (
KeyEsc = KeyESC KeyEsc = KeyESC
KeyEscape = KeyESC KeyEscape = KeyESC
KeyEnter = KeyCR KeyEnter = KeyCR
// NB: This key will be translated to KeyBackspace
KeyBackspace2 = KeyDEL KeyBackspace2 = KeyDEL
) )
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The TCell Authors // Copyright 2025 The TCell Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License. // you may not use file except in compliance with the License.
+70 -20
View File
@@ -1,4 +1,4 @@
// Copyright 2024 The TCell Authors // Copyright 2025 The TCell Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License. // you may not use file except in compliance with the License.
@@ -35,17 +35,37 @@ type Screen interface {
// is called (or Sync). // is called (or Sync).
Fill(rune, Style) Fill(rune, Style)
// SetCell is an older API, and will be removed. Please use // Put writes the first graphme of the given string with th
// SetContent instead; SetCell is implemented in terms of SetContent. // given style at the given coordinates. (Only the first grapheme
// occupying either one or two cells is stored.) It returns the
// remainder of the string, and the width displayed.
Put(x int, y int, str string, style Style) (string, int)
// PutStr writes a string starting at the given position, using the
// default style. The content is clipped to the screen dimensions.
PutStr(x int, y int, str string)
// PutStrStyled writes a string starting at the given position, using
// the given style. The cont4ent is clipped to the screen dimensions.
PutStrStyled(x int, y int, str string, style Style)
// SetCell is an older API, and will be removed.
//jj
// Deprecated: Please use Put instead.
SetCell(x int, y int, style Style, ch ...rune) SetCell(x int, y int, style Style, ch ...rune)
// GetContent returns the contents at the given location. If the // Get the contents at the given location. If the
// coordinates are out of range, then the values will be 0, nil, // coordinates are out of range, then the values will be 0, nil,
// StyleDefault. Note that the contents returned are logical contents // StyleDefault. Note that the contents returned are logical contents
// and may not actually be what is displayed, but rather are what will // 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 // 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 // in screen cells; most often this will be 1, but some East Asian
// characters and emoji require two cells. // characters and emoji require two cells.
Get(x, y int) (str string, style Style, width int)
// GetContent is the old way to get cell contents.
//
// Deprecated: Use Get() instead.
GetContent(x, y int) (primary rune, combining []rune, style Style, width int) GetContent(x, y int) (primary rune, combining []rune, style Style, width int)
// SetContent sets the contents of the given cell location. If // SetContent sets the contents of the given cell location. If
@@ -221,6 +241,9 @@ type Screen interface {
// fallbacks are registered, this will return true. This will // fallbacks are registered, this will return true. This will
// also return true if the terminal can replace the glyph with // also return true if the terminal can replace the glyph with
// one that is visually indistinguishable from the one requested. // one that is visually indistinguishable from the one requested.
//
// Deprecated: This is not a particularly useful or reliable function,
// due to limitations in fonts, etc. It will be removed in the future.
CanDisplay(r rune, checkFallbacks bool) bool CanDisplay(r rune, checkFallbacks bool) bool
// Resize does nothing, since it's generally not possible to // Resize does nothing, since it's generally not possible to
@@ -228,14 +251,13 @@ type Screen interface {
// the View interface. // the View interface.
Resize(int, int, int, int) Resize(int, int, int, int)
// HasKey returns true if the keyboard is believed to have the // HasKey always returns true.
// 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 // Deprecated: This function always returns true. Applications
// as supported but not actually be usable (such as some emulators // cannot reliably detect whether a key is supported or not with
// that hijack certain keys). Its best not to depend to strictly // modern terminal emulators. (The intended use here was to help
// on this function, but it can be used for hinting when building // applications determine whether a given key stroke was supported
// menus, displayed hot-keys, etc. Note that KeyRune (literal // by the terminal, but it was never reliable.)
// runes) is always true.
HasKey(Key) bool HasKey(Key) bool
// Suspend pauses input and output processing. It also restores the // Suspend pauses input and output processing. It also restores the
@@ -288,10 +310,9 @@ type Screen interface {
// NewScreen returns a default Screen suitable for the user's terminal // NewScreen returns a default Screen suitable for the user's terminal
// environment. // environment.
func NewScreen() (Screen, error) { func NewScreen() (Screen, error) {
// Windows is happier if we try for a console screen first. if s, e := NewTerminfoScreen(); s != nil {
if s, _ := NewConsoleScreen(); s != nil {
return s, nil return s, nil
} else if s, e := NewTerminfoScreen(); s != nil { } else if s, _ := NewConsoleScreen(); s != nil {
return s, nil return s, nil
} else { } else {
return nil, e return nil, e
@@ -382,11 +403,37 @@ type baseScreen struct {
screenImpl screenImpl
} }
func (b *baseScreen) Put(x int, y int, str string, style Style) (remain string, width int) {
cells := b.GetCells()
b.Lock()
defer b.Unlock()
return cells.Put(x, y, str, style)
}
func (b *baseScreen) PutStrStyled(x int, y int, str string, style Style) {
cells := b.GetCells()
b.Lock()
cols, rows := cells.Size()
width := 0
for str != "" && x < cols && y < rows {
str, width = cells.Put(x, y, str, style)
if width == 0 {
break
}
x += width
}
defer b.Unlock()
}
func (b *baseScreen) PutStr(x, y int, str string) {
b.PutStrStyled(x, y, str, StyleDefault)
}
func (b *baseScreen) SetCell(x int, y int, style Style, ch ...rune) { func (b *baseScreen) SetCell(x int, y int, style Style, ch ...rune) {
if len(ch) > 0 { if len(ch) > 0 {
b.SetContent(x, y, ch[0], ch[1:], style) b.Put(x, y, string(ch), style)
} else { } else {
b.SetContent(x, y, ' ', nil, style) b.Put(x, y, " ", style)
} }
} }
@@ -401,12 +448,15 @@ func (b *baseScreen) Fill(r rune, style Style) {
b.Unlock() b.Unlock()
} }
func (b *baseScreen) SetContent(x, y int, mainc rune, combc []rune, st Style) { func (b *baseScreen) SetContent(x, y int, mainc rune, combc []rune, style Style) {
b.Put(x, y, string(append([]rune{mainc}, combc...)), style)
}
func (b *baseScreen) Get(x, y int) (string, Style, int) {
cells := b.GetCells() cells := b.GetCells()
b.Lock() b.Lock()
cells.SetContent(x, y, mainc, combc, st) defer b.Unlock()
b.Unlock() return cells.Get(x, y)
} }
func (b *baseScreen) GetContent(x, y int) (rune, []rune, Style, int) { func (b *baseScreen) GetContent(x, y int) (rune, []rune, Style, int) {
+14 -3
View File
@@ -143,6 +143,10 @@ func (s *simscreen) Init() error {
func (s *simscreen) Fini() { func (s *simscreen) Fini() {
s.Lock() s.Lock()
if s.fini {
s.Unlock()
return
}
s.fini = true s.fini = true
s.back.Resize(0, 0) s.back.Resize(0, 0)
s.Unlock() s.Unlock()
@@ -356,11 +360,18 @@ outer:
} }
if b[0] < 0x80 { if b[0] < 0x80 {
mod := ModNone
// No encodings start with low numbered values // No encodings start with low numbered values
if Key(b[0]) >= KeyCtrlA && Key(b[0]) <= KeyCtrlZ { if b[0] > 0 && b[0] < ' ' { // control keys
mod = ModCtrl switch Key(b[0]) {
case KeyESC, KeyEnter, KeyTAB:
s.postEvent(NewEventKey(Key(b[0]), 0, 0))
continue;
default:
s.postEvent(NewEventKey(Key(b[0]), rune(b[0])+'\x60', ModCtrl))
continue
} }
}
mod := ModNone
ev := NewEventKey(Key(b[0]), 0, mod) ev := NewEventKey(Key(b[0]), 0, mod)
s.postEvent(ev) s.postEvent(ev)
b = b[1:] b = b[1:]
+42 -2
View File
@@ -14,6 +14,11 @@
package tcell package tcell
import (
"strings"
"unicode/utf8"
)
// Style represents a complete text style, including both foreground color, // Style represents a complete text style, including both foreground color,
// background color, and additional attributes such as "bold" or "underline". // background color, and additional attributes such as "bold" or "underline".
// //
@@ -164,6 +169,16 @@ func (s Style) Underline(params ...interface{}) Style {
return s2 return s2
} }
// GetUnderlineStyle returns the underline style for the style.
func (s Style) GetUnderlineStyle() UnderlineStyle {
return s.ulStyle
}
// GetUnderlineColor returns the underline color for the style.
func (s Style) GetUnderlineColor() Color {
return s.ulColor
}
// Attributes returns a new style based on s, with its attributes set as // Attributes returns a new style based on s, with its attributes set as
// specified. // specified.
func (s Style) Attributes(attrs AttrMask) Style { func (s Style) Attributes(attrs AttrMask) Style {
@@ -177,7 +192,7 @@ func (s Style) Attributes(attrs AttrMask) Style {
// link to that Url. If the Url is empty, then this mode is turned off. // link to that Url. If the Url is empty, then this mode is turned off.
func (s Style) Url(url string) Style { func (s Style) Url(url string) Style {
s2 := s s2 := s
s2.url = url s2.url = stripOSCControls(url)
return s2 return s2
} }
@@ -187,6 +202,31 @@ func (s Style) Url(url string) Style {
// were one Url, even if it spans multiple lines. // were one Url, even if it spans multiple lines.
func (s Style) UrlId(id string) Style { func (s Style) UrlId(id string) Style {
s2 := s s2 := s
s2.urlId = "id=" + id s2.urlId = "id=" + stripOSCControls(id)
return s2 return s2
} }
func stripOSCControls(s string) string {
var b strings.Builder
b.Grow(len(s))
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
if r == utf8.RuneError && size == 1 {
c := s[i]
if c <= 0x1f || c == 0x7f || (c >= 0x80 && c <= 0x9f) {
i++
continue
}
_ = b.WriteByte(c)
i++
continue
}
if r <= 0x1f || r == 0x7f || (r >= 0x80 && r <= 0x9f) {
i += size
continue
}
b.WriteString(s[i : i+size])
i += size
}
return b.String()
}
-52
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 25, Lines: 25,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
AttrOff: "\x1b[0;10m\x1b(B", AttrOff: "\x1b[0;10m\x1b(B",
Underline: "\x1b[4m", Underline: "\x1b[4m",
@@ -27,57 +26,6 @@ func init() {
EnterAcs: "\x1b(0", EnterAcs: "\x1b(0",
ExitAcs: "\x1b(B", ExitAcs: "\x1b(B",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[139q",
KeyDelete: "\x1b[P",
KeyBackspace: "\b",
KeyHome: "\x1b[H",
KeyEnd: "\x1b[146q",
KeyPgUp: "\x1b[150q",
KeyPgDn: "\x1b[154q",
KeyF1: "\x1b[001q",
KeyF2: "\x1b[002q",
KeyF3: "\x1b[003q",
KeyF4: "\x1b[004q",
KeyF5: "\x1b[005q",
KeyF6: "\x1b[006q",
KeyF7: "\x1b[007q",
KeyF8: "\x1b[008q",
KeyF9: "\x1b[009q",
KeyF10: "\x1b[010q",
KeyF11: "\x1b[011q",
KeyF12: "\x1b[012q",
KeyF13: "\x1b[013q",
KeyF14: "\x1b[014q",
KeyF15: "\x1b[015q",
KeyF16: "\x1b[016q",
KeyF17: "\x1b[017q",
KeyF18: "\x1b[018q",
KeyF19: "\x1b[019q",
KeyF20: "\x1b[020q",
KeyF21: "\x1b[021q",
KeyF22: "\x1b[022q",
KeyF23: "\x1b[023q",
KeyF24: "\x1b[024q",
KeyF25: "\x1b[025q",
KeyF26: "\x1b[026q",
KeyF27: "\x1b[027q",
KeyF28: "\x1b[028q",
KeyF29: "\x1b[029q",
KeyF30: "\x1b[030q",
KeyF31: "\x1b[031q",
KeyF32: "\x1b[032q",
KeyF33: "\x1b[033q",
KeyF34: "\x1b[034q",
KeyF35: "\x1b[035q",
KeyF36: "\x1b[036q",
KeyClear: "\x1b[144q",
KeyBacktab: "\x1b[Z",
AutoMargin: true, AutoMargin: true,
}) })
} }
-28
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 16777216, Colors: 16777216,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h\x1b[22;0;0t", EnterCA: "\x1b[?1049h\x1b[22;0;0t",
ExitCA: "\x1b[?1049l\x1b[23;0;0t", ExitCA: "\x1b[?1049l\x1b[23;0;0t",
@@ -36,33 +35,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
TrueColor: true, TrueColor: true,
AutoMargin: true, AutoMargin: true,
}) })
-32
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h\x1b[22;0;0t", EnterCA: "\x1b[?1049h\x1b[22;0;0t",
ExitCA: "\x1b[?1049l\x1b[23;0;0t", ExitCA: "\x1b[?1049l\x1b[23;0;0t",
@@ -39,38 +38,7 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[<", Mouse: "\x1b[<",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
DoubleUnderline: "\x1b[4:2m",
CurlyUnderline: "\x1b[4:3m",
DottedUnderline: "\x1b[4:4m",
DashedUnderline: "\x1b[4:5m",
XTermLike: true, XTermLike: true,
}) })
} }
-11
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
AttrOff: "\x1b[0;10m", AttrOff: "\x1b[0;10m",
Underline: "\x1b[4m", Underline: "\x1b[4m",
@@ -28,16 +27,6 @@ func init() {
EnterAcs: "\x1b[11m", EnterAcs: "\x1b[11m",
ExitAcs: "\x1b[10m", ExitAcs: "\x1b[10m",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\x1b[D",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[L",
KeyBackspace: "\b",
KeyHome: "\x1b[H",
KeyBacktab: "\x1b[Z",
AutoMargin: true, AutoMargin: true,
}) })
} }
-57
View File
@@ -1,57 +0,0 @@
// Generated automatically. DO NOT HAND-EDIT.
package beterm
import "github.com/gdamore/tcell/v2/terminfo"
func init() {
// BeOS Terminal
terminfo.AddTerminfo(&terminfo.Terminfo{
Name: "beterm",
Columns: 80,
Lines: 25,
Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J",
AttrOff: "\x1b[0;10m",
Underline: "\x1b[4m",
Bold: "\x1b[1m",
Reverse: "\x1b[7m",
EnterKeypad: "\x1b[?4h",
ExitKeypad: "\x1b[?4l",
SetFg: "\x1b[3%p1%dm",
SetBg: "\x1b[4%p1%dm",
SetFgBg: "\x1b[3%p1%d;4%p2%dm",
ResetFgBg: "\x1b[m",
PadChar: "\x00",
SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\b",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[11~",
KeyF2: "\x1b[12~",
KeyF3: "\x1b[13~",
KeyF4: "\x1b[14~",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[16~",
KeyF7: "\x1b[17~",
KeyF8: "\x1b[18~",
KeyF9: "\x1b[19~",
KeyF10: "\x1b[20~",
KeyF11: "\x1b[21~",
KeyF12: "\x1b[22~",
AutoMargin: true,
InsertChar: "\x1b[@",
})
}
+1
View File
@@ -25,6 +25,7 @@ import (
// The following imports just register themselves -- // The following imports just register themselves --
// these are the terminal types we aggregate in this package. // these are the terminal types we aggregate in this package.
_ "github.com/gdamore/tcell/v2/terminfo/a/ansi" _ "github.com/gdamore/tcell/v2/terminfo/a/ansi"
_ "github.com/gdamore/tcell/v2/terminfo/t/tmux"
_ "github.com/gdamore/tcell/v2/terminfo/v/vt100" _ "github.com/gdamore/tcell/v2/terminfo/v/vt100"
_ "github.com/gdamore/tcell/v2/terminfo/v/vt102" _ "github.com/gdamore/tcell/v2/terminfo/v/vt102"
_ "github.com/gdamore/tcell/v2/terminfo/v/vt220" _ "github.com/gdamore/tcell/v2/terminfo/v/vt220"
-34
View File
@@ -10,7 +10,6 @@ func init() {
terminfo.AddTerminfo(&terminfo.Terminfo{ terminfo.AddTerminfo(&terminfo.Terminfo{
Name: "cygwin", Name: "cygwin",
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -27,39 +26,6 @@ func init() {
EnterAcs: "\x1b[11m", EnterAcs: "\x1b[11m",
ExitAcs: "\x1b[10m", ExitAcs: "\x1b[10m",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\b",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[[A",
KeyF2: "\x1b[[B",
KeyF3: "\x1b[[C",
KeyF4: "\x1b[[D",
KeyF5: "\x1b[[E",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
AutoMargin: true, AutoMargin: true,
InsertChar: "\x1b[@", InsertChar: "\x1b[@",
}) })
-33
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
ShowCursor: "\x1b[?25h", ShowCursor: "\x1b[?25h",
HideCursor: "\x1b[?25l", HideCursor: "\x1b[?25l",
@@ -34,38 +33,6 @@ func init() {
EnableAutoMargin: "\x1b[?7h", EnableAutoMargin: "\x1b[?7h",
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\b",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[11~",
KeyF2: "\x1b[12~",
KeyF3: "\x1b[13~",
KeyF4: "\x1b[14~",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
KeyHelp: "\x1b[28~",
AutoMargin: true, AutoMargin: true,
}) })
} }
+3 -160
View File
@@ -24,6 +24,7 @@ package dynamic
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv" "strconv"
@@ -126,7 +127,7 @@ func (tc *termcap) setupterm(name string) error {
tc.nums = make(map[string]int) tc.nums = make(map[string]int)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return err return fmt.Errorf("couldn't open terminfo ($TERM) file for %s: %w", name, err)
} }
// Now parse the output. // Now parse the output.
@@ -144,9 +145,7 @@ func (tc *termcap) setupterm(name string) error {
lines = lines[:len(lines)-1] lines = lines[:len(lines)-1]
} }
header := lines[0] header := lines[0]
if strings.HasSuffix(header, ",") { header = strings.TrimSuffix(header, ",")
header = header[:len(header)-1]
}
names := strings.Split(header, "|") names := strings.Split(header, "|")
tc.name = names[0] tc.name = names[0]
names = names[1:] names = names[1:]
@@ -193,7 +192,6 @@ func LoadTerminfo(name string) (*terminfo.Terminfo, string, error) {
t.Colors = tc.getnum("colors") t.Colors = tc.getnum("colors")
t.Columns = tc.getnum("cols") t.Columns = tc.getnum("cols")
t.Lines = tc.getnum("lines") t.Lines = tc.getnum("lines")
t.Bell = tc.getstr("bel")
t.Clear = tc.getstr("clear") t.Clear = tc.getstr("clear")
t.EnterCA = tc.getstr("smcup") t.EnterCA = tc.getstr("smcup")
t.ExitCA = tc.getstr("rmcup") t.ExitCA = tc.getstr("rmcup")
@@ -211,166 +209,11 @@ func LoadTerminfo(name string) (*terminfo.Terminfo, string, error) {
t.SetFg = tc.getstr("setaf") t.SetFg = tc.getstr("setaf")
t.SetBg = tc.getstr("setab") t.SetBg = tc.getstr("setab")
t.SetCursor = tc.getstr("cup") t.SetCursor = tc.getstr("cup")
t.CursorBack1 = tc.getstr("cub1")
t.CursorUp1 = tc.getstr("cuu1")
t.KeyF1 = tc.getstr("kf1")
t.KeyF2 = tc.getstr("kf2")
t.KeyF3 = tc.getstr("kf3")
t.KeyF4 = tc.getstr("kf4")
t.KeyF5 = tc.getstr("kf5")
t.KeyF6 = tc.getstr("kf6")
t.KeyF7 = tc.getstr("kf7")
t.KeyF8 = tc.getstr("kf8")
t.KeyF9 = tc.getstr("kf9")
t.KeyF10 = tc.getstr("kf10")
t.KeyF11 = tc.getstr("kf11")
t.KeyF12 = tc.getstr("kf12")
t.KeyF13 = tc.getstr("kf13")
t.KeyF14 = tc.getstr("kf14")
t.KeyF15 = tc.getstr("kf15")
t.KeyF16 = tc.getstr("kf16")
t.KeyF17 = tc.getstr("kf17")
t.KeyF18 = tc.getstr("kf18")
t.KeyF19 = tc.getstr("kf19")
t.KeyF20 = tc.getstr("kf20")
t.KeyF21 = tc.getstr("kf21")
t.KeyF22 = tc.getstr("kf22")
t.KeyF23 = tc.getstr("kf23")
t.KeyF24 = tc.getstr("kf24")
t.KeyF25 = tc.getstr("kf25")
t.KeyF26 = tc.getstr("kf26")
t.KeyF27 = tc.getstr("kf27")
t.KeyF28 = tc.getstr("kf28")
t.KeyF29 = tc.getstr("kf29")
t.KeyF30 = tc.getstr("kf30")
t.KeyF31 = tc.getstr("kf31")
t.KeyF32 = tc.getstr("kf32")
t.KeyF33 = tc.getstr("kf33")
t.KeyF34 = tc.getstr("kf34")
t.KeyF35 = tc.getstr("kf35")
t.KeyF36 = tc.getstr("kf36")
t.KeyF37 = tc.getstr("kf37")
t.KeyF38 = tc.getstr("kf38")
t.KeyF39 = tc.getstr("kf39")
t.KeyF40 = tc.getstr("kf40")
t.KeyF41 = tc.getstr("kf41")
t.KeyF42 = tc.getstr("kf42")
t.KeyF43 = tc.getstr("kf43")
t.KeyF44 = tc.getstr("kf44")
t.KeyF45 = tc.getstr("kf45")
t.KeyF46 = tc.getstr("kf46")
t.KeyF47 = tc.getstr("kf47")
t.KeyF48 = tc.getstr("kf48")
t.KeyF49 = tc.getstr("kf49")
t.KeyF50 = tc.getstr("kf50")
t.KeyF51 = tc.getstr("kf51")
t.KeyF52 = tc.getstr("kf52")
t.KeyF53 = tc.getstr("kf53")
t.KeyF54 = tc.getstr("kf54")
t.KeyF55 = tc.getstr("kf55")
t.KeyF56 = tc.getstr("kf56")
t.KeyF57 = tc.getstr("kf57")
t.KeyF58 = tc.getstr("kf58")
t.KeyF59 = tc.getstr("kf59")
t.KeyF60 = tc.getstr("kf60")
t.KeyF61 = tc.getstr("kf61")
t.KeyF62 = tc.getstr("kf62")
t.KeyF63 = tc.getstr("kf63")
t.KeyF64 = tc.getstr("kf64")
t.KeyInsert = tc.getstr("kich1")
t.KeyDelete = tc.getstr("kdch1")
t.KeyBackspace = tc.getstr("kbs")
t.KeyHome = tc.getstr("khome")
t.KeyEnd = tc.getstr("kend")
t.KeyUp = tc.getstr("kcuu1")
t.KeyDown = tc.getstr("kcud1")
t.KeyRight = tc.getstr("kcuf1")
t.KeyLeft = tc.getstr("kcub1")
t.KeyPgDn = tc.getstr("knp")
t.KeyPgUp = tc.getstr("kpp")
t.KeyBacktab = tc.getstr("kcbt")
t.KeyExit = tc.getstr("kext")
t.KeyCancel = tc.getstr("kcan")
t.KeyPrint = tc.getstr("kprt")
t.KeyHelp = tc.getstr("khlp")
t.KeyClear = tc.getstr("kclr")
t.AltChars = tc.getstr("acsc") t.AltChars = tc.getstr("acsc")
t.EnterAcs = tc.getstr("smacs") t.EnterAcs = tc.getstr("smacs")
t.ExitAcs = tc.getstr("rmacs") t.ExitAcs = tc.getstr("rmacs")
t.EnableAcs = tc.getstr("enacs") t.EnableAcs = tc.getstr("enacs")
t.Mouse = tc.getstr("kmous") t.Mouse = tc.getstr("kmous")
t.KeyShfRight = tc.getstr("kRIT")
t.KeyShfLeft = tc.getstr("kLFT")
t.KeyShfHome = tc.getstr("kHOM")
t.KeyShfEnd = tc.getstr("kEND")
// Terminfo lacks descriptions for a bunch of modified keys,
// but modern XTerm and emulators often have them. Let's add them,
// if the shifted right and left arrows are defined.
if t.KeyShfRight == "\x1b[1;2C" && t.KeyShfLeft == "\x1b[1;2D" {
t.Modifiers = terminfo.ModifiersXTerm
t.KeyShfUp = "\x1b[1;2A"
t.KeyShfDown = "\x1b[1;2B"
t.KeyMetaUp = "\x1b[1;9A"
t.KeyMetaDown = "\x1b[1;9B"
t.KeyMetaRight = "\x1b[1;9C"
t.KeyMetaLeft = "\x1b[1;9D"
t.KeyAltUp = "\x1b[1;3A"
t.KeyAltDown = "\x1b[1;3B"
t.KeyAltRight = "\x1b[1;3C"
t.KeyAltLeft = "\x1b[1;3D"
t.KeyCtrlUp = "\x1b[1;5A"
t.KeyCtrlDown = "\x1b[1;5B"
t.KeyCtrlRight = "\x1b[1;5C"
t.KeyCtrlLeft = "\x1b[1;5D"
t.KeyAltShfUp = "\x1b[1;4A"
t.KeyAltShfDown = "\x1b[1;4B"
t.KeyAltShfRight = "\x1b[1;4C"
t.KeyAltShfLeft = "\x1b[1;4D"
t.KeyMetaShfUp = "\x1b[1;10A"
t.KeyMetaShfDown = "\x1b[1;10B"
t.KeyMetaShfRight = "\x1b[1;10C"
t.KeyMetaShfLeft = "\x1b[1;10D"
t.KeyCtrlShfUp = "\x1b[1;6A"
t.KeyCtrlShfDown = "\x1b[1;6B"
t.KeyCtrlShfRight = "\x1b[1;6C"
t.KeyCtrlShfLeft = "\x1b[1;6D"
t.KeyShfPgUp = "\x1b[5;2~"
t.KeyShfPgDn = "\x1b[6;2~"
}
// And also for Home and End
if t.KeyShfHome == "\x1b[1;2H" && t.KeyShfEnd == "\x1b[1;2F" {
t.KeyCtrlHome = "\x1b[1;5H"
t.KeyCtrlEnd = "\x1b[1;5F"
t.KeyAltHome = "\x1b[1;9H"
t.KeyAltEnd = "\x1b[1;9F"
t.KeyCtrlShfHome = "\x1b[1;6H"
t.KeyCtrlShfEnd = "\x1b[1;6F"
t.KeyAltShfHome = "\x1b[1;4H"
t.KeyAltShfEnd = "\x1b[1;4F"
t.KeyMetaShfHome = "\x1b[1;10H"
t.KeyMetaShfEnd = "\x1b[1;10F"
}
// And the same thing for rxvt and workalikes (Eterm, aterm, etc.)
// It seems that urxvt at least send escaped as ALT prefix for these,
// although some places seem to indicate a separate ALT key sesquence.
if t.KeyShfRight == "\x1b[c" && t.KeyShfLeft == "\x1b[d" {
t.KeyShfUp = "\x1b[a"
t.KeyShfDown = "\x1b[b"
t.KeyCtrlUp = "\x1b[Oa"
t.KeyCtrlDown = "\x1b[Ob"
t.KeyCtrlRight = "\x1b[Oc"
t.KeyCtrlLeft = "\x1b[Od"
}
if t.KeyShfHome == "\x1b[7$" && t.KeyShfEnd == "\x1b[8$" {
t.KeyCtrlHome = "\x1b[7^"
t.KeyCtrlEnd = "\x1b[8^"
}
// Technically the RGB flag that is provided for xterm-direct is not // Technically the RGB flag that is provided for xterm-direct is not
// quite right. The problem is that the -direct flag that was introduced // quite right. The problem is that the -direct flag that was introduced
-17
View File
@@ -11,7 +11,6 @@ func init() {
Name: "eterm", Name: "eterm",
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -21,8 +20,6 @@ func init() {
Reverse: "\x1b[7m", Reverse: "\x1b[7m",
PadChar: "\x00", PadChar: "\x00",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
AutoMargin: true, AutoMargin: true,
}) })
@@ -32,7 +29,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -47,19 +43,6 @@ func init() {
ResetFgBg: "\x1b[39;49m", ResetFgBg: "\x1b[39;49m",
PadChar: "\x00", PadChar: "\x00",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
AutoMargin: true, AutoMargin: true,
}) })
} }
-6
View File
@@ -24,13 +24,11 @@ import (
_ "github.com/gdamore/tcell/v2/terminfo/a/aixterm" _ "github.com/gdamore/tcell/v2/terminfo/a/aixterm"
_ "github.com/gdamore/tcell/v2/terminfo/a/alacritty" _ "github.com/gdamore/tcell/v2/terminfo/a/alacritty"
_ "github.com/gdamore/tcell/v2/terminfo/a/ansi" _ "github.com/gdamore/tcell/v2/terminfo/a/ansi"
_ "github.com/gdamore/tcell/v2/terminfo/b/beterm"
_ "github.com/gdamore/tcell/v2/terminfo/c/cygwin" _ "github.com/gdamore/tcell/v2/terminfo/c/cygwin"
_ "github.com/gdamore/tcell/v2/terminfo/d/dtterm" _ "github.com/gdamore/tcell/v2/terminfo/d/dtterm"
_ "github.com/gdamore/tcell/v2/terminfo/e/emacs" _ "github.com/gdamore/tcell/v2/terminfo/e/emacs"
_ "github.com/gdamore/tcell/v2/terminfo/f/foot" _ "github.com/gdamore/tcell/v2/terminfo/f/foot"
_ "github.com/gdamore/tcell/v2/terminfo/g/gnome" _ "github.com/gdamore/tcell/v2/terminfo/g/gnome"
_ "github.com/gdamore/tcell/v2/terminfo/h/hpterm"
_ "github.com/gdamore/tcell/v2/terminfo/k/konsole" _ "github.com/gdamore/tcell/v2/terminfo/k/konsole"
_ "github.com/gdamore/tcell/v2/terminfo/k/kterm" _ "github.com/gdamore/tcell/v2/terminfo/k/kterm"
_ "github.com/gdamore/tcell/v2/terminfo/l/linux" _ "github.com/gdamore/tcell/v2/terminfo/l/linux"
@@ -46,10 +44,6 @@ import (
_ "github.com/gdamore/tcell/v2/terminfo/v/vt320" _ "github.com/gdamore/tcell/v2/terminfo/v/vt320"
_ "github.com/gdamore/tcell/v2/terminfo/v/vt400" _ "github.com/gdamore/tcell/v2/terminfo/v/vt400"
_ "github.com/gdamore/tcell/v2/terminfo/v/vt420" _ "github.com/gdamore/tcell/v2/terminfo/v/vt420"
_ "github.com/gdamore/tcell/v2/terminfo/v/vt52"
_ "github.com/gdamore/tcell/v2/terminfo/w/wy50"
_ "github.com/gdamore/tcell/v2/terminfo/w/wy60"
_ "github.com/gdamore/tcell/v2/terminfo/w/wy99_ansi"
_ "github.com/gdamore/tcell/v2/terminfo/x/xfce" _ "github.com/gdamore/tcell/v2/terminfo/x/xfce"
_ "github.com/gdamore/tcell/v2/terminfo/x/xterm" _ "github.com/gdamore/tcell/v2/terminfo/x/xterm"
_ "github.com/gdamore/tcell/v2/terminfo/x/xterm_ghostty" _ "github.com/gdamore/tcell/v2/terminfo/x/xterm_ghostty"
-28
View File
@@ -13,7 +13,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h\x1b[22;0;0t", EnterCA: "\x1b[?1049h\x1b[22;0;0t",
ExitCA: "\x1b[?1049l\x1b[23;0;0t", ExitCA: "\x1b[?1049l\x1b[23;0;0t",
@@ -38,33 +37,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\u007f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
}) })
} }
-56
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -39,33 +38,6 @@ func init() {
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
@@ -76,7 +48,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -103,33 +74,6 @@ func init() {
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
-51
View File
@@ -1,51 +0,0 @@
// Generated automatically. DO NOT HAND-EDIT.
package hpterm
import "github.com/gdamore/tcell/v2/terminfo"
func init() {
// HP X11 terminal emulator (old)
terminfo.AddTerminfo(&terminfo.Terminfo{
Name: "hpterm",
Aliases: []string{"X-hpterm"},
Columns: 80,
Lines: 24,
Bell: "\a",
Clear: "\x1b&a0y0C\x1bJ",
AttrOff: "\x1b&d@\x0f",
Underline: "\x1b&dD",
Bold: "\x1b&dB",
Dim: "\x1b&dH",
Reverse: "\x1b&dB",
EnterKeypad: "\x1b&s1A",
ExitKeypad: "\x1b&s0A",
PadChar: "\x00",
EnterAcs: "\x0e",
ExitAcs: "\x0f",
SetCursor: "\x1b&a%p1%dy%p2%dC",
CursorBack1: "\b",
CursorUp1: "\x1bA",
KeyUp: "\x1bA",
KeyDown: "\x1bB",
KeyRight: "\x1bC",
KeyLeft: "\x1bD",
KeyInsert: "\x1bQ",
KeyDelete: "\x1bP",
KeyBackspace: "\b",
KeyHome: "\x1bh",
KeyPgUp: "\x1bV",
KeyPgDn: "\x1bU",
KeyF1: "\x1bp",
KeyF2: "\x1bq",
KeyF3: "\x1br",
KeyF4: "\x1bs",
KeyF5: "\x1bt",
KeyF6: "\x1bu",
KeyF7: "\x1bv",
KeyF8: "\x1bw",
KeyClear: "\x1bJ",
AutoMargin: true,
})
}
-56
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -40,33 +39,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[<", Mouse: "\x1b[<",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
@@ -77,7 +49,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -105,33 +76,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[<", Mouse: "\x1b[<",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
-32
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -34,37 +33,6 @@ func init() {
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[11~",
KeyF2: "\x1b[12~",
KeyF3: "\x1b[13~",
KeyF4: "\x1b[14~",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
-35
View File
@@ -10,7 +10,6 @@ func init() {
terminfo.AddTerminfo(&terminfo.Terminfo{ terminfo.AddTerminfo(&terminfo.Terminfo{
Name: "linux", Name: "linux",
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
ShowCursor: "\x1b[?25h\x1b[?0c", ShowCursor: "\x1b[?25h\x1b[?0c",
HideCursor: "\x1b[?25l\x1b[?1c", HideCursor: "\x1b[?25l\x1b[?1c",
@@ -33,40 +32,6 @@ func init() {
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[[A",
KeyF2: "\x1b[[B",
KeyF3: "\x1b[[C",
KeyF4: "\x1b[[D",
KeyF5: "\x1b[[E",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
KeyBacktab: "\x1b\t",
AutoMargin: true, AutoMargin: true,
InsertChar: "\x1b[@", InsertChar: "\x1b[@",
}) })
-2
View File
@@ -1,7 +1,6 @@
aixterm aixterm
alacritty alacritty
ansi ansi
beterm
cygwin cygwin
dtterm dtterm
eterm,eterm-color|emacs eterm,eterm-color|emacs
@@ -15,7 +14,6 @@ rxvt,rxvt-256color,rxvt-88color,rxvt-unicode,rxvt-unicode-256color
screen,screen-256color screen,screen-256color
st,st-256color|simpleterm st,st-256color|simpleterm
tmux,tmux-256color tmux,tmux-256color
vt52
vt100 vt100
vt102 vt102
vt220 vt220
-9
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
AttrOff: "\x1b[0;10m", AttrOff: "\x1b[0;10m",
Underline: "\x1b[4m", Underline: "\x1b[4m",
@@ -28,14 +27,6 @@ func init() {
EnterAcs: "\x1b[12m", EnterAcs: "\x1b[12m",
ExitAcs: "\x1b[10m", ExitAcs: "\x1b[10m",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\x1b[D",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyBackspace: "\b",
KeyHome: "\x1b[H",
AutoMargin: true, AutoMargin: true,
}) })
} }
-317
View File
@@ -13,7 +13,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -37,78 +36,6 @@ func init() {
EnableAcs: "\x1b(B\x1b)0", EnableAcs: "\x1b(B\x1b)0",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[7~",
KeyEnd: "\x1b[8~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[11~",
KeyF2: "\x1b[12~",
KeyF3: "\x1b[13~",
KeyF4: "\x1b[14~",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
KeyF21: "\x1b[23$",
KeyF22: "\x1b[24$",
KeyF23: "\x1b[11^",
KeyF24: "\x1b[12^",
KeyF25: "\x1b[13^",
KeyF26: "\x1b[14^",
KeyF27: "\x1b[15^",
KeyF28: "\x1b[17^",
KeyF29: "\x1b[18^",
KeyF30: "\x1b[19^",
KeyF31: "\x1b[20^",
KeyF32: "\x1b[21^",
KeyF33: "\x1b[23^",
KeyF34: "\x1b[24^",
KeyF35: "\x1b[25^",
KeyF36: "\x1b[26^",
KeyF37: "\x1b[28^",
KeyF38: "\x1b[29^",
KeyF39: "\x1b[31^",
KeyF40: "\x1b[32^",
KeyF41: "\x1b[33^",
KeyF42: "\x1b[34^",
KeyF43: "\x1b[23@",
KeyF44: "\x1b[24@",
KeyBacktab: "\x1b[Z",
KeyShfLeft: "\x1b[d",
KeyShfRight: "\x1b[c",
KeyShfUp: "\x1b[a",
KeyShfDown: "\x1b[b",
KeyShfHome: "\x1b[7$",
KeyShfEnd: "\x1b[8$",
KeyShfInsert: "\x1b[2$",
KeyShfDelete: "\x1b[3$",
KeyCtrlUp: "\x1b[Oa",
KeyCtrlDown: "\x1b[Ob",
KeyCtrlRight: "\x1b[Oc",
KeyCtrlLeft: "\x1b[Od",
KeyCtrlHome: "\x1b[7^",
KeyCtrlEnd: "\x1b[8^",
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
@@ -119,7 +46,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -143,78 +69,6 @@ func init() {
EnableAcs: "\x1b(B\x1b)0", EnableAcs: "\x1b(B\x1b)0",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[7~",
KeyEnd: "\x1b[8~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[11~",
KeyF2: "\x1b[12~",
KeyF3: "\x1b[13~",
KeyF4: "\x1b[14~",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
KeyF21: "\x1b[23$",
KeyF22: "\x1b[24$",
KeyF23: "\x1b[11^",
KeyF24: "\x1b[12^",
KeyF25: "\x1b[13^",
KeyF26: "\x1b[14^",
KeyF27: "\x1b[15^",
KeyF28: "\x1b[17^",
KeyF29: "\x1b[18^",
KeyF30: "\x1b[19^",
KeyF31: "\x1b[20^",
KeyF32: "\x1b[21^",
KeyF33: "\x1b[23^",
KeyF34: "\x1b[24^",
KeyF35: "\x1b[25^",
KeyF36: "\x1b[26^",
KeyF37: "\x1b[28^",
KeyF38: "\x1b[29^",
KeyF39: "\x1b[31^",
KeyF40: "\x1b[32^",
KeyF41: "\x1b[33^",
KeyF42: "\x1b[34^",
KeyF43: "\x1b[23@",
KeyF44: "\x1b[24@",
KeyBacktab: "\x1b[Z",
KeyShfLeft: "\x1b[d",
KeyShfRight: "\x1b[c",
KeyShfUp: "\x1b[a",
KeyShfDown: "\x1b[b",
KeyShfHome: "\x1b[7$",
KeyShfEnd: "\x1b[8$",
KeyShfInsert: "\x1b[2$",
KeyShfDelete: "\x1b[3$",
KeyCtrlUp: "\x1b[Oa",
KeyCtrlDown: "\x1b[Ob",
KeyCtrlRight: "\x1b[Oc",
KeyCtrlLeft: "\x1b[Od",
KeyCtrlHome: "\x1b[7^",
KeyCtrlEnd: "\x1b[8^",
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
@@ -225,7 +79,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 88, Colors: 88,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -249,78 +102,6 @@ func init() {
EnableAcs: "\x1b(B\x1b)0", EnableAcs: "\x1b(B\x1b)0",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[7~",
KeyEnd: "\x1b[8~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[11~",
KeyF2: "\x1b[12~",
KeyF3: "\x1b[13~",
KeyF4: "\x1b[14~",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
KeyF21: "\x1b[23$",
KeyF22: "\x1b[24$",
KeyF23: "\x1b[11^",
KeyF24: "\x1b[12^",
KeyF25: "\x1b[13^",
KeyF26: "\x1b[14^",
KeyF27: "\x1b[15^",
KeyF28: "\x1b[17^",
KeyF29: "\x1b[18^",
KeyF30: "\x1b[19^",
KeyF31: "\x1b[20^",
KeyF32: "\x1b[21^",
KeyF33: "\x1b[23^",
KeyF34: "\x1b[24^",
KeyF35: "\x1b[25^",
KeyF36: "\x1b[26^",
KeyF37: "\x1b[28^",
KeyF38: "\x1b[29^",
KeyF39: "\x1b[31^",
KeyF40: "\x1b[32^",
KeyF41: "\x1b[33^",
KeyF42: "\x1b[34^",
KeyF43: "\x1b[23@",
KeyF44: "\x1b[24@",
KeyBacktab: "\x1b[Z",
KeyShfLeft: "\x1b[d",
KeyShfRight: "\x1b[c",
KeyShfUp: "\x1b[a",
KeyShfDown: "\x1b[b",
KeyShfHome: "\x1b[7$",
KeyShfEnd: "\x1b[8$",
KeyShfInsert: "\x1b[2$",
KeyShfDelete: "\x1b[3$",
KeyCtrlUp: "\x1b[Oa",
KeyCtrlDown: "\x1b[Ob",
KeyCtrlRight: "\x1b[Oc",
KeyCtrlLeft: "\x1b[Od",
KeyCtrlHome: "\x1b[7^",
KeyCtrlEnd: "\x1b[8^",
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
@@ -331,7 +112,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 88, Colors: 88,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h", EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[r\x1b[?1049l", ExitCA: "\x1b[r\x1b[?1049l",
@@ -356,54 +136,6 @@ func init() {
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[7~",
KeyEnd: "\x1b[8~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[11~",
KeyF2: "\x1b[12~",
KeyF3: "\x1b[13~",
KeyF4: "\x1b[14~",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
KeyBacktab: "\x1b[Z",
KeyShfLeft: "\x1b[d",
KeyShfRight: "\x1b[c",
KeyShfUp: "\x1b[a",
KeyShfDown: "\x1b[b",
KeyShfHome: "\x1b[7$",
KeyShfEnd: "\x1b[8$",
KeyShfInsert: "\x1b[2$",
KeyShfDelete: "\x1b[3$",
KeyCtrlUp: "\x1b[Oa",
KeyCtrlDown: "\x1b[Ob",
KeyCtrlRight: "\x1b[Oc",
KeyCtrlLeft: "\x1b[Od",
KeyCtrlHome: "\x1b[7^",
KeyCtrlEnd: "\x1b[8^",
AutoMargin: true, AutoMargin: true,
InsertChar: "\x1b[@", InsertChar: "\x1b[@",
}) })
@@ -414,7 +146,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h", EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[r\x1b[?1049l", ExitCA: "\x1b[r\x1b[?1049l",
@@ -439,54 +170,6 @@ func init() {
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[7~",
KeyEnd: "\x1b[8~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1b[11~",
KeyF2: "\x1b[12~",
KeyF3: "\x1b[13~",
KeyF4: "\x1b[14~",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
KeyBacktab: "\x1b[Z",
KeyShfLeft: "\x1b[d",
KeyShfRight: "\x1b[c",
KeyShfUp: "\x1b[a",
KeyShfDown: "\x1b[b",
KeyShfHome: "\x1b[7$",
KeyShfEnd: "\x1b[8$",
KeyShfInsert: "\x1b[2$",
KeyShfDelete: "\x1b[3$",
KeyCtrlUp: "\x1b[Oa",
KeyCtrlDown: "\x1b[Ob",
KeyCtrlRight: "\x1b[Oc",
KeyCtrlLeft: "\x1b[Od",
KeyCtrlHome: "\x1b[7^",
KeyCtrlEnd: "\x1b[8^",
AutoMargin: true, AutoMargin: true,
InsertChar: "\x1b[@", InsertChar: "\x1b[@",
}) })
-54
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
EnterCA: "\x1b[?1049h", EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[?1049l", ExitCA: "\x1b[?1049l",
@@ -37,32 +36,6 @@ func init() {
EnableAcs: "\x1b(B\x1b)0", EnableAcs: "\x1b(B\x1b)0",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1bM",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
AutoMargin: true, AutoMargin: true,
}) })
@@ -72,7 +45,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
EnterCA: "\x1b[?1049h", EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[?1049l", ExitCA: "\x1b[?1049l",
@@ -97,32 +69,6 @@ func init() {
EnableAcs: "\x1b(B\x1b)0", EnableAcs: "\x1b(B\x1b)0",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1bM",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
AutoMargin: true, AutoMargin: true,
}) })
} }
-56
View File
@@ -13,7 +13,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h", EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[?1049l", ExitCA: "\x1b[?1049l",
@@ -39,33 +38,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyClear: "\x1b[3;5~",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
@@ -77,7 +49,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h", EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[?1049l", ExitCA: "\x1b[?1049l",
@@ -103,33 +74,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyClear: "\x1b[3;5~",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
-52
View File
@@ -30,37 +30,11 @@ func init() {
Aliases: []string{"sun1", "sun2"}, Aliases: []string{"sun1", "sun2"},
Columns: 80, Columns: 80,
Lines: 34, Lines: 34,
Bell: "\a",
Clear: "\f", Clear: "\f",
AttrOff: "\x1b[m", AttrOff: "\x1b[m",
Reverse: "\x1b[7m", Reverse: "\x1b[7m",
PadChar: "\x00", PadChar: "\x00",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[247z",
KeyDelete: "\u007f",
KeyBackspace: "\b",
KeyHome: "\x1b[214z",
KeyEnd: "\x1b[220z",
KeyPgUp: "\x1b[216z",
KeyPgDn: "\x1b[222z",
KeyF1: "\x1b[224z",
KeyF2: "\x1b[225z",
KeyF3: "\x1b[226z",
KeyF4: "\x1b[227z",
KeyF5: "\x1b[228z",
KeyF6: "\x1b[229z",
KeyF7: "\x1b[230z",
KeyF8: "\x1b[231z",
KeyF9: "\x1b[232z",
KeyF10: "\x1b[233z",
KeyF11: "\x1b[234z",
KeyF12: "\x1b[235z",
AutoMargin: true, AutoMargin: true,
InsertChar: "\x1b[@", InsertChar: "\x1b[@",
}) })
@@ -71,7 +45,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 34, Lines: 34,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\f", Clear: "\f",
AttrOff: "\x1b[m", AttrOff: "\x1b[m",
Bold: "\x1b[1m", Bold: "\x1b[1m",
@@ -81,31 +54,6 @@ func init() {
ResetFgBg: "\x1b[0m", ResetFgBg: "\x1b[0m",
PadChar: "\x00", PadChar: "\x00",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[247z",
KeyDelete: "\u007f",
KeyBackspace: "\b",
KeyHome: "\x1b[214z",
KeyEnd: "\x1b[220z",
KeyPgUp: "\x1b[216z",
KeyPgDn: "\x1b[222z",
KeyF1: "\x1b[224z",
KeyF2: "\x1b[225z",
KeyF3: "\x1b[226z",
KeyF4: "\x1b[227z",
KeyF5: "\x1b[228z",
KeyF6: "\x1b[229z",
KeyF7: "\x1b[230z",
KeyF8: "\x1b[231z",
KeyF9: "\x1b[232z",
KeyF10: "\x1b[233z",
KeyF11: "\x1b[234z",
KeyF12: "\x1b[235z",
AutoMargin: true, AutoMargin: true,
InsertChar: "\x1b[@", InsertChar: "\x1b[@",
}) })
+2 -64
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
EnterCA: "\x1b[?1049h", EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[?1049l", ExitCA: "\x1b[?1049l",
@@ -39,38 +38,8 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1bM",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
DoubleUnderline: "\x1b[4:2m", XTermLike: true,
CurlyUnderline: "\x1b[4:3m",
DottedUnderline: "\x1b[4:4m",
DashedUnderline: "\x1b[4:5m",
}) })
// tmux with 256 colors // tmux with 256 colors
@@ -79,7 +48,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
EnterCA: "\x1b[?1049h", EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[?1049l", ExitCA: "\x1b[?1049l",
@@ -106,37 +74,7 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1bM",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[1~",
KeyEnd: "\x1b[4~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
DoubleUnderline: "\x1b[4:2m", XTermLike: true,
CurlyUnderline: "\x1b[4:3m",
DottedUnderline: "\x1b[4:4m",
DashedUnderline: "\x1b[4:5m",
}) })
} }
+15 -168
View File
@@ -1,4 +1,4 @@
// Copyright 2024 The TCell Authors // Copyright 2025 The TCell Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License. // you may not use file except in compliance with the License.
@@ -46,7 +46,6 @@ type Terminfo struct {
Columns int // cols Columns int // cols
Lines int // lines Lines int // lines
Colors int // colors Colors int // colors
Bell string // bell
Clear string // clear Clear string // clear
EnterCA string // smcup EnterCA string // smcup
ExitCA string // rmcup ExitCA string // rmcup
@@ -65,101 +64,12 @@ type Terminfo struct {
SetBg string // setab SetBg string // setab
ResetFgBg string // op ResetFgBg string // op
SetCursor string // cup SetCursor string // cup
CursorBack1 string // cub1
CursorUp1 string // cuu1
PadChar string // pad PadChar string // pad
KeyBackspace string // kbs
KeyF1 string // kf1
KeyF2 string // kf2
KeyF3 string // kf3
KeyF4 string // kf4
KeyF5 string // kf5
KeyF6 string // kf6
KeyF7 string // kf7
KeyF8 string // kf8
KeyF9 string // kf9
KeyF10 string // kf10
KeyF11 string // kf11
KeyF12 string // kf12
KeyF13 string // kf13
KeyF14 string // kf14
KeyF15 string // kf15
KeyF16 string // kf16
KeyF17 string // kf17
KeyF18 string // kf18
KeyF19 string // kf19
KeyF20 string // kf20
KeyF21 string // kf21
KeyF22 string // kf22
KeyF23 string // kf23
KeyF24 string // kf24
KeyF25 string // kf25
KeyF26 string // kf26
KeyF27 string // kf27
KeyF28 string // kf28
KeyF29 string // kf29
KeyF30 string // kf30
KeyF31 string // kf31
KeyF32 string // kf32
KeyF33 string // kf33
KeyF34 string // kf34
KeyF35 string // kf35
KeyF36 string // kf36
KeyF37 string // kf37
KeyF38 string // kf38
KeyF39 string // kf39
KeyF40 string // kf40
KeyF41 string // kf41
KeyF42 string // kf42
KeyF43 string // kf43
KeyF44 string // kf44
KeyF45 string // kf45
KeyF46 string // kf46
KeyF47 string // kf47
KeyF48 string // kf48
KeyF49 string // kf49
KeyF50 string // kf50
KeyF51 string // kf51
KeyF52 string // kf52
KeyF53 string // kf53
KeyF54 string // kf54
KeyF55 string // kf55
KeyF56 string // kf56
KeyF57 string // kf57
KeyF58 string // kf58
KeyF59 string // kf59
KeyF60 string // kf60
KeyF61 string // kf61
KeyF62 string // kf62
KeyF63 string // kf63
KeyF64 string // kf64
KeyInsert string // kich1
KeyDelete string // kdch1
KeyHome string // khome
KeyEnd string // kend
KeyHelp string // khlp
KeyPgUp string // kpp
KeyPgDn string // knp
KeyUp string // kcuu1
KeyDown string // kcud1
KeyLeft string // kcub1
KeyRight string // kcuf1
KeyBacktab string // kcbt
KeyExit string // kext
KeyClear string // kclr
KeyPrint string // kprt
KeyCancel string // kcan
Mouse string // kmous Mouse string // kmous
AltChars string // acsc AltChars string // acsc
EnterAcs string // smacs EnterAcs string // smacs
ExitAcs string // rmacs ExitAcs string // rmacs
EnableAcs string // enacs EnableAcs string // enacs
KeyShfRight string // kRIT
KeyShfLeft string // kLFT
KeyShfHome string // kHOM
KeyShfEnd string // kEND
KeyShfInsert string // kIC
KeyShfDelete string // kDC
// These are non-standard extensions to terminfo. This includes // These are non-standard extensions to terminfo. This includes
// true color support, and some additional keys. Its kind of bizarre // true color support, and some additional keys. Its kind of bizarre
@@ -172,90 +82,17 @@ type Terminfo struct {
SetFgBgRGB string // setfgbgrgb SetFgBgRGB string // setfgbgrgb
SetFgRGB string // setfrgb SetFgRGB string // setfrgb
SetBgRGB string // setbrgb SetBgRGB string // setbrgb
KeyShfUp string // shift-up
KeyShfDown string // shift-down
KeyShfPgUp string // shift-kpp
KeyShfPgDn string // shift-knp
KeyCtrlUp string // ctrl-up
KeyCtrlDown string // ctrl-left
KeyCtrlRight string // ctrl-right
KeyCtrlLeft string // ctrl-left
KeyMetaUp string // meta-up
KeyMetaDown string // meta-left
KeyMetaRight string // meta-right
KeyMetaLeft string // meta-left
KeyAltUp string // alt-up
KeyAltDown string // alt-left
KeyAltRight string // alt-right
KeyAltLeft string // alt-left
KeyCtrlHome string
KeyCtrlEnd string
KeyMetaHome string
KeyMetaEnd string
KeyAltHome string
KeyAltEnd string
KeyAltShfUp string
KeyAltShfDown string
KeyAltShfLeft string
KeyAltShfRight string
KeyMetaShfUp string
KeyMetaShfDown string
KeyMetaShfLeft string
KeyMetaShfRight string
KeyCtrlShfUp string
KeyCtrlShfDown string
KeyCtrlShfLeft string
KeyCtrlShfRight string
KeyCtrlShfHome string
KeyCtrlShfEnd string
KeyAltShfHome string
KeyAltShfEnd string
KeyMetaShfHome string
KeyMetaShfEnd string
EnablePaste string // bracketed paste mode
DisablePaste string
PasteStart string
PasteEnd string
Modifiers int
InsertChar string // string to insert a character (ich1) InsertChar string // string to insert a character (ich1)
AutoMargin bool // true if writing to last cell in line advances AutoMargin bool // true if writing to last cell in line advances
TrueColor bool // true if the terminal supports direct color TrueColor bool // true if the terminal supports direct color
CursorDefault string
CursorBlinkingBlock string
CursorSteadyBlock string
CursorBlinkingUnderline string
CursorSteadyUnderline string
CursorBlinkingBar string
CursorSteadyBar string
CursorColor string // nothing uses it yet
CursorColorRGB string // Cs (but not really because Cs uses X11 color string)
CursorColorReset string // Cr
EnterUrl string
ExitUrl string
SetWindowSize string
SetWindowTitle string // no terminfo extension
EnableFocusReporting string
DisableFocusReporting string
DisableAutoMargin string // smam DisableAutoMargin string // smam
EnableAutoMargin string // rmam EnableAutoMargin string // rmam
DoubleUnderline string // Smulx with param 2
CurlyUnderline string // Smulx with param 3
DottedUnderline string // Smulx with param 4
DashedUnderline string // Smulx with param 5
UnderlineColor string // Setuc1
UnderlineColorRGB string // Setulc
UnderlineColorReset string // ol
XTermLike bool // (XT) has XTerm extensions XTermLike bool // (XT) has XTerm extensions
} }
const ( type stack []any
ModifiersNone = 0
ModifiersXTerm = 1
)
type stack []interface{} func (st stack) Push(v any) stack {
func (st stack) Push(v interface{}) stack {
if b, ok := v.(bool); ok { if b, ok := v.(bool); ok {
if b { if b {
return append(st, 1) return append(st, 1)
@@ -337,12 +174,12 @@ func (pb *paramsBuffer) PutString(s string) {
// TParm takes a terminfo parameterized string, such as setaf or cup, and // TParm takes a terminfo parameterized string, such as setaf or cup, and
// evaluates the string, and returns the result with the parameter // evaluates the string, and returns the result with the parameter
// applied. // applied.
func (t *Terminfo) TParm(s string, p ...interface{}) string { func (t *Terminfo) TParm(s string, p ...any) string {
var stk stack var stk stack
var a string var a string
var ai, bi int var ai, bi int
var dvars [26]string var dvars [26]string
var params [9]interface{} var params [9]any
var pb = &paramsBuffer{} var pb = &paramsBuffer{}
pb.Start(s) pb.Start(s)
@@ -682,6 +519,7 @@ var (
// AddTerminfo can be called to register a new Terminfo entry. // AddTerminfo can be called to register a new Terminfo entry.
func AddTerminfo(t *Terminfo) { func AddTerminfo(t *Terminfo) {
dblock.Lock() dblock.Lock()
terminfos[t.Name] = t terminfos[t.Name] = t
for _, x := range t.Aliases { for _, x := range t.Aliases {
terminfos[x] = t terminfos[x] = t
@@ -777,5 +615,14 @@ func LookupTerminfo(name string) (*Terminfo, error) {
t.SetFgBg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;;%?%p2%{8}%<%t4%p2%d%e%p2%{16}%<%t10%p2%{8}%-%d%e48;5;%p2%d%;m" t.SetFgBg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;;%?%p2%{8}%<%t4%p2%d%e%p2%{16}%<%t10%p2%{8}%-%d%e48;5;%p2%d%;m"
t.ResetFgBg = "\x1b[39;49m" t.ResetFgBg = "\x1b[39;49m"
} }
return t, nil return t, nil
} }
func TerminfoNames() []string {
res := make([]string, 0, len(terminfos))
for m := range terminfos {
res = append(res, m)
}
return res
}
-18
View File
@@ -12,7 +12,6 @@ func init() {
Aliases: []string{"vt100-am"}, Aliases: []string{"vt100-am"},
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Bell: "\a",
Clear: "\x1b[H\x1b[J$<50>", Clear: "\x1b[H\x1b[J$<50>",
AttrOff: "\x1b[m\x0f$<2>", AttrOff: "\x1b[m\x0f$<2>",
Underline: "\x1b[4m$<2>", Underline: "\x1b[4m$<2>",
@@ -29,23 +28,6 @@ func init() {
EnableAutoMargin: "\x1b[?7h", EnableAutoMargin: "\x1b[?7h",
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
SetCursor: "\x1b[%i%p1%d;%p2%dH$<5>", SetCursor: "\x1b[%i%p1%d;%p2%dH$<5>",
CursorBack1: "\b",
CursorUp1: "\x1b[A$<2>",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyBackspace: "\b",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1bOt",
KeyF6: "\x1bOu",
KeyF7: "\x1bOv",
KeyF8: "\x1bOl",
KeyF9: "\x1bOw",
KeyF10: "\x1bOx",
AutoMargin: true, AutoMargin: true,
}) })
} }
-18
View File
@@ -11,7 +11,6 @@ func init() {
Name: "vt102", Name: "vt102",
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Bell: "\a",
Clear: "\x1b[H\x1b[J$<50>", Clear: "\x1b[H\x1b[J$<50>",
AttrOff: "\x1b[m\x0f$<2>", AttrOff: "\x1b[m\x0f$<2>",
Underline: "\x1b[4m$<2>", Underline: "\x1b[4m$<2>",
@@ -28,23 +27,6 @@ func init() {
EnableAutoMargin: "\x1b[?7h", EnableAutoMargin: "\x1b[?7h",
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
SetCursor: "\x1b[%i%p1%d;%p2%dH$<5>", SetCursor: "\x1b[%i%p1%d;%p2%dH$<5>",
CursorBack1: "\b",
CursorUp1: "\x1b[A$<2>",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyBackspace: "\b",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1bOt",
KeyF6: "\x1bOu",
KeyF7: "\x1bOv",
KeyF8: "\x1bOl",
KeyF9: "\x1bOw",
KeyF10: "\x1bOx",
AutoMargin: true, AutoMargin: true,
}) })
} }
-30
View File
@@ -12,7 +12,6 @@ func init() {
Aliases: []string{"vt200"}, Aliases: []string{"vt200"},
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Bell: "\a",
Clear: "\x1b[H\x1b[J", Clear: "\x1b[H\x1b[J",
ShowCursor: "\x1b[?25h", ShowCursor: "\x1b[?25h",
HideCursor: "\x1b[?25l", HideCursor: "\x1b[?25l",
@@ -29,35 +28,6 @@ func init() {
EnableAutoMargin: "\x1b[?7h", EnableAutoMargin: "\x1b[?7h",
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\b",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
KeyHelp: "\x1b[28~",
AutoMargin: true, AutoMargin: true,
}) })
} }
-32
View File
@@ -12,7 +12,6 @@ func init() {
Aliases: []string{"vt300"}, Aliases: []string{"vt300"},
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
ShowCursor: "\x1b[?25h", ShowCursor: "\x1b[?25h",
HideCursor: "\x1b[?25l", HideCursor: "\x1b[?25l",
@@ -30,37 +29,6 @@ func init() {
EnableAutoMargin: "\x1b[?7h", EnableAutoMargin: "\x1b[?7h",
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1b[1~",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[25~",
KeyF14: "\x1b[26~",
KeyF15: "\x1b[28~",
KeyF16: "\x1b[29~",
KeyF17: "\x1b[31~",
KeyF18: "\x1b[32~",
KeyF19: "\x1b[33~",
KeyF20: "\x1b[34~",
AutoMargin: true, AutoMargin: true,
}) })
} }
-15
View File
@@ -29,21 +29,6 @@ func init() {
EnableAutoMargin: "\x1b[?7h", EnableAutoMargin: "\x1b[?7h",
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyBackspace: "\b",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
AutoMargin: true, AutoMargin: true,
InsertChar: "\x1b[@", InsertChar: "\x1b[@",
}) })
+4 -23
View File
@@ -1,4 +1,7 @@
// Generated automatically. DO NOT HAND-EDIT. // This file was originally generated automatically,
// but it is edited to correct for errors in the VT420
// terminfo data. Additionally we have added extended
// information for the extended F-keys.
package vt420 package vt420
@@ -11,7 +14,6 @@ func init() {
Name: "vt420", Name: "vt420",
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Bell: "\a",
Clear: "\x1b[H\x1b[2J$<50>", Clear: "\x1b[H\x1b[2J$<50>",
ShowCursor: "\x1b[?25h", ShowCursor: "\x1b[?25h",
HideCursor: "\x1b[?25l", HideCursor: "\x1b[?25l",
@@ -30,27 +32,6 @@ func init() {
EnableAutoMargin: "\x1b[?7h", EnableAutoMargin: "\x1b[?7h",
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
SetCursor: "\x1b[%i%p1%d;%p2%dH$<10>", SetCursor: "\x1b[%i%p1%d;%p2%dH$<10>",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1b[A",
KeyDown: "\x1b[B",
KeyRight: "\x1b[C",
KeyLeft: "\x1b[D",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\b",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[17~",
KeyF6: "\x1b[18~",
KeyF7: "\x1b[19~",
KeyF8: "\x1b[20~",
KeyF9: "\x1b[21~",
KeyF10: "\x1b[29~",
AutoMargin: true, AutoMargin: true,
}) })
} }
-39
View File
@@ -1,39 +0,0 @@
// Generated automatically. DO NOT HAND-EDIT.
package vt52
import "github.com/gdamore/tcell/v2/terminfo"
func init() {
// DEC VT52
terminfo.AddTerminfo(&terminfo.Terminfo{
Name: "vt52",
Columns: 80,
Lines: 24,
Bell: "\a",
Clear: "\x1bH\x1bJ",
EnterKeypad: "\x1b=",
ExitKeypad: "\x1b>",
PadChar: "\x00",
AltChars: "+h.k0affggolpnqprrss",
EnterAcs: "\x1bF",
ExitAcs: "\x1bG",
SetCursor: "\x1bY%p1%' '%+%c%p2%' '%+%c",
CursorBack1: "\x1bD",
CursorUp1: "\x1bA",
KeyUp: "\x1bA",
KeyDown: "\x1bB",
KeyRight: "\x1bC",
KeyLeft: "\x1bD",
KeyBackspace: "\b",
KeyF1: "\x1bP",
KeyF2: "\x1bQ",
KeyF3: "\x1bR",
KeyF5: "\x1b?t",
KeyF6: "\x1b?u",
KeyF7: "\x1b?v",
KeyF8: "\x1b?w",
KeyF9: "\x1b?x",
})
}
-60
View File
@@ -1,60 +0,0 @@
// Generated automatically. DO NOT HAND-EDIT.
package wy50
import "github.com/gdamore/tcell/v2/terminfo"
func init() {
// Wyse 50
terminfo.AddTerminfo(&terminfo.Terminfo{
Name: "wy50",
Aliases: []string{"wyse50"},
Columns: 80,
Lines: 24,
Bell: "\a",
Clear: "\x1b+$<20>",
ShowCursor: "\x1b`1",
HideCursor: "\x1b`0",
AttrOff: "\x1b(\x1bH\x03",
Dim: "\x1b`7\x1b)",
Reverse: "\x1b`6\x1b)",
PadChar: "\x00",
AltChars: "a;j5k3l2m1n8q:t4u9v=w0x6",
EnterAcs: "\x1bH\x02",
ExitAcs: "\x1bH\x03",
SetCursor: "\x1b=%p1%' '%+%c%p2%' '%+%c",
CursorBack1: "\b",
CursorUp1: "\v",
KeyUp: "\v",
KeyDown: "\n",
KeyRight: "\f",
KeyLeft: "\b",
KeyInsert: "\x1bQ",
KeyDelete: "\x1bW",
KeyBackspace: "\b",
KeyHome: "\x1e",
KeyPgUp: "\x1bJ",
KeyPgDn: "\x1bK",
KeyF1: "\x01@\r",
KeyF2: "\x01A\r",
KeyF3: "\x01B\r",
KeyF4: "\x01C\r",
KeyF5: "\x01D\r",
KeyF6: "\x01E\r",
KeyF7: "\x01F\r",
KeyF8: "\x01G\r",
KeyF9: "\x01H\r",
KeyF10: "\x01I\r",
KeyF11: "\x01J\r",
KeyF12: "\x01K\r",
KeyF13: "\x01L\r",
KeyF14: "\x01M\r",
KeyF15: "\x01N\r",
KeyF16: "\x01O\r",
KeyPrint: "\x1bP",
KeyBacktab: "\x1bI",
KeyShfHome: "\x1b{",
AutoMargin: true,
})
}
-66
View File
@@ -1,66 +0,0 @@
// Generated automatically. DO NOT HAND-EDIT.
package wy60
import "github.com/gdamore/tcell/v2/terminfo"
func init() {
// Wyse 60
terminfo.AddTerminfo(&terminfo.Terminfo{
Name: "wy60",
Aliases: []string{"wyse60"},
Columns: 80,
Lines: 24,
Bell: "\a",
Clear: "\x1b+$<100>",
EnterCA: "\x1bw0",
ExitCA: "\x1bw1",
ShowCursor: "\x1b`1",
HideCursor: "\x1b`0",
AttrOff: "\x1b(\x1bH\x03\x1bG0\x1bcD",
Underline: "\x1bG8",
Dim: "\x1bGp",
Blink: "\x1bG2",
Reverse: "\x1bG4",
PadChar: "\x00",
AltChars: "+/,.0[a2fxgqh1ihjYk?lZm@nEqDtCu4vAwBx3yszr{c~~",
EnterAcs: "\x1bcE",
ExitAcs: "\x1bcD",
EnableAutoMargin: "\x1bd/",
DisableAutoMargin: "\x1bd.",
SetCursor: "\x1b=%p1%' '%+%c%p2%' '%+%c",
CursorBack1: "\b",
CursorUp1: "\v",
KeyUp: "\v",
KeyDown: "\n",
KeyRight: "\f",
KeyLeft: "\b",
KeyInsert: "\x1bQ",
KeyDelete: "\x1bW",
KeyBackspace: "\b",
KeyHome: "\x1e",
KeyPgUp: "\x1bJ",
KeyPgDn: "\x1bK",
KeyF1: "\x01@\r",
KeyF2: "\x01A\r",
KeyF3: "\x01B\r",
KeyF4: "\x01C\r",
KeyF5: "\x01D\r",
KeyF6: "\x01E\r",
KeyF7: "\x01F\r",
KeyF8: "\x01G\r",
KeyF9: "\x01H\r",
KeyF10: "\x01I\r",
KeyF11: "\x01J\r",
KeyF12: "\x01K\r",
KeyF13: "\x01L\r",
KeyF14: "\x01M\r",
KeyF15: "\x01N\r",
KeyF16: "\x01O\r",
KeyPrint: "\x1bP",
KeyBacktab: "\x1bI",
KeyShfHome: "\x1b{",
AutoMargin: true,
})
}
-120
View File
@@ -1,120 +0,0 @@
// Generated automatically. DO NOT HAND-EDIT.
package wy99_ansi
import "github.com/gdamore/tcell/v2/terminfo"
func init() {
// Wyse WY-99GT in ANSI mode (int'l PC keyboard)
terminfo.AddTerminfo(&terminfo.Terminfo{
Name: "wy99-ansi",
Columns: 80,
Lines: 25,
Bell: "\a",
Clear: "\x1b[H\x1b[J$<200>",
ShowCursor: "\x1b[34h\x1b[?25h",
HideCursor: "\x1b[?25l",
AttrOff: "\x1b[m\x0f\x1b[\"q",
Underline: "\x1b[4m",
Bold: "\x1b[1m",
Dim: "\x1b[2m",
Blink: "\x1b[5m",
Reverse: "\x1b[7m",
EnterKeypad: "\x1b[?1h",
ExitKeypad: "\x1b[?1l",
PadChar: "\x00",
AltChars: "``aaffggjjkkllmmnnooqqssttuuvvwwxx{{||}}~~",
EnterAcs: "\x0e",
ExitAcs: "\x0f",
EnableAcs: "\x1b)0",
EnableAutoMargin: "\x1b[?7h",
DisableAutoMargin: "\x1b[?7l",
SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b$<1>",
CursorUp1: "\x1bM",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyBackspace: "\b",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[M",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF17: "\x1b[K",
KeyF18: "\x1b[31~",
KeyF19: "\x1b[32~",
KeyF20: "\x1b[33~",
KeyF21: "\x1b[34~",
KeyF22: "\x1b[35~",
KeyF23: "\x1b[1~",
KeyF24: "\x1b[2~",
KeyBacktab: "\x1b[z",
AutoMargin: true,
})
// Wyse WY-99GT in ANSI mode (US PC keyboard)
terminfo.AddTerminfo(&terminfo.Terminfo{
Name: "wy99a-ansi",
Columns: 80,
Lines: 25,
Bell: "\a",
Clear: "\x1b[H\x1b[J$<200>",
ShowCursor: "\x1b[34h\x1b[?25h",
HideCursor: "\x1b[?25l",
AttrOff: "\x1b[m\x0f\x1b[\"q",
Underline: "\x1b[4m",
Bold: "\x1b[1m",
Dim: "\x1b[2m",
Blink: "\x1b[5m",
Reverse: "\x1b[7m",
EnterKeypad: "\x1b[?1h",
ExitKeypad: "\x1b[?1l",
PadChar: "\x00",
AltChars: "``aaffggjjkkllmmnnooqqssttuuvvwwxx{{||}}~~",
EnterAcs: "\x0e",
ExitAcs: "\x0f",
EnableAcs: "\x1b)0",
EnableAutoMargin: "\x1b[?7h",
DisableAutoMargin: "\x1b[?7l",
SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b$<1>",
CursorUp1: "\x1bM",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyBackspace: "\b",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[M",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF17: "\x1b[K",
KeyF18: "\x1b[31~",
KeyF19: "\x1b[32~",
KeyF20: "\x1b[33~",
KeyF21: "\x1b[34~",
KeyF22: "\x1b[35~",
KeyF23: "\x1b[1~",
KeyF24: "\x1b[2~",
KeyBacktab: "\x1b[z",
AutoMargin: true,
})
}
-28
View File
@@ -12,7 +12,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b7\x1b[?47h", EnterCA: "\x1b7\x1b[?47h",
ExitCA: "\x1b[2J\x1b[?47l\x1b8", ExitCA: "\x1b[2J\x1b[?47l\x1b8",
@@ -37,33 +36,6 @@ func init() {
DisableAutoMargin: "\x1b[?7l", DisableAutoMargin: "\x1b[?7l",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
-28
View File
@@ -31,7 +31,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h\x1b[22;0;0t", EnterCA: "\x1b[?1049h\x1b[22;0;0t",
ExitCA: "\x1b[?1049l\x1b[23;0;0t", ExitCA: "\x1b[?1049l\x1b[23;0;0t",
@@ -59,33 +58,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[M", Mouse: "\x1b[M",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\u007f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
TrueColor: true, TrueColor: true,
}) })
-84
View File
@@ -13,7 +13,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 8, Colors: 8,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h\x1b[22;0;0t", EnterCA: "\x1b[?1049h\x1b[22;0;0t",
ExitCA: "\x1b[?1049l\x1b[23;0;0t", ExitCA: "\x1b[?1049l\x1b[23;0;0t",
@@ -40,33 +39,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[<", Mouse: "\x1b[<",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
@@ -77,7 +49,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 88, Colors: 88,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h\x1b[22;0;0t", EnterCA: "\x1b[?1049h\x1b[22;0;0t",
ExitCA: "\x1b[?1049l\x1b[23;0;0t", ExitCA: "\x1b[?1049l\x1b[23;0;0t",
@@ -104,33 +75,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[<", Mouse: "\x1b[<",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
@@ -141,7 +85,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h\x1b[22;0;0t", EnterCA: "\x1b[?1049h\x1b[22;0;0t",
ExitCA: "\x1b[?1049l\x1b[23;0;0t", ExitCA: "\x1b[?1049l\x1b[23;0;0t",
@@ -168,33 +111,6 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[<", Mouse: "\x1b[<",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
AutoMargin: true, AutoMargin: true,
XTermLike: true, XTermLike: true,
}) })
-32
View File
@@ -13,7 +13,6 @@ func init() {
Columns: 80, Columns: 80,
Lines: 24, Lines: 24,
Colors: 256, Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J", Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h", EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[?1049l", ExitCA: "\x1b[?1049l",
@@ -40,40 +39,9 @@ func init() {
StrikeThrough: "\x1b[9m", StrikeThrough: "\x1b[9m",
Mouse: "\x1b[<", Mouse: "\x1b[<",
SetCursor: "\x1b[%i%p1%d;%p2%dH", SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\x7f",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyBacktab: "\x1b[Z",
Modifiers: 1,
TrueColor: true, TrueColor: true,
AutoMargin: true, AutoMargin: true,
InsertChar: "\x1b[@", InsertChar: "\x1b[@",
DoubleUnderline: "\x1b[4:2m",
CurlyUnderline: "\x1b[4:3m",
DottedUnderline: "\x1b[4:4m",
DashedUnderline: "\x1b[4:5m",
XTermLike: true, XTermLike: true,
}) })
} }

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