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

                  

                
                 
                  
 
                    
                       
                 
                   
                   
                 
                      
                      
                            
                     
                    
                     


                          
                    
                 
                  
                   
                
                     
                
                     
                   
                      
                     
                   
                      
 
                       
 







                          
                     
                  

                            

                                                                           


                               
                   


                     
                   
                     
               
                       

                 
                              
                     
                
                  

                 
                      
                        

                      


                       


                         

                         


                                            
 



                 


                            
                   

                                   


                                                                             



                           
                         

                                                                            

                                                             
                     





                                                                                

                                       
                         
                                     
                           

                          
                                       

                                       
                                      


                                                                                

                             
                              
                           
                     



                             

                                                                             
                    

                                                                              
                     
                         
                                  
                                    
                       
                   
                                         


                                 
                         
                       
                     
                       
                              
                           
                                   
                    

















                                                                 
 
                       

                       















































































































































































































































































                                                                          
                                                                          


                                                                          
                                    
                   
             
                     
                             



                             

                                 

                        
      
                                                          
                     
                
                           
                 

                 

   
                                                     
                      
 



                                                                   


                             
                 






                                                                     

                                              
                                           

                 


                                                                 



                        
                           



                     

   
                                                     

                                                           
                                                                


                                                   

                                                                             


                              











                                                     




                                                                 
 
                                                

                                                 
                                                                      


                               
               
                       
                    

          
                                                                             

                                                        
                                                                            

                                                     


                                                                               

                                      

           
             

                            
                          



                        
          



                                                                              

                                      

           
             

                                 
                          


                      




















                                                                        
                                           
                                                   


                                           




                                                                           
                                                                  

                           
                                                        
 
                                                  


                                      

                                                      
 

                                                             




                                                               





                                                      


                               



               
 




                                 
       
                     



                                 






                               


                               
 
                                                             














                                                             
                                                           

                                     
               
                         
                
                                                 
               





                                                      
              


                                                   
       
                                               
 
                                                                    
                                 
                                 

                    

                                                  


                                    
 
                                                        
                                                      
                                                          
 
                                                                
                                 
 
                                                                

                                              
                                                                    





                                               
 

                                       
 
                                                                       
                            
                               
                              
                                                                     

                                 

                                             


                                                  

                                               
                                          
                                         




                                    
                                                                   
                                                                   
                           
                                 
   
 
                                                

                            

                                                                   



                                                  
                                       
                   

                                 
 
                                                        

                                                            
                               
                         
 
                                                                        

                                                            
                                               

                                                      

                                      
                         
 
                                                            


                       
                                            

                                                                              

                                                                           
                                                                          
                

                                                 
                                       
                                 


                                       
            
                                


                                                                                
                             
                                    
                                         
                              
                                                                          


                                                                               



                                                      

                                           

                                                                             
                                                                            
                             


                                                                     
                         

                                  


                                          
                                                      

                                                                    
 
                                                                          









                                                                         
                                                                              
                         
                                     
                            

                                  
 
                                                                               






                                                           
                                             

                    

            


                         
                                                        




                                                              
                                                             



                                 
                                                         

                               

                                                                
    
                                                              



                                 
                                                        




                                       
                                                             



                                 
                                                            







                                                            
                                                                






                                                                 


                                                               
                                                                   





                                  

                                                                       
                          
                          
                                      

                           

                            
 
                                                         
                             

                                 
                                               
 
                                                       
                             

                               
                                               
 
                                                         
                             

                                 
                                                      
 
                                                          
                             

                                  
                                                     
 
                                                       
                          
 
                                                     

                                                        


















                                                                              
 
                                                                 



                                        

                                                    
                                                              
       

                           



                                                          
                                                    


                      
                                      

















                                                         
                                                                 





                                             
                                                                
         

                             


                                                            
                                                      


                        
                                           

          

















                                                         
                                                                








                                             
                                               



                      
                                      

                                                 
                                   
                                                                               


                                             
                                                    




                      
                                              







                                                  








                                                        
                                                                  


                                        
            
            

                                             
              
                                                     
                                                
                                                 


                        
                                           

                                                    

                                                     
                                                   

                                                       
                                                      




                        

          
           
                       







                                 








                                                          
                                                       

                                                                

                            
                                                     

                                                                

                            
                                                       
                                                           
 
                                                        
                                                           
 



                                                                       
                                                         

                                                                            

                            
                                                           

                                                                            

                            
                                                           
                                                                       
 
                                                            
                                                                       
 







                                                                
                                                       
                             

                                      
                        
                           
                       
 
                                                       
                             

                                     
                        
                                                
                       
 
                                                        
                      

                                               
                     
 
                                                    
                      
                                                                      
                     
 
                                                           
                      

                                                              
                     
 
                                                      

                                       
                                                          

                                                                     
                                                       

                                                             
                                                          
                            


                                                                           
                                           
                                                               
       
                           
 
                                                        


                                     
                                                               

                                                              
       
                         
 
                                                           



                                                                           
 
                                                          


                                     
 
                                               
                                                               
 
                                                



                                                                       
 

                                         

                                                                       

                                          
                                             

                                                           
 
                                                           

                   
                             




                                 
                                              
                                
                            
                                            
                           
           

                                                
                        
                               
                       
 
                                           
                             


                                     
 
                                                          
                             






                                         
 

                                               
                                  
 
                                                              

                            
                      
                 
                                                          
                                       
                                    


                                                 
 
                                                              

                            
                      
                 
                                                          
                                       
                                    


                                                 
 
                                                                   

                            
                      



                                            
                         

     
                                                                   

                            
                      



                                            
                         

     
                                                              




                                     
                         


                                
                         

               
                                                                    

                                
                       
 
                                                                            





                                   
                                                                   








                                               
                                                                    



                                     
                                          



                       
                                                                       













                                                                           
                                                                       













                                                                           
                                                             

                            





                                                  
                                                                

                            





                                                  

                                                   
                                              
                                 
 
                                                                     
                 
                                                      

                                       
                                                
                         
                     




                  
                                  
                           
                            
                                 

                                     
                         


                               
                                                                              
                                           
                             


                                                   
                               
       

                              
                          
                                                                          
                                    
                                        
 
                                                                              
                                           
                             


                                                   
                               
       

                              
                        
                          
                                                                          
                                    


                                       
 



                                     
                                                       






                                                                 
                                     
                 
                            
                       
                   
                                        
             
                            
                            



                                   
                       



                                               
                                                                 
                                

                            
                                                            
                       










                                                                              
                

                                                      
                                                                        





                                                      
                                                             
                                            
               
                               
                                              



                                                                
              



                               


            
                                               

                            






                                                                 






                                                    
                                                     
                          
                                        
 
















                                                           
                                           
                                             
                                       

               
                                  
                             
                                     
                                     

                                                                          
                                                                       
                                           
                                          
                     
                                                   

                                           
       

                                 

                               
                                                       


                                                        
 




                                                      
                       



                                                                        
                                                                               



                                                 

                                                             

                               
                                                  





                                                       
                                                                
       
                                                        

                                                                        

                                                 

                                                                      
                                             






                                                                  


                                                           






                                                              
 






                                          
                                               
                             
                             

                                        



                                       
 
                                                        

                                                             
                                                                          
         

                                                                            
 
                                          


                                                           
 





                                                                              
                   
                                 

                                                                           
   
 
                                                             

                            
                                                                  

                                   
 
                                                                      













                                                                   
 
                                                                  

                               
                     

                                          
                        


                                    

                                           
                                          
 
                                              
                             

                            

                              
                                                               






                                                                          
 










                                                                   


                                                                            
                              



                                   


                                                       
 
                                            
                                 
 
                                        
                                     
 
                                                  
                            
             



                           



                                                               
 
                                                         
                                    

                                                          



                                                          
 
                                          


                                                                              
 









                                                       
                                                           
                                 
                                       
                                                           
                       

                                                                
                                 
                                    

                                                       
                       
 

                                                                    



                            
             





                                                                
                                             
                                                                               

                         
                   

                                                                
                                      
                            
                               
 
                                                                                   





                                                                       












                                                                             

                                   


                                  
                                  
           











                                                




                                                                              
                                      


                                 
                                    
               





                                                  

                                                              
                                                                      
                  

                                                              
                    
                                       
               




                                                       
                                                                  









                                                                        



                                                                   

            




                                         

                                          
                             
                                              
import std/deques
import std/options
import std/os
import std/posix
import std/tables
import std/unicode

import config/config
import config/mimetypes
import img/bitmap
import io/bufstream
import io/dynstream
import io/promise
import io/serversocket
import io/socketstream
import layout/renderdocument
import loader/headers
import loader/loader
import loader/request
import monoucha/javascript
import monoucha/jsregex
import monoucha/jstypes
import server/buffer
import types/cell
import types/color
import types/cookie
import types/opt
import types/referrer
import types/url
import types/winattrs
import utils/luwrap
import utils/mimeguess
import utils/strwidth
import utils/twtstr
import utils/wordbreak

import chagashi/charset

type
  CursorPosition* = object
    cursorx*: int
    cursory*: int
    xend*: int
    fromx*: int
    fromy*: int
    setx: int
    setxrefresh: bool
    setxsave: bool

  ContainerEventType* = enum
    cetAnchor, cetNoAnchor, cetReadLine, cetReadArea, cetReadFile, cetOpen,
    cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel

  ContainerEvent* = object
    case t*: ContainerEventType
    of cetReadLine:
      prompt*: string
      value*: string
      password*: bool
    of cetReadArea:
      tvalue*: string
    of cetOpen:
      request*: Request
      url*: URL
      save*: bool
    of cetAnchor, cetNoAnchor:
      anchor*: string
    of cetAlert:
      msg*: string
    else: discard

  HighlightType = enum
    hltSearch, hltSelect

  SelectionType = enum
    stNormal = "normal"
    stBlock = "block"
    stLine = "line"

  Highlight = ref object
    case t: HighlightType
    of hltSearch: discard
    of hltSelect:
      selectionType {.jsget.}: SelectionType
    x1, y1: int
    x2, y2: int

  PagePos = tuple
    x: int
    y: int

  BufferFilter* = ref object
    cmd*: string

  LoadState* = enum
    lsLoading, lsCanceled, lsLoaded

  ContainerFlag* = enum
    cfCloned, cfUserRequested, cfHasStart, cfCanReinterpret, cfSave, cfIsHTML

  CachedImage* = ref object
    loaded*: bool
    bmp*: NetworkBitmap

  Container* = ref object
    # note: this is not the same as source.request.url (but should be synced
    # with buffer.url)
    url* {.jsget.}: URL
    #TODO this is inaccurate, because charsetStack can desync
    charset*: Charset
    charsetStack*: seq[Charset]
    # note: this is *not* the same as Buffer.cacheId. buffer has the cache ID of
    # the output, while container holds that of the input. Thus pager can
    # re-interpret the original input, and buffer can rewind the (potentially
    # mailcap) output.
    cacheId* {.jsget.}: int
    parent* {.jsget.}: Container
    children* {.jsget.}: seq[Container]
    config*: BufferConfig
    loaderConfig*: LoaderClientConfig
    iface*: BufferInterface
    width* {.jsget.}: int
    height* {.jsget.}: int
    title*: string # used in status msg
    hoverText: array[HoverType, string]
    lastPeek: HoverType
    request*: Request # source request
    # if set, this *overrides* any content type received from the network. (this
    # is because it stores the content type from the -T flag.)
    contentType* {.jsget.}: Option[string]
    pos: CursorPosition
    bpos: seq[CursorPosition]
    highlights: seq[Highlight]
    process* {.jsget.}: int
    loadinfo*: string
    lines: SimpleFlexibleGrid
    lineshift: int
    numLines*: int
    replace*: Container
    code*: int # note: this is not the status code, but the ConnectErrorCode.
    errorMessage*: string
    retry*: seq[URL]
    hlon*: bool # highlight on?
    sourcepair*: Container # pointer to buffer with a source view (may be nil)
    needslines*: bool
    loadState*: LoadState
    events*: Deque[ContainerEvent]
    startpos: Option[CursorPosition]
    redirectDepth*: int
    select*: Select
    currentSelection {.jsget.}: Highlight
    tmpJumpMark: PagePos
    jumpMark: PagePos
    marks: Table[string, PagePos]
    filter*: BufferFilter
    bgcolor*: CellColor
    tailOnLoad*: bool
    mainConfig*: Config
    flags*: set[ContainerFlag]
    images*: seq[PosBitmap]
    cachedImages*: seq[CachedImage]
    luctx: LUContext
    redraw*: bool

  Select = ref object
    container: Container
    options: seq[string]
    multiple: bool
    oselected: seq[int] # old selection
    selected: seq[int] # new selection
    cursor: int # cursor distance from y
    maxw: int # widest option
    maxh: int # maximum height on screen (yes the naming is dumb)
    si: int # first index to display
    # location on screen
    #TODO make this absolute
    x: int
    y: int
    redraw*: bool
    bpos: seq[int]

jsDestructor(Highlight)
jsDestructor(Container)

# Forward declarations
proc onclick(container: Container; res: ClickResult; save: bool)
proc updateCursor(container: Container)
proc cursorLastLine*(container: Container)
proc triggerEvent(container: Container; t: ContainerEventType)

proc queueDraw(select: Select) =
  select.redraw = true

proc windowChange(select: Select; height: int) =
  select.maxh = height - 2
  if select.y + select.options.len >= select.maxh:
    select.y = height - select.options.len
    if select.y < 0:
      select.si = -select.y
      select.y = 0
  if select.selected.len > 0:
    let i = select.selected[0]
    if select.si > i:
      select.si = i
    elif select.si + select.maxh < i:
      select.si = max(i - select.maxh, 0)
  select.queueDraw()

# index of option currently under cursor
func hover(select: Select): int =
  return select.cursor + select.si

func dispheight(select: Select): int =
  return select.maxh - select.y

proc `hover=`(select: Select; i: int) =
  let i = clamp(i, 0, select.options.high)
  if i >= select.si + select.dispheight:
    select.si = i - select.dispheight + 1
    select.cursor = select.dispheight - 1
  elif i < select.si:
    select.si = i
    select.cursor = 0
  else:
    select.cursor = i - select.si

proc cursorDown(select: Select) =
  if select.hover < select.options.high and
      select.cursor + select.y < select.maxh - 1:
    inc select.cursor
    select.queueDraw()
  elif select.si < select.options.len - select.maxh:
    inc select.si
    select.queueDraw()

proc cursorUp(select: Select) =
  if select.cursor > 0:
    dec select.cursor
    select.queueDraw()
  elif select.si > 0:
    dec select.si
    select.queueDraw()
  elif select.multiple and select.cursor > -1:
    select.cursor = -1

proc close(select: Select) =
  let container = select.container
  container.select = nil

proc cancel(select: Select) =
  let container = select.container
  container.iface.select(select.oselected).then(proc(res: ClickResult) =
    container.onclick(res, save = false))
  select.close()

proc submit(select: Select) =
  let container = select.container
  container.iface.select(select.selected).then(proc(res: ClickResult) =
    container.onclick(res, save = false))
  select.close()

proc click(select: Select) =
  if not select.multiple:
    select.selected = @[select.hover]
    select.submit()
  elif select.cursor == -1:
    select.submit()
  else:
    var k = select.selected.len
    let i = select.hover
    for j in 0 ..< select.selected.len:
      if select.selected[j] >= i:
        k = j
        break
    if k < select.selected.len and select.selected[k] == i:
      select.selected.delete(k)
    else:
      select.selected.insert(i, k)
    select.queueDraw()

proc cursorLeft(select: Select) =
  select.submit()

proc cursorRight(select: Select) =
  select.click()

proc getCursorX*(select: Select): int =
  if select.cursor == -1:
    return select.x
  return select.x + 1

proc getCursorY*(select: Select): int =
  return select.y + 1 + select.cursor

proc cursorFirstLine(select: Select) =
  if select.cursor != 0 or select.si != 0:
    select.cursor = 0
    select.si = 0
    select.queueDraw()

proc cursorLastLine(select: Select) =
  if select.hover < select.options.len:
    select.cursor = select.dispheight - 1
    select.si = max(select.options.len - select.maxh, 0)
    select.queueDraw()

proc cursorNextMatch(select: Select; regex: Regex; wrap: bool) =
  var j = -1
  for i in select.hover + 1 ..< select.options.len:
    if regex.exec(select.options[i]).success:
      j = i
      break
  if j != -1:
    select.hover = j
    select.queueDraw()
  elif wrap:
    for i in 0 ..< select.hover:
      if regex.exec(select.options[i]).success:
        j = i
        break
    if j != -1:
      select.hover = j
      select.queueDraw()

proc cursorPrevMatch(select: Select; regex: Regex; wrap: bool) =
  var j = -1
  for i in countdown(select.hover - 1, 0):
    if regex.exec(select.options[i]).success:
      j = i
      break
  if j != -1:
    select.hover = j
    select.queueDraw()
  elif wrap:
    for i in countdown(select.options.high, select.hover):
      if regex.exec(select.options[i]).success:
        j = i
        break
    if j != -1:
      select.hover = j
      select.queueDraw()

proc pushCursorPos(select: Select) =
  select.bpos.add(select.hover)

proc popCursorPos(select: Select; nojump = false) =
  select.hover = select.bpos.pop()
  if not nojump:
    select.queueDraw()

const HorizontalBar = $Rune(0x2500)
const VerticalBar = $Rune(0x2502)
const CornerTopLeft = $Rune(0x250C)
const CornerTopRight = $Rune(0x2510)
const CornerBottomLeft = $Rune(0x2514)
const CornerBottomRight = $Rune(0x2518)

proc drawBorders(display: var FixedGrid; sx, ex, sy, ey: int;
    upmore, downmore: bool) =
  for y in sy .. ey:
    var x = 0
    while x < sx:
      if display[y * display.width + x].str == "":
        display[y * display.width + x].str = " "
        inc x
      else:
        #x = display[y * display.width + x].str.twidth(x)
        inc x
  # Draw corners.
  let tl = if upmore: VerticalBar else: CornerTopLeft
  let tr = if upmore: VerticalBar else: CornerTopRight
  let bl = if downmore: VerticalBar else: CornerBottomLeft
  let br = if downmore: VerticalBar else: CornerBottomRight
  const fmt = Format()
  display[sy * display.width + sx].str = tl
  display[sy * display.width + ex].str = tr
  display[ey * display.width + sx].str = bl
  display[ey * display.width + ex].str = br
  display[sy * display.width + sx].format = fmt
  display[sy * display.width + ex].format = fmt
  display[ey * display.width + sx].format = fmt
  display[ey * display.width + ex].format = fmt
  # Draw top, bottom borders.
  let ups = if upmore: " " else: HorizontalBar
  let downs = if downmore: " " else: HorizontalBar
  for x in sx + 1 .. ex - 1:
    display[sy * display.width + x].str = ups
    display[ey * display.width + x].str = downs
    display[sy * display.width + x].format = fmt
    display[ey * display.width + x].format = fmt
  if upmore:
    display[sy * display.width + sx + (ex - sx) div 2].str = ":"
  if downmore:
    display[ey * display.width + sx + (ex - sx) div 2].str = ":"
  # Draw left, right borders.
  for y in sy + 1 .. ey - 1:
    display[y * display.width + sx].str = VerticalBar
    display[y * display.width + ex].str = VerticalBar
    display[y * display.width + sx].format = fmt
    display[y * display.width + ex].format = fmt

proc drawSelect*(select: Select; display: var FixedGrid) =
  if display.width < 2 or display.height < 2:
    return # border does not fit...
  # Max width, height with one row/column on the sides.
  let mw = display.width - 2
  let mh = display.height - 2
  var sy = select.y
  let si = select.si
  var ey = min(sy + select.options.len, mh) + 1
  var sx = select.x
  if sx + select.maxw >= mw:
    sx = display.width - select.maxw
    if sx < 0:
      # This means the widest option is wider than the available screen.
      # w3m simply cuts off the part that doesn't fit, and we do that too,
      # but I feel like this may not be the best solution.
      sx = 0
  var ex = min(sx + select.maxw, mw) + 1
  let upmore = select.si > 0
  let downmore = select.si + mh < select.options.len
  drawBorders(display, sx, ex, sy, ey, upmore, downmore)
  if select.multiple and not upmore:
    display[sy * display.width + sx].str = "X"
  # move inside border
  inc sy
  inc sx
  var r: Rune
  var k = 0
  var format = Format()
  while k < select.selected.len and select.selected[k] < si:
    inc k
  for y in sy ..< ey:
    let i = y - sy + si
    var j = 0
    var x = sx
    let dls = y * display.width
    if k < select.selected.len and select.selected[k] == i:
      format.flags.incl(ffReverse)
      inc k
    else:
      format.flags.excl(ffReverse)
    while j < select.options[i].len:
      fastRuneAt(select.options[i], j, r)
      let rw = r.twidth(x)
      let ox = x
      x += rw
      if x > ex:
        break
      display[dls + ox].str = $r
      display[dls + ox].format = format
    while x < ex:
      display[dls + x].str = " "
      display[dls + x].format = format
      inc x

proc newContainer*(config: BufferConfig; loaderConfig: LoaderClientConfig;
    url: URL; request: Request; luctx: LUContext; attrs: WindowAttributes;
    title: string; redirectDepth: int; flags: set[ContainerFlag];
    contentType: Option[string]; charsetStack: seq[Charset]; cacheId: int;
    mainConfig: Config): Container =
  return Container(
    url: url,
    request: request,
    contentType: contentType,
    width: attrs.width,
    height: attrs.height - 1,
    title: title,
    config: config,
    loaderConfig: loaderConfig,
    redirectDepth: redirectDepth,
    pos: CursorPosition(
      setx: -1
    ),
    loadinfo: "Connecting to " & request.url.host & "...",
    cacheId: cacheId,
    process: -1,
    mainConfig: mainConfig,
    flags: flags,
    luctx: luctx,
    redraw: true
  )

func location(container: Container): URL {.jsfget.} =
  return container.url

proc clone*(container: Container; newurl: URL; loader: FileLoader):
    Promise[Container] =
  if container.iface == nil:
    return nil
  let url = if newurl != nil:
    newurl
  else:
    container.url
  let p = container.iface.clone(url)
  # create a server socket, pass it on to the buffer, then move it to
  # the expected path after the buffer forked itself
  #TODO this is very ugly
  let ssock = initServerSocket(loader.sockDir, loader.sockDirFd,
    loader.clientPid)
  SocketStream(container.iface.stream.source)
    .sendFileHandle(FileHandle(ssock.getFd()))
  ssock.close(unlink = false)
  return p.then(proc(pid: int): Container =
    if pid == -1:
      return nil
    let newPath = getSocketPath(loader.sockDir, pid)
    let oldPath = getSocketPath(loader.sockDir, loader.clientPid)
    moveFile(oldPath, newPath)
    let nc = Container()
    nc[] = container[]
    nc.url = url
    nc.process = pid
    nc.flags.incl(cfCloned)
    nc.retry = @[]
    nc.parent = nil
    nc.children = @[]
    return nc
  )

func lineLoaded(container: Container; y: int): bool =
  return y - container.lineshift in 0..container.lines.high

func getLine(container: Container; y: int): SimpleFlexibleLine =
  if container.lineLoaded(y):
    return container.lines[y - container.lineshift]

iterator ilines*(container: Container; slice: Slice[int]): SimpleFlexibleLine
    {.inline.} =
  for y in slice:
    yield container.getLine(y)

func cursorx*(container: Container): int {.jsfget.} =
  container.pos.cursorx

func cursory*(container: Container): int {.jsfget.} =
  container.pos.cursory

func fromx*(container: Container): int {.jsfget.} =
  container.pos.fromx

func fromy*(container: Container): int {.jsfget.} =
  container.pos.fromy

func xend(container: Container): int {.inline.} =
  container.pos.xend

func lastVisibleLine(container: Container): int =
  min(container.fromy + container.height, container.numLines) - 1

func currentLine(container: Container): string =
  return container.getLine(container.cursory).str

func findColBytes(s: string; endx: int; startx = 0; starti = 0): int =
  var w = startx
  var i = starti
  while i < s.len and w < endx:
    var r: Rune
    fastRuneAt(s, i, r)
    w += r.twidth(w)
  return i

func cursorBytes(container: Container; y: int; cc = container.cursorx): int =
  return container.getLine(y).str.findColBytes(cc, 0, 0)

func currentCursorBytes(container: Container; cc = container.cursorx): int =
  return container.cursorBytes(container.cursory, cc)

# Returns the X position of the first cell occupied by the character the cursor
# currently points to.
func cursorFirstX(container: Container): int =
  if container.numLines == 0: return 0
  let line = container.currentLine
  var w = 0
  var i = 0
  var r: Rune
  let cc = container.cursorx
  while i < line.len:
    fastRuneAt(line, i, r)
    let tw = r.twidth(w)
    if w + tw > cc:
      return w
    w += tw
  return w

# Returns the X position of the last cell occupied by the character the cursor
# currently points to.
func cursorLastX(container: Container): int =
  if container.numLines == 0: return 0
  let line = container.currentLine
  var w = 0
  var i = 0
  var r: Rune
  let cc = container.cursorx
  while i < line.len and w <= cc:
    fastRuneAt(line, i, r)
    w += r.twidth(w)
  return max(w - 1, 0)

# Last cell for tab, first cell for everything else (e.g. double width.)
# This is needed because moving the cursor to the 2nd cell of a double
# width character clears it on some terminals.
func cursorDispX(container: Container): int =
  if container.numLines == 0: return 0
  let line = container.currentLine
  if line.len == 0: return 0
  var w = 0
  var pw = 0
  var i = 0
  var r: Rune
  let cc = container.cursorx
  while i < line.len and w <= cc:
    fastRuneAt(line, i, r)
    pw = w
    w += r.twidth(w)
  if r == Rune('\t'):
    return max(w - 1, 0)
  else:
    return pw

func acursorx*(container: Container): int =
  max(0, container.cursorDispX() - container.fromx)

func acursory*(container: Container): int =
  container.cursory - container.fromy

func maxScreenWidth(container: Container): int =
  for line in container.ilines(container.fromy..container.lastVisibleLine):
    result = max(line.str.width(), result)

func getTitle*(container: Container): string {.jsfget: "title".} =
  if container.title != "":
    return container.title
  return container.url.serialize(excludepassword = true)

func currentLineWidth(container: Container): int =
  if container.numLines == 0: return 0
  return container.currentLine.width()

func maxfromy(container: Container): int =
  return max(container.numLines - container.height, 0)

func maxfromx(container: Container): int =
  return max(container.maxScreenWidth() - container.width, 0)

func atPercentOf*(container: Container): int =
  if container.numLines == 0: return 100
  return (100 * (container.cursory + 1)) div container.numLines

func lineWindow(container: Container): Slice[int] =
  if container.numLines == 0: # not loaded
    return 0..container.height * 5
  let n = (container.height * 5) div 2
  var x = container.fromy - n + container.height div 2
  var y = container.fromy + n + container.height div 2
  if y >= container.numLines:
    x -= y - container.numLines
    y = container.numLines
  if x < 0:
    y += -x
    x = 0
  return x .. y

func startx(hl: Highlight): int =
  if hl.y1 < hl.y2:
    hl.x1
  elif hl.y2 < hl.y1:
    hl.x2
  else:
    min(hl.x1, hl.x2)

func starty(hl: Highlight): int =
  return min(hl.y1, hl.y2)

func endx(hl: Highlight): int =
  if hl.y1 > hl.y2:
    hl.x1
  elif hl.y2 > hl.y1:
    hl.x2
  else:
    max(hl.x1, hl.x2)

func endy(hl: Highlight): int =
  return max(hl.y1, hl.y2)

func colorNormal(container: Container; hl: Highlight; y: int;
    limitx: Slice[int]): Slice[int] =
  let starty = hl.starty
  let endy = hl.endy
  if y in starty + 1 .. endy - 1:
    let w = container.getLine(y).str.width()
    return min(limitx.a, w) .. min(limitx.b, w)
  if y == starty and y == endy:
    return max(hl.startx, limitx.a) .. min(hl.endx, limitx.b)
  if y == starty:
    let w = container.getLine(y).str.width()
    return max(hl.startx, limitx.a) .. min(limitx.b, w)
  if y == endy:
    let w = container.getLine(y).str.width()
    return min(limitx.a, w) .. min(hl.endx, limitx.b)

func colorArea(container: Container; hl: Highlight; y: int;
    limitx: Slice[int]): Slice[int] =
  case hl.t
  of hltSelect:
    case hl.selectionType
    of stNormal:
      return container.colorNormal(hl, y, limitx)
    of stBlock:
      if y in hl.starty .. hl.endy:
        let (x, endx) = if hl.x1 < hl.x2:
          (hl.x1, hl.x2)
        else:
          (hl.x2, hl.x1)
        return max(x, limitx.a) .. min(endx, limitx.b)
    of stLine:
      if y in hl.starty .. hl.endy:
        let w = container.getLine(y).str.width()
        return min(limitx.a, w) .. min(limitx.b, w)
  else:
    return container.colorNormal(hl, y, limitx)

func findHighlights*(container: Container; y: int): seq[Highlight] =
  for hl in container.highlights:
    if y in hl.starty .. hl.endy:
      result.add(hl)

func getHoverText*(container: Container): string =
  for t in HoverType:
    if container.hoverText[t] != "":
      return container.hoverText[t]
  ""

func isHoverURL*(container: Container; url: URL): bool =
  let hoverurl = parseURL(container.hoverText[htLink])
  return hoverurl.isSome and url.host == hoverurl.get.host

proc triggerEvent(container: Container; event: ContainerEvent) =
  container.events.addLast(event)

proc triggerEvent(container: Container; t: ContainerEventType) =
  container.triggerEvent(ContainerEvent(t: t))

proc setNumLines(container: Container; lines: int; finish = false) =
  if container.numLines != lines:
    container.numLines = lines
    if container.startpos.isSome and finish:
      container.pos = container.startpos.get
      container.startpos = none(CursorPosition)
    container.updateCursor()

proc queueDraw*(container: Container) =
  container.redraw = true

proc requestLines(container: Container): EmptyPromise {.discardable.} =
  if container.iface == nil:
    return newResolvedPromise()
  let w = container.lineWindow
  return container.iface.getLines(w).then(proc(res: GetLinesResult) =
    container.lines.setLen(w.len)
    container.lineshift = w.a
    for y in 0 ..< min(res.lines.len, w.len):
      container.lines[y] = res.lines[y]
    var isBgNew = container.bgcolor != res.bgcolor
    if isBgNew:
      container.bgcolor = res.bgcolor
    if res.numLines != container.numLines:
      container.setNumLines(res.numLines, true)
      if container.loadState != lsLoading:
        container.triggerEvent(cetStatus)
    if res.numLines > 0:
      container.updateCursor()
      if container.tailOnLoad:
        container.tailOnLoad = false
        container.cursorLastLine()
    let cw = container.fromy ..< container.fromy + container.height
    if w.a in cw or w.b in cw or cw.a in w or cw.b in w or isBgNew:
      container.queueDraw()
    container.images = res.images
  )

proc sendCursorPosition*(container: Container) =
  if container.iface == nil:
    return
  container.iface.updateHover(container.cursorx, container.cursory)
      .then(proc(res: UpdateHoverResult) =
    if res.hover.len > 0:
      assert res.hover.high <= int(HoverType.high)
      for (ht, s) in res.hover:
        container.hoverText[ht] = s
      container.triggerEvent(cetStatus)
    if res.repaint:
      container.needslines = true
  )

proc setFromY(container: Container; y: int) {.jsfunc.} =
  if container.pos.fromy != y:
    container.pos.fromy = max(min(y, container.maxfromy), 0)
    container.needslines = true
    container.queueDraw()

proc setFromX(container: Container; x: int; refresh = true) {.jsfunc.} =
  if container.pos.fromx != x:
    container.pos.fromx = max(min(x, container.maxfromx), 0)
    if container.pos.fromx > container.cursorx:
      container.pos.cursorx = min(container.pos.fromx,
        container.currentLineWidth())
      if refresh:
        container.sendCursorPosition()
    container.queueDraw()

proc setFromXY(container: Container; x, y: int) {.jsfunc.} =
  container.setFromY(y)
  container.setFromX(x)

# Set the cursor to the xth column. 0-based.
# * `refresh = false' inhibits reporting of the cursor position to the buffer.
# * `save = false' inhibits cursor movement if it is currently outside the
#   screen, and makes it so cursorx is not saved for restoration on cursory
#   movement.
proc setCursorX(container: Container; x: int; refresh = true; save = true)
    {.jsfunc.} =
  if not container.lineLoaded(container.cursory):
    container.pos.setx = x
    container.pos.setxrefresh = refresh
    container.pos.setxsave = save
    return
  container.pos.setx = -1
  let cw = container.currentLineWidth()
  let x2 = x
  let x = max(min(x, cw - 1), 0)
  # we check for save here, because it is only set by restoreCursorX where
  # we do not want to move the cursor just because it is outside the window.
  if not save or container.fromx <= x and x < container.fromx + container.width:
    container.pos.cursorx = x
  elif save and container.fromx > x:
    # target x is before the screen start
    if x2 < container.cursorx:
      # desired X position is lower than cursor X; move screen back to the
      # desired position if valid, to 0 if the desired position is less than 0,
      # otherwise the last cell of the current line.
      if x2 <= x:
        container.setFromX(x, false)
      else:
        container.setFromX(cw - 1, false)
    # take whatever position the jump has resulted in.
    container.pos.cursorx = container.fromx
  elif x > container.cursorx:
    # target x is greater than current x; a simple case, just shift fromx too
    # accordingly
    container.setFromX(max(x - container.width + 1, container.fromx), false)
    container.pos.cursorx = x
  if container.cursorx == x and container.currentSelection != nil and
      container.currentSelection.x2 != x:
    container.currentSelection.x2 = x
    container.queueDraw()
  if refresh:
    container.sendCursorPosition()
  if save:
    container.pos.xend = container.cursorx

proc restoreCursorX(container: Container) {.jsfunc.} =
  let x = clamp(container.currentLineWidth() - 1, 0, container.xend)
  container.setCursorX(x, false, false)

proc setCursorY(container: Container; y: int; refresh = true) {.jsfunc.} =
  let y = max(min(y, container.numLines - 1), 0)
  if container.cursory == y: return
  if y - container.fromy >= 0 and y - container.height < container.fromy:
    container.pos.cursory = y
  else:
    if y > container.cursory:
      container.setFromY(y - container.height + 1)
    else:
      container.setFromY(y)
    container.pos.cursory = y
  if container.currentSelection != nil and container.currentSelection.y2 != y:
    container.queueDraw()
    container.currentSelection.y2 = y
  container.restoreCursorX()
  if refresh:
    container.sendCursorPosition()

proc setCursorXY*(container: Container; x, y: int; refresh = true) {.jsfunc.} =
  container.setCursorY(y, refresh)
  container.setCursorX(x, refresh)

proc cursorLineTextStart(container: Container) {.jsfunc.} =
  if container.numLines == 0: return
  var x = 0
  for r in container.currentLine.runes:
    if not container.luctx.isWhiteSpaceLU(r):
      break
    x += r.twidth(x)
  if x == 0:
    dec x
  container.setCursorX(x)

# zb
proc lowerPage(container: Container; n = 0) {.jsfunc.} =
  if n != 0:
    container.setCursorY(n - 1)
  container.setFromY(container.cursory - container.height + 1)

# z-
proc lowerPageBegin(container: Container; n = 0) {.jsfunc.} =
  container.lowerPage(n)
  container.cursorLineTextStart()

# zz
proc centerLine(container: Container; n = 0) {.jsfunc.} =
  if n != 0:
    container.setCursorY(n - 1)
  container.setFromY(container.cursory - container.height div 2)

# z.
proc centerLineBegin(container: Container; n = 0) {.jsfunc.} =
  container.centerLine(n)
  container.cursorLineTextStart()

# zt
proc raisePage(container: Container; n = 0) {.jsfunc.} =
  if n != 0:
    container.setCursorY(n - 1)
  container.setFromY(container.cursory)

# z^M
proc raisePageBegin(container: Container; n = 0) {.jsfunc.} =
  container.raisePage(n)
  container.cursorLineTextStart()

# z+
proc nextPageBegin(container: Container; n = 0) {.jsfunc.} =
  if n == 0:
    container.setCursorY(container.fromy + container.height)
  else:
    container.setCursorY(n - 1)
  container.cursorLineTextStart()
  container.raisePage()

# z^
proc previousPageBegin(container: Container; n = 0) {.jsfunc.} =
  if n == 0:
    container.setCursorY(container.fromy - 1)
  else:
    container.setCursorY(n - container.height) # +- 1 cancels out
  container.cursorLineTextStart()
  container.lowerPage()

proc centerColumn(container: Container) {.jsfunc.} =
  container.setFromX(container.cursorx - container.width div 2)

proc setCursorYCenter(container: Container; y: int; refresh = true)
    {.jsfunc.} =
  let fy = container.fromy
  container.setCursorY(y, refresh)
  if fy != container.fromy:
    container.centerLine()

proc setCursorXYCenter(container: Container; x, y: int; refresh = true)
    {.jsfunc.} =
  let fy = container.fromy
  let fx = container.fromx
  container.setCursorXY(x, y, refresh)
  if fy != container.fromy:
    container.centerLine()
  if fx != container.fromx:
    container.centerColumn()

proc cursorDown(container: Container; n = 1) {.jsfunc.} =
  if container.select != nil:
    container.select.cursorDown()
  else:
    container.setCursorY(container.cursory + n)

proc cursorUp(container: Container; n = 1) {.jsfunc.} =
  if container.select != nil:
    container.select.cursorUp()
  else:
    container.setCursorY(container.cursory - n)

proc cursorLeft(container: Container; n = 1) {.jsfunc.} =
  if container.select != nil:
    container.select.cursorLeft()
  else:
    container.setCursorX(container.cursorFirstX() - n)

proc cursorRight(container: Container; n = 1) {.jsfunc.} =
  if container.select != nil:
    container.select.cursorRight()
  else:
    container.setCursorX(container.cursorLastX() + n)

proc cursorLineBegin(container: Container) {.jsfunc.} =
  container.setCursorX(-1)

proc cursorLineEnd(container: Container) {.jsfunc.} =
  container.setCursorX(container.currentLineWidth() - 1)

type BreakFunc = proc(ctx: LUContext; r: Rune): BreakCategory {.nimcall.}

proc skipSpace(container: Container; b, x: var int; breakFunc: BreakFunc) =
  while b < container.currentLine.len:
    var r: Rune
    let pb = b
    fastRuneAt(container.currentLine, b, r)
    if container.luctx.breakFunc(r) != bcSpace:
      b = pb
      break
    x += r.twidth(x)

proc skipSpaceRev(container: Container; b, x: var int; breakFunc: BreakFunc) =
  while b >= 0:
    let (r, o) = lastRune(container.currentLine, b)
    if container.luctx.breakFunc(r) != bcSpace:
      break
    b -= o
    x -= r.twidth(x)

proc cursorNextWord(container: Container; breakFunc: BreakFunc) =
  if container.numLines == 0: return
  var r: Rune
  var b = container.currentCursorBytes()
  var x = container.cursorx
  # meow
  let currentCat = if b < container.currentLine.len:
    container.luctx.breakFunc(container.currentLine.runeAt(b))
  else:
    bcSpace
  if currentCat != bcSpace:
    # not in space, skip chars that have the same category
    while b < container.currentLine.len:
      let pb = b
      fastRuneAt(container.currentLine, b, r)
      if container.luctx.breakFunc(r) != currentCat:
        b = pb
        break
      x += r.twidth(x)
  container.skipSpace(b, x, breakFunc)
  if b < container.currentLine.len:
    container.setCursorX(x)
  else:
    if container.cursory < container.numLines - 1:
      container.cursorDown()
      container.cursorLineBegin()
    else:
      container.cursorLineEnd()

proc cursorNextWord(container: Container) {.jsfunc.} =
  container.cursorNextWord(breaksWordCat)

proc cursorNextViWord(container: Container) {.jsfunc.} =
  container.cursorNextWord(breaksViWordCat)

proc cursorNextBigWord(container: Container) {.jsfunc.} =
  container.cursorNextWord(breaksBigWordCat)

proc cursorPrevWord(container: Container; breakFunc: BreakFunc) =
  if container.numLines == 0: return
  var b = container.currentCursorBytes()
  var x = container.cursorx
  if container.currentLine.len > 0:
    b = min(b, container.currentLine.len - 1)
    let currentCat = if b >= 0:
      container.luctx.breakFunc(container.currentLine.runeAt(b))
    else:
      bcSpace
    if currentCat != bcSpace:
      # not in space, skip chars that have the same category
      while b >= 0:
        let (r, o) = lastRune(container.currentLine, b)
        if container.luctx.breakFunc(r) != currentCat:
          break
        b -= o
        x -= r.twidth(x)
    container.skipSpaceRev(b, x, breakFunc)
  else:
    b = -1
  if b >= 0:
    container.setCursorX(x)
  else:
    if container.cursory > 0:
      container.cursorUp()
      container.cursorLineEnd()
    else:
      container.cursorLineBegin()

proc cursorPrevWord(container: Container) {.jsfunc.} =
  container.cursorPrevWord(breaksWordCat)

proc cursorPrevViWord(container: Container) {.jsfunc.} =
  container.cursorPrevWord(breaksViWordCat)

proc cursorPrevBigWord(container: Container) {.jsfunc.} =
  container.cursorPrevWord(breaksBigWordCat)

proc cursorWordEnd(container: Container; breakFunc: BreakFunc) =
  if container.numLines == 0: return
  var r: Rune
  var b = container.currentCursorBytes()
  var x = container.cursorx
  var px = x
  # if not in space, move to the right by one
  if b < container.currentLine.len:
    let pb = b
    fastRuneAt(container.currentLine, b, r)
    if container.luctx.breakFunc(r) == bcSpace:
      b = pb
    else:
      px = x
      x += r.twidth(x)
  container.skipSpace(b, x, breakFunc)
  # move to the last char in the current category
  let ob = b
  if b < container.currentLine.len:
    let currentCat = container.luctx.breakFunc(container.currentLine.runeAt(b))
    while b < container.currentLine.len:
      let pb = b
      fastRuneAt(container.currentLine, b, r)
      if container.luctx.breakFunc(r) != currentCat:
        b = pb
        break
      px = x
      x += r.twidth(x)
    x = px
  if b < container.currentLine.len or ob != b:
    container.setCursorX(x)
  else:
    if container.cursory < container.numLines - 1:
      container.cursorDown()
      container.cursorLineBegin()
    else:
      container.cursorLineEnd()

proc cursorWordEnd(container: Container) {.jsfunc.} =
  container.cursorWordEnd(breaksWordCat)

proc cursorViWordEnd(container: Container) {.jsfunc.} =
  container.cursorWordEnd(breaksViWordCat)

proc cursorBigWordEnd(container: Container) {.jsfunc.} =
  container.cursorWordEnd(breaksBigWordCat)

proc cursorWordBegin(container: Container; breakFunc: BreakFunc) =
  if container.numLines == 0: return
  var b = container.currentCursorBytes()
  var x = container.cursorx
  var px = x
  var ob = b
  if container.currentLine.len > 0:
    b = min(b, container.currentLine.len - 1)
    if b >= 0:
      let (r, o) = lastRune(container.currentLine, b)
      # if not in space, move to the left by one
      if container.luctx.breakFunc(r) != bcSpace:
        b -= o
        px = x
        x -= r.twidth(x)
    container.skipSpaceRev(b, x, breakFunc)
    # move to the first char in the current category
    ob = b
    if b >= 0:
      let (r, _) = lastRune(container.currentLine, b)
      let currentCat = container.luctx.breakFunc(r)
      while b >= 0:
        let (r, o) = lastRune(container.currentLine, b)
        if container.luctx.breakFunc(r) != currentCat:
          break
        b -= o
        px = x
        x -= r.twidth(x)
    x = px
  else:
    b = -1
    ob = -1
  if b >= 0 or ob != b:
    container.setCursorX(x)
  else:
    if container.cursory > 0:
      container.cursorUp()
      container.cursorLineEnd()
    else:
      container.cursorLineBegin()

proc cursorWordBegin(container: Container) {.jsfunc.} =
  container.cursorWordBegin(breaksWordCat)

proc cursorViWordBegin(container: Container) {.jsfunc.} =
  container.cursorWordBegin(breaksViWordCat)

proc cursorBigWordBegin(container: Container) {.jsfunc.} =
  container.cursorWordBegin(breaksBigWordCat)

proc pageDown(container: Container; n = 1) {.jsfunc.} =
  container.setFromY(container.fromy + container.height * n)
  container.setCursorY(container.cursory + container.height * n)
  container.restoreCursorX()

proc pageUp(container: Container; n = 1) {.jsfunc.} =
  container.setFromY(container.fromy - container.height * n)
  container.setCursorY(container.cursory - container.height * n)
  container.restoreCursorX()

proc pageLeft(container: Container; n = 1) {.jsfunc.} =
  container.setFromX(container.fromx - container.width * n)

proc pageRight(container: Container; n = 1) {.jsfunc.} =
  container.setFromX(container.fromx + container.width * n)

# I am not cloning the vi behavior here because it is counter-intuitive
# and annoying.
# Users who disagree are free to implement it themselves. (It is about
# 5 lines of JS.)
proc halfPageUp(container: Container; n = 1) {.jsfunc.} =
  container.setFromY(container.fromy - (container.height + 1) div 2 * n)
  container.setCursorY(container.cursory - (container.height + 1) div 2 * n)
  container.restoreCursorX()

proc halfPageDown(container: Container; n = 1) {.jsfunc.} =
  container.setFromY(container.fromy + (container.height + 1) div 2 * n)
  container.setCursorY(container.cursory + (container.height + 1) div 2 * n)
  container.restoreCursorX()

proc halfPageLeft(container: Container; n = 1) {.jsfunc.} =
  container.setFromX(container.fromx - (container.width + 1) div 2 * n)

proc halfPageRight(container: Container; n = 1) {.jsfunc.} =
  container.setFromX(container.fromx + (container.width + 1) div 2 * n)

proc markPos0*(container: Container) =
  container.tmpJumpMark = (container.cursorx, container.cursory)

proc markPos*(container: Container) =
  let pos = container.tmpJumpMark
  if container.cursorx != pos.x or container.cursory != pos.y:
    container.jumpMark = pos

proc cursorFirstLine(container: Container) {.jsfunc.} =
  if container.select != nil:
    container.select.cursorFirstLine()
  else:
    container.markPos0()
    container.setCursorY(0)
    container.markPos()

proc cursorLastLine*(container: Container) {.jsfunc.} =
  if container.select != nil:
    container.select.cursorLastLine()
  else:
    container.markPos0()
    container.setCursorY(container.numLines - 1)
    container.markPos()

proc cursorTop(container: Container; i = 1) {.jsfunc.} =
  container.markPos0()
  let i = clamp(i - 1, 0, container.height - 1)
  container.setCursorY(container.fromy + i)
  container.markPos()

proc cursorMiddle(container: Container) {.jsfunc.} =
  container.markPos0()
  container.setCursorY(container.fromy + (container.height - 2) div 2)
  container.markPos()

proc cursorBottom(container: Container; i = 1) {.jsfunc.} =
  container.markPos0()
  let i = clamp(i, 0, container.height)
  container.setCursorY(container.fromy + container.height - i)
  container.markPos()

proc cursorLeftEdge(container: Container) {.jsfunc.} =
  container.setCursorX(container.fromx)

proc cursorMiddleColumn(container: Container) {.jsfunc.} =
  container.setCursorX(container.fromx + (container.width - 2) div 2)

proc cursorRightEdge(container: Container) {.jsfunc.} =
  container.setCursorX(container.fromx + container.width - 1)

proc scrollDown*(container: Container; n = 1) {.jsfunc.} =
  let H = container.numLines
  let y = min(container.fromy + container.height + n, H) - container.height
  if y > container.fromy:
    container.setFromY(y)
    if container.fromy > container.cursory:
      container.cursorDown(container.fromy - container.cursory)
  else:
    container.cursorDown(n)

proc scrollUp*(container: Container; n = 1) {.jsfunc.} =
  let y = max(container.fromy - n, 0)
  if y < container.fromy:
    container.setFromY(y)
    if container.fromy + container.height <= container.cursory:
      container.cursorUp(container.cursory - container.fromy -
        container.height + 1)
  else:
    container.cursorUp(n)

proc scrollRight*(container: Container; n = 1) {.jsfunc.} =
  let msw = container.maxScreenWidth()
  let x = min(container.fromx + container.width + n, msw) - container.width
  if x > container.fromx:
    container.setFromX(x)

proc scrollLeft*(container: Container; n = 1) {.jsfunc.} =
  let x = max(container.fromx - n, 0)
  if x < container.fromx:
    container.setFromX(x)

proc alert(container: Container; msg: string) =
  container.triggerEvent(ContainerEvent(t: cetAlert, msg: msg))

proc lineInfo(container: Container) {.jsfunc.} =
  container.alert("line " & $(container.cursory + 1) & "/" &
    $container.numLines & " (" & $container.atPercentOf() & "%) col " &
    $(container.cursorx + 1) & "/" & $container.currentLineWidth &
    " (byte " & $container.currentCursorBytes & ")")

proc updateCursor(container: Container) =
  if container.pos.setx > -1:
    container.setCursorX(container.pos.setx, container.pos.setxrefresh,
      container.pos.setxsave)
  if container.fromy > container.maxfromy:
    container.setFromY(container.maxfromy)
  if container.cursory >= container.numLines:
    container.setCursorY(container.lastVisibleLine)
    container.alert("Last line is #" & $container.numLines)

proc gotoLine*[T: string|int](container: Container; s: T) =
  when s is string:
    if s == "":
      container.redraw = true
    elif s[0] == '^':
      container.cursorFirstLine()
    elif s[0] == '$':
      container.cursorLastLine()
    else:
      let i = parseUInt32(s, allowSign = true)
      if i.isSome and i.get > 0:
        container.markPos0()
        container.setCursorY(int(i.get - 1))
        container.markPos()
      else:
        container.alert("First line is #1") # :)
  else:
    container.markPos0()
    container.setCursorY(s - 1)
    container.markPos()

proc pushCursorPos*(container: Container) =
  if container.select != nil:
    container.select.pushCursorPos()
  else:
    container.bpos.add(container.pos)

proc popCursorPos*(container: Container; nojump = false) =
  if container.select != nil:
    container.select.popCursorPos(nojump)
  else:
    container.pos = container.bpos.pop()
    if not nojump:
      container.updateCursor()
      container.sendCursorPosition()
      container.needslines = true

proc copyCursorPos*(container, c2: Container) =
  container.startpos = some(c2.pos)
  container.flags.incl(cfHasStart)

proc cursorNextLink*(container: Container; n = 1) {.jsfunc.} =
  if container.iface == nil:
    return
  container.markPos0()
  container.iface
    .findNextLink(container.cursorx, container.cursory, n)
    .then(proc(res: tuple[x, y: int]) =
      if res.x > -1 and res.y != -1:
        container.setCursorXYCenter(res.x, res.y)
        container.markPos()
    )

proc cursorPrevLink*(container: Container; n = 1) {.jsfunc.} =
  if container.iface == nil:
    return
  container.markPos0()
  container.iface
    .findPrevLink(container.cursorx, container.cursory, n)
    .then(proc(res: tuple[x, y: int]) =
      if res.x > -1 and res.y != -1:
        container.setCursorXYCenter(res.x, res.y)
        container.markPos()
    )

proc cursorNextParagraph*(container: Container; n = 1) {.jsfunc.} =
  if container.iface == nil:
    return
  container.markPos0()
  container.iface
    .findNextParagraph(container.cursory, n)
    .then(proc(res: int) =
      container.setCursorY(res)
      container.markPos()
    )

proc cursorPrevParagraph*(container: Container; n = 1) {.jsfunc.} =
  if container.iface == nil:
    return
  container.markPos0()
  container.iface
    .findPrevParagraph(container.cursory, n)
    .then(proc(res: int) =
      container.setCursorY(res)
      container.markPos()
    )

proc setMark*(container: Container; id: string; x = none(int);
    y = none(int)): bool {.jsfunc.} =
  let x = x.get(container.cursorx)
  let y = y.get(container.cursory)
  container.marks.withValue(id, p):
    p[] = (x, y)
    container.queueDraw()
    return false
  do:
    container.marks[id] = (x, y)
    container.queueDraw()
    return true

proc clearMark*(container: Container; id: string): bool {.jsfunc.} =
  result = id in container.marks
  container.marks.del(id)
  container.queueDraw()

proc getMarkPos(container: Container; id: string): Opt[PagePos] {.jsfunc.} =
  if id == "`" or id == "'":
    return ok(container.jumpMark)
  container.marks.withValue(id, p):
    return ok(p[])
  return err()

proc gotoMark*(container: Container; id: string): bool {.jsfunc.} =
  container.markPos0()
  let mark = container.getMarkPos(id)
  if mark.isSome:
    let mark = mark.get
    container.setCursorXYCenter(mark.x, mark.y)
    container.markPos()
    return true
  return false

proc gotoMarkY*(container: Container; id: string): bool {.jsfunc.} =
  container.markPos0()
  let mark = container.getMarkPos(id)
  if mark.isSome:
    let mark = mark.get
    container.setCursorXYCenter(0, mark.y)
    container.markPos()
    return true
  return false

proc findNextMark*(container: Container; x = none(int), y = none(int)):
    Option[string] {.jsfunc.} =
  #TODO optimize (maybe store marks in an OrderedTable and sort on insert?)
  let x = x.get(container.cursorx)
  let y = y.get(container.cursory)
  var best: PagePos = (high(int), high(int))
  var bestid = none(string)
  for id, mark in container.marks:
    if mark.y < y or mark.y == y and mark.x <= x:
      continue
    if mark.y < best.y or mark.y == best.y and mark.x < best.x:
      best = mark
      bestid = some(id)
  return bestid

proc findPrevMark*(container: Container; x = none(int), y = none(int)):
    Option[string] {.jsfunc.} =
  #TODO optimize (maybe store marks in an OrderedTable and sort on insert?)
  let x = x.get(container.cursorx)
  let y = y.get(container.cursory)
  var best: PagePos = (-1, -1)
  var bestid = none(string)
  for id, mark in container.marks:
    if mark.y > y or mark.y == y and mark.x >= x:
      continue
    if mark.y > best.y or mark.y == best.y and mark.x > best.x:
      best = mark
      bestid = some(id)
  return bestid

proc cursorNthLink*(container: Container; n = 1) {.jsfunc.} =
  if container.iface == nil:
    return
  container.iface
    .findNthLink(n)
    .then(proc(res: tuple[x, y: int]) =
      if res.x > -1 and res.y != -1:
        container.setCursorXYCenter(res.x, res.y))

proc cursorRevNthLink*(container: Container; n = 1) {.jsfunc.} =
  if container.iface == nil:
    return
  container.iface
    .findRevNthLink(n)
    .then(proc(res: tuple[x, y: int]) =
      if res.x > -1 and res.y != -1:
        container.setCursorXYCenter(res.x, res.y))

proc clearSearchHighlights*(container: Container) =
  for i in countdown(container.highlights.high, 0):
    if container.highlights[i].t == hltSearch:
      container.highlights.del(i)

proc onMatch(container: Container; res: BufferMatch; refresh: bool) =
  if res.success:
    container.setCursorXYCenter(res.x, res.y, refresh)
    if container.hlon:
      container.clearSearchHighlights()
      let ex = res.x + res.str.twidth(res.x) - 1
      let hl = Highlight(
        t: hltSearch,
        x1: res.x,
        y1: res.y,
        x2: ex,
        y2: res.y
      )
      container.highlights.add(hl)
      container.queueDraw()
      container.hlon = false
      container.needslines = true
  elif container.hlon:
    container.clearSearchHighlights()
    container.queueDraw()
    container.needslines = true
    container.hlon = false

proc cursorNextMatch*(container: Container; regex: Regex; wrap, refresh: bool;
    n: int): EmptyPromise {.discardable.} =
  if container.select != nil:
    #TODO
    for _ in 0 ..< n:
      container.select.cursorNextMatch(regex, wrap)
    return newResolvedPromise()
  else:
    if container.iface == nil:
      return
    return container.iface
      .findNextMatch(regex, container.cursorx, container.cursory, wrap, n)
      .then(proc(res: BufferMatch) =
        container.onMatch(res, refresh))

proc cursorPrevMatch*(container: Container; regex: Regex; wrap, refresh: bool;
    n: int): EmptyPromise {.discardable.} =
  if container.select != nil:
    #TODO
    for _ in 0 ..< n:
      container.select.cursorPrevMatch(regex, wrap)
    return newResolvedPromise()
  else:
    if container.iface == nil:
      return
    container.markPos0()
    return container.iface
      .findPrevMatch(regex, container.cursorx, container.cursory, wrap, n)
      .then(proc(res: BufferMatch) =
        container.onMatch(res, refresh)
        container.markPos()
      )

type
  SelectionOptions = object of JSDict
    selectionType: SelectionType

proc cursorToggleSelection(container: Container; n = 1;
    opts = SelectionOptions()): Highlight {.jsfunc.} =
  if container.currentSelection != nil:
    let i = container.highlights.find(container.currentSelection)
    if i != -1:
      container.highlights.delete(i)
    container.currentSelection = nil
  else:
    let cx = container.cursorFirstX()
    let n = n - 1
    container.cursorRight(n)
    let hl = Highlight(
      t: hltSelect,
      selectionType: opts.selectionType,
      x1: cx,
      y1: container.cursory,
      x2: container.cursorx,
      y2: container.cursory
    )
    container.highlights.add(hl)
    container.currentSelection = hl
  container.queueDraw()
  return container.currentSelection

#TODO I don't like this API
# maybe make selection a subclass of highlight?
proc getSelectionText(container: Container; hl: Highlight = nil):
    Promise[string] {.jsfunc.} =
  if container.iface == nil:
    return
  let hl = if hl == nil: container.currentSelection else: hl
  if hl.t != hltSelect:
    let p = newPromise[string]()
    p.resolve("")
    return p
  let startx = hl.startx
  let starty = hl.starty
  let endx = hl.endx
  let endy = hl.endy
  let nw = starty .. endy
  return container.iface.getLines(nw).then(proc(res: GetLinesResult): string =
    var s = ""
    case hl.selectionType
    of stNormal:
      if starty == endy:
        let si = res.lines[0].str.findColBytes(startx)
        let ei = res.lines[0].str.findColBytes(endx + 1, startx, si) - 1
        s = res.lines[0].str.substr(si, ei)
      else:
        let si = res.lines[0].str.findColBytes(startx)
        s &= res.lines[0].str.substr(si) & '\n'
        for i in 1 .. res.lines.high - 1:
          s &= res.lines[i].str & '\n'
        let ei = res.lines[^1].str.findColBytes(endx + 1) - 1
        s &= res.lines[^1].str.substr(0, ei)
    of stBlock:
      for i, line in res.lines:
        let si = line.str.findColBytes(startx)
        let ei = line.str.findColBytes(endx + 1, startx, si) - 1
        if i > 0:
          s &= '\n'
        s &= line.str.substr(si, ei)
    of stLine:
      for i, line in res.lines:
        if i > 0:
          s &= '\n'
        s &= line.str
    return s
  )

proc markURL(container: Container) {.jsfunc.} =
  if container.iface == nil:
    return
  var schemes: seq[string] = @[]
  for key in container.mainConfig.external.urimethodmap.map.keys:
    schemes.add(key.until(':'))
  container.iface.markURL(schemes).then(proc() =
    container.needslines = true
  )

proc toggleImages(container: Container) {.jsfunc.} =
  if container.iface == nil:
    return
  container.iface.toggleImages().then(proc() =
    container.needslines = true
  )

proc setLoadInfo(container: Container; msg: string) =
  container.loadinfo = msg
  container.triggerEvent(cetSetLoadInfo)

proc onReadLine(container: Container; rl: ReadLineResult) =
  case rl.t
  of rltText:
    container.triggerEvent(ContainerEvent(
      t: cetReadLine,
      prompt: rl.prompt,
      value: rl.value,
      password: rl.hide
    ))
  of rltArea:
    container.triggerEvent(ContainerEvent(
      t: cetReadArea,
      tvalue: rl.value
    ))
  of rltFile:
    container.triggerEvent(ContainerEvent(t: cetReadFile))

#TODO this should be called with a timeout.
proc onload(container: Container; res: int) =
  if container.loadState == lsCanceled:
    return
  if res == -1:
    container.loadState = lsLoaded
    container.setLoadInfo("")
    container.triggerEvent(cetStatus)
    container.triggerEvent(cetLoaded)
    if cfHasStart notin container.flags and (container.url.anchor != "" or
        container.config.autofocus):
      container.requestLines().then(proc(): Promise[GotoAnchorResult] =
        return container.iface.gotoAnchor()
      ).then(proc(res: GotoAnchorResult) =
        if res.found:
          container.setCursorXYCenter(res.x, res.y)
          if res.focus != nil:
            container.onReadLine(res.focus)
      )
    else:
      container.needslines = true
  else:
    container.needslines = true
    container.setLoadInfo(convertSize(res) & " loaded")
    discard container.iface.load().then(proc(res: int) =
      container.onload(res)
    )

proc extractCookies(response: Response): seq[Cookie] =
  result = @[]
  if "Set-Cookie" in response.headers.table:
    for s in response.headers.table["Set-Cookie"]:
      let cookie = newCookie(s, response.url)
      if cookie.isSome:
        result.add(cookie.get)

proc extractReferrerPolicy(response: Response): Option[ReferrerPolicy] =
  if "Referrer-Policy" in response.headers:
    return strictParseEnum[ReferrerPolicy](response.headers["Referrer-Policy"])
  return none(ReferrerPolicy)

# Apply data received in response.
# Note: pager must call this before checkMailcap.
proc applyResponse*(container: Container; response: Response;
    mimeTypes: MimeTypes) =
  container.code = response.res
  # accept cookies
  let cookieJar = container.loaderConfig.cookieJar
  if cookieJar != nil:
    cookieJar.add(response.extractCookies())
  # set referrer policy, if any
  let referrerPolicy = response.extractReferrerPolicy()
  if container.config.referer_from:
    if referrerPolicy.isSome:
      container.loaderConfig.referrerPolicy = referrerPolicy.get
  else:
    container.loaderConfig.referrerPolicy = rpNoReferrer
  # setup content type; note that isSome means an override so we skip it
  if container.contentType.isNone:
    var contentType = response.getContentType()
    if contentType == "application/octet-stream":
      contentType = mimeTypes.guessContentType(container.url.pathname,
        "text/plain")
    container.contentType = some(contentType)
  # setup charsets:
  # * override charset
  # * network charset
  # * default charset guesses
  # HTML may override the last two (but not the override charset).
  if container.config.charsetOverride != CHARSET_UNKNOWN:
    container.charsetStack = @[container.config.charsetOverride]
  elif (let charset = response.getCharset(CHARSET_UNKNOWN);
      charset != CHARSET_UNKNOWN):
    container.charsetStack = @[charset]
  else:
    container.charsetStack = @[]
    for i in countdown(container.config.charsets.high, 0):
      container.charsetStack.add(container.config.charsets[i])
    if container.charsetStack.len == 0:
      container.charsetStack.add(DefaultCharset)
  container.charset = container.charsetStack[^1]

proc remoteCancel*(container: Container) =
  container.iface.cancel().then(proc() =
    container.needslines = true
  )
  container.setLoadInfo("")
  container.alert("Canceled loading")

proc cancel*(container: Container) {.jsfunc.} =
  if container.select != nil:
    container.select.cancel()
  elif container.loadState == lsLoading:
    container.loadState = lsCanceled
    if container.iface != nil:
      container.remoteCancel()
    else:
      container.triggerEvent(cetCancel)

proc findAnchor*(container: Container; anchor: string) =
  container.iface.findAnchor(anchor).then(proc(found: bool) =
    if found:
      container.triggerEvent(ContainerEvent(t: cetAnchor, anchor: anchor))
    else:
      container.triggerEvent(ContainerEvent(t: cetNoAnchor, anchor: anchor))
  )

proc readCanceled*(container: Container) =
  container.iface.readCanceled().then(proc(repaint: bool) =
    if repaint:
      container.needslines = true)

proc readSuccess*(container: Container; s: string; fd = -1) =
  let p = container.iface.readSuccess(s, fd != -1)
  if fd != -1:
    container.iface.stream.reallyFlush()
    SocketStream(container.iface.stream.source).sendFileHandle(FileHandle(fd))
  p.then(proc(res: ReadSuccessResult) =
    if res.repaint:
      container.needslines = true
    if res.open != nil:
      container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open))
  )

proc reshape(container: Container): EmptyPromise {.jsfunc.} =
  if container.iface == nil:
    return
  return container.iface.forceRender().then(proc(): EmptyPromise =
    return container.requestLines()
  )

proc displaySelect(container: Container; selectResult: SelectResult) =
  container.select = Select(
    container: container,
    multiple: selectResult.multiple,
    options: selectResult.options,
    oselected: selectResult.selected,
    selected: selectResult.selected,
    x: container.acursorx,
    y: container.acursory
  )
  for opt in container.select.options.mitems:
    opt.mnormalize()
    container.select.maxw = max(container.select.maxw, opt.width())
  container.select.windowChange(container.height)
  container.queueDraw()

proc onclick(container: Container; res: ClickResult; save: bool) =
  if res.repaint:
    container.needslines = true
  if res.open != nil:
    container.triggerEvent(ContainerEvent(
      t: cetOpen,
      request: res.open,
      save: save
    ))
  if res.select.isSome and not save:
    container.displaySelect(res.select.get)
  if res.readline.isSome:
    container.onReadLine(res.readline.get)

proc click*(container: Container) {.jsfunc.} =
  if container.select != nil:
    container.select.click()
  else:
    if container.iface == nil:
      return
    container.iface.click(container.cursorx, container.cursory)
      .then(proc(res: ClickResult) = container.onclick(res, save = false))

proc saveLink*(container: Container) {.jsfunc.} =
  if container.iface == nil:
    return
  container.iface.click(container.cursorx, container.cursory)
    .then(proc(res: ClickResult) = container.onclick(res, save = true))

proc saveSource*(container: Container) {.jsfunc.} =
  if container.iface == nil:
    return
  container.triggerEvent(ContainerEvent(
    t: cetOpen,
    request: newRequest(newURL("cache:" & $container.cacheId).get),
    save: true,
    url: container.url
  ))

proc windowChange*(container: Container; attrs: WindowAttributes) =
  if attrs.width != container.width or attrs.height - 1 != container.height:
    container.width = attrs.width
    container.height = attrs.height - 1
    if container.iface != nil:
      var attrs = attrs
      # subtract status line height
      attrs.height -= 1
      attrs.height_px -= attrs.ppl
      container.iface.windowChange(attrs).then(proc() =
        container.needslines = true
      )

proc peek(container: Container) {.jsfunc.} =
  container.alert($container.url)

proc clearHover*(container: Container) =
  container.lastPeek = low(HoverType)

proc peekCursor(container: Container) {.jsfunc.} =
  var p = container.lastPeek
  while true:
    if p < high(HoverType):
      inc p
    else:
      p = low(HoverType)
    if container.hoverText[p] != "" or p == container.lastPeek:
      break
  container.alert($p & ": " & container.hoverText[p])
  container.lastPeek = p

func hoverLink(container: Container): string {.jsfget.} =
  return container.hoverText[htLink]

func hoverTitle(container: Container): string {.jsfget.} =
  return container.hoverText[htTitle]

func hoverImage(container: Container): string {.jsfget.} =
  return container.hoverText[htImage]

proc handleCommand(container: Container) =
  var packet: array[3, int] # 0 len, 1 auxLen, 2 packetid
  container.iface.stream.recvDataLoop(addr packet[0], sizeof(packet))
  container.iface.resolve(packet[2], packet[0] - sizeof(packet[2]), packet[1])

proc startLoad(container: Container) =
  container.iface.load().then(proc(res: int) =
    container.onload(res)
  )
  container.iface.getTitle().then(proc(title: string) =
    if title != "":
      container.title = title
      container.triggerEvent(cetTitle)
  )

proc setStream*(container: Container; stream: SocketStream;
    registerFun: proc(fd: int)) =
  assert cfCloned notin container.flags
  container.iface = newBufferInterface(stream, registerFun)
  container.startLoad()

proc setCloneStream*(container: Container; stream: SocketStream;
    registerFun: proc(fd: int)) =
  assert cfCloned in container.flags
  container.iface = cloneInterface(stream, registerFun)
  # Maybe we have to resume loading. Let's try.
  container.startLoad()

proc onreadline(container: Container; w: Slice[int];
    handle: (proc(line: SimpleFlexibleLine)); res: GetLinesResult) =
  for line in res.lines:
    handle(line)
  if res.numLines > w.b + 1:
    var w = w
    w.a += 24
    w.b += 24
    container.iface.getLines(w).then(proc(res: GetLinesResult) =
      container.onreadline(w, handle, res))
  else:
    container.setNumLines(res.numLines, true)

# Synchronously read all lines in the buffer.
proc readLines*(container: Container; handle: proc(line: SimpleFlexibleLine)) =
  if container.code == 0:
    # load succeded
    let w = 0 .. 23
    container.iface.getLines(w).then(proc(res: GetLinesResult) =
      container.onreadline(w, handle, res))
    while container.iface.hasPromises:
      # fulfill all promises
      container.handleCommand()

proc drawLines*(container: Container; display: var FixedGrid; hlcolor: CellColor) =
  let bgcolor = container.bgcolor
  template set_fmt(cell, cf: typed) =
    if cf.pos != -1:
      cell.format = cf.format
    if bgcolor != defaultColor and cell.format.bgcolor == defaultColor:
      cell.format.bgcolor = bgcolor
  var r: Rune
  var by = 0
  let endy = min(container.fromy + display.height, container.numLines)
  for line in container.ilines(container.fromy ..< endy):
    var w = 0 # width of the row so far
    var i = 0 # byte in line.str
    # Skip cells till fromx.
    while w < container.fromx and i < line.str.len:
      fastRuneAt(line.str, i, r)
      w += r.twidth(w)
    let dls = by * display.width # starting position of row in display
    # Fill in the gap in case we skipped more cells than fromx mandates (i.e.
    # we encountered a double-width character.)
    var cf = line.findFormat(w)
    var nf = line.findNextFormat(w)
    var k = 0
    while k < w - container.fromx:
      display[dls + k].str &= ' '
      set_fmt display[dls + k], cf
      inc k
    let startw = w # save this for later
    # Now fill in the visible part of the row.
    while i < line.str.len:
      let pw = w
      fastRuneAt(line.str, i, r)
      let rw = r.twidth(w)
      w += rw
      if w > container.fromx + display.width:
        break # die on exceeding the width limit
      if nf.pos != -1 and nf.pos <= pw:
        cf = nf
        nf = line.findNextFormat(pw)
      if r == Rune('\t'):
        # Needs to be replaced with spaces, otherwise bgcolor isn't displayed.
        let tk = k + rw
        while k < tk:
          display[dls + k].str &= ' '
          set_fmt display[dls + k], cf
          inc k
      else:
        display[dls + k].str &= r
        set_fmt display[dls + k], cf
        k += rw
    if bgcolor != defaultColor:
      # Fill the screen if bgcolor is not default.
      while k < display.width:
        display[dls + k].str &= ' '
        display[dls + k].format.bgcolor = bgcolor
        inc k
    # Finally, override cell formatting for highlighted cells.
    let hls = container.findHighlights(container.fromy + by)
    let aw = display.width - (startw - container.fromx) # actual width
    for hl in hls:
      let area = container.colorArea(hl, container.fromy + by,
        startw .. startw + aw)
      for i in area:
        if i - startw >= display.width:
          break
        var hlformat = display[dls + i - startw].format
        hlformat.bgcolor = hlcolor
        display[dls + i - startw].format = hlformat
    inc by

proc highlightMarks*(container: Container; display: var FixedGrid;
    hlcolor: CellColor) =
  for mark in container.marks.values:
    if mark.x in container.fromx ..< container.fromx + display.width and
        mark.y in container.fromy ..< container.fromy + display.height:
      let x = mark.x - container.fromx
      let y = mark.y - container.fromy
      var hlformat = display[y * display.width + x].format
      hlformat.bgcolor = hlcolor
      display[y * display.width + x].format = hlformat

func findCachedImage*(container: Container; id: int): CachedImage =
  for image in container.cachedImages:
    if image.bmp.imageId == id:
      return image
  return nil

proc handleEvent*(container: Container) =
  container.handleCommand()
  if container.needslines:
    container.requestLines()
    container.needslines = false

proc addContainerModule*(ctx: JSContext) =
  ctx.registerType(Highlight)
  ctx.registerType(Container, name = "Buffer")