1 (*************************************************************************)
3 (* Objective Caml LablTk library *)
5 (* Jacques Garrigue, Kyoto University RIMS *)
7 (* Copyright 1999 Institut National de Recherche en Informatique et *)
8 (* en Automatique and Kyoto University. All rights reserved. *)
9 (* This file is distributed under the terms of the GNU Library *)
10 (* General Public License, with the special exception on linking *)
11 (* described in file ../../../LICENSE. *)
13 (*************************************************************************)
15 (* $Id: shell.ml 7327 2006-01-18 13:26:03Z garrigue $ *)
18 module Unix = UnixLabels
23 (* Here again, memoize regexps *)
25 let (~!) = Jg_memo.fast ~f:Str.regexp
27 (* Nice history class. May reuse *)
29 class ['a] history () = object
30 val mutable history = ([] : 'a list)
32 method empty = history = []
33 method add s = count <- 0; history <- s :: history
35 let s = List.nth history count in
36 count <- (count + 1) mod List.length history;
39 let l = List.length history in
40 count <- (l + count - 1) mod l;
41 List.nth history ((l + count - 1) mod l)
44 let dump_handle (h : Unix.file_descr) =
45 let obj = Obj.repr h in
46 if Obj.is_int obj || Obj.tag obj <> Obj.custom_tag then
47 invalid_arg "Shell.dump_handle";
48 Nativeint.format "%x" (Obj.obj obj)
50 (* The shell class. Now encapsulated *)
52 let protect f x = try f x with _ -> ()
54 let is_win32 = Sys.os_type = "Win32"
55 let use_threads = is_win32
56 let use_sigpipe = is_win32
58 class shell ~textw ~prog ~args ~env ~history =
59 let (in2,out1) = Unix.pipe ()
60 and (in1,out2) = Unix.pipe ()
61 and (err1,err2) = Unix.pipe ()
62 and (sig2,sig1) = Unix.pipe () in
67 let sigdef = "CAMLSIGPIPE=" ^ dump_handle sig2 in
68 Array.append env [|sigdef|]
71 Unix.create_process_env ~prog ~args ~env
72 ~stdin:in2 ~stdout:out2 ~stderr:err2
73 val out = Unix.out_channel_of_descr out1
74 val h : _ history = history
75 val mutable alive = true
76 val mutable reading = false
77 val ibuffer = Buffer.create 1024
78 val imutex = Mutex.create ()
79 val mutable ithreads = []
82 if Winfo.exists textw then Text.configure textw ~state:`Disabled;
85 protect close_out out;
87 if use_sigpipe then ignore (Unix.write sig1 ~buf:"T" ~pos:0 ~len:1);
88 List.iter ~f:(protect Unix.close) [in1; err1; sig1; sig2];
89 if not use_threads then begin
90 Fileevent.remove_fileinput ~fd:in1;
91 Fileevent.remove_fileinput ~fd:err1;
93 if not use_sigpipe then begin
94 Unix.kill ~pid ~signal:Sys.sigkill;
95 ignore (Unix.waitpid ~mode:[] pid)
102 if use_sigpipe then begin
103 ignore (Unix.write sig1 ~buf:"C" ~pos:0 ~len:1);
106 Unix.kill ~pid ~signal:Sys.sigint
107 with Unix.Unix_error _ -> ()
112 with Sys_error _ -> ()
113 method private read ~fd ~len =
115 let buf = String.create len in
116 let len = Unix.read fd ~buf ~pos:0 ~len in
117 if len > 0 then begin
118 self#insert (String.sub buf ~pos:0 ~len);
119 Text.mark_set textw ~mark:"input" ~index:(`Mark"insert",[`Char(-1)])
122 with Unix.Unix_error _ -> 0
124 method history (dir : [`Next|`Previous]) =
125 if not h#empty then begin
126 if reading then begin
127 Text.delete textw ~start:(`Mark"input",[`Char 1])
128 ~stop:(`Mark"insert",[])
131 Text.mark_set textw ~mark:"input"
132 ~index:(`Mark"insert",[`Char(-1)])
134 self#insert (if dir = `Previous then h#previous else h#next)
136 method private lex ?(start = `Mark"insert",[`Linestart])
137 ?(stop = `Mark"insert",[`Lineend]) () =
138 Lexical.tag textw ~start ~stop
140 let idx = Text.index textw
141 ~index:(`Mark"insert",[`Char(-1);`Linestart]) in
142 Text.insert textw ~text ~index:(`Mark"insert",[]);
143 self#lex ~start:(idx,[`Linestart]) ();
144 Text.see textw ~index:(`Mark"insert",[])
145 method private keypress c =
146 if not reading && c > " " then begin
148 Text.mark_set textw ~mark:"input" ~index:(`Mark"insert",[`Char(-1)])
150 method private keyrelease c = if c <> "" then self#lex ()
151 method private return =
152 if reading then reading <- false
153 else Text.mark_set textw ~mark:"input"
154 ~index:(`Mark"insert",[`Linestart;`Char 1]);
155 Text.mark_set textw ~mark:"insert" ~index:(`Mark"insert",[`Lineend]);
156 self#lex ~start:(`Mark"input",[`Linestart]) ();
158 (* input is one character before real input *)
159 Text.get textw ~start:(`Mark"input",[`Char 1])
160 ~stop:(`Mark"insert",[]) in
162 Text.insert textw ~index:(`Mark"insert",[]) ~text:"\n";
163 Text.yview_index textw ~index:(`Mark"insert",[]);
166 method private paste ev =
167 if not reading then begin
169 Text.mark_set textw ~mark:"input"
170 ~index:(`Atxy(ev.ev_MouseX, ev.ev_MouseY),[`Char(-1)])
173 Lexical.init_tags textw;
175 [ ([], `KeyPress, [`Char], fun ev -> self#keypress ev.ev_Char);
176 ([], `KeyRelease, [`Char], fun ev -> self#keyrelease ev.ev_Char);
177 (* [], `KeyPressDetail"Return", [], fun _ -> self#return; *)
178 ([], `ButtonPressDetail 2, [`MouseX; `MouseY], self#paste);
179 ([`Alt], `KeyPressDetail"p", [], fun _ -> self#history `Previous);
180 ([`Alt], `KeyPressDetail"n", [], fun _ -> self#history `Next);
181 ([`Meta], `KeyPressDetail"p", [], fun _ -> self#history `Previous);
182 ([`Meta], `KeyPressDetail"n", [], fun _ -> self#history `Next);
183 ([`Control], `KeyPressDetail"c", [], fun _ -> self#interrupt);
184 ([], `Destroy, [], fun _ -> self#kill) ]
186 List.iter bindings ~f:
187 begin fun (modif,event,fields,action) ->
188 bind textw ~events:[`Modified(modif,event)] ~fields ~action
190 bind textw ~events:[`KeyPressDetail"Return"] ~breakable:true
191 ~action:(fun _ -> self#return; break());
192 List.iter ~f:Unix.close [in2;out2;err2];
193 if use_threads then begin
194 let fileinput_thread fd =
195 let buf = String.create 1024 in
197 try while len := Unix.read fd ~buf ~pos:0 ~len:1024; !len > 0 do
199 Buffer.add_substring ibuffer buf 0 !len;
201 done with Unix.Unix_error _ -> ()
203 ithreads <- List.map [in1; err1] ~f:(Thread.create fileinput_thread);
204 let rec read_buffer () =
206 if Buffer.length ibuffer > 0 then begin
207 self#insert (Str.global_replace ~!"\r\n" "\n"
208 (Buffer.contents ibuffer));
209 Buffer.reset ibuffer;
210 Text.mark_set textw ~mark:"input" ~index:(`Mark"insert",[`Char(-1)])
213 Timer.set ~ms:100 ~callback:read_buffer
218 List.iter [in1;err1] ~f:
220 Fileevent.add_fileinput ~fd
221 ~callback:(fun () -> ignore (self#read ~fd ~len:1024))
227 (* Specific use of shell, for OCamlBrowser *)
229 let shells : (string * shell) list ref = ref []
231 (* Called before exiting *)
233 List.iter !shells ~f:(fun (_,sh) -> if sh#alive then sh#kill);
237 let all = List.filter !shells ~f:(fun (_,sh) -> sh#alive) in
241 let may_exec_unix prog =
242 try Unix.access prog ~perm:[Unix.X_OK]; prog
243 with Unix.Unix_error _ -> ""
245 let may_exec_win prog =
247 List.exists ~f:(Filename.check_suffix prog) ["exe"; "com"; "bat"] in
248 if has_ext then may_exec_unix prog else
249 List.fold_left [prog^".bat"; prog^".exe"; prog^".com"] ~init:""
250 ~f:(fun res prog -> if res = "" then may_exec_unix prog else res)
253 if is_win32 then may_exec_win else may_exec_unix
255 let path_sep = if is_win32 then ";" else ":"
257 let warnings = ref "Al"
259 let program_not_found prog =
260 Jg_message.info ~title:"Error"
261 ("Program \"" ^ prog ^ "\"\nwas not found in path")
264 if String.contains s ' ' then "\"" ^ s ^ "\"" else s
268 List.filter ~f:((<>) "") (Str.split ~!" " prog) in
269 if progargs = [] then () else
270 let prog = List.hd progargs in
272 try Sys.getenv "PATH" with Not_found -> "/bin" ^ path_sep ^ "/usr/bin" in
273 let exec_path = Str.split ~!path_sep path in
274 let exec_path = if is_win32 then "."::exec_path else exec_path in
276 if not (Filename.is_implicit prog) then may_exec prog else
277 List.fold_left exec_path ~init:"" ~f:
279 if res = "" then may_exec (Filename.concat dir prog) else res) in
280 if progpath = "" then program_not_found prog else
281 let tl = Jg_toplevel.titled title in
282 let menus = Menu.create tl ~name:"menubar" ~typ:`Menubar in
283 Toplevel.configure tl ~menu:menus;
284 let file_menu = new Jg_menu.c "File" ~parent:menus
285 and history_menu = new Jg_menu.c "History" ~parent:menus
286 and signal_menu = new Jg_menu.c "Signal" ~parent:menus in
287 let frame, tw, sb = Jg_text.create_with_scrollbar tl in
288 Text.configure tw ~background:`White;
289 pack [sb] ~fill:`Y ~side:`Right;
290 pack [tw] ~fill:`Both ~expand:true ~side:`Left;
291 pack [frame] ~fill:`Both ~expand:true;
292 let env = Array.map (Unix.environment ()) ~f:
294 if Str.string_match ~!"TERM=" s 0 then "TERM=dumb" else s
297 List2.flat_map !Config.load_path ~f:(fun dir -> ["-I"; dir]) in
299 if is_win32 then List.map ~f:protect_arg load_path else load_path in
300 let labels = if !Clflags.classic then ["-nolabels"] else [] in
301 let rectypes = if !Clflags.recursive_types then ["-rectypes"] else [] in
303 if List.mem "-w" progargs || !warnings = "Al" then []
304 else ["-w"; !warnings]
307 Array.of_list (progargs @ labels @ warnings @ rectypes @ load_path) in
308 let history = new history () in
310 let sh = new shell ~textw:tw ~prog:progpath ~env ~args ~history in
311 shells := (title, sh) :: !shells;
314 let sh = ref (start_shell ()) in
315 let current_dir = ref (Unix.getcwd ()) in
316 file_menu#add_command "Restart" ~command:
319 Text.configure tw ~state:`Normal;
320 Text.insert tw ~index:(`End,[]) ~text:"\n";
321 Text.see tw ~index:(`End,[]);
322 Text.mark_set tw ~mark:"insert" ~index:(`End,[]);
323 sh := start_shell ();
325 file_menu#add_command "Use..." ~command:
327 Fileselect.f ~title:"Use File" ~filter:"*.ml"
328 ~sync:true ~dir:!current_dir ()
330 if l = [] then () else
331 let name = Fileselect.caml_dir (List.hd l) in
332 current_dir := Filename.dirname name;
333 if Filename.check_suffix name ".ml"
335 let cmd = "#use \"" ^ String.escaped name ^ "\";;\n" in
336 (!sh)#insert cmd; (!sh)#send cmd)
338 file_menu#add_command "Load..." ~command:
340 Fileselect.f ~title:"Load File" ~filter:"*.cm[oa]" ~sync:true ()
343 if l = [] then () else
344 let name = Fileselect.caml_dir (List.hd l) in
345 current_dir := Filename.dirname name;
346 if Filename.check_suffix name ".cmo" ||
347 Filename.check_suffix name ".cma"
349 let cmd = "#load \"" ^ String.escaped name ^ "\";;\n" in
350 (!sh)#insert cmd; (!sh)#send cmd)
352 file_menu#add_command "Import path" ~command:
354 List.iter (List.rev !Config.load_path) ~f:
356 (!sh)#send ("#directory \"" ^ String.escaped dir ^ "\";;\n"))
358 file_menu#add_command "Close" ~command:(fun () -> destroy tl);
359 history_menu#add_command "Previous " ~accelerator:"M-p"
360 ~command:(fun () -> (!sh)#history `Previous);
361 history_menu#add_command "Next" ~accelerator:"M-n"
362 ~command:(fun () -> (!sh)#history `Next);
363 signal_menu#add_command "Interrupt " ~accelerator:"C-c"
364 ~command:(fun () -> (!sh)#interrupt);
365 signal_menu#add_command "Kill" ~command:(fun () -> (!sh)#kill)