summary refs log tree commit diff stats
path: root/lib/msgstore.go
blob: 27b63f39374dd025b439a5ddb30593ecd3c065ce (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
package lib

import (
	"io"
	"time"

	"github.com/emersion/go-imap"

	"git.sr.ht/~sircmpwn/aerc/models"
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

// Accesses to fields must be guarded by MessageStore.Lock/Unlock
type MessageStore struct {
	Deleted  map[uint32]interface{}
	DirInfo  models.DirectoryInfo
	Messages map[uint32]*models.MessageInfo
	// Ordered list of known UIDs
	Uids []uint32

	selected        int
	bodyCallbacks   map[uint32][]func(io.Reader)
	headerCallbacks map[uint32][]func(*types.MessageInfo)

	// Search/filter results
	results     []uint32
	resultIndex int

	// Map of uids we've asked the worker to fetch
	onUpdate       func(store *MessageStore) // TODO: multiple onUpdate handlers
	pendingBodies  map[uint32]interface{}
	pendingHeaders map[uint32]interface{}
	worker         *types.Worker
}

func NewMessageStore(worker *types.Worker,
	dirInfo *models.DirectoryInfo) *MessageStore {

	return &MessageStore{
		Deleted: make(map[uint32]interface{}),
		DirInfo: *dirInfo,

		selected:        0,
		bodyCallbacks:   make(map[uint32][]func(io.Reader)),
		headerCallbacks: make(map[uint32][]func(*types.MessageInfo)),

		pendingBodies:  make(map[uint32]interface{}),
		pendingHeaders: make(map[uint32]interface{}),
		worker:         worker,
	}
}

func (store *MessageStore) FetchHeaders(uids []uint32,
	cb func(*types.MessageInfo)) {

	// TODO: this could be optimized by pre-allocating toFetch and trimming it
	// at the end. In practice we expect to get most messages back in one frame.
	var toFetch []uint32
	for _, uid := range uids {
		if _, ok := store.pendingHeaders[uid]; !ok {
			toFetch = append(toFetch, uid)
			store.pendingHeaders[uid] = nil
			if cb != nil {
				if list, ok := store.headerCallbacks[uid]; ok {
					store.headerCallbacks[uid] = append(list, cb)
				} else {
					store.headerCallbacks[uid] = []func(*types.MessageInfo){cb}
				}
			}
		}
	}
	if len(toFetch) > 0 {
		store.worker.PostAction(&types.FetchMessageHeaders{Uids: toFetch}, nil)
	}
}

func (store *MessageStore) FetchFull(uids []uint32, cb func(io.Reader)) {
	// TODO: this could be optimized by pre-allocating toFetch and trimming it
	// at the end. In practice we expect to get most messages back in one frame.
	var toFetch []uint32
	for _, uid := range uids {
		if _, ok := store.pendingBodies[uid]; !ok {
			toFetch = append(toFetch, uid)
			store.pendingBodies[uid] = nil
			if cb != nil {
				if list, ok := store.bodyCallbacks[uid]; ok {
					store.bodyCallbacks[uid] = append(list, cb)
				} else {
					store.bodyCallbacks[uid] = []func(io.Reader){cb}
				}
			}
		}
	}
	if len(toFetch) > 0 {
		store.worker.PostAction(&types.FetchFullMessages{
			Uids: toFetch,
		}, func(msg types.WorkerMessage) {
			switch msg.(type) {
			case *types.Error:
				for _, uid := range toFetch {
					if _, ok := store.bodyCallbacks[uid]; ok {
						delete(store.bodyCallbacks, uid)
					}
				}
			}
		})
	}
}

func (store *MessageStore) FetchBodyPart(
	uid uint32, part []int, cb func(io.Reader)) {

	store.worker.PostAction(&types.FetchMessageBodyPart{
		Uid:  uid,
		Part: part,
	}, func(resp types.WorkerMessage) {
		msg, ok := resp.(*types.MessageBodyPart)
		if !ok {
			return
		}
		cb(msg.Part.Reader)
	})
}

func merge(to *models.MessageInfo, from *models.MessageInfo) {
	if from.BodyStructure != nil {
		to.BodyStructure = from.BodyStructure
	}
	if from.Envelope != nil {
		to.Envelope = from.Envelope
	}
	to.Flags = from.Flags
	if from.Size != 0 {
		to.Size = from.Size
	}
	var zero time.Time
	if from.InternalDate != zero {
		to.InternalDate = from.InternalDate
	}
}

func (store *MessageStore) Update(msg types.WorkerMessage) {
	update := false
	switch msg := msg.(type) {
	case *types.DirectoryInfo:
		store.DirInfo = *msg.Info
		store.worker.PostAction(&types.FetchDirectoryContents{}, nil)
		update = true
	case *types.DirectoryContents:
		newMap := make(map[uint32]*models.MessageInfo)
		for _, uid := range msg.Uids {
			if msg, ok := store.Messages[uid]; ok {
				newMap[uid] = msg
			} else {
				newMap[uid] = nil
			}
		}
		store.Messages = newMap
		store.Uids = msg.Uids
		update = true
	case *types.MessageInfo:
		if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil {
			merge(existing, msg.Info)
		} else {
			store.Messages[msg.Info.Uid] = msg.Info
		}
		if _, ok := store.pendingHeaders[msg.Info.Uid]; msg.Info.Envelope != nil && ok {
			delete(store.pendingHeaders, msg.Info.Uid)
			if cbs, ok := store.headerCallbacks[msg.Info.Uid]; ok {
				for _, cb := range cbs {
					cb(msg)
				}
			}
		}
		update = true
	case *types.FullMessage:
		if _, ok := store.pendingBodies[msg.Content.Uid]; ok {
			delete(store.pendingBodies, msg.Content.Uid)
			if cbs, ok := store.bodyCallbacks[msg.Content.Uid]; ok {
				for _, cb := range cbs {
					cb(msg.Content.Reader)
				}
				delete(store.bodyCallbacks, msg.Content.Uid)
			}
		}
	case *types.MessagesDeleted:
		toDelete := make(map[uint32]interface{})
		for _, uid := range msg.Uids {
			toDelete[uid] = nil
			delete(store.Messages, uid)
			if _, ok := store.Deleted[uid]; ok {
				delete(store.Deleted, uid)
			}
		}
		uids := make([]uint32, len(store.Uids)-len(msg.Uids))
		j := 0
		for _, uid := range store.Uids {
			if _, deleted := toDelete[uid]; !deleted && j < len(uids) {
				uids[j] = uid
				j += 1
			}
		}
		store.Uids = uids
		update = true
	}

	if update {
		store.update()
	}
}

func (store *MessageStore) OnUpdate(fn func(store *MessageStore)) {
	store.onUpdate = fn
}

func (store *MessageStore) update() {
	if store.onUpdate != nil {
		store.onUpdate(store)
	}
}

func (store *MessageStore) Delete(uids []uint32,
	cb func(msg types.WorkerMessage)) {

	for _, uid := range uids {
		store.Deleted[uid] = nil
	}

	store.worker.PostAction(&types.DeleteMessages{Uids: uids}, cb)
	store.update()
}

func (store *MessageStore) Copy(uids []uint32, dest string, createDest bool,
	cb func(msg types.WorkerMessage)) {

	if createDest {
		store.worker.PostAction(&types.CreateDirectory{
			Directory: dest,
			Quiet:     true,
		}, cb)
	}

	store.worker.PostAction(&types.CopyMessages{
		Destination: dest,
		Uids:        uids,
	}, cb)
}

func (store *MessageStore) Move(uids []uint32, dest string, createDest bool,
	cb func(msg types.WorkerMessage)) {

	for _, uid := range uids {
		store.Deleted[uid] = nil
	}

	if createDest {
		store.worker.PostAction(&types.CreateDirectory{
			Directory: dest,
			Quiet:     true,
		}, cb)
	}

	store.worker.PostAction(&types.CopyMessages{
		Destination: dest,
		Uids:        uids,
	}, func(msg types.WorkerMessage) {
		switch msg.(type) {
		case *types.Error:
			cb(msg)
		case *types.Done:
			store.worker.PostAction(&types.DeleteMessages{Uids: uids}, cb)
		}
	})

	store.update()
}

func (store *MessageStore) Read(uids []uint32, read bool,
	cb func(msg types.WorkerMessage)) {

	store.worker.PostAction(&types.ReadMessages{
		Read: read,
		Uids: uids,
	}, cb)
}

func (store *MessageStore) Selected() *models.MessageInfo {
	return store.Messages[store.Uids[len(store.Uids)-store.selected-1]]
}

func (store *MessageStore) SelectedIndex() int {
	return store.selected
}

func (store *MessageStore) Select(index int) {
	store.selected = index
	for ; store.selected < 0; store.selected = len(store.Uids) + store.selected {
		/* This space deliberately left blank */
	}
	if store.selected > len(store.Uids) {
		store.selected = len(store.Uids)
	}
}

func (store *MessageStore) nextPrev(delta int) {
	if len(store.Uids) == 0 {
		return
	}
	store.selected += delta
	if store.selected < 0 {
		store.selected = 0
	}
	if store.selected >= len(store.Uids) {
		store.selected = len(store.Uids) - 1
	}
}

func (store *MessageStore) Next() {
	store.nextPrev(1)
}

func (store *MessageStore) Prev() {
	store.nextPrev(-1)
}

func (store *MessageStore) Search(c *imap.SearchCriteria, cb func([]uint32)) {
	store.worker.PostAction(&types.SearchDirectory{
		Criteria: c,
	}, func(msg types.WorkerMessage) {
		switch msg := msg.(type) {
		case *types.SearchResults:
			cb(msg.Uids)
		}
	})
}

func (store *MessageStore) ApplySearch(results []uint32) {
	store.results = results
	store.resultIndex = -1
	store.NextResult()
}

func (store *MessageStore) nextPrevResult(delta int) {
	if len(store.results) == 0 {
		return
	}
	store.resultIndex += delta
	if store.resultIndex >= len(store.results) {
		store.resultIndex = 0
	}
	if store.resultIndex < 0 {
		store.resultIndex = len(store.results) - 1
	}
	for i, uid := range store.Uids {
		if store.results[len(store.results)-store.resultIndex-1] == uid {
			store.Select(len(store.Uids) - i - 1)
			break
		}
	}
	store.update()
}

func (store *MessageStore) NextResult() {
	store.nextPrevResult(1)
}

func (store *MessageStore) PrevResult() {
	store.nextPrevResult(-1)
}
an class="n">STYLED_ELEMENT and node.node != nil: let element = Element(node.node) if element.attrb("title"): return element.attr("title") if node.node != nil: var node = node.node for element in node.ancestors: if element.attrb("title"): return element.attr("title") #TODO pseudo-elements const ClickableElements = { TAG_A, TAG_INPUT, TAG_OPTION, TAG_BUTTON, TAG_TEXTAREA, TAG_LABEL } func getClickable(styledNode: StyledNode): Element = if styledNode == nil: return nil var styledNode = styledNode while styledNode.node == nil: styledNode = styledNode.parent if styledNode == nil: return nil if styledNode.t == STYLED_ELEMENT: let element = Element(styledNode.node) if element.tagType in ClickableElements and (element.tagType != TAG_A or HTMLAnchorElement(element).href != ""): return element var node = styledNode.node while true: result = node.findAncestor(ClickableElements) if result == nil: break if result.tagType != TAG_A or HTMLAnchorElement(result).href != "": break node = result func getClickHover(styledNode: StyledNode): string = let clickable = styledNode.getClickable() if clickable != nil: case clickable.tagType of TAG_A: return HTMLAnchorElement(clickable).href of TAG_INPUT: return "<input>" of TAG_OPTION: return "<option>" of TAG_BUTTON: return "<button>" of TAG_TEXTAREA: return "<textarea>" else: discard func getCursorClickable(buffer: Buffer, cursorx, cursory: int): Element = let i = buffer.lines[cursory].findFormatN(cursorx) - 1 if i >= 0: return buffer.lines[cursory].formats[i].node.getClickable() func cursorBytes(buffer: Buffer, y: int, cc: int): int = let line = buffer.lines[y].str var w = 0 var i = 0 while i < line.len and w < cc: var r: Rune fastRuneAt(line, i, r) w += r.twidth(w) return i proc findPrevLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} = if cursory >= buffer.lines.len: return (-1, -1) let line = buffer.lines[cursory] var i = line.findFormatN(cursorx) - 1 var link: Element = nil if i >= 0: link = line.formats[i].node.getClickable() dec i var ly = 0 #last y var lx = 0 #last x template link_beginning() = #go to beginning of link ly = y #last y lx = format.pos #last x #on the current line let line = buffer.lines[y] while i >= 0: let format = line.formats[i] let nl = format.node.getClickable() if nl == fl: lx = format.pos dec i #on previous lines for iy in countdown(ly - 1, 0): let line = buffer.lines[iy] i = line.formats.len - 1 while i >= 0: let format = line.formats[i] let nl = format.node.getClickable() if nl == fl: ly = iy lx = format.pos dec i while i >= 0: let format = line.formats[i] let fl = format.node.getClickable() if fl != nil and fl != link: let y = cursory link_beginning return (lx, ly) dec i for y in countdown(cursory - 1, 0): let line = buffer.lines[y] i = line.formats.len - 1 while i >= 0: let format = line.formats[i] let fl = format.node.getClickable() if fl != nil and fl != link: link_beginning return (lx, ly) dec i return (-1, -1) proc findNextLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} = if cursory >= buffer.lines.len: return (-1, -1) let line = buffer.lines[cursory] var i = line.findFormatN(cursorx) - 1 var link: Element = nil if i >= 0: link = line.formats[i].node.getClickable() inc i while i < line.formats.len: let format = line.formats[i] let fl = format.node.getClickable() if fl != nil and fl != link: return (format.pos, cursory) inc i for y in (cursory + 1)..(buffer.lines.len - 1): let line = buffer.lines[y] i = 0 while i < line.formats.len: let format = line.formats[i] let fl = format.node.getClickable() if fl != nil and fl != link: return (format.pos, y) inc i return (-1, -1) proc findPrevMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} = if cursory >= buffer.lines.len: return var y = cursory let b = buffer.cursorBytes(y, cursorx) let res = regex.exec(buffer.lines[y].str, 0, b) if res.success and res.captures.len > 0: let cap = res.captures[^1] let x = buffer.lines[y].str.width(0, cap.s) let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) return BufferMatch(success: true, x: x, y: y, str: str) dec y while true: if y < 0: if wrap: y = buffer.lines.high else: break let res = regex.exec(buffer.lines[y].str) if res.success and res.captures.len > 0: let cap = res.captures[^1] let x = buffer.lines[y].str.width(0, cap.s) let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) return BufferMatch(success: true, x: x, y: y, str: str) if y == cursory: break dec y proc findNextMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} = if cursory >= buffer.lines.len: return var y = cursory let b = buffer.cursorBytes(y, cursorx + 1) let res = regex.exec(buffer.lines[y].str, b, buffer.lines[y].str.len) if res.success and res.captures.len > 0: let cap = res.captures[0] let x = buffer.lines[y].str.width(0, cap.s) let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) return BufferMatch(success: true, x: x, y: y, str: str) inc y while true: if y > buffer.lines.high: if wrap: y = 0 else: break let res = regex.exec(buffer.lines[y].str) if res.success and res.captures.len > 0: let cap = res.captures[0] let x = buffer.lines[y].str.width(0, cap.s) let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) return BufferMatch(success: true, x: x, y: y, str: str) if y == cursory: break inc y proc gotoAnchor*(buffer: Buffer): tuple[x, y: int] {.proxy.} = if buffer.document == nil: return (-1, -1) let anchor = buffer.document.getElementById(buffer.url.anchor) if anchor == nil: return for y in 0 ..< buffer.lines.len: let line = buffer.lines[y] for i in 0 ..< line.formats.len: let format = line.formats[i] if format.node != nil and anchor in format.node.node: return (format.pos, y) return (-1, -1) proc do_reshape(buffer: Buffer) = case buffer.contenttype of "text/html": if buffer.viewport == nil: buffer.viewport = Viewport(window: buffer.attrs) let ret = renderDocument(buffer.document, buffer.attrs, buffer.userstyle, buffer.viewport, buffer.prevstyled) buffer.lines = ret[0] buffer.prevstyled = ret[1] else: buffer.lines.renderStream(buffer.srenderer, buffer.available) buffer.available = 0 proc windowChange*(buffer: Buffer, attrs: WindowAttributes) {.proxy.} = buffer.attrs = attrs buffer.viewport = Viewport(window: buffer.attrs) buffer.width = buffer.attrs.width buffer.height = buffer.attrs.height - 1 type UpdateHoverResult* = object link*: Option[string] title*: Option[string] repaint*: bool proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.proxy.} = if buffer.lines.len == 0: return var thisnode: StyledNode let i = buffer.lines[cursory].findFormatN(cursorx) - 1 if i >= 0: thisnode = buffer.lines[cursory].formats[i].node let prevnode = buffer.prevnode if thisnode != prevnode and (thisnode == nil or prevnode == nil or thisnode.node != prevnode.node): for styledNode in thisnode.branch: if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: let elem = Element(styledNode.node) if not elem.hover: elem.hover = true result.repaint = true let title = thisnode.getTitleAttr() if buffer.hovertext[HOVER_TITLE] != title: result.title = some(title) buffer.hovertext[HOVER_TITLE] = title let click = thisnode.getClickHover() if buffer.hovertext[HOVER_LINK] != click: result.link = some(click) buffer.hovertext[HOVER_LINK] = click for styledNode in prevnode.branch: if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: let elem = Element(styledNode.node) if elem.hover: elem.hover = false result.repaint = true if result.repaint: buffer.do_reshape() buffer.prevnode = thisnode proc loadResource(buffer: Buffer, document: Document, elem: HTMLLinkElement) = let href = elem.attr("href") if href == "": return let url = parseURL(href, document.url.some) if url.isSome: let url = url.get if url.scheme == buffer.url.scheme: let media = elem.media if media != "": let media = parseMediaQueryList(parseListOfComponentValues(newStringStream(media))) if not media.applies(): return let fs = buffer.loader.doRequest(newRequest(url)) if fs.body != nil and fs.contenttype == "text/css": elem.sheet = parseStylesheet(fs.body) proc loadResources(buffer: Buffer, document: Document) = if document.html != nil: for elem in document.html.elements(TAG_LINK): let elem = HTMLLinkElement(elem) if elem.rel == "stylesheet": buffer.loadResource(document, elem) type ConnectResult* = object code*: int needsAuth*: bool redirect*: Request contentType*: string cookies*: seq[Cookie] referrerpolicy*: Option[ReferrerPolicy] proc setupSource(buffer: Buffer): ConnectResult = if buffer.connected: result.code = -2 return let source = buffer.source let setct = source.contenttype.isNone if not setct: buffer.contenttype = source.contenttype.get buffer.url = source.location case source.t of CLONE: let s = connectSocketStream(source.clonepid, blocking = false) buffer.istream = s buffer.fd = cast[int](s.source.getFd()) if buffer.istream == nil: result.code = -2 return if setct: buffer.contenttype = "text/plain" of LOAD_PIPE: discard fcntl(source.fd, F_SETFL, fcntl(source.fd, F_GETFL, 0) or O_NONBLOCK) buffer.istream = newPosixStream(source.fd) buffer.fd = source.fd if setct: buffer.contenttype = "text/plain" of LOAD_REQUEST: let request = source.request let response = buffer.loader.doRequest(request, blocking = false) if response.body == nil: result.code = response.res return if setct: buffer.contenttype = response.contenttype buffer.istream = response.body let fd = SocketStream(response.body).source.getFd() buffer.fd = cast[int](fd) result.needsAuth = response.status == 401 # Unauthorized result.redirect = response.redirect if "Set-Cookie" in response.headers.table: for s in response.headers.table["Set-Cookie"]: let cookie = newCookie(s) if cookie != nil: result.cookies.add(cookie) if "Referrer-Policy" in response.headers.table: result.referrerpolicy = getReferrerPolicy(response.headers.table["Referrer-Policy"][0]) buffer.istream = newTeeStream(buffer.istream, buffer.sstream, closedest = false) buffer.selector.registerHandle(buffer.fd, {Read}, 0) if setct: result.contentType = buffer.contenttype buffer.connected = true proc connect*(buffer: Buffer): ConnectResult {.proxy.} = let code = buffer.setupSource() return code const BufferSize = 4096 proc finishLoad(buffer: Buffer) = if buffer.loaded: return case buffer.contenttype of "text/html": buffer.sstream.setPosition(0) buffer.available = 0 if buffer.window == nil: buffer.window = newWindow(buffer.config.scripting) let (doc, cs) = parseHTML(buffer.sstream, fallbackcs = buffer.cs, window = buffer.window, url = buffer.url) buffer.document = doc if buffer.document == nil: # needsreinterpret buffer.sstream.setPosition(0) let (doc, _) = parseHTML(buffer.sstream, cs = some(cs), window = buffer.window, url = buffer.url) buffer.document = doc buffer.loadResources(buffer.document) buffer.selector.unregister(buffer.fd) buffer.istream.close() buffer.loaded = true type LoadResult* = tuple[ atend: bool, lines: int, bytes: int ] proc load*(buffer: Buffer): LoadResult {.proxy, task.} = if buffer.loaded: return (true, buffer.lines.len, -1) else: buffer.savetask = true proc resolveTask[T](buffer: Buffer, cmd: BufferCommand, res: T) = let packetid = buffer.tasks[cmd] if packetid == 0: return # no task to resolve (TODO this is kind of inefficient) let len = slen(buffer.tasks[cmd]) + slen(res) buffer.pstream.swrite(len) buffer.pstream.swrite(packetid) buffer.tasks[cmd] = 0 buffer.pstream.swrite(res) buffer.pstream.flush() proc onload(buffer: Buffer) = var res: LoadResult = (false, buffer.lines.len, -1) if buffer.loaded: buffer.resolveTask(LOAD, res) return let op = buffer.sstream.getPosition() var s = newString(buffer.readbufsize) try: buffer.sstream.setPosition(op + buffer.available) let n = buffer.istream.readData(addr s[0], buffer.readbufsize) assert n != 0 s.setLen(n) buffer.sstream.setPosition(op) if buffer.readbufsize < BufferSize: buffer.readbufsize = min(BufferSize, buffer.readbufsize * 2) buffer.available += s.len case buffer.contenttype of "text/html": res.bytes = buffer.available else: buffer.do_reshape() buffer.resolveTask(LOAD, res) except EOFError: res.atend = true buffer.finishLoad() buffer.resolveTask(LOAD, res) except ErrorAgain, ErrorWouldBlock: if buffer.readbufsize > 1: buffer.readbufsize = buffer.readbufsize div 2 proc getTitle*(buffer: Buffer): string {.proxy.} = if buffer.document != nil: return buffer.document.title proc render*(buffer: Buffer): int {.proxy.} = buffer.do_reshape() return buffer.lines.len proc cancel*(buffer: Buffer): int {.proxy.} = if buffer.loaded: return buffer.istream.close() buffer.loaded = true case buffer.contenttype of "text/html": buffer.sstream.setPosition(0) buffer.available = 0 if buffer.window == nil: buffer.window = newWindow(buffer.config.scripting) let (doc, _) = parseHTML(buffer.sstream, cs = some(buffer.cs), window = buffer.window, url = buffer.url) # confidence: certain buffer.document = doc buffer.do_reshape() return buffer.lines.len # https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encoding: string = ""): seq[tuple[name, value: string]] = if form.constructingentrylist: return form.constructingentrylist = true var entrylist: seq[tuple[name, value: string]] for field in form.controls: if field.findAncestor({TAG_DATALIST}) != nil or field.attrb("disabled") or field.isButton() and Element(field) != submitter: continue if field.tagType == TAG_INPUT: let field = HTMLInputElement(field) if field.inputType == INPUT_IMAGE: let name = if field.attr("name") != "": field.attr("name") & '.' else: "" entrylist.add((name & 'x', $field.xcoord)) entrylist.add((name & 'y', $field.ycoord)) continue #TODO custom elements let name = field.attr("name") if name == "": continue if field.tagType == TAG_SELECT: let field = HTMLSelectElement(field) for option in field.options: if option.selected or option.disabled: entrylist.add((name, option.value)) elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}: let value = if field.attr("value") != "": field.attr("value") else: "on" entrylist.add((name, value)) elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_FILE: #TODO file discard elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"): let charset = if encoding != "": encoding else: "UTF-8" entrylist.add((name, charset)) else: case field.tagType of TAG_INPUT: entrylist.add((name, HTMLInputElement(field).value)) of TAG_BUTTON: entrylist.add((name, HTMLButtonElement(field).value)) of TAG_TEXTAREA: entrylist.add((name, HTMLTextAreaElement(field).value)) else: assert false, "Tag type " & $field.tagType & " not accounted for in constructEntryList" if field.tagType == TAG_TEXTAREA or field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}: if field.attr("dirname") != "": let dirname = field.attr("dirname") let dir = "ltr" #TODO bidi entrylist.add((dirname, dir)) form.constructingentrylist = false return entrylist #https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm proc serializeMultipartFormData(kvs: seq[(string, string)]): MimeData = for it in kvs: let name = makeCRLF(it[0]) let value = makeCRLF(it[1]) result[name] = value proc serializePlainTextFormData(kvs: seq[(string, string)]): string = for it in kvs: let (name, value) = it result &= name result &= '=' result &= value result &= "\r\n" proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] = let entrylist = form.constructEntryList(submitter) let action = if submitter.action() == "": $form.document.url else: submitter.action() let url = submitter.document.parseURL(action) if url.isnone: return none(Request) var parsedaction = url.get let scheme = parsedaction.scheme let enctype = submitter.enctype() let formmethod = submitter.formmethod() if formmethod == FORM_METHOD_DIALOG: #TODO return none(Request) let httpmethod = if formmethod == FORM_METHOD_GET: HTTP_GET else: assert formmethod == FORM_METHOD_POST HTTP_POST #let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"): # submitter.attr("formtarget") #else: # submitter.target() #let noopener = true #TODO template mutateActionUrl() = let query = serializeApplicationXWWWFormUrlEncoded(entrylist) parsedaction.query = query.some return newRequest(parsedaction, httpmethod).some template submitAsEntityBody() = var mimetype: string var body = none(string) var multipart = none(MimeData) case enctype of FORM_ENCODING_TYPE_URLENCODED: body = serializeApplicationXWWWFormUrlEncoded(entrylist).some mimeType = $enctype of FORM_ENCODING_TYPE_MULTIPART: multipart = serializeMultipartFormData(entrylist).some mimetype = $enctype of FORM_ENCODING_TYPE_TEXT_PLAIN: body = serializePlainTextFormData(entrylist).some mimetype = $enctype return newRequest(parsedaction, httpmethod, @{"Content-Type": mimetype}, body, multipart).some template getActionUrl() = return newRequest(parsedaction).some case scheme of "http", "https": if formmethod == FORM_METHOD_GET: mutateActionUrl else: assert formmethod == FORM_METHOD_POST submitAsEntityBody of "ftp": getActionUrl of "data": if formmethod == FORM_METHOD_GET: mutateActionUrl else: assert formmethod == FORM_METHOD_POST getActionUrl proc setFocus(buffer: Buffer, e: Element): bool = if buffer.document.focus != e: buffer.document.focus = e buffer.do_reshape() return true proc restoreFocus(buffer: Buffer): bool = if buffer.document.focus != nil: buffer.document.focus = nil buffer.do_reshape() return true type ReadSuccessResult* = object open*: Option[Request] repaint*: bool proc implicitSubmit(buffer: Buffer, input: HTMLInputElement): Option[Request] = if input.form != nil and input.form.canSubmitImplicitly(): return submitForm(input.form, input.form) proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} = if buffer.document.focus != nil: case buffer.document.focus.tagType of TAG_INPUT: let input = HTMLInputElement(buffer.document.focus) case input.inputType of INPUT_SEARCH, INPUT_TEXT, INPUT_PASSWORD: input.value = s input.invalid = true buffer.do_reshape() result.repaint = true result.open = buffer.implicitSubmit(input) of INPUT_FILE: let cdir = parseURL("file://" & getCurrentDir() & DirSep) let path = parseURL(s, cdir) if path.issome: input.file = path input.invalid = true buffer.do_reshape() result.repaint = true result.open = buffer.implicitSubmit(input) else: discard of TAG_TEXTAREA: let textarea = HTMLTextAreaElement(buffer.document.focus) textarea.value = s textarea.invalid = true buffer.do_reshape() result.repaint = true else: discard let r = buffer.restoreFocus() if not result.repaint: result.repaint = r type ReadLineResult* = object prompt*: string value*: string hide*: bool area*: bool type ClickResult* = object open*: Option[Request] readline*: Option[ReadLineResult] repaint*: bool proc click(buffer: Buffer, clickable: Element): ClickResult = case clickable.tagType of TAG_LABEL: let label = HTMLLabelElement(clickable) let control = label.control if control != nil: return buffer.click(control) of TAG_SELECT: result.repaint = buffer.setFocus(clickable) of TAG_A: result.repaint = buffer.restoreFocus() let url = parseURL(HTMLAnchorElement(clickable).href, clickable.document.baseURL.some) if url.issome: result.open = some(newRequest(url.get, HTTP_GET)) of TAG_OPTION: let option = HTMLOptionElement(clickable) let select = option.select if select != nil: if buffer.document.focus == select: # select option if not select.attrb("multiple"): for option in select.options: option.selected = false option.selected = true result.repaint = buffer.restoreFocus() else: # focus on select result.repaint = buffer.setFocus(select) of TAG_BUTTON: let button = HTMLButtonElement(clickable) if button.form != nil: case button.ctype of BUTTON_SUBMIT: result.open = submitForm(button.form, button) of BUTTON_RESET: button.form.reset() result.repaint = true buffer.do_reshape() of BUTTON_BUTTON: discard of TAG_TEXTAREA: result.repaint = buffer.setFocus(clickable) let textarea = HTMLTextAreaElement(clickable) result.readline = some(ReadLineResult( value: textarea.value, area: true )) of TAG_INPUT: result.repaint = buffer.restoreFocus() let input = HTMLInputElement(clickable) case input.inputType of INPUT_SEARCH: result.repaint = buffer.setFocus(input) result.readline = some(ReadLineResult( prompt: "SEARCH: ", value: input.value )) of INPUT_TEXT, INPUT_PASSWORD: result.repaint = buffer.setFocus(input) result.readline = some(ReadLineResult( prompt: "TEXT: ", value: input.value, hide: input.inputType == INPUT_PASSWORD )) of INPUT_FILE: result.repaint = buffer.setFocus(input) var path = if input.file.issome: input.file.get.path.serialize_unicode() else: "" result.readline = some(ReadLineResult( prompt: "Filename: ", value: path )) of INPUT_CHECKBOX: input.checked = not input.checked input.invalid = true result.repaint = true buffer.do_reshape() of INPUT_RADIO: for radio in input.radiogroup: radio.checked = false radio.invalid = true input.checked = true input.invalid = true result.repaint = true buffer.do_reshape() of INPUT_RESET: if input.form != nil: input.form.reset() result.repaint = true buffer.do_reshape() of INPUT_SUBMIT, INPUT_BUTTON: if input.form != nil: result.open = submitForm(input.form, input) else: result.repaint = buffer.restoreFocus() else: result.repaint = buffer.restoreFocus() proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = if buffer.lines.len <= cursory: return let clickable = buffer.getCursorClickable(cursorx, cursory) if clickable != nil: return buffer.click(clickable) proc readCanceled*(buffer: Buffer): bool {.proxy.} = return buffer.restoreFocus() proc findAnchor*(buffer: Buffer, anchor: string): bool {.proxy.} = return buffer.document != nil and buffer.document.getElementById(anchor) != nil type GetLinesResult* = tuple[ numLines: int, lines: seq[SimpleFlexibleLine] ] proc getLines*(buffer: Buffer, w: Slice[int]): GetLinesResult {.proxy.} = var w = w if w.b < 0 or w.b > buffer.lines.high: w.b = buffer.lines.high #TODO this is horribly inefficient for y in w: var line = SimpleFlexibleLine(str: buffer.lines[y].str) for f in buffer.lines[y].formats: line.formats.add(SimpleFormatCell(format: f.format, pos: f.pos)) result.lines.add(line) result.numLines = buffer.lines.len proc passFd*(buffer: Buffer) {.proxy.} = let fd = SocketStream(buffer.pstream).recvFileHandle() buffer.source.fd = fd proc getSource*(buffer: Buffer) {.proxy.} = let ssock = initServerSocket() let stream = ssock.acceptSocketStream() buffer.finishLoad() buffer.sstream.setPosition(0) stream.write(buffer.sstream.readAll()) stream.close() ssock.close() macro bufferDispatcher(funs: static ProxyMap, buffer: Buffer, cmd: BufferCommand, packetid: int) = let switch = newNimNode(nnkCaseStmt) switch.add(ident("cmd")) for k, v in funs: let ofbranch = newNimNode(nnkOfBranch) ofbranch.add(v.ename) let stmts = newStmtList() let call = newCall(v.iname, buffer) for i in 2 ..< v.params.len: let param = v.params[i] for i in 0 ..< param.len - 2: let id = ident(param[i].strVal) let typ = param[^2] stmts.add(quote do: var `id`: `typ` `buffer`.pstream.sread(`id`)) call.add(id) var rval: NimNode if v.params[0].kind == nnkEmpty: stmts.add(call) else: rval = ident("retval") stmts.add(quote do: let `rval` = `call`) var resolve = newStmtList() if rval == nil: resolve.add(quote do: let len = slen(`packetid`) buffer.pstream.swrite(len) buffer.pstream.swrite(`packetid`) buffer.pstream.flush()) else: resolve.add(quote do: let len = slen(`packetid`) + slen(`rval`) buffer.pstream.swrite(len) buffer.pstream.swrite(`packetid`) buffer.pstream.swrite(`rval`) buffer.pstream.flush()) if v.istask: let en = v.ename stmts.add(quote do: if buffer.savetask: buffer.savetask = false buffer.tasks[BufferCommand.`en`] = `packetid` else: `resolve`) else: stmts.add(resolve) ofbranch.add(stmts) switch.add(ofbranch) return switch proc readCommand(buffer: Buffer) = var cmd: BufferCommand buffer.pstream.sread(cmd) var packetid: int buffer.pstream.sread(packetid) bufferDispatcher(ProxyFunctions, buffer, cmd, packetid) proc runBuffer(buffer: Buffer, rfd: int) = block loop: while buffer.alive: let events = buffer.selector.select(-1) for event in events: if event.fd == rfd: if Error in event.events: # Connection reset by peer, probably. Close the buffer. break loop elif Read in event.events: try: buffer.readCommand() except EOFError: #eprint "EOF error", $buffer.url & "\nMESSAGE:", # getCurrentExceptionMsg() & "\n", # getStackTrace(getCurrentException()) break loop else: assert false elif event.fd == buffer.fd: if Read in event.events or Error in event.events: buffer.onload() else: assert false elif event.fd in buffer.timeouts: if Event.Timer in event.events: buffer.selector.unregister(event.fd) var timeout: proc() if buffer.timeouts.pop(event.fd, timeout): timeout() else: assert false else: assert false else: assert false buffer.pstream.close() buffer.loader.quit() quit(0) proc launchBuffer*(config: BufferConfig, source: BufferSource, attrs: WindowAttributes, loader: FileLoader, mainproc: Pid) = let buffer = Buffer( alive: true, cs: CHARSET_UTF_8, userstyle: parseStylesheet(config.userstyle), attrs: attrs, config: config, loader: loader, source: source, sstream: newStringStream(), viewport: Viewport(window: attrs), width: attrs.width, height: attrs.height - 1 ) buffer.readbufsize = BufferSize buffer.selector = newSelector[int]() buffer.srenderer = newStreamRenderer(buffer.sstream) if buffer.config.scripting: buffer.window = newWindow(buffer.config.scripting, some(buffer.loader)) let socks = connectSocketStream(mainproc, false) socks.swrite(getpid()) buffer.pstream = socks let rfd = int(socks.source.getFd()) buffer.selector.registerHandle(rfd, {Read}, 0) buffer.runBuffer(rfd)