Allow narrowing outlines
[llpp.git] / main.ml
blob47f1532ba960e30fb06d09aaa0c3a159abc37f50
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 state.y <- y + top;
409 gotoy state.y;
412 let reshape ~w ~h =
413 let ratio = float w /. float state.w in
414 let fixbookmark (s, l, pageno, pagey) =
415 let pagey = truncate (float pagey *. ratio) in
416 (s, l, pageno, pagey)
418 state.bookmarks <- List.map fixbookmark state.bookmarks;
419 state.w <- w;
420 state.h <- h;
421 GlDraw.viewport 0 0 w h;
422 GlMat.mode `modelview;
423 GlMat.load_identity ();
424 GlMat.mode `projection;
425 GlMat.load_identity ();
426 GlMat.rotate ~x:1.0 ~angle:180.0 ();
427 GlMat.translate ~x:~-.1.0 ~y:~-.1.0 ();
428 GlMat.scale3 (2.0 /. float w, 2.0 /. float state.h, 1.0);
429 GlClear.color (1., 1., 1.);
430 GlClear.clear [`color];
431 state.layout <- [];
432 state.pages <- [];
433 state.rects <- [];
434 state.text <- "";
435 wcmd "geometry" [`i (state.w - conf.scrollw); `i h];
438 let showtext c s =
439 GlDraw.color (0.0, 0.0, 0.0);
440 GlDraw.rect
441 (0.0, float (state.h - 18))
442 (float (state.w - conf.scrollw - 1), float state.h)
444 let font = Glut.BITMAP_8_BY_13 in
445 GlDraw.color (1.0, 1.0, 1.0);
446 GlPix.raster_pos ~x:0.0 ~y:(float (state.h - 5)) ();
447 Glut.bitmapCharacter ~font ~c:(Char.code c);
448 String.iter (fun c -> Glut.bitmapCharacter ~font ~c:(Char.code c)) s;
451 let enttext () =
452 let len = String.length state.text in
453 match state.textentry with
454 | None ->
455 if len > 0 then showtext ' ' state.text
457 | Some (c, text, _, _) ->
458 let s =
459 if len > 0
460 then
461 text ^ " [" ^ state.text ^ "]"
462 else
463 text
465 showtext c s;
468 let act cmd =
469 match cmd.[0] with
470 | 'c' ->
471 state.pages <- [];
472 state.outlines <- Olist []
474 | 'D' ->
475 state.rects <- state.rects1;
476 Glut.postRedisplay ()
478 | 'd' ->
479 state.rects <- state.rects1;
480 Glut.postRedisplay ()
482 | 'C' ->
483 let n = Scanf.sscanf cmd "C %d" (fun n -> n) in
484 state.pagecount <- n;
485 let rely = yratio state.y in
486 let maxy = calcheight () in
487 state.y <- truncate (float maxy *. rely);
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 state.y <- y;
860 gotoy (getpagey (n + conf.pagebias - 1))
863 let pageentry text key =
864 match Char.unsafe_chr key with
865 | 'g' -> TEdone text
866 | _ -> intentry text key
868 let text = "x" in text.[0] <- c;
869 enttext (Some (':', text, pageentry, ondone))
871 | 'b' ->
872 conf.scrollw <- if conf.scrollw > 0 then 0 else 5;
873 reshape state.w state.h;
875 | 'a' ->
876 conf.autoscroll <- not conf.autoscroll
878 | 'f' ->
879 begin match state.fullscreen with
880 | None ->
881 state.fullscreen <- Some (state.w, state.h);
882 Glut.fullScreen ()
883 | Some (w, h) ->
884 state.fullscreen <- None;
885 Glut.reshapeWindow ~w ~h
888 | 'g' ->
889 gotoy 0
891 | 'n' ->
892 search state.searchpattern true
894 | 'p' | 'N' ->
895 search state.searchpattern false
897 | 't' ->
898 begin match state.layout with
899 | [] -> ()
900 | l :: _ ->
901 gotoy (state.y - l.pagey);
904 | ' ' ->
905 begin match List.rev state.layout with
906 | [] -> ()
907 | l :: _ ->
908 gotoy (clamp (l.pageh - l.pagey))
911 | '\127' ->
912 begin match state.layout with
913 | [] -> ()
914 | l :: _ ->
915 gotoy (clamp (-l.pageh));
918 | '=' ->
919 let f (fn, ln) l =
920 if fn = -1 then l.pageno, l.pageno else fn, l.pageno
922 let fn, ln = List.fold_left f (-1, -1) state.layout in
923 let s =
924 let maxy = state.maxy - (if conf.maxhfit then state.h else 0) in
925 let percent =
926 if maxy <= 0
927 then 100.
928 else (100. *. (float state.y /. float maxy)) in
929 if fn = ln
930 then
931 Printf.sprintf "Page %d of %d %.2f%%"
932 (fn+1) state.pagecount percent
933 else
934 Printf.sprintf
935 "Pages %d-%d of %d %.2f%%"
936 (fn+1) (ln+1) state.pagecount percent
938 showtext ' ' s;
939 Glut.swapBuffers ()
941 | 'w' ->
942 begin match state.layout with
943 | [] -> ()
944 | l :: _ ->
945 Glut.reshapeWindow (l.pagew + conf.scrollw) l.pageh;
946 Glut.postRedisplay ();
949 | '\'' ->
950 enterbookmarkmode ()
952 | 'm' ->
953 let ondone s =
954 match state.layout with
955 | l :: _ ->
956 state.bookmarks <- (s, 0, l.pageno, l.pagey) :: state.bookmarks
957 | _ -> ()
959 enttext (Some ('~', "", textentry, ondone))
961 | '~' ->
962 quickbookmark ();
963 showtext ' ' "Quick bookmark added";
964 Glut.swapBuffers ()
966 | 'z' ->
967 begin match state.layout with
968 | l :: _ ->
969 let a = getpagewh l.pagedimno in
970 let w, h =
971 if conf.crophack
972 then
973 (truncate (1.8 *. (a.(1) -. a.(0))),
974 truncate (1.4 *. (a.(3) -. a.(0))))
975 else
976 (truncate (a.(1) -. a.(0)),
977 truncate (a.(3) -. a.(0)))
979 Glut.reshapeWindow (w + conf.scrollw) h;
980 Glut.postRedisplay ();
982 | [] -> ()
985 | '<' | '>' ->
986 state.rotate <- state.rotate + (if c = '>' then 30 else -30);
987 wcmd "rotate" [`i state.rotate]
989 | _ ->
990 vlog "huh? %d %c" key (Char.chr key);
993 | Some (c, text, onkey, ondone) when key = 8 ->
994 let len = String.length text in
995 if len = 0
996 then (
997 state.textentry <- None;
998 Glut.postRedisplay ();
1000 else (
1001 let s = String.sub text 0 (len - 1) in
1002 enttext (Some (c, s, onkey, ondone))
1005 | Some (c, text, onkey, ondone) ->
1006 begin match Char.unsafe_chr key with
1007 | '\r' | '\n' ->
1008 ondone text;
1009 state.textentry <- None;
1010 Glut.postRedisplay ()
1012 | '\027' ->
1013 state.textentry <- None;
1014 Glut.postRedisplay ()
1016 | _ ->
1017 begin match onkey text key with
1018 | TEdone text ->
1019 state.textentry <- None;
1020 ondone text;
1021 Glut.postRedisplay ()
1023 | TEcont text ->
1024 enttext (Some (c, text, onkey, ondone));
1026 | TEstop ->
1027 state.textentry <- None;
1028 Glut.postRedisplay ()
1030 | TEswitch te ->
1031 state.textentry <- Some te;
1032 Glut.postRedisplay ()
1033 end;
1034 end;
1037 let narrow outlines pattern =
1038 let reopt = try Some (Str.regexp_case_fold pattern) with _ -> None in
1039 match reopt with
1040 | None -> None
1041 | Some re ->
1042 let rec fold accu n =
1043 if n = -1 then accu else
1044 let (s, _, _, _) as o = outlines.(n) in
1045 let accu =
1046 if (try ignore (Str.search_forward re s 0); true
1047 with Not_found -> false)
1048 then (o :: accu)
1049 else accu
1051 fold accu (n-1)
1053 let matched = fold [] (Array.length outlines - 1) in
1054 if matched = [] then None else Some (Array.of_list matched)
1057 let outlinekeyboard ~key ~x ~y (allowdel, active, first, outlines, qsearch) =
1058 let search active pattern incr =
1059 let dosearch re =
1060 let rec loop n =
1061 if n = Array.length outlines || n = -1 then None else
1062 let (s, _, _, _) = outlines.(n) in
1064 (try ignore (Str.search_forward re s 0); true
1065 with Not_found -> false)
1066 then (
1067 let maxrows = (min (Array.length outlines) (maxoutlinerows ())) / 2 in
1068 if first > n
1069 then Some (n, max 0 (n - maxrows))
1070 else Some (n, max first (n - maxrows))
1072 else loop (n + incr)
1074 loop active
1077 let re = Str.regexp_case_fold pattern in
1078 dosearch re
1079 with Failure s ->
1080 state.text <- s;
1081 None
1083 match key with
1084 | 27 ->
1085 if String.length qsearch = 0
1086 then (
1087 state.text <- "";
1088 state.outline <- None;
1089 Glut.postRedisplay ();
1091 else (
1092 state.text <- "";
1093 state.outline <- Some (allowdel, active, first, outlines, "");
1094 Glut.postRedisplay ();
1097 | 18 | 19 ->
1098 let incr = if key = 18 then -1 else 1 in
1099 let active, first =
1100 match search (active + incr) qsearch incr with
1101 | None ->
1102 state.text <- qsearch ^ " [not found]";
1103 active, first
1104 | Some af ->
1105 state.text <- qsearch;
1108 state.outline <- Some (allowdel, active, first, outlines, qsearch);
1109 Glut.postRedisplay ();
1111 | 8 ->
1112 let len = String.length qsearch in
1113 if len = 0
1114 then ()
1115 else (
1116 if len = 1
1117 then (
1118 state.text <- "";
1119 state.outline <- Some (allowdel, active, first, outlines, "");
1121 else
1122 let qsearch = String.sub qsearch 0 (len - 1) in
1123 let active, first =
1124 match search active qsearch ~-1 with
1125 | None ->
1126 state.text <- qsearch ^ " [not found]";
1127 active, first
1128 | Some af ->
1129 state.text <- qsearch;
1132 state.outline <- Some (allowdel, active, first, outlines, qsearch);
1134 Glut.postRedisplay ()
1136 | 13 ->
1137 if active < Array.length outlines
1138 then (
1139 let (_, _, n, t) = outlines.(active) in
1140 gotopage n t;
1142 state.text <- "";
1143 if allowdel then state.bookmarks <- Array.to_list outlines;
1144 state.outline <- None;
1145 Glut.postRedisplay ();
1147 | _ when key >= 32 && key < 127 ->
1148 let pattern = addchar qsearch (Char.chr key) in
1149 let active, first =
1150 match search active pattern 1 with
1151 | None ->
1152 state.text <- pattern ^ " [not found]";
1153 active, first
1154 | Some (active, first) ->
1155 state.text <- pattern;
1156 active, first
1158 state.outline <- Some (allowdel, active, first, outlines, pattern);
1159 Glut.postRedisplay ()
1161 | 14 ->
1162 let optoutlines = narrow outlines qsearch in
1163 begin match optoutlines with
1164 | None -> state.text <- "can't narrow"
1165 | Some outlines ->
1166 state.outline <- Some (allowdel, 0, 0, outlines, qsearch);
1167 match state.outlines with
1168 | Olist l -> ()
1169 | Oarray a -> state.outlines <- Onarrow (outlines, a)
1170 | Onarrow (a, b) -> state.outlines <- Onarrow (outlines, b)
1171 end;
1172 Glut.postRedisplay ()
1174 | 21 ->
1175 let outline =
1176 match state.outlines with
1177 | Oarray a -> a
1178 | Olist l ->
1179 let a = Array.of_list (List.rev l) in
1180 state.outlines <- Oarray a;
1182 | Onarrow (a, b) -> b
1184 state.outline <- Some (allowdel, 0, 0, outline, qsearch);
1185 Glut.postRedisplay ()
1187 | 127 when allowdel ->
1188 let len = Array.length outlines - 1 in
1189 if len = 0
1190 then (
1191 state.outline <- None;
1192 state.bookmarks <- [];
1194 else (
1195 let bookmarks = Array.init len
1196 (fun i ->
1197 let i = if i >= active then i + 1 else i in
1198 outlines.(i)
1201 state.outline <-
1202 Some (allowdel,
1203 min active (len-1),
1204 min first (len-1),
1205 bookmarks, qsearch)
1208 Glut.postRedisplay ()
1210 | _ -> log "unknown key %d" key
1213 let keyboard ~key ~x ~y =
1214 match state.outline with
1215 | None -> viewkeyboard ~key ~x ~y
1216 | Some outline -> outlinekeyboard ~key ~x ~y outline
1219 let special ~key ~x ~y =
1220 match state.outline with
1221 | None ->
1222 let y =
1223 match key with
1224 | Glut.KEY_F3 -> search state.searchpattern true; state.y
1225 | Glut.KEY_UP -> clamp (-conf.scrollincr)
1226 | Glut.KEY_DOWN -> clamp conf.scrollincr
1227 | Glut.KEY_PAGE_UP -> clamp (-state.h)
1228 | Glut.KEY_PAGE_DOWN -> clamp state.h
1229 | Glut.KEY_HOME -> addnav (); 0
1230 | Glut.KEY_END ->
1231 addnav ();
1232 state.maxy - (if conf.maxhfit then state.h else 0)
1233 | _ -> state.y
1235 state.text <- "";
1236 gotoy y
1238 | Some (allowdel, active, first, outlines, qsearch) ->
1239 let maxrows = maxoutlinerows () in
1240 let navigate incr =
1241 let active = active + incr in
1242 let active = max 0 (min active (Array.length outlines - 1)) in
1243 let first =
1244 if active > first
1245 then
1246 let rows = active - first in
1247 if rows > maxrows then first + incr else first
1248 else active
1250 state.outline <- Some (allowdel, active, first, outlines, qsearch);
1251 Glut.postRedisplay ()
1253 match key with
1254 | Glut.KEY_UP -> navigate ~-1
1255 | Glut.KEY_DOWN -> navigate 1
1256 | Glut.KEY_PAGE_UP -> navigate ~-maxrows
1257 | Glut.KEY_PAGE_DOWN -> navigate maxrows
1259 | Glut.KEY_HOME ->
1260 state.outline <- Some (allowdel, 0, 0, outlines, qsearch);
1261 Glut.postRedisplay ()
1263 | Glut.KEY_END ->
1264 let active = Array.length outlines - 1 in
1265 let first = max 0 (active - maxrows) in
1266 state.outline <- Some (allowdel, active, first, outlines, qsearch);
1267 Glut.postRedisplay ()
1269 | _ -> ()
1272 let drawplaceholder l =
1273 GlDraw.color (1.0, 1.0, 1.0);
1274 GlDraw.rect
1275 (0.0, float l.pagedispy)
1276 (float l.pagew, float (l.pagedispy + l.pagevh))
1278 let x = 0.0
1279 and y = float (l.pagedispy + 13) in
1280 let font = Glut.BITMAP_8_BY_13 in
1281 GlDraw.color (0.0, 0.0, 0.0);
1282 GlPix.raster_pos ~x ~y ();
1283 String.iter (fun c -> Glut.bitmapCharacter ~font ~c:(Char.code c))
1284 ("Loading " ^ string_of_int l.pageno);
1287 let now () = Unix.gettimeofday ();;
1289 let drawpage i l =
1290 begin match getopaque l.pageno with
1291 | Some opaque when validopaque opaque ->
1292 if state.textentry = None
1293 then GlDraw.color (1.0, 1.0, 1.0)
1294 else GlDraw.color (0.4, 0.4, 0.4);
1295 let a = now () in
1296 draw l.pagedispy l.pagew l.pagevh l.pagey opaque;
1297 let b = now () in
1298 let d = b-.a in
1299 vlog "draw %f sec" d;
1301 | Some _ ->
1302 drawplaceholder l
1304 | None ->
1305 drawplaceholder l;
1306 if state.inflight < cblen state.pagecache
1307 then (
1308 List.iter preload state.layout;
1310 else (
1311 vlog "inflight %d" state.inflight;
1313 end;
1314 GlDraw.color (0.5, 0.5, 0.5);
1315 GlDraw.rect
1316 (0., float i)
1317 (float (state.w - conf.scrollw), float (i + (l.pagedispy - i)))
1319 l.pagedispy + l.pagevh;
1322 let scrollindicator () =
1323 let maxy = state.maxy - (if conf.maxhfit then state.h else 0) in
1324 GlDraw.color (0.64 , 0.64, 0.64);
1325 GlDraw.rect
1326 (float (state.w - conf.scrollw), 0.)
1327 (float state.w, float state.h)
1329 GlDraw.color (0.0, 0.0, 0.0);
1330 let sh = (float (maxy + state.h) /. float state.h) in
1331 let sh = float state.h /. sh in
1332 let sh = max sh (float conf.scrollh) in
1334 let percent =
1335 if state.y = state.maxy
1336 then 1.0
1337 else float state.y /. float maxy
1339 let position = (float state.h -. sh) *. percent in
1341 let position =
1342 if position +. sh > float state.h
1343 then
1344 float state.h -. sh
1345 else
1346 position
1348 GlDraw.rect
1349 (float (state.w - conf.scrollw), position)
1350 (float state.w, position +. sh)
1354 let showsel () =
1355 match state.mstate with
1356 | Mnone ->
1359 | Msel ((x0, y0), (x1, y1)) ->
1360 let y0' = min y0 y1
1361 and y1 = max y0 y1 in
1362 let y0 = y0' in
1363 let f l =
1364 if (y0 >= l.pagedispy && y0 <= (l.pagedispy + l.pagevh))
1365 || ((y1 >= l.pagedispy)) (* && y1 <= (dy + vh))) *)
1366 then
1367 match getopaque l.pageno with
1368 | Some opaque when validopaque opaque ->
1369 let oy = -l.pagey + l.pagedispy in
1370 gettext opaque (min x0 x1, y0, max x1 x0, y1) oy conf.rectsel
1371 | _ -> ()
1373 List.iter f state.layout
1376 let showrects () =
1377 Gl.enable `blend;
1378 GlDraw.color (0.0, 0.0, 1.0) ~alpha:0.5;
1379 GlFunc.blend_func `src_alpha `one_minus_src_alpha;
1380 List.iter
1381 (fun (pageno, c, (x0, y0), (x1, y1)) ->
1382 List.iter (fun l ->
1383 if l.pageno = pageno
1384 then (
1385 let d = float (l.pagedispy - l.pagey) in
1386 GlDraw.color (0.0, 0.0, 1.0 /. float c) ~alpha:0.5;
1387 GlDraw.rect (x0, y0 +. d) (x1, y1 +. d)
1389 ) state.layout
1390 ) state.rects
1392 Gl.disable `blend;
1395 let showoutline = function
1396 | None -> ()
1397 | Some (allowdel, active, first, outlines, qsearch) ->
1398 Gl.enable `blend;
1399 GlFunc.blend_func `src_alpha `one_minus_src_alpha;
1400 GlDraw.color (0., 0., 0.) ~alpha:0.85;
1401 GlDraw.rect (0., 0.) (float state.w, float state.h);
1402 Gl.disable `blend;
1404 GlDraw.color (1., 1., 1.);
1405 let font = Glut.BITMAP_9_BY_15 in
1406 let draw_string x y s =
1407 GlPix.raster_pos ~x ~y ();
1408 String.iter (fun c -> Glut.bitmapCharacter ~font ~c:(Char.code c)) s
1410 let rec loop row =
1411 if row = Array.length outlines || (row - first) * 16 > state.h
1412 then ()
1413 else (
1414 let (s, l, _, _) = outlines.(row) in
1415 let y = (row - first) * 16 in
1416 let x = 5 + 5*l in
1417 if row = active
1418 then (
1419 Gl.enable `blend;
1420 GlDraw.polygon_mode `both `line;
1421 GlFunc.blend_func `src_alpha `one_minus_src_alpha;
1422 GlDraw.color (1., 1., 1.) ~alpha:0.9;
1423 GlDraw.rect (0., float (y + 1))
1424 (float (state.w - conf.scrollw - 1), float (y + 18));
1425 GlDraw.polygon_mode `both `fill;
1426 Gl.disable `blend;
1427 GlDraw.color (1., 1., 1.);
1429 draw_string (float x) (float (y + 16)) s;
1430 loop (row+1)
1433 loop first
1436 let display () =
1437 let lasty = List.fold_left drawpage 0 (state.layout) in
1438 GlDraw.color (0.5, 0.5, 0.5);
1439 GlDraw.rect
1440 (0., float lasty)
1441 (float (state.w - conf.scrollw), float state.h)
1443 showrects ();
1444 scrollindicator ();
1445 showsel ();
1446 showoutline state.outline;
1447 enttext ();
1448 Glut.swapBuffers ();
1451 let getlink x y =
1452 let rec f = function
1453 | l :: rest ->
1454 begin match getopaque l.pageno with
1455 | Some opaque when validopaque opaque ->
1456 let y = y - l.pagedispy in
1457 if y > 0
1458 then
1459 let y = l.pagey + y in
1460 match getlink opaque x y with
1461 | None -> f rest
1462 | some -> some
1463 else
1464 f rest
1465 | _ ->
1466 f rest
1468 | [] -> None
1470 f state.layout
1473 let checklink x y =
1474 let rec f = function
1475 | l :: rest ->
1476 begin match getopaque l.pageno with
1477 | Some opaque when validopaque opaque ->
1478 let y = y - l.pagedispy in
1479 if y > 0
1480 then
1481 let y = l.pagey + y in
1482 if checklink opaque x y then true else f rest
1483 else
1484 f rest
1485 | _ ->
1486 f rest
1488 | [] -> false
1490 f state.layout
1493 let mouse ~button ~bstate ~x ~y =
1494 match button with
1495 | Glut.OTHER_BUTTON n when n == 3 || n == 4 && bstate = Glut.UP ->
1496 let incr =
1497 if n = 3
1498 then
1499 -conf.scrollincr
1500 else
1501 conf.scrollincr
1503 let incr = incr * 2 in
1504 let y = clamp incr in
1505 gotoy y
1507 | Glut.LEFT_BUTTON when state.outline = None ->
1508 let dest = if bstate = Glut.DOWN then getlink x y else None in
1509 begin match dest with
1510 | Some (pageno, top) ->
1511 gotopage pageno top
1513 | None ->
1514 if bstate = Glut.DOWN
1515 then (
1516 Glut.setCursor Glut.CURSOR_CROSSHAIR;
1517 state.mstate <- Msel ((x, y), (x, y));
1518 Glut.postRedisplay ()
1520 else (
1521 Glut.setCursor Glut.CURSOR_INHERIT;
1522 state.mstate <- Mnone;
1526 | _ ->
1529 let mouse ~button ~state ~x ~y = mouse button state x y;;
1531 let motion ~x ~y =
1532 if state.outline = None
1533 then
1534 match state.mstate with
1535 | Mnone -> ()
1536 | Msel (a, _) ->
1537 state.mstate <- Msel (a, (x, y));
1538 Glut.postRedisplay ()
1541 let pmotion ~x ~y =
1542 if state.outline = None
1543 then
1544 match state.mstate with
1545 | Mnone when (checklink x y) ->
1546 Glut.setCursor Glut.CURSOR_INFO
1548 | Mnone ->
1549 Glut.setCursor Glut.CURSOR_INHERIT
1551 | Msel (a, _) ->
1555 let () =
1556 let statepath =
1557 let home =
1558 if Sys.os_type = "Win32"
1559 then
1560 try Sys.getenv "HOMEPATH" with Not_found -> ""
1561 else
1562 try Filename.concat (Sys.getenv "HOME") ".config" with Not_found -> ""
1564 Filename.concat home "llpp"
1566 let pstate =
1568 let ic = open_in_bin statepath in
1569 let hash = input_value ic in
1570 close_in ic;
1571 hash
1572 with exn ->
1573 if false
1574 then
1575 prerr_endline ("Error loading state " ^ Printexc.to_string exn)
1577 Hashtbl.create 1
1579 let savestate () =
1581 let w, h =
1582 match state.fullscreen with
1583 | None -> state.w, state.h
1584 | Some wh -> wh
1586 Hashtbl.replace pstate state.path (state.bookmarks, w, h);
1587 let oc = open_out_bin statepath in
1588 output_value oc pstate
1589 with exn ->
1590 if false
1591 then
1592 prerr_endline ("Error saving state " ^ Printexc.to_string exn)
1595 let setstate () =
1597 let statebookmarks, statew, stateh = Hashtbl.find pstate state.path in
1598 state.w <- statew;
1599 state.h <- stateh;
1600 state.bookmarks <- statebookmarks;
1601 with Not_found -> ()
1602 | exn ->
1603 prerr_endline ("Error setting state " ^ Printexc.to_string exn)
1606 Arg.parse [] (fun s -> state.path <- s) "options:";
1607 let name =
1608 if String.length state.path = 0
1609 then (prerr_endline "filename missing"; exit 1)
1610 else state.path
1613 setstate ();
1614 let _ = Glut.init Sys.argv in
1615 let () = Glut.initDisplayMode ~depth:false ~double_buffer:true () in
1616 let () = Glut.initWindowSize state.w state.h in
1617 let _ = Glut.createWindow ("llpp " ^ Filename.basename name) in
1619 let csock, ssock =
1620 if Sys.os_type = "Unix"
1621 then
1622 Unix.socketpair Unix.PF_UNIX Unix.SOCK_STREAM 0
1623 else
1624 let addr = Unix.ADDR_INET (Unix.inet_addr_loopback, 1337) in
1625 let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
1626 Unix.setsockopt sock Unix.SO_REUSEADDR true;
1627 Unix.bind sock addr;
1628 Unix.listen sock 1;
1629 let csock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
1630 Unix.connect csock addr;
1631 let ssock, _ = Unix.accept sock in
1632 Unix.close sock;
1633 let opts sock =
1634 Unix.setsockopt sock Unix.TCP_NODELAY true;
1635 Unix.setsockopt_optint sock Unix.SO_LINGER None;
1637 opts ssock;
1638 opts csock;
1639 at_exit (fun () -> Unix.shutdown ssock Unix.SHUTDOWN_ALL);
1640 ssock, csock
1643 let () = Glut.displayFunc display in
1644 let () = Glut.reshapeFunc reshape in
1645 let () = Glut.keyboardFunc keyboard in
1646 let () = Glut.specialFunc special in
1647 let () = Glut.idleFunc (Some idle) in
1648 let () = Glut.mouseFunc mouse in
1649 let () = Glut.motionFunc motion in
1650 let () = Glut.passiveMotionFunc pmotion in
1652 init ssock;
1653 state.csock <- csock;
1654 state.ssock <- ssock;
1655 writecmd csock ("open " ^ name ^ "\000");
1657 at_exit savestate;
1659 let rec handlelablglutbug () =
1661 Glut.mainLoop ();
1662 with Glut.BadEnum "key in special_of_int" ->
1663 showtext '!' " LablGlut bug: special key not recognized";
1664 Glut.swapBuffers ();
1665 handlelablglutbug ()
1667 handlelablglutbug ();