Fix some y related manipulations
[llpp.git] / main.ml
blob1cdcf3aadabe6b29d90e247366f6f9e04f1e18e2
1 let log fmt = Printf.kprintf prerr_endline fmt;;
2 let dolog fmt = Printf.kprintf prerr_endline fmt;;
4 external init : Unix.file_descr -> unit = "ml_init";;
5 external draw : int -> int -> int -> int -> string -> unit = "ml_draw";;
6 external gettext : string -> (int * int * int * int) -> int -> bool -> unit =
7 "ml_gettext";;
8 external checklink : string -> int -> int -> bool = "ml_checklink";;
9 external getlink : string -> int -> int -> (int * int) option = "ml_getlink";;
10 external getpagewh : int -> float array = "ml_getpagewh";;
12 type mstate = Msel of ((int * int) * (int * int)) | Mnone;;
14 type textentry = char * string * (string -> int -> te) * (string -> unit)
15 and te =
16 | TEstop
17 | TEdone of string
18 | TEcont of string
19 | TEswitch of textentry
22 type 'a circbuf =
23 { store : 'a array
24 ; mutable rc : int
25 ; mutable wc : int
26 ; mutable len : int
30 let cbnew n v =
31 { store = Array.create n v
32 ; rc = 0
33 ; wc = 0
34 ; len = 0
38 let cblen b = Array.length b.store;;
40 let cbput b v =
41 let len = cblen b in
42 b.store.(b.wc) <- v;
43 b.wc <- (b.wc + 1) mod len;
44 b.len <- min (b.len + 1) len;
47 let cbpeekw b = b.store.(b.wc);;
49 let cbget b =
50 let v = b.store.(b.rc) in
51 if b.len = 0
52 then
54 else (
55 let rc = if b.rc = 0 then b.len - 1 else b.rc - 1 in
56 b.rc <- rc;
61 let cbrfollowlen b =
62 b.rc <- b.len - 1;
65 type layout =
66 { pageno : int
67 ; pagedimno : int
68 ; pagew : int
69 ; pageh : int
70 ; pagedispy : int
71 ; pagey : int
72 ; pagevh : int
76 type conf =
77 { mutable scrollw : int
78 ; mutable scrollh : int
79 ; mutable rectsel : bool
80 ; mutable icase : bool
81 ; mutable preload : bool
82 ; mutable pagebias : int
83 ; mutable redispimm : bool
84 ; mutable verbose : bool
85 ; mutable scrollincr : int
86 ; mutable maxhfit : bool
87 ; mutable crophack : bool
88 ; mutable autoscroll : bool
89 ; mutable showall : bool
93 type outline = string * int * int * int;;
94 type outlines =
95 | Oarray of outline array
96 | Olist of outline list
97 | Onarrow of outline array * outline array
100 type state =
101 { mutable csock : Unix.file_descr
102 ; mutable ssock : Unix.file_descr
103 ; mutable w : int
104 ; mutable h : int
105 ; mutable rotate : int
106 ; mutable y : int
107 ; mutable ty : int
108 ; mutable prevy : int
109 ; mutable maxy : int
110 ; mutable layout : layout list
111 ; pagemap : ((int * int * int), string) Hashtbl.t
112 ; mutable pages : (int * int * int) list
113 ; mutable pagecount : int
114 ; pagecache : string circbuf
115 ; navhist : float circbuf
116 ; mutable inflight : int
117 ; mutable mstate : mstate
118 ; mutable searchpattern : string
119 ; mutable rects : (int * int * Gl.point2 * Gl.point2) list
120 ; mutable rects1 : (int * int * Gl.point2 * Gl.point2) list
121 ; mutable text : string
122 ; mutable fullscreen : (int * int) option
123 ; mutable textentry : textentry option
124 ; mutable outlines : outlines
125 ; mutable outline : (bool * int * int * outline array * string) option
126 ; mutable bookmarks : outline list
127 ; mutable path : string
131 let conf =
132 { scrollw = 5
133 ; scrollh = 12
134 ; icase = true
135 ; rectsel = true
136 ; preload = false
137 ; pagebias = 0
138 ; redispimm = false
139 ; verbose = false
140 ; scrollincr = 24
141 ; maxhfit = true
142 ; crophack = false
143 ; autoscroll = false
144 ; showall = false
148 let state =
149 { csock = Unix.stdin
150 ; ssock = Unix.stdin
151 ; w = 900
152 ; h = 900
153 ; rotate = 0
154 ; y = 0
155 ; ty = 0
156 ; prevy = 0
157 ; layout = []
158 ; maxy = max_int
159 ; pagemap = Hashtbl.create 10
160 ; pagecache = cbnew 10 ""
161 ; pages = []
162 ; pagecount = 0
163 ; inflight = 0
164 ; mstate = Mnone
165 ; navhist = cbnew 100 0.0
166 ; rects = []
167 ; rects1 = []
168 ; text = ""
169 ; fullscreen = None
170 ; textentry = None
171 ; searchpattern = ""
172 ; outlines = Olist []
173 ; outline = None
174 ; bookmarks = []
175 ; path = ""
179 let vlog fmt =
180 if conf.verbose
181 then
182 Printf.kprintf prerr_endline fmt
183 else
184 Printf.kprintf ignore fmt
187 let writecmd fd s =
188 let len = String.length s in
189 let n = 4 + len in
190 let b = Buffer.create n in
191 Buffer.add_char b (Char.chr ((len lsr 24) land 0xff));
192 Buffer.add_char b (Char.chr ((len lsr 16) land 0xff));
193 Buffer.add_char b (Char.chr ((len lsr 8) land 0xff));
194 Buffer.add_char b (Char.chr ((len lsr 0) land 0xff));
195 Buffer.add_string b s;
196 let s' = Buffer.contents b in
197 let n' = Unix.write fd s' 0 n in
198 if n' != n then failwith "write failed";
201 let readcmd fd =
202 let s = "xxxx" in
203 let n = Unix.read fd s 0 4 in
204 if n != 4 then failwith "incomplete read(len)";
205 let len = 0
206 lor (Char.code s.[0] lsl 24)
207 lor (Char.code s.[1] lsl 16)
208 lor (Char.code s.[2] lsl 8)
209 lor (Char.code s.[3] lsl 0)
211 let s = String.create len in
212 let n = Unix.read fd s 0 len in
213 if n != len then failwith "incomplete read(data)";
217 let yratio y =
218 if y = state.maxy then 1.0
219 else float y /. float state.maxy
222 let makecmd s l =
223 let b = Buffer.create 10 in
224 Buffer.add_string b s;
225 let rec combine = function
226 | [] -> b
227 | x :: xs ->
228 Buffer.add_char b ' ';
229 let s =
230 match x with
231 | `b b -> if b then "1" else "0"
232 | `s s -> s
233 | `i i -> string_of_int i
234 | `f f -> string_of_float f
235 | `I f -> string_of_int (truncate f)
237 Buffer.add_string b s;
238 combine xs;
240 combine l;
243 let wcmd s l =
244 let cmd = Buffer.contents (makecmd s l) in
245 writecmd state.csock cmd;
248 let calcheight () =
249 let rec f pn ph fh l =
250 match l with
251 | (n, _, h) :: rest ->
252 let fh = fh + (n - pn) * ph in
253 f n h fh rest
255 | [] ->
256 let fh = fh + (ph * (state.pagecount - pn)) in
257 max 0 fh
259 let fh = f 0 0 0 state.pages in
263 let getpagey pageno =
264 let rec f pn ph y l =
265 match l with
266 | (n, _, h) :: rest ->
267 if n >= pageno
268 then
269 y + (pageno - pn) * ph
270 else
271 let y = y + (n - pn) * ph in
272 f n h y rest
274 | [] ->
275 y + (pageno - pn) * ph
277 f 0 0 0 state.pages;
280 let layout y sh =
281 let rec f pageno pdimno prev vy py dy l cacheleft accu =
282 if pageno = state.pagecount || cacheleft = 0
283 then accu
284 else
285 let ((_, w, h) as curr), rest, pdimno =
286 match l with
287 | ((pageno', _, _) as curr) :: rest when pageno' = pageno ->
288 curr, rest, pdimno + 1
289 | _ ->
290 prev, l, pdimno
292 let pageno' = pageno + 1 in
293 if py + h > vy
294 then
295 let py' = vy - py in
296 let vh = h - py' in
297 if dy + vh > sh
298 then
299 let vh = sh - dy in
300 if vh <= 0
301 then
302 accu
303 else
304 let e =
305 { pageno = pageno
306 ; pagedimno = pdimno
307 ; pagew = w
308 ; pageh = h
309 ; pagedispy = dy
310 ; pagey = py'
311 ; pagevh = vh
314 e :: accu
315 else
316 let e =
317 { pageno = pageno
318 ; pagedimno = pdimno
319 ; pagew = w
320 ; pageh = h
321 ; pagedispy = dy
322 ; pagey = py'
323 ; pagevh = vh
326 let accu = e :: accu in
327 f pageno' pdimno curr
328 (vy + vh) (py + h) (dy + vh + 2) rest
329 (pred cacheleft) accu
330 else
331 f pageno' pdimno curr vy (py + h) dy rest cacheleft accu
333 let accu = f 0 ~-1 (0,0,0) y 0 0 state.pages (cblen state.pagecache) [] in
334 state.maxy <- calcheight ();
335 List.rev accu
338 let clamp incr =
339 let y = state.y + incr in
340 let y = max 0 y in
341 let y = min y (state.maxy - (if conf.maxhfit then state.h else 0)) in
345 let getopaque pageno =
346 try Some (Hashtbl.find state.pagemap (pageno + 1, state.w - conf.scrollw,
347 state.rotate))
348 with Not_found -> None
351 let cache pageno opaque =
352 Hashtbl.replace state.pagemap (pageno + 1, state.w - conf.scrollw,
353 state.rotate) opaque
356 let validopaque opaque = String.length opaque > 0;;
358 let preload l =
359 match getopaque l.pageno with
360 | None when state.inflight < 2+0*(cblen state.pagecache) ->
361 state.inflight <- succ state.inflight;
362 cache l.pageno "";
363 wcmd "render" [`i (l.pageno + 1)
364 ;`i l.pagedimno
365 ;`i l.pagew
366 ;`i l.pageh];
368 | _ -> ()
371 let gotoy y =
372 let y = max 0 y in
373 let y = min state.maxy y in
374 let pages = layout y state.h in
375 let rec f all = function
376 | l :: ls ->
377 begin match getopaque l.pageno with
378 | None -> preload l; f false ls
379 | Some opaque -> f (all && validopaque opaque) ls
381 | [] -> all
383 if not conf.showall || f true pages
384 then (
385 state.y <- y;
386 state.layout <- pages;
388 state.ty <- y;
389 if conf.redispimm
390 then
391 Glut.postRedisplay ()
395 let addnav () =
396 cbput state.navhist (yratio state.y);
397 cbrfollowlen state.navhist;
400 let getnav () =
401 let y = cbget state.navhist in
402 truncate (y *. float state.maxy)
405 let gotopage n top =
406 let y = getpagey n in
407 addnav ();
408 gotoy (y + top);
411 let reshape ~w ~h =
412 let ratio = float w /. float state.w in
413 let fixbookmark (s, l, pageno, pagey) =
414 let pagey = truncate (float pagey *. ratio) in
415 (s, l, pageno, pagey)
417 state.bookmarks <- List.map fixbookmark state.bookmarks;
418 state.w <- w;
419 state.h <- h;
420 GlDraw.viewport 0 0 w h;
421 GlMat.mode `modelview;
422 GlMat.load_identity ();
423 GlMat.mode `projection;
424 GlMat.load_identity ();
425 GlMat.rotate ~x:1.0 ~angle:180.0 ();
426 GlMat.translate ~x:~-.1.0 ~y:~-.1.0 ();
427 GlMat.scale3 (2.0 /. float w, 2.0 /. float state.h, 1.0);
428 GlClear.color (1., 1., 1.);
429 GlClear.clear [`color];
430 state.layout <- [];
431 state.pages <- [];
432 state.rects <- [];
433 state.text <- "";
434 wcmd "geometry" [`i (state.w - conf.scrollw); `i h];
437 let showtext c s =
438 GlDraw.color (0.0, 0.0, 0.0);
439 GlDraw.rect
440 (0.0, float (state.h - 18))
441 (float (state.w - conf.scrollw - 1), float state.h)
443 let font = Glut.BITMAP_8_BY_13 in
444 GlDraw.color (1.0, 1.0, 1.0);
445 GlPix.raster_pos ~x:0.0 ~y:(float (state.h - 5)) ();
446 Glut.bitmapCharacter ~font ~c:(Char.code c);
447 String.iter (fun c -> Glut.bitmapCharacter ~font ~c:(Char.code c)) s;
450 let enttext () =
451 let len = String.length state.text in
452 match state.textentry with
453 | None ->
454 if len > 0 then showtext ' ' state.text
456 | Some (c, text, _, _) ->
457 let s =
458 if len > 0
459 then
460 text ^ " [" ^ state.text ^ "]"
461 else
462 text
464 showtext c s;
467 let act cmd =
468 match cmd.[0] with
469 | 'c' ->
470 state.pages <- [];
471 state.outlines <- Olist []
473 | 'D' ->
474 state.rects <- state.rects1;
475 Glut.postRedisplay ()
477 | 'd' ->
478 state.rects <- state.rects1;
479 Glut.postRedisplay ()
481 | 'C' ->
482 let n = Scanf.sscanf cmd "C %d" (fun n -> n) in
483 state.pagecount <- n;
484 let rely = yratio state.y in
485 let maxy = calcheight () in
486 state.y <- truncate (float maxy *. rely);
487 state.ty <- state.y;
488 let pages = layout state.y state.h in
489 state.layout <- pages;
490 Glut.postRedisplay ();
492 | 't' ->
493 let s = Scanf.sscanf cmd "t %n"
494 (fun n -> String.sub cmd n (String.length cmd - n))
496 Glut.setWindowTitle s
498 | 'T' ->
499 let s = Scanf.sscanf cmd "T %n"
500 (fun n -> String.sub cmd n (String.length cmd - n))
502 state.text <- s;
503 showtext ' ' s;
504 Glut.swapBuffers ();
506 | 'V' ->
507 if conf.verbose
508 then
509 let s = Scanf.sscanf cmd "V %n"
510 (fun n -> String.sub cmd n (String.length cmd - n))
512 state.text <- s;
513 showtext ' ' s;
514 Glut.swapBuffers ();
516 | 'F' ->
517 let pageno, c, x0, y0, x1, y1 =
518 Scanf.sscanf cmd "F %d %d %f %f %f %f"
519 (fun p c x0 y0 x1 y1 -> (p, c, x0, y0, x1, y1))
521 let y = (getpagey pageno) + truncate y0 in
522 addnav ();
523 gotoy y;
524 state.rects1 <- [pageno, c, (x0, y0), (x1, y1)]
526 | 'R' ->
527 let pageno, c, x0, y0, x1, y1 =
528 Scanf.sscanf cmd "R %d %d %f %f %f %f"
529 (fun pageno c x0 y0 x1 y1 -> (pageno, c, x0, y0, x1, y1))
531 state.rects1 <- (pageno, c, (x0, y0), (x1, y1)) :: state.rects1
533 | 'r' ->
534 let n, w, h, r, p =
535 Scanf.sscanf cmd "r %d %d %d %d %s"
536 (fun n w h r p -> (n, w, h, r, p))
538 Hashtbl.replace state.pagemap (n, w, r) p;
539 let evicted = cbpeekw state.pagecache in
540 if String.length evicted > 0
541 then begin
542 wcmd "free" [`s evicted];
543 let l = Hashtbl.fold (fun k p a ->
544 if evicted = p then k :: a else a) state.pagemap []
546 List.iter (fun k -> Hashtbl.remove state.pagemap k) l;
547 end;
548 cbput state.pagecache p;
549 state.inflight <- pred state.inflight;
550 if conf.showall then gotoy state.ty;
551 Glut.postRedisplay ()
553 | 'l' ->
554 let (n, w, h) as pagelayout =
555 Scanf.sscanf cmd "l %d %d %d" (fun n w h -> n, w, h)
557 state.pages <- pagelayout :: state.pages
559 | 'o' ->
560 let (l, n, t, pos) =
561 Scanf.sscanf cmd "o %d %d %d %n" (fun l n t pos -> l, n, t, pos)
563 let s = String.sub cmd pos (String.length cmd - pos) in
564 let outline = (s, l, n, t) in
565 let outlines =
566 match state.outlines with
567 | Olist outlines -> Olist (outline :: outlines)
568 | Oarray _ -> Olist [outline]
569 | Onarrow _ -> Olist [outline]
571 state.outlines <- outlines
573 | _ ->
574 log "unknown cmd `%S'" cmd
577 let idle () =
578 if not conf.redispimm && state.y != state.prevy
579 then (
580 state.prevy <- state.y;
581 Glut.postRedisplay ();
583 else
584 let r, _, _ = Unix.select [state.csock] [] [] 0.001 in
586 begin match r with
587 | [] ->
588 if conf.preload then begin
589 let h = state.h in
590 let y = if state.y < state.h then 0 else state.y - state.h in
591 let pages = layout y (h*3) in
592 List.iter preload pages;
593 end;
594 if conf.autoscroll then begin
595 let y = state.y + conf.scrollincr in
596 let y = if y >= state.maxy then 0 else y in
597 gotoy y;
598 state.text <- "";
599 state.prevy <- state.y;
600 Glut.postRedisplay ();
601 end;
603 | _ ->
604 let cmd = readcmd state.csock in
605 act cmd;
606 end;
609 let search pattern forward =
610 if String.length pattern > 0
611 then
612 let pn, py =
613 match state.layout with
614 | [] -> 0, 0
615 | l :: _ ->
616 l.pageno, (l.pagey + if forward then 0 else 0*l.pagevh)
618 let cmd =
619 let b = makecmd "search"
620 [`b conf.icase; `i pn; `i py; `i (if forward then 1 else 0)]
622 Buffer.add_char b ',';
623 Buffer.add_string b pattern;
624 Buffer.add_char b '\000';
625 Buffer.contents b;
627 writecmd state.csock cmd;
630 let intentry text key =
631 let c = Char.unsafe_chr key in
632 match c with
633 | '0' .. '9' ->
634 let s = "x" in s.[0] <- c;
635 let text = text ^ s in
636 TEcont text
638 | _ ->
639 state.text <- Printf.sprintf "invalid char (%d, `%c')" key c;
640 TEcont text
643 let addchar s c =
644 let b = Buffer.create (String.length s + 1) in
645 Buffer.add_string b s;
646 Buffer.add_char b c;
647 Buffer.contents b;
650 let textentry text key =
651 let c = Char.unsafe_chr key in
652 match c with
653 | _ when key >= 32 && key < 127 ->
654 let text = addchar text c in
655 TEcont text
657 | _ ->
658 log "unhandled key %d char `%c'" key (Char.unsafe_chr key);
659 TEcont text
662 let optentry text key =
663 let btos b = if b then "on" else "off" in
664 let c = Char.unsafe_chr key in
665 match c with
666 | 'r' ->
667 conf.rectsel <- not conf.rectsel;
668 TEdone ("rectsel " ^ (btos conf.rectsel))
670 | 's' ->
671 let ondone s =
672 try conf.scrollincr <- int_of_string s with exc ->
673 state.text <- Printf.sprintf "bad integer `%s': %s"
674 s (Printexc.to_string exc)
676 TEswitch ('#', "", intentry, ondone)
678 | 'R' ->
679 let ondone s =
681 state.rotate <- int_of_string s;
682 wcmd "rotate" [`i state.rotate]
683 with exc ->
684 state.text <- Printf.sprintf "bad integer `%s': %s"
685 s (Printexc.to_string exc)
687 TEswitch ('^', "", intentry, ondone)
689 | 'i' ->
690 conf.icase <- not conf.icase;
691 TEdone ("case insensitive search " ^ (btos conf.icase))
693 | 'p' ->
694 conf.preload <- not conf.preload;
695 TEdone ("preload " ^ (btos conf.preload))
697 | 'd' ->
698 conf.redispimm <- not conf.redispimm;
699 TEdone ("immediate redisplay " ^ (btos conf.redispimm))
701 | 'v' ->
702 conf.verbose <- not conf.verbose;
703 TEdone ("verbose " ^ (btos conf.verbose))
705 | 'h' ->
706 conf.maxhfit <- not conf.maxhfit;
707 state.maxy <- state.maxy + (if conf.maxhfit then -state.h else state.h);
708 TEdone ("maxhfit " ^ (btos conf.maxhfit))
710 | 'c' ->
711 conf.crophack <- not conf.crophack;
712 TEdone ("crophack " ^ btos conf.crophack)
714 | 'a' ->
715 conf.showall <- not conf.showall;
716 TEdone ("showall " ^ btos conf.showall)
718 | _ ->
719 state.text <- Printf.sprintf "bad option %d `%c'" key c;
720 TEstop
723 let maxoutlinerows () = (state.h - 31) / 16;;
725 let enterselector allowdel outlines errmsg =
726 if Array.length outlines = 0
727 then (
728 showtext ' ' errmsg;
729 Glut.swapBuffers ()
731 else
732 let pageno =
733 match state.layout with
734 | [] -> -1
735 | {pageno=pageno} :: rest -> pageno
737 let active =
738 let rec loop n =
739 if n = Array.length outlines
740 then 0
741 else
742 let (_, _, outlinepageno, _) = outlines.(n) in
743 if outlinepageno >= pageno then n else loop (n+1)
745 loop 0
747 state.outline <-
748 Some (allowdel, active, max 0 (active - maxoutlinerows ()), outlines, "");
749 Glut.postRedisplay ();
752 let enteroutlinemode () =
753 let outlines =
754 match state.outlines with
755 | Oarray a -> a
756 | Olist l ->
757 let a = Array.of_list (List.rev l) in
758 state.outlines <- Oarray a;
760 | Onarrow (a, b) -> a
762 enterselector false outlines "Documents has no outline";
765 let enterbookmarkmode () =
766 let bookmarks = Array.of_list state.bookmarks in
767 enterselector true bookmarks "Documents has no bookmarks (yet)";
771 let quickbookmark ?title () =
772 match state.layout with
773 | [] -> ()
774 | l :: _ ->
775 let title =
776 match title with
777 | None ->
778 let sec = Unix.gettimeofday () in
779 let tm = Unix.localtime sec in
780 Printf.sprintf "Quick %d visited (%d/%d/%d %d:%d)"
781 l.pageno
782 tm.Unix.tm_mday
783 tm.Unix.tm_mon
784 (tm.Unix.tm_year + 1900)
785 tm.Unix.tm_hour
786 tm.Unix.tm_min
787 | Some title -> title
789 state.bookmarks <-
790 (title, 0, l.pageno, l.pagey) :: state.bookmarks
793 let viewkeyboard ~key ~x ~y =
794 let enttext te =
795 state.textentry <- te;
796 state.text <- "";
797 enttext ();
798 Glut.postRedisplay ()
800 match state.textentry with
801 | None ->
802 let c = Char.chr key in
803 begin match c with
804 | '\027' | 'q' ->
805 exit 0
807 | '\008' ->
808 let y = getnav () in
809 gotoy y
811 | 'o' ->
812 enteroutlinemode ()
814 | 'u' ->
815 state.rects <- [];
816 state.text <- "";
817 Glut.postRedisplay ()
819 | '/' | '?' ->
820 let ondone isforw s =
821 state.searchpattern <- s;
822 search s isforw
824 enttext (Some (c, "", textentry, ondone (c ='/')))
826 | '+' ->
827 let ondone s =
828 let n =
829 try int_of_string s with exc ->
830 state.text <- Printf.sprintf "bad integer `%s': %s"
831 s (Printexc.to_string exc);
832 max_int
834 if n != max_int
835 then (
836 conf.pagebias <- n;
837 state.text <- "page bias is now " ^ string_of_int n;
840 enttext (Some ('+', "", intentry, ondone))
842 | '-' ->
843 let ondone msg =
844 state.text <- msg;
846 enttext (Some ('-', "", optentry, ondone))
848 | '0' .. '9' ->
849 let ondone s =
850 let n =
851 try int_of_string s with exc ->
852 state.text <- Printf.sprintf "bad integer `%s': %s"
853 s (Printexc.to_string exc);
856 if n >= 0
857 then (
858 addnav ();
859 gotoy (getpagey (n + conf.pagebias - 1))
862 let pageentry text key =
863 match Char.unsafe_chr key with
864 | 'g' -> TEdone text
865 | _ -> intentry text key
867 let text = "x" in text.[0] <- c;
868 enttext (Some (':', text, pageentry, ondone))
870 | 'b' ->
871 conf.scrollw <- if conf.scrollw > 0 then 0 else 5;
872 reshape state.w state.h;
874 | 'a' ->
875 conf.autoscroll <- not conf.autoscroll
877 | 'f' ->
878 begin match state.fullscreen with
879 | None ->
880 state.fullscreen <- Some (state.w, state.h);
881 Glut.fullScreen ()
882 | Some (w, h) ->
883 state.fullscreen <- None;
884 Glut.reshapeWindow ~w ~h
887 | 'g' ->
888 gotoy 0
890 | 'n' ->
891 search state.searchpattern true
893 | 'p' | 'N' ->
894 search state.searchpattern false
896 | 't' ->
897 begin match state.layout with
898 | [] -> ()
899 | l :: _ ->
900 gotoy (state.y - l.pagey);
903 | ' ' ->
904 begin match List.rev state.layout with
905 | [] -> ()
906 | l :: _ ->
907 gotoy (clamp (l.pageh - l.pagey))
910 | '\127' ->
911 begin match state.layout with
912 | [] -> ()
913 | l :: _ ->
914 gotoy (clamp (-l.pageh));
917 | '=' ->
918 let f (fn, ln) l =
919 if fn = -1 then l.pageno, l.pageno else fn, l.pageno
921 let fn, ln = List.fold_left f (-1, -1) state.layout in
922 let s =
923 let maxy = state.maxy - (if conf.maxhfit then state.h else 0) in
924 let percent =
925 if maxy <= 0
926 then 100.
927 else (100. *. (float state.y /. float maxy)) in
928 if fn = ln
929 then
930 Printf.sprintf "Page %d of %d %.2f%%"
931 (fn+1) state.pagecount percent
932 else
933 Printf.sprintf
934 "Pages %d-%d of %d %.2f%%"
935 (fn+1) (ln+1) state.pagecount percent
937 showtext ' ' s;
938 Glut.swapBuffers ()
940 | 'w' ->
941 begin match state.layout with
942 | [] -> ()
943 | l :: _ ->
944 Glut.reshapeWindow (l.pagew + conf.scrollw) l.pageh;
945 Glut.postRedisplay ();
948 | '\'' ->
949 enterbookmarkmode ()
951 | 'm' ->
952 let ondone s =
953 match state.layout with
954 | l :: _ ->
955 state.bookmarks <- (s, 0, l.pageno, l.pagey) :: state.bookmarks
956 | _ -> ()
958 enttext (Some ('~', "", textentry, ondone))
960 | '~' ->
961 quickbookmark ();
962 showtext ' ' "Quick bookmark added";
963 Glut.swapBuffers ()
965 | 'z' ->
966 begin match state.layout with
967 | l :: _ ->
968 let a = getpagewh l.pagedimno in
969 let w, h =
970 if conf.crophack
971 then
972 (truncate (1.8 *. (a.(1) -. a.(0))),
973 truncate (1.4 *. (a.(3) -. a.(0))))
974 else
975 (truncate (a.(1) -. a.(0)),
976 truncate (a.(3) -. a.(0)))
978 Glut.reshapeWindow (w + conf.scrollw) h;
979 Glut.postRedisplay ();
981 | [] -> ()
984 | '<' | '>' ->
985 state.rotate <- state.rotate + (if c = '>' then 30 else -30);
986 wcmd "rotate" [`i state.rotate]
988 | _ ->
989 vlog "huh? %d %c" key (Char.chr key);
992 | Some (c, text, onkey, ondone) when key = 8 ->
993 let len = String.length text in
994 if len = 0
995 then (
996 state.textentry <- None;
997 Glut.postRedisplay ();
999 else (
1000 let s = String.sub text 0 (len - 1) in
1001 enttext (Some (c, s, onkey, ondone))
1004 | Some (c, text, onkey, ondone) ->
1005 begin match Char.unsafe_chr key with
1006 | '\r' | '\n' ->
1007 ondone text;
1008 state.textentry <- None;
1009 Glut.postRedisplay ()
1011 | '\027' ->
1012 state.textentry <- None;
1013 Glut.postRedisplay ()
1015 | _ ->
1016 begin match onkey text key with
1017 | TEdone text ->
1018 state.textentry <- None;
1019 ondone text;
1020 Glut.postRedisplay ()
1022 | TEcont text ->
1023 enttext (Some (c, text, onkey, ondone));
1025 | TEstop ->
1026 state.textentry <- None;
1027 Glut.postRedisplay ()
1029 | TEswitch te ->
1030 state.textentry <- Some te;
1031 Glut.postRedisplay ()
1032 end;
1033 end;
1036 let narrow outlines pattern =
1037 let reopt = try Some (Str.regexp_case_fold pattern) with _ -> None in
1038 match reopt with
1039 | None -> None
1040 | Some re ->
1041 let rec fold accu n =
1042 if n = -1 then accu else
1043 let (s, _, _, _) as o = outlines.(n) in
1044 let accu =
1045 if (try ignore (Str.search_forward re s 0); true
1046 with Not_found -> false)
1047 then (o :: accu)
1048 else accu
1050 fold accu (n-1)
1052 let matched = fold [] (Array.length outlines - 1) in
1053 if matched = [] then None else Some (Array.of_list matched)
1056 let outlinekeyboard ~key ~x ~y (allowdel, active, first, outlines, qsearch) =
1057 let search active pattern incr =
1058 let dosearch re =
1059 let rec loop n =
1060 if n = Array.length outlines || n = -1 then None else
1061 let (s, _, _, _) = outlines.(n) in
1063 (try ignore (Str.search_forward re s 0); true
1064 with Not_found -> false)
1065 then (
1066 let maxrows = (min (Array.length outlines) (maxoutlinerows ())) / 2 in
1067 if first > n
1068 then Some (n, max 0 (n - maxrows))
1069 else Some (n, max first (n - maxrows))
1071 else loop (n + incr)
1073 loop active
1076 let re = Str.regexp_case_fold pattern in
1077 dosearch re
1078 with Failure s ->
1079 state.text <- s;
1080 None
1082 match key with
1083 | 27 ->
1084 if String.length qsearch = 0
1085 then (
1086 state.text <- "";
1087 state.outline <- None;
1088 Glut.postRedisplay ();
1090 else (
1091 state.text <- "";
1092 state.outline <- Some (allowdel, active, first, outlines, "");
1093 Glut.postRedisplay ();
1096 | 18 | 19 ->
1097 let incr = if key = 18 then -1 else 1 in
1098 let active, first =
1099 match search (active + incr) qsearch incr with
1100 | None ->
1101 state.text <- qsearch ^ " [not found]";
1102 active, first
1103 | Some af ->
1104 state.text <- qsearch;
1107 state.outline <- Some (allowdel, active, first, outlines, qsearch);
1108 Glut.postRedisplay ();
1110 | 8 ->
1111 let len = String.length qsearch in
1112 if len = 0
1113 then ()
1114 else (
1115 if len = 1
1116 then (
1117 state.text <- "";
1118 state.outline <- Some (allowdel, active, first, outlines, "");
1120 else
1121 let qsearch = String.sub qsearch 0 (len - 1) in
1122 let active, first =
1123 match search active qsearch ~-1 with
1124 | None ->
1125 state.text <- qsearch ^ " [not found]";
1126 active, first
1127 | Some af ->
1128 state.text <- qsearch;
1131 state.outline <- Some (allowdel, active, first, outlines, qsearch);
1133 Glut.postRedisplay ()
1135 | 13 ->
1136 if active < Array.length outlines
1137 then (
1138 let (_, _, n, t) = outlines.(active) in
1139 gotopage n t;
1141 state.text <- "";
1142 if allowdel then state.bookmarks <- Array.to_list outlines;
1143 state.outline <- None;
1144 Glut.postRedisplay ();
1146 | _ when key >= 32 && key < 127 ->
1147 let pattern = addchar qsearch (Char.chr key) in
1148 let active, first =
1149 match search active pattern 1 with
1150 | None ->
1151 state.text <- pattern ^ " [not found]";
1152 active, first
1153 | Some (active, first) ->
1154 state.text <- pattern;
1155 active, first
1157 state.outline <- Some (allowdel, active, first, outlines, pattern);
1158 Glut.postRedisplay ()
1160 | 14 when not allowdel ->
1161 let optoutlines = narrow outlines qsearch in
1162 begin match optoutlines with
1163 | None -> state.text <- "can't narrow"
1164 | Some outlines ->
1165 state.outline <- Some (allowdel, 0, 0, outlines, qsearch);
1166 match state.outlines with
1167 | Olist l -> ()
1168 | Oarray a -> state.outlines <- Onarrow (outlines, a)
1169 | Onarrow (a, b) -> state.outlines <- Onarrow (outlines, b)
1170 end;
1171 Glut.postRedisplay ()
1173 | 21 when not allowdel ->
1174 let outline =
1175 match state.outlines with
1176 | Oarray a -> a
1177 | Olist l ->
1178 let a = Array.of_list (List.rev l) in
1179 state.outlines <- Oarray a;
1181 | Onarrow (a, b) -> b
1183 state.outline <- Some (allowdel, 0, 0, outline, qsearch);
1184 Glut.postRedisplay ()
1186 | 127 when allowdel ->
1187 let len = Array.length outlines - 1 in
1188 if len = 0
1189 then (
1190 state.outline <- None;
1191 state.bookmarks <- [];
1193 else (
1194 let bookmarks = Array.init len
1195 (fun i ->
1196 let i = if i >= active then i + 1 else i in
1197 outlines.(i)
1200 state.outline <-
1201 Some (allowdel,
1202 min active (len-1),
1203 min first (len-1),
1204 bookmarks, qsearch)
1207 Glut.postRedisplay ()
1209 | _ -> log "unknown key %d" key
1212 let keyboard ~key ~x ~y =
1213 match state.outline with
1214 | None -> viewkeyboard ~key ~x ~y
1215 | Some outline -> outlinekeyboard ~key ~x ~y outline
1218 let special ~key ~x ~y =
1219 match state.outline with
1220 | None ->
1221 let y =
1222 match key with
1223 | Glut.KEY_F3 -> search state.searchpattern true; state.y
1224 | Glut.KEY_UP -> clamp (-conf.scrollincr)
1225 | Glut.KEY_DOWN -> clamp conf.scrollincr
1226 | Glut.KEY_PAGE_UP -> clamp (-state.h)
1227 | Glut.KEY_PAGE_DOWN -> clamp state.h
1228 | Glut.KEY_HOME -> addnav (); 0
1229 | Glut.KEY_END ->
1230 addnav ();
1231 state.maxy - (if conf.maxhfit then state.h else 0)
1232 | _ -> state.y
1234 state.text <- "";
1235 gotoy y
1237 | Some (allowdel, active, first, outlines, qsearch) ->
1238 let maxrows = maxoutlinerows () in
1239 let navigate incr =
1240 let active = active + incr in
1241 let active = max 0 (min active (Array.length outlines - 1)) in
1242 let first =
1243 if active > first
1244 then
1245 let rows = active - first in
1246 if rows > maxrows then first + incr else first
1247 else active
1249 state.outline <- Some (allowdel, active, first, outlines, qsearch);
1250 Glut.postRedisplay ()
1252 match key with
1253 | Glut.KEY_UP -> navigate ~-1
1254 | Glut.KEY_DOWN -> navigate 1
1255 | Glut.KEY_PAGE_UP -> navigate ~-maxrows
1256 | Glut.KEY_PAGE_DOWN -> navigate maxrows
1258 | Glut.KEY_HOME ->
1259 state.outline <- Some (allowdel, 0, 0, outlines, qsearch);
1260 Glut.postRedisplay ()
1262 | Glut.KEY_END ->
1263 let active = Array.length outlines - 1 in
1264 let first = max 0 (active - maxrows) in
1265 state.outline <- Some (allowdel, active, first, outlines, qsearch);
1266 Glut.postRedisplay ()
1268 | _ -> ()
1271 let drawplaceholder l =
1272 GlDraw.color (1.0, 1.0, 1.0);
1273 GlDraw.rect
1274 (0.0, float l.pagedispy)
1275 (float l.pagew, float (l.pagedispy + l.pagevh))
1277 let x = 0.0
1278 and y = float (l.pagedispy + 13) in
1279 let font = Glut.BITMAP_8_BY_13 in
1280 GlDraw.color (0.0, 0.0, 0.0);
1281 GlPix.raster_pos ~x ~y ();
1282 String.iter (fun c -> Glut.bitmapCharacter ~font ~c:(Char.code c))
1283 ("Loading " ^ string_of_int l.pageno);
1286 let now () = Unix.gettimeofday ();;
1288 let drawpage i l =
1289 begin match getopaque l.pageno with
1290 | Some opaque when validopaque opaque ->
1291 if state.textentry = None
1292 then GlDraw.color (1.0, 1.0, 1.0)
1293 else GlDraw.color (0.4, 0.4, 0.4);
1294 let a = now () in
1295 draw l.pagedispy l.pagew l.pagevh l.pagey opaque;
1296 let b = now () in
1297 let d = b-.a in
1298 vlog "draw %f sec" d;
1300 | Some _ ->
1301 drawplaceholder l
1303 | None ->
1304 drawplaceholder l;
1305 if state.inflight < cblen state.pagecache
1306 then (
1307 List.iter preload state.layout;
1309 else (
1310 vlog "inflight %d" state.inflight;
1312 end;
1313 GlDraw.color (0.5, 0.5, 0.5);
1314 GlDraw.rect
1315 (0., float i)
1316 (float (state.w - conf.scrollw), float (i + (l.pagedispy - i)))
1318 l.pagedispy + l.pagevh;
1321 let scrollindicator () =
1322 let maxy = state.maxy - (if conf.maxhfit then state.h else 0) in
1323 GlDraw.color (0.64 , 0.64, 0.64);
1324 GlDraw.rect
1325 (float (state.w - conf.scrollw), 0.)
1326 (float state.w, float state.h)
1328 GlDraw.color (0.0, 0.0, 0.0);
1329 let sh = (float (maxy + state.h) /. float state.h) in
1330 let sh = float state.h /. sh in
1331 let sh = max sh (float conf.scrollh) in
1333 let percent =
1334 if state.y = state.maxy
1335 then 1.0
1336 else float state.y /. float maxy
1338 let position = (float state.h -. sh) *. percent in
1340 let position =
1341 if position +. sh > float state.h
1342 then
1343 float state.h -. sh
1344 else
1345 position
1347 GlDraw.rect
1348 (float (state.w - conf.scrollw), position)
1349 (float state.w, position +. sh)
1353 let showsel () =
1354 match state.mstate with
1355 | Mnone ->
1358 | Msel ((x0, y0), (x1, y1)) ->
1359 let y0' = min y0 y1
1360 and y1 = max y0 y1 in
1361 let y0 = y0' in
1362 let f l =
1363 if (y0 >= l.pagedispy && y0 <= (l.pagedispy + l.pagevh))
1364 || ((y1 >= l.pagedispy)) (* && y1 <= (dy + vh))) *)
1365 then
1366 match getopaque l.pageno with
1367 | Some opaque when validopaque opaque ->
1368 let oy = -l.pagey + l.pagedispy in
1369 gettext opaque (min x0 x1, y0, max x1 x0, y1) oy conf.rectsel
1370 | _ -> ()
1372 List.iter f state.layout
1375 let showrects () =
1376 Gl.enable `blend;
1377 GlDraw.color (0.0, 0.0, 1.0) ~alpha:0.5;
1378 GlFunc.blend_func `src_alpha `one_minus_src_alpha;
1379 List.iter
1380 (fun (pageno, c, (x0, y0), (x1, y1)) ->
1381 List.iter (fun l ->
1382 if l.pageno = pageno
1383 then (
1384 let d = float (l.pagedispy - l.pagey) in
1385 GlDraw.color (0.0, 0.0, 1.0 /. float c) ~alpha:0.5;
1386 GlDraw.rect (x0, y0 +. d) (x1, y1 +. d)
1388 ) state.layout
1389 ) state.rects
1391 Gl.disable `blend;
1394 let showoutline = function
1395 | None -> ()
1396 | Some (allowdel, active, first, outlines, qsearch) ->
1397 Gl.enable `blend;
1398 GlFunc.blend_func `src_alpha `one_minus_src_alpha;
1399 GlDraw.color (0., 0., 0.) ~alpha:0.85;
1400 GlDraw.rect (0., 0.) (float state.w, float state.h);
1401 Gl.disable `blend;
1403 GlDraw.color (1., 1., 1.);
1404 let font = Glut.BITMAP_9_BY_15 in
1405 let draw_string x y s =
1406 GlPix.raster_pos ~x ~y ();
1407 String.iter (fun c -> Glut.bitmapCharacter ~font ~c:(Char.code c)) s
1409 let rec loop row =
1410 if row = Array.length outlines || (row - first) * 16 > state.h
1411 then ()
1412 else (
1413 let (s, l, _, _) = outlines.(row) in
1414 let y = (row - first) * 16 in
1415 let x = 5 + 5*l in
1416 if row = active
1417 then (
1418 Gl.enable `blend;
1419 GlDraw.polygon_mode `both `line;
1420 GlFunc.blend_func `src_alpha `one_minus_src_alpha;
1421 GlDraw.color (1., 1., 1.) ~alpha:0.9;
1422 GlDraw.rect (0., float (y + 1))
1423 (float (state.w - conf.scrollw - 1), float (y + 18));
1424 GlDraw.polygon_mode `both `fill;
1425 Gl.disable `blend;
1426 GlDraw.color (1., 1., 1.);
1428 draw_string (float x) (float (y + 16)) s;
1429 loop (row+1)
1432 loop first
1435 let display () =
1436 let lasty = List.fold_left drawpage 0 (state.layout) in
1437 GlDraw.color (0.5, 0.5, 0.5);
1438 GlDraw.rect
1439 (0., float lasty)
1440 (float (state.w - conf.scrollw), float state.h)
1442 showrects ();
1443 scrollindicator ();
1444 showsel ();
1445 showoutline state.outline;
1446 enttext ();
1447 Glut.swapBuffers ();
1450 let getlink x y =
1451 let rec f = function
1452 | l :: rest ->
1453 begin match getopaque l.pageno with
1454 | Some opaque when validopaque opaque ->
1455 let y = y - l.pagedispy in
1456 if y > 0
1457 then
1458 let y = l.pagey + y in
1459 match getlink opaque x y with
1460 | None -> f rest
1461 | some -> some
1462 else
1463 f rest
1464 | _ ->
1465 f rest
1467 | [] -> None
1469 f state.layout
1472 let checklink x y =
1473 let rec f = function
1474 | l :: rest ->
1475 begin match getopaque l.pageno with
1476 | Some opaque when validopaque opaque ->
1477 let y = y - l.pagedispy in
1478 if y > 0
1479 then
1480 let y = l.pagey + y in
1481 if checklink opaque x y then true else f rest
1482 else
1483 f rest
1484 | _ ->
1485 f rest
1487 | [] -> false
1489 f state.layout
1492 let mouse ~button ~bstate ~x ~y =
1493 match button with
1494 | Glut.OTHER_BUTTON n when n == 3 || n == 4 && bstate = Glut.UP ->
1495 let incr =
1496 if n = 3
1497 then
1498 -conf.scrollincr
1499 else
1500 conf.scrollincr
1502 let incr = incr * 2 in
1503 let y = clamp incr in
1504 gotoy y
1506 | Glut.LEFT_BUTTON when state.outline = None ->
1507 let dest = if bstate = Glut.DOWN then getlink x y else None in
1508 begin match dest with
1509 | Some (pageno, top) ->
1510 gotopage pageno top
1512 | None ->
1513 if bstate = Glut.DOWN
1514 then (
1515 Glut.setCursor Glut.CURSOR_CROSSHAIR;
1516 state.mstate <- Msel ((x, y), (x, y));
1517 Glut.postRedisplay ()
1519 else (
1520 Glut.setCursor Glut.CURSOR_INHERIT;
1521 state.mstate <- Mnone;
1525 | _ ->
1528 let mouse ~button ~state ~x ~y = mouse button state x y;;
1530 let motion ~x ~y =
1531 if state.outline = None
1532 then
1533 match state.mstate with
1534 | Mnone -> ()
1535 | Msel (a, _) ->
1536 state.mstate <- Msel (a, (x, y));
1537 Glut.postRedisplay ()
1540 let pmotion ~x ~y =
1541 if state.outline = None
1542 then
1543 match state.mstate with
1544 | Mnone when (checklink x y) ->
1545 Glut.setCursor Glut.CURSOR_INFO
1547 | Mnone ->
1548 Glut.setCursor Glut.CURSOR_INHERIT
1550 | Msel (a, _) ->
1554 let () =
1555 let statepath =
1556 let home =
1557 if Sys.os_type = "Win32"
1558 then
1559 try Sys.getenv "HOMEPATH" with Not_found -> ""
1560 else
1561 try Filename.concat (Sys.getenv "HOME") ".config" with Not_found -> ""
1563 Filename.concat home "llpp"
1565 let pstate =
1567 let ic = open_in_bin statepath in
1568 let hash = input_value ic in
1569 close_in ic;
1570 hash
1571 with exn ->
1572 if false
1573 then
1574 prerr_endline ("Error loading state " ^ Printexc.to_string exn)
1576 Hashtbl.create 1
1578 let savestate () =
1580 let w, h =
1581 match state.fullscreen with
1582 | None -> state.w, state.h
1583 | Some wh -> wh
1585 Hashtbl.replace pstate state.path (state.bookmarks, w, h);
1586 let oc = open_out_bin statepath in
1587 output_value oc pstate
1588 with exn ->
1589 if false
1590 then
1591 prerr_endline ("Error saving state " ^ Printexc.to_string exn)
1594 let setstate () =
1596 let statebookmarks, statew, stateh = Hashtbl.find pstate state.path in
1597 state.w <- statew;
1598 state.h <- stateh;
1599 state.bookmarks <- statebookmarks;
1600 with Not_found -> ()
1601 | exn ->
1602 prerr_endline ("Error setting state " ^ Printexc.to_string exn)
1605 Arg.parse [] (fun s -> state.path <- s) "options:";
1606 let name =
1607 if String.length state.path = 0
1608 then (prerr_endline "filename missing"; exit 1)
1609 else state.path
1612 setstate ();
1613 let _ = Glut.init Sys.argv in
1614 let () = Glut.initDisplayMode ~depth:false ~double_buffer:true () in
1615 let () = Glut.initWindowSize state.w state.h in
1616 let _ = Glut.createWindow ("llpp " ^ Filename.basename name) in
1618 let csock, ssock =
1619 if Sys.os_type = "Unix"
1620 then
1621 Unix.socketpair Unix.PF_UNIX Unix.SOCK_STREAM 0
1622 else
1623 let addr = Unix.ADDR_INET (Unix.inet_addr_loopback, 1337) in
1624 let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
1625 Unix.setsockopt sock Unix.SO_REUSEADDR true;
1626 Unix.bind sock addr;
1627 Unix.listen sock 1;
1628 let csock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
1629 Unix.connect csock addr;
1630 let ssock, _ = Unix.accept sock in
1631 Unix.close sock;
1632 let opts sock =
1633 Unix.setsockopt sock Unix.TCP_NODELAY true;
1634 Unix.setsockopt_optint sock Unix.SO_LINGER None;
1636 opts ssock;
1637 opts csock;
1638 at_exit (fun () -> Unix.shutdown ssock Unix.SHUTDOWN_ALL);
1639 ssock, csock
1642 let () = Glut.displayFunc display in
1643 let () = Glut.reshapeFunc reshape in
1644 let () = Glut.keyboardFunc keyboard in
1645 let () = Glut.specialFunc special in
1646 let () = Glut.idleFunc (Some idle) in
1647 let () = Glut.mouseFunc mouse in
1648 let () = Glut.motionFunc motion in
1649 let () = Glut.passiveMotionFunc pmotion in
1651 init ssock;
1652 state.csock <- csock;
1653 state.ssock <- ssock;
1654 writecmd csock ("open " ^ name ^ "\000");
1656 at_exit savestate;
1658 let rec handlelablglutbug () =
1660 Glut.mainLoop ();
1661 with Glut.BadEnum "key in special_of_int" ->
1662 showtext '!' " LablGlut bug: special key not recognized";
1663 Glut.swapBuffers ();
1664 handlelablglutbug ()
1666 handlelablglutbug ();