From f78c98d738b425316b8a88e6f0acf412ce9b3c8f Mon Sep 17 00:00:00 2001
From: ~byakuren
Date: Fri, 12 Sep 2025 06:06:21 +0100
Subject: Add readline functions 1!:13, 1!:14, 1!:15

Adds three line-reading foreign functions for file I/O:

- 1!:13: Read one line from file (includes trailing newline)
- 1!:14: Read one line, stripping trailing CR/LF characters
- 1!:15: Peek one line without advancing file position

These complement existing block-based file operations with convenient
line-by-line reading for text processing.
---
 jsrc/je.h |   3 ++
 jsrc/x.c  |   3 ++
 jsrc/xf.c | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 120 insertions(+)

diff --git a/jsrc/je.h b/jsrc/je.h
index 810bccdc..a755ef50 100644
--- a/jsrc/je.h
+++ b/jsrc/je.h
@@ -189,6 +189,9 @@ extern DF1(jtjferase);
 extern F1(jtjfiles);
 extern DF1(jtjfperm1);
 extern DF1(jtjfread);
+extern DF1(jtjfreadline);
+extern DF1(jtjfreadlinetrim);
+extern DF1(jtjfreadlinepeek);
 extern DF1(jtjfsize);
 extern DF1(jtjgetenv);
 extern F1(jtjgetx);
diff --git a/jsrc/x.c b/jsrc/x.c
index ff8d1e91..75595098 100644
--- a/jsrc/x.c
+++ b/jsrc/x.c
@@ -124,6 +124,9 @@ void jtforeigninit(J jt){UI i;

  MN(1,11)  XPRIM(VERB, jtjiread,     0,            VASGSAFE,VF2NONE,1,   RMAX,RMAX);
  MN(1,12)  XPRIM(VERB, 0,            jtjiwrite,    VASGSAFE,VF2NONE,RMAX,RMAX,1   );
+ MN(1,13)  XPRIM(VERB, jtjfreadline, 0,            VASGSAFE,VF2NONE,0,   RMAX,RMAX);
+ MN(1,14)  XPRIM(VERB, jtjfreadlinetrim, 0,        VASGSAFE,VF2NONE,0,   RMAX,RMAX);
+ MN(1,15)  XPRIM(VERB, jtjfreadlinepeek, 0,        VASGSAFE,VF2NONE,0,   RMAX,RMAX);

  MN(3,0)   XPRIM(VERB, jtstype,      0,            VASGSAFE,VF2NONE,RMAX,RMAX,RMAX);
  MN(3,10)  XPRIM(VERB, jttobase64,   0,            VASGSAFE,VF2NONE,RMAX,RMAX,RMAX);
diff --git a/jsrc/xf.c b/jsrc/xf.c
index 334d6643..bc7746d9 100644
--- a/jsrc/xf.c
+++ b/jsrc/xf.c
@@ -178,6 +178,120 @@ DF2(jtjfwrite){F12IP;B b;F f;
  RE(0); EPILOG(mtm);   // check error, return i. 0 0 if none
 }

+// 1!:13 - read one line from file (like fgets)
+DF1(jtjfreadline){F12IP;A z;F f,fp;C buf[1000];C *result;
+ ASSERT(!JT(jt,seclev),EVSECURE)
+ F1RANK(0,jtjfreadline,self);
+ PROLOG(000);
+ RE(f=stdf(w));  // f=file#, or 0 if w is a filename
+ if(f){ // if special file, handle appropriately
+  if(1==(I)f)R jgets(GETSKBD);  // keyboard input
+  if(3==(I)f)fp=stdin;
+  else RZ(fp=vfn(f));  // ensure actual file pointer
+ }else{RZ(fp=jope(w,FREAD_O));}  // otherwise open named file
+
+ // Use fgets to read one line
+ result = fgets(buf, sizeof(buf), fp);
+ if(result == NULL) {
+  // EOF or error - return empty string
+  GATV0(z,LIT,0,1);
+ } else {
+  // Return the line (including newline if present)
+  I len = strlen(buf);
+  GATV0(z,LIT,len,1);
+  MC(CAV1(z),buf,len);
+ }
+
+ if(!f){fclose(fp);} //if we opened the file, close it
+ else{jtunvfn(jt,f,0);} //otherwise, release the lock on it
+ EPILOG(z);
+}
+
+// 1!:14 - read one line from file without trailing newlines (stripped fgets)
+DF1(jtjfreadlinetrim){F12IP;A z;F f,fp;C buf[1000];C *result;
+ ASSERT(!JT(jt,seclev),EVSECURE)
+ F1RANK(0,jtjfreadlinetrim,self);
+ PROLOG(000);
+ RE(f=stdf(w));  // f=file#, or 0 if w is a filename
+ if(f){ // if special file, handle appropriately
+  if(1==(I)f)R jgets(GETSKBD);  // keyboard input
+  if(3==(I)f)fp=stdin;
+  else RZ(fp=vfn(f));  // ensure actual file pointer
+ }else{RZ(fp=jope(w,FREAD_O));}  // otherwise open named file
+
+ // Use fgets to read one line from regular files
+ result = fgets(buf, sizeof(buf), fp);
+ if(result == NULL) {
+  // EOF or error - return empty string
+  GATV0(z,LIT,0,1);
+ } else {
+  // Strip trailing CR/LF characters
+  I len = strlen(buf);
+  while(len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r')) {
+   len--;
+  }
+  // Return the line without newline characters
+  GATV0(z,LIT,len,1);
+  MC(CAV1(z),buf,len);
+ }
+
+ if(!f){fclose(fp);} //if we opened the file, close it
+ else{jtunvfn(jt,f,0);} //otherwise, release the lock on it
+ EPILOG(z);
+}
+
+// 1!:15 - peek one line from file without advancing file position
+DF1(jtjfreadlinepeek){F12IP;A z;F f,fp;C buf[1000];C *result;long pos;
+ ASSERT(!JT(jt,seclev),EVSECURE)
+ F1RANK(0,jtjfreadlinepeek,self);
+ PROLOG(000);
+ RE(f=stdf(w));  // f=file#, or 0 if w is a filename
+ if(f){ // if special file, handle appropriately
+  if(1==(I)f)R jgets(GETSKBD);  // keyboard input
+  if(3==(I)f)fp=stdin;
+  else RZ(fp=vfn(f));  // ensure actual file pointer
+ }else{RZ(fp=jope(w,FREAD_O));}  // otherwise open named file
+
+ // Save current file position
+ pos = ftell(fp);
+ if(pos == -1L) {
+  // If ftell fails (e.g., on stdin), fallback to regular read
+  result = fgets(buf, sizeof(buf), fp);
+  if(result == NULL) {
+   GATV0(z,LIT,0,1);
+  } else {
+   // Strip trailing CR/LF characters like 1!:14
+   I len = strlen(buf);
+   while(len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r')) {
+    len--;
+   }
+   GATV0(z,LIT,len,1);
+   MC(CAV1(z),buf,len);
+  }
+ } else {
+  // Use fgets to peek one line
+  result = fgets(buf, sizeof(buf), fp);
+  if(result == NULL) {
+   // EOF or error - return empty string
+   GATV0(z,LIT,0,1);
+  } else {
+   // Strip trailing CR/LF characters like 1!:14
+   I len = strlen(buf);
+   while(len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r')) {
+    len--;
+   }
+   GATV0(z,LIT,len,1);
+   MC(CAV1(z),buf,len);
+  }
+  // Restore file position to before the read
+  fseek(fp, pos, SEEK_SET);
+ }
+
+ if(!f){fclose(fp);} //if we opened the file, close it
+ else{jtunvfn(jt,f,0);} //otherwise, release the lock on it
+ EPILOG(z);
+}
+
 // 1!:3
 DF2(jtjfappend){F12IP;B b;F f;
  ASSERT(!JT(jt,seclev),EVSECURE)
--
2.51.2

