about summary refs log blame commit diff stats
path: root/src/local/lineedit.nim
blob: edb6ee0911c7dfd71b2a668d216b4765e1c3ae11 (plain) (tree)
1
2
3
4
5
6
7
8
9
                   
 

                       
                          
                       
                 
                
                     
                   
                     
                   
                   
                      
 

                       
                                 
 


                           
                        
                 
                  
                

                         




                                      
                 

                         


                     
                    
                 
 

                      

                                     
 



                                           
                                                    
                                          
















                                                       

                                                 




                                                        
                                                            


                                                          
 
                                                 
                  

                                                                         
           


                              






                               

                                 
                     
                       
                                    
                           

                            
                                                   

             

                                       




                                    

                                       
                                                                 
 
                                               



                                   
                      
                
          
                                   

                              
                    
 
                                        
                        
 
                                        

                                                                  
                        
 
                                           
                      



                                            
                      
 
                                                           
                         
                                  


                         



                                    

                         

             
                                                        



                                
 
                                        
                                  
                                                
                                                         
                      
 
                                        

                     
                                       



                                         
                      
 
                                      

                                  
                      
 
                                          
                      

                                            
                                  
                        
 
                                         
                                  

                                            
                                                   
                        
 
                                          

                       





                                          
                         



                                            
           
                             
                                
                      
 
                                          

                                   



                                          
       
                     
                                     



                                            
           
                             
                                                 
                      
 
                                           
                       
                 

                                            
                      
 
                                          

                                   










                                      
 
                                       


                     
                      
 
                                       



                                                   
                        
 




                                          
                                               


                                                                     
              
                      
 


                                              
                                               
                
              
                      

                                            
                            
                
              
                     
 
                                                             

                                                
                                                                              
                                                                

                              
                   
                     
                  

                           
                 




                                              

                              
   


                                         
import std/strutils

import chagashi/charset
import chagashi/decoder
import monoucha/javascript
import monoucha/quickjs
import types/cell
import types/opt
import types/winattrs
import utils/luwrap
import utils/strwidth
import utils/twtstr
import utils/twtuni
import utils/wordbreak

type
  LineEditState* = enum
    lesEdit, lesFinish, lesCancel

  LineHistory* = ref object
    lines: seq[string]

  LineEdit* = ref object
    news*: string
    prompt: string
    promptw: int
    state*: LineEditState
    escNext*: bool
    cursorx: int # 0 ..< news.notwidth
    cursori: int # 0 ..< news.len
    shiftx: int # 0 ..< news.notwidth
    shifti: int # 0 ..< news.len
    padding: int # 0 or 1
    maxwidth: int
    disallowed: set[char]
    hide: bool
    hist: LineHistory
    histindex: int
    histtmp: string
    luctx: LUContext
    redraw*: bool

jsDestructor(LineEdit)

func newLineHistory*(): LineHistory =
  return LineHistory()

# Note: capped at edit.maxwidth.
func getDisplayWidth(edit: LineEdit): int =
  var dispw = 0
  var i = edit.shifti
  while i < edit.news.len and dispw < edit.maxwidth:
    dispw += edit.news.nextUTF8(i).width()
  return dispw

proc shiftView(edit: LineEdit) =
  # Shift view so it contains the cursor.
  if edit.cursorx < edit.shiftx:
    edit.shiftx = edit.cursorx
    edit.shifti = edit.cursori
  # Shift view so it is completely filled.
  if edit.shiftx > 0:
    let dispw = edit.getDisplayWidth()
    if dispw < edit.maxwidth:
      let targetx = edit.shiftx - edit.maxwidth + dispw
      if targetx <= 0:
        edit.shiftx = 0
        edit.shifti = 0
      else:
        while edit.shiftx > targetx:
          let u = edit.news.prevUTF8(edit.shifti)
          edit.shiftx -= u.width()
  edit.padding = 0
  # Shift view so it contains the cursor. (act 2)
  if edit.shiftx < edit.cursorx - edit.maxwidth:
    while edit.shiftx < edit.cursorx - edit.maxwidth and
        edit.shifti < edit.news.len:
      edit.shiftx += edit.news.nextUTF8(edit.shifti).width()
    if edit.shiftx > edit.cursorx - edit.maxwidth:
      # skipped over a cell because of a double-width char
      edit.padding = 1

proc generateOutput*(edit: LineEdit): FixedGrid =
  edit.shiftView()
  # Make the output grid +1 cell wide, so it covers the whole input area.
  result = newFixedGrid(edit.promptw + edit.maxwidth + 1)
  var x = 0
  for u in edit.prompt.points:
    result[x].str.addUTF8(u)
    x += u.width()
    if x >= result.width: break
  for i in 0 ..< edit.padding:
    if x < result.width:
      result[x].str = " "
      inc x
  var i = edit.shifti
  while i < edit.news.len:
    let pi = i
    let u = edit.news.nextUTF8(i)
    if not edit.hide:
      let w = u.width()
      if x + w > result.width: break
      if u.isControlChar():
        result[x].str &= '^'
        inc x
        result[x].str &= char(u).getControlLetter()
        inc x
      else:
        for j in pi ..< i:
          result[x].str &= edit.news[j]
        x += w
    else:
      if x + 1 > result.width: break
      result[x].str &= '*'
      inc x

proc getCursorX*(edit: LineEdit): int =
  return edit.promptw + edit.cursorx + edit.padding - edit.shiftx

proc insertCharseq(edit: LineEdit; s: string) =
  let s = if edit.escNext:
    s
  else:
    deleteChars(s, edit.disallowed)
  edit.escNext = false
  if s.len == 0:
    return
  edit.news.insert(s, edit.cursori)
  edit.cursori += s.len
  edit.cursorx += s.notwidth()
  edit.redraw = true

proc cancel(edit: LineEdit) {.jsfunc.} =
  edit.state = lesCancel

proc submit(edit: LineEdit) {.jsfunc.} =
  if edit.hist.lines.len == 0 or edit.news != edit.hist.lines[^1]:
    edit.hist.lines.add(edit.news)
  edit.state = lesFinish

proc backspace(edit: LineEdit) {.jsfunc.} =
  if edit.cursori > 0:
    let pi = edit.cursori
    let u = edit.news.prevUTF8(edit.cursori)
    edit.news.delete(edit.cursori ..< pi)
    edit.cursorx -= u.width()
    edit.redraw = true
 
proc write*(edit: LineEdit; s: string; cs: Charset): bool =
  if cs == CHARSET_UTF_8:
    if s.validateUTF8Surr() != -1:
      return false
    edit.insertCharseq(s)
  else:
    let td = newTextDecoder(cs)
    var success = false
    let s = td.decodeAll(s, success)
    if not success:
      return false
    edit.insertCharseq(s)
  return true

proc write(edit: LineEdit; s: string): bool {.jsfunc.} =
  if s.validateUTF8Surr() != -1:
    return false
  edit.insertCharseq(s)
  return true

proc delete(edit: LineEdit) {.jsfunc.} =
  if edit.cursori < edit.news.len:
    let len = edit.news.pointLenAt(edit.cursori)
    edit.news.delete(edit.cursori ..< edit.cursori + len)
    edit.redraw = true

proc escape(edit: LineEdit) {.jsfunc.} =
  edit.escNext = true

proc clear(edit: LineEdit) {.jsfunc.} =
  if edit.cursori > 0:
    edit.news.delete(0..edit.cursori - 1)
    edit.cursori = 0
    edit.cursorx = 0
    edit.redraw = true

proc kill(edit: LineEdit) {.jsfunc.} =
  if edit.cursori < edit.news.len:
    edit.news.setLen(edit.cursori)
    edit.redraw = true

proc backward(edit: LineEdit) {.jsfunc.} =
  if edit.cursori > 0:
    let u = edit.news.prevUTF8(edit.cursori)
    edit.cursorx -= u.width()
    if edit.cursorx < edit.shiftx:
      edit.redraw = true

proc forward(edit: LineEdit) {.jsfunc.} =
  if edit.cursori < edit.news.len:
    let u = edit.news.nextUTF8(edit.cursori)
    edit.cursorx += u.width()
    if edit.cursorx >= edit.shiftx + edit.maxwidth:
      edit.redraw = true

proc prevWord(edit: LineEdit) {.jsfunc.} =
  if edit.cursori == 0:
    return
  let pi = edit.cursori
  let u = edit.news.prevUTF8(edit.cursori)
  if edit.luctx.breaksWord(u):
    edit.cursorx -= u.width()
  else:
    edit.cursori = pi
  while edit.cursori > 0:
    let pi = edit.cursori
    let u = edit.news.prevUTF8(edit.cursori)
    if edit.luctx.breaksWord(u):
      edit.cursori = pi
      break
    edit.cursorx -= u.width()
  if edit.cursorx < edit.shiftx:
    edit.redraw = true

proc nextWord(edit: LineEdit) {.jsfunc.} =
  if edit.cursori >= edit.news.len:
    return
  let pi = edit.cursori
  let u = edit.news.nextUTF8(edit.cursori)
  if edit.luctx.breaksWord(u):
    edit.cursorx += u.width()
  else:
    edit.cursori = pi
  while edit.cursori < edit.news.len:
    let pi = edit.cursori
    let u = edit.news.nextUTF8(edit.cursori)
    if edit.luctx.breaksWord(u):
      edit.cursori = pi
      break
    edit.cursorx += u.width()
  if edit.cursorx >= edit.shiftx + edit.maxwidth:
    edit.redraw = true

proc clearWord(edit: LineEdit) {.jsfunc.} =
  let oc = edit.cursori
  edit.prevWord()
  if oc != edit.cursori:
    edit.news.delete(edit.cursori .. oc - 1)
    edit.redraw = true

proc killWord(edit: LineEdit) {.jsfunc.} =
  if edit.cursori >= edit.news.len:
    return
  var i = edit.cursori
  var u = edit.news.nextUTF8(i)
  if not edit.luctx.breaksWord(u):
    while i < edit.news.len:
      let pi = i
      let u = edit.news.nextUTF8(i)
      if edit.luctx.breaksWord(u):
        i = pi
        break
  edit.news.delete(edit.cursori ..< i)
  edit.redraw = true

proc begin(edit: LineEdit) {.jsfunc.} =
  edit.cursori = 0
  edit.cursorx = 0
  if edit.shiftx > 0:
    edit.redraw = true

proc `end`(edit: LineEdit) {.jsfunc.} =
  if edit.cursori < edit.news.len:
    edit.cursori = edit.news.len
    edit.cursorx = edit.news.notwidth()
    if edit.cursorx >= edit.shiftx + edit.maxwidth:
      edit.redraw = true

proc prevHist(edit: LineEdit) {.jsfunc.} =
  if edit.histindex > 0:
    if edit.news.len > 0:
      edit.histtmp = $edit.news
    dec edit.histindex
    edit.news = edit.hist.lines[edit.histindex]
    # The begin call is needed so the cursor doesn't get lost outside
    # the string.
    edit.begin()
    edit.end()
    edit.redraw = true

proc nextHist(edit: LineEdit) {.jsfunc.} =
  if edit.histindex + 1 < edit.hist.lines.len:
    inc edit.histindex
    edit.news = edit.hist.lines[edit.histindex]
    edit.begin()
    edit.end()
    edit.redraw = true
  elif edit.histindex < edit.hist.lines.len:
    inc edit.histindex
    edit.news = edit.histtmp
    edit.begin()
    edit.end()
    edit.histtmp = ""

proc windowChange*(edit: LineEdit; attrs: WindowAttributes) =
  edit.maxwidth = attrs.width - edit.promptw - 1

proc readLine*(prompt, current: string; termwidth: int; disallowed: set[char];
    hide: bool; hist: LineHistory; luctx: LUContext): LineEdit =
  let promptw = prompt.width()
  return LineEdit(
    prompt: prompt,
    promptw: promptw,
    news: current,
    disallowed: disallowed,
    hide: hide,
    redraw: true,
    cursori: current.len,
    cursorx: current.notwidth(),
    # - 1, so that the cursor always has place
    maxwidth: termwidth - promptw - 1,
    hist: hist,
    histindex: hist.lines.len,
    luctx: luctx
  )

proc addLineEditModule*(ctx: JSContext) =
  ctx.registerType(LineEdit)