github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/portal.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "os" 8 "path/filepath" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/xyproto/env/v2" 14 "github.com/xyproto/files" 15 ) 16 17 var portalFilename = env.ExpandUser(filepath.Join(tempDir, env.Str("LOGNAME", "o")+"_portal.txt")) 18 19 var errPortalTimedOut = errors.New("portal timed out") 20 21 // Portal is a filename and a line number, for pulling text from 22 type Portal struct { 23 timestamp time.Time 24 absFilename string 25 lineNumber LineNumber 26 } 27 28 // NewPortal returns a new portal to this filename and line number, 29 // but does not save the new portal. Use the Save() method for that. 30 func (e *Editor) NewPortal() (*Portal, error) { 31 absFilename, err := e.AbsFilename() 32 if err != nil { 33 return nil, err 34 } 35 return &Portal{time.Now(), absFilename, e.LineNumber()}, nil 36 } 37 38 // SameFile checks if the portal exist in the same file as the editor is editing 39 func (p *Portal) SameFile(e *Editor) bool { 40 absFilename, err := e.AbsFilename() 41 if err != nil { 42 return false 43 } 44 return absFilename == p.absFilename 45 } 46 47 // MoveDown is useful when using portals within the same file. 48 func (p *Portal) MoveDown() { 49 // PopLine handles overflows. 50 p.lineNumber++ 51 } 52 53 // ClosePortal will clear the portal by removing the portal file 54 func (e *Editor) ClosePortal() error { 55 e.sameFilePortal = nil 56 return os.Remove(portalFilename) 57 } 58 59 // ClearPortal will clear the portal by removing the portal file 60 func ClearPortal() error { 61 return os.Remove(portalFilename) 62 } 63 64 // HasPortal checks if a portal is currently active 65 func HasPortal() bool { 66 return files.Exists(portalFilename) 67 } 68 69 // LoadPortal will load a filename + line number from the portal.txt file 70 func LoadPortal(maxPortalAge time.Duration) (*Portal, error) { 71 //logf("Loading %s\n", portalFilename) 72 data, err := os.ReadFile(portalFilename) 73 if err != nil { 74 return nil, err 75 } 76 if !bytes.Contains(data, []byte{'\n'}) { 77 return nil, errors.New(portalFilename + " does not have a newline, it's not a portal file") 78 } 79 lines := strings.Split(strings.TrimSpace(string(data)), "\n") 80 lineCounter := 0 81 timestamp := time.Now() 82 switch len(lines) { 83 case 3: // optional timestamp on the first line, if there are 3 lines 84 timestampInt, err := strconv.ParseInt(lines[lineCounter], 10, 64) 85 if err != nil { 86 return nil, err 87 } 88 lineCounter++ 89 timestamp = time.Unix(timestampInt, 0) 90 fallthrough 91 case 2: // 2 lines without a timestamp, use time.Now() from above 92 absFilename, err := filepath.Abs(lines[lineCounter]) 93 if err != nil { 94 return nil, err 95 } 96 lineCounter++ 97 lineInt, err := strconv.Atoi(lines[lineCounter]) 98 if err != nil { 99 return nil, err 100 } 101 lineNumber := LineNumber(lineInt) 102 portalAge := time.Since(timestamp) 103 // Check if the portal was created for too long ago to be used for the current session 104 if portalAge > maxPortalAge { 105 return nil, errPortalTimedOut 106 } 107 return &Portal{timestamp, absFilename, lineNumber}, nil 108 default: 109 //logf("%s contains too few lines!\n%s\n", portalFilename, strings.Join(lines, ";")) 110 return nil, errors.New(portalFilename + " contains too few lines") 111 } 112 } 113 114 // LineIndex returns the current line index that the portal points to 115 func (p *Portal) LineIndex() LineIndex { 116 return p.lineNumber.LineIndex() 117 } 118 119 // Save will save the portal 120 func (p *Portal) Save() error { 121 //logf("Saving %s\n", portalFilename) 122 s := fmt.Sprintf("%d\n%s\n%s\n", p.timestamp.Unix(), p.absFilename, p.lineNumber) 123 // Anyone can read this file 124 if err := os.WriteFile(portalFilename, []byte(s), 0o600); err != nil { 125 return err 126 } 127 return os.Chmod(portalFilename, 0o666) 128 } 129 130 // String returns the current portal (filename + line number) as a colon separated string 131 func (p *Portal) String() string { 132 return filepath.Base(p.absFilename) + ":" + p.lineNumber.String() 133 } 134 135 // NewLineInserted reacts when the editor inserts a new line in the same file, 136 // and moves the portal source one line down, if needed. 137 func (p *Portal) NewLineInserted(y LineIndex) { 138 if y < p.LineIndex() { 139 p.MoveDown() 140 } 141 } 142 143 // PopLine removes (!) a line from the portal file, then removes that line 144 func (p *Portal) PopLine(e *Editor, removeLine bool) (string, error) { 145 // popping a line from the same file is a special case 146 if p == e.sameFilePortal { 147 if removeLine { 148 return "", errors.New("not implemented") // not implemented and currently not in use 149 } 150 // The line moving is done by the editor InsertAbove and InsertBelow functions 151 return e.Line(p.LineIndex()), nil 152 } 153 data, err := os.ReadFile(p.absFilename) 154 if err != nil { 155 return "", err 156 } 157 lines := strings.Split(string(data), "\n") 158 foundLine := "" 159 found := false 160 if removeLine { 161 modifiedLines := make([]string, 0, len(lines)-1) 162 for i, line := range lines { 163 if LineIndex(i) == p.lineNumber.LineIndex() { 164 foundLine = line 165 found = true 166 } else { 167 modifiedLines = append(modifiedLines, line) 168 } 169 } 170 if !found { 171 return "", errors.New("Could not teleport line " + p.String()) 172 } 173 data = []byte(strings.Join(modifiedLines, "\n")) 174 if err = os.WriteFile(p.absFilename, data, 0o600); err != nil { 175 return "", err 176 } 177 } else { 178 for i, line := range lines { 179 if LineIndex(i) == p.lineNumber.LineIndex() { 180 foundLine = line 181 found = true 182 break 183 } 184 } 185 if !found { 186 return "", errors.New("Could not teleport line " + p.String()) 187 } 188 // Now move the line number +1 189 p.lineNumber++ 190 // And save the new portal 191 if err := p.Save(); err != nil { 192 return foundLine, err 193 } 194 } 195 return foundLine, nil 196 }