(* $Id$ *) open Tk open Jg_tk (* Nice history class. May reuse *) class ['a] history () = object val mutable history = ([] : 'a list) val mutable count = 0 method empty = history = [] method add s = count <- 0; history <- s :: history method previous = let s = List.nth pos:count history in count <- (count + 1) mod List.length history; s method next = let l = List.length history in count <- (l + count - 1) mod l; List.nth history pos:((l + count - 1) mod l) end (* The shell class. Now encapsulated *) let protect f x = try f x with _ -> () class shell :textw :prog :args :env = let (in2,out1) = Unix.pipe () and (in1,out2) = Unix.pipe () and (err1,err2) = Unix.pipe () in object (self) val pid = Unix.create_process_env name:prog :args :env stdin:in2 stdout:out2 stderr:err2 val out = Unix.out_channel_of_descr out1 val h = new history () val mutable alive = true val mutable reading = false method alive = alive method kill = if Winfo.exists textw then Text.configure textw state:`Disabled; if alive then begin alive <- false; protect close_out out; List.iter fun:(protect Unix.close) [in1; err1; in2; out2; err2]; try Fileevent.remove_fileinput fd:in1; Fileevent.remove_fileinput fd:err1; Unix.kill :pid signal:Sys.sigkill; Unix.waitpid mode:[] pid; () with _ -> () end method interrupt = if alive then try reading <- false; Unix.kill :pid signal:Sys.sigint with Unix.Unix_error _ -> () method send s = if alive then try output_string s to:out; flush out with Sys_error _ -> () method private read :fd :len = try let buf = String.create :len in let len = Unix.read fd :buf pos:0 :len in self#insert (String.sub buf pos:0 :len); Text.mark_set textw mark:"input" index:(`Mark"insert",[`Char(-1)]) with Unix.Unix_error _ -> () method history (dir : [`next|`previous]) = if not h#empty then begin if reading then begin Text.delete textw start:(`Mark"input",[`Char 1]) end:(`Mark"insert",[]) end else begin reading <- true; Text.mark_set textw mark:"input" index:(`Mark"insert",[`Char(-1)]) end; self#insert (if dir = `previous then h#previous else h#next) end method private lex ?(:start = `Mark"insert",[`Linestart]) ?(:end = `Mark"insert",[`Lineend]) () = Lexical.tag textw :start :end method insert text = let idx = Text.index textw index:(`Mark"insert",[`Char(-1);`Linestart]) in Text.insert textw :text index:(`Mark"insert",[]); self#lex start:(idx,[`Linestart]) (); Text.see textw index:(`Mark"insert",[]) method private keypress c = if not reading & c > " " then begin reading <- true; Text.mark_set textw mark:"input" index:(`Mark"insert",[`Char(-1)]) end method private keyrelease c = if c <> "" then self#lex () method private return = if reading then reading <- false else Text.mark_set textw mark:"input" index:(`Mark"insert",[`Linestart;`Char 1]); self#lex start:(`Mark"input",[`Linestart]) (); let s = (* input is one character before real input *) Text.get textw start:(`Mark"input",[`Char 1]) end:(`Mark"insert",[]) in h#add s; self#send s; self#send "\n" method private paste ev = if not reading then begin reading <- true; Text.mark_set textw mark:"input" index:(`Atxy(ev.ev_MouseX, ev.ev_MouseY),[`Char(-1)]) end initializer Lexical.init_tags textw; let rec bindings = [ ([[],`KeyPress],[`Char],fun ev -> self#keypress ev.ev_Char); ([[],`KeyRelease],[`Char],fun ev -> self#keyrelease ev.ev_Char); ([[],`KeyPressDetail"Return"],[],fun _ -> self#return); ([[],`ButtonPressDetail 2], [`MouseX; `MouseY], self#paste); ([[`Alt],`KeyPressDetail"p"],[],fun _ -> self#history `previous); ([[`Alt],`KeyPressDetail"n"],[],fun _ -> self#history `next); ([[`Meta],`KeyPressDetail"p"],[],fun _ -> self#history `previous); ([[`Meta],`KeyPressDetail"n"],[],fun _ -> self#history `next); ([[`Control],`KeyPressDetail"c"],[],fun _ -> self#interrupt); ([[],`Destroy],[],fun _ -> self#kill) ] in List.iter bindings fun:(fun (events,fields,f) -> bind textw :events action:(`Set(fields,f))); begin try List.iter [in1;err1] fun: begin fun fd -> Fileevent.add_fileinput :fd callback:(fun () -> self#read :fd len:1024) end with _ -> () end end (* Specific use of shell, for LablBrowser *) let shells : (string * shell) list ref = ref [] (* Called before exiting *) let kill_all () = List.iter !shells fun:(fun (_,sh) -> if sh#alive then sh#kill); shells := [] let get_all () = let all = List.filter !shells pred:(fun (_,sh) -> sh#alive) in shells := all; all let may_exec prog = try Unix.access name:prog perm:[Unix.X_OK]; true with Unix.Unix_error _ -> false let f :prog :title = let progargs = List.filter pred:((<>) "") (Str.split sep:(Str.regexp " ") prog) in if progargs = [] then () else let prog = List.hd progargs in let path = try Sys.getenv "PATH" with Not_found -> "/bin:/usr/bin" in let exec_path = Str.split sep:(Str.regexp":") path in let exists = if not (Filename.is_implicit prog) then may_exec prog else List.exists exec_path pred:(fun dir -> may_exec (Filename.concat dir prog)) in if not exists then () else let tl = Jg_toplevel.titled title in let menus = Frame.create tl name:"menubar" in let file_menu = new Jg_menu.c "File" parent:menus and history_menu = new Jg_menu.c "History" parent:menus and signal_menu = new Jg_menu.c "Signal" parent:menus in pack [menus] side:`Top fill:`X; pack [file_menu#button; history_menu#button; signal_menu#button] side:`Left ipadx:(`Pix 5) anchor:`W; let frame, tw, sb = Jg_text.create_with_scrollbar tl in Text.configure tw background:`White; pack [sb] fill:`Y side:`Right; pack [tw] fill:`Both expand:true side:`Left; pack [frame] fill:`Both expand:true; let reg = Str.regexp "TERM=" in let env = Array.map (Unix.environment ()) fun: begin fun s -> if Str.string_match pat:reg s pos:0 then "TERM=dumb" else s end in let load_path = List2.flat_map !Config.load_path fun:(fun dir -> ["-I"; dir]) in let args = Array.of_list (progargs @ load_path) in let sh = new shell textw:tw :prog :env :args in let current_dir = ref (Unix.getcwd ()) in file_menu#add_command "Use..." command: begin fun () -> Fileselect.f title:"Use File" filter:"*.ml" sync:true dir:!current_dir () action:(fun l -> if l = [] then () else let name = List.hd l in current_dir := Filename.dirname name; if Filename.check_suffix name suff:".ml" then let cmd = "#use \"" ^ name ^ "\";;\n" in sh#insert cmd; sh#send cmd) end; file_menu#add_command "Load..." command: begin fun () -> Fileselect.f title:"Load File" filter:"*.cm[oa]" sync:true () dir:!current_dir action:(fun l -> if l = [] then () else let name = List.hd l in current_dir := Filename.dirname name; if Filename.check_suffix name suff:".cmo" or Filename.check_suffix name suff:".cma" then let cmd = "#load \"" ^ name ^ "\";;\n" in sh#insert cmd; sh#send cmd) end; file_menu#add_command "Import path" command: begin fun () -> List.iter (List.rev !Config.load_path) fun:(fun dir -> sh#send ("#directory \"" ^ dir ^ "\";;\n")) end; file_menu#add_command "Close" command:(fun () -> destroy tl); history_menu#add_command "Previous " accelerator:"M-p" command:(fun () -> sh#history `previous); history_menu#add_command "Next" accelerator:"M-n" command:(fun () -> sh#history `next); signal_menu#add_command "Interrupt " accelerator:"C-c" command:(fun () -> sh#interrupt); signal_menu#add_command "Kill" command:(fun () -> sh#kill); shells := (title, sh) :: !shells