Set destructor = NULL in destroy func to prevent it being called twice
[luakit.git] / rc.lua
blob276d9c3b9b277a591ca9e36415e006cfa532e4ab
1 -- Luakit configuration file, more information at http://luakit.org/
3 require("math")
4 require("mode")
5 require("bind")
7 -- Widget construction aliases
8 function eventbox() return widget{type="eventbox"} end
9 function hbox() return widget{type="hbox"} end
10 function label() return widget{type="label"} end
11 function notebook() return widget{type="notebook"} end
12 function vbox() return widget{type="vbox"} end
13 function webview() return widget{type="webview"} end
14 function window() return widget{type="window"} end
15 function entry() return widget{type="entry"} end
18 -- Variable definitions
19 HOMEPAGE = "http://luakit.org/"
20 --HOMEPAGE = "http://github.com/mason-larobina/luakit"
21 SCROLL_STEP = 20
22 MAX_CMD_HISTORY = 100
23 MAX_SRCH_HISTORY = 100
24 --HTTPPROXY = "http://example.com:3128"
26 -- Setup download directory
27 DOWNLOAD_DIR = luakit.get_special_dir("DOWNLOAD") or (os.getenv("HOME") .. "/downloads")
29 -- Per-domain webview properties
30 domain_props = { --[[
31 ["all"] = {
32 ["enable-scripts"] = false,
33 ["enable-plugins"] = false,
34 ["enable-private-browsing"] = false,
35 ["user-stylesheet-uri"] = "",
37 ["youtube.com"] = {
38 ["enable-scripts"] = true,
39 ["enable-plugins"] = true,
41 ["forums.archlinux.org"] = {
42 ["user-stylesheet-uri"] = luakit.data_dir .. "/styles/dark.css",
43 ["enable-private-browsing"] = true,
44 }, ]]
47 -- Luakit theme
48 theme = theme or {
49 -- Default settings
50 font = "monospace normal 9",
51 fg = "#fff",
52 bg = "#000",
54 -- General settings
55 statusbar_fg = "#fff",
56 statusbar_bg = "#000",
57 inputbar_fg = "#000",
58 inputbar_bg = "#fff",
60 -- Specific settings
61 loaded_fg = "#33AADD",
62 tablabel_fg = "#999",
63 tablabel_bg = "#111",
64 selected_tablabel_fg = "#fff",
65 selected_tablabel_bg = "#000",
67 -- Enforce a minimum tab width of 30 characters to prevent longer tab
68 -- titles overshadowing small tab titles when things get crowded.
69 tablabel_format = "%-30s",
72 -- Small util functions
73 function info(...) if luakit.verbose then print(string.format(...)) end end
75 widget.add_signal("new", function (wi)
76 wi:add_signal("init", function (wi)
77 if wi.type == "window" then
78 wi:add_signal("destroy", function ()
79 -- Call the quit function if this was the last window left
80 if #luakit.windows == 0 then luakit.quit() end
81 end)
82 end
83 end)
84 end)
86 -- Search engines
87 search_engines = {
88 luakit = "http://luakit.org/search/index/luakit?q={0}",
89 google = "http://google.com/search?q={0}",
90 wikipedia = "http://en.wikipedia.org/wiki/Special:Search?search={0}",
91 debbugs = "http://bugs.debian.org/{0}",
92 imdb = "http://imdb.com/find?s=all&q={0}",
93 sourceforge = "http://sf.net/search/?words={0}",
96 -- Add key bindings to be used across all windows
97 mode_binds = {
98 -- bind.buf(Pattern, function (w, buffer, opts) .. end, opts),
99 -- bind.key({Modifiers}, Key name, function (w, opts) .. end, opts),
100 -- bind.but({Modifiers}, Button num, function (w, opts) .. end, opts),
101 all = {
102 bind.key({}, "Escape", function (w) w:set_mode() end),
103 bind.key({"Control"}, "[", function (w) w:set_mode() end),
105 -- Mouse bindings
106 bind.but({}, 2, function (w)
107 -- Open hovered uri in new tab
108 local uri = w:get_current().hovered_uri
109 if uri then w:new_tab(uri)
110 else -- Open selection in current tab
111 uri = luakit.get_selection()
112 if uri then w:get_current().uri = uri end
114 end),
115 bind.but({}, 8, function (w) w:back() end),
116 bind.but({}, 9, function (w) w:forward() end),
118 normal = {
119 bind.key({}, "i", function (w) w:set_mode("insert") end),
120 bind.key({}, ":", function (w) w:set_mode("command") end),
122 -- Scrolling
123 bind.key({}, "h", function (w) w:scroll_horiz("-"..SCROLL_STEP.."px") end),
124 bind.key({}, "j", function (w) w:scroll_vert ("+"..SCROLL_STEP.."px") end),
125 bind.key({}, "k", function (w) w:scroll_vert ("-"..SCROLL_STEP.."px") end),
126 bind.key({}, "l", function (w) w:scroll_horiz("+"..SCROLL_STEP.."px") end),
127 bind.key({}, "Left", function (w) w:scroll_horiz("-"..SCROLL_STEP.."px") end),
128 bind.key({}, "Down", function (w) w:scroll_vert ("+"..SCROLL_STEP.."px") end),
129 bind.key({}, "Up", function (w) w:scroll_vert ("-"..SCROLL_STEP.."px") end),
130 bind.key({}, "Right", function (w) w:scroll_horiz("+"..SCROLL_STEP.."px") end),
131 bind.key({"Control"}, "d", function (w) w:scroll_page(0.5) end),
132 bind.key({"Control"}, "u", function (w) w:scroll_page(-0.5) end),
133 bind.key({"Control"}, "f", function (w) w:scroll_page(1.0) end),
134 bind.key({"Control"}, "b", function (w) w:scroll_page(-1.0) end),
135 bind.buf("^gg$", function (w) w:scroll_vert("0%") end),
136 bind.buf("^G$", function (w) w:scroll_vert("100%") end),
137 bind.buf("^[\-\+]?[0-9]+[%%G]$", function (w, b) w:scroll_vert(string.match(b, "^([\-\+]?%d+)[%%G]$") .. "%") end),
139 -- Clipboard
140 bind.key({}, "p", function (w) w:navigate(luakit.get_selection()) end),
141 bind.key({}, "P", function (w) w:new_tab(luakit.get_selection()) end),
142 bind.buf("^yy$", function (w) luakit.set_selection(w:get_current().uri) end),
143 bind.buf("^yt$", function (w) luakit.set_selection(w.win.title) end),
145 -- Commands
146 bind.buf("^o$", function (w, c) w:enter_cmd(":open ") end),
147 bind.buf("^t$", function (w, c) w:enter_cmd(":tabopen ") end),
148 bind.buf("^,g$", function (w, c) w:enter_cmd(":websearch google ") end),
150 -- Searching
151 bind.key({}, "/", function (w) w:start_search(true) end),
152 bind.key({}, "?", function (w) w:start_search(false) end),
153 bind.key({}, "n", function (w) w:search(nil, true) end),
154 bind.key({}, "N", function (w) w:search(nil, false) end),
156 -- History
157 bind.buf("^[0-9]*H$", function (w, b) w:back (tonumber(string.match(b, "^(%d*)H$") or 1)) end),
158 bind.buf("^[0-9]*L$", function (w, b) w:forward(tonumber(string.match(b, "^(%d*)L$") or 1)) end),
160 -- Tab
161 bind.buf("^[0-9]*gT$", function (w, b) w:prev_tab(tonumber(string.match(b, "^(%d*)gT$") or 1)) end),
162 bind.buf("^[0-9]*gt$", function (w, b) w:next_tab(tonumber(string.match(b, "^(%d*)gt$") or 1)) end),
163 bind.buf("^gH$", function (w) w:new_tab(HOMEPAGE) end),
164 bind.buf("^d$", function (w) w:close_tab() end),
166 bind.key({}, "r", function (w) w:reload() end),
167 bind.buf("^gh$", function (w) w:navigate(HOMEPAGE) end),
168 bind.buf("^ZZ$", function (w) luakit.quit() end),
170 -- Link following
171 bind.key({}, "f", function (w) w:set_mode("follow") end),
174 command = {
175 bind.key({"Shift"}, "Insert", function (w) w:insert_cmd(luakit.get_selection()) end),
176 bind.key({}, "Up", function (w) w:cmd_hist_prev() end),
177 bind.key({}, "Down", function (w) w:cmd_hist_next() end),
178 bind.key({}, "Tab", function (w) w:cmd_completion() end),
179 bind.key({"Control"}, "w", function (w) w:del_word() end),
180 bind.key({"Control"}, "u", function (w) w:del_line() end),
182 search = {
183 bind.key({}, "Up", function (w) w:srch_hist_prev() end),
184 bind.key({}, "Down", function (w) w:srch_hist_next() end),
186 insert = { },
189 -- Commands
190 commands = {
191 -- bind.cmd({Command, Alias1, ...}, function (w, arg, opts) .. end, opts),
192 bind.cmd({"open", "o" }, function (w, a) w:navigate(a) end),
193 bind.cmd({"tabopen", "t" }, function (w, a) w:new_tab(a) end),
194 bind.cmd({"back" }, function (w, a) w:back(tonumber(a) or 1) end),
195 bind.cmd({"forward", "f" }, function (w, a) w:forward(tonumber(a) or 1) end),
196 bind.cmd({"scroll" }, function (w, a) w:scroll_vert(a) end),
197 bind.cmd({"quit", "q" }, function (w) luakit.quit() end),
198 bind.cmd({"close", "c" }, function (w) w:close_tab() end),
199 bind.cmd({"websearch", "ws" }, function (w, e, s) w:websearch(e, s) end),
200 bind.cmd({"reload", }, function (w) w:reload() end),
201 bind.cmd({"viewsource", "vs" }, function (w) w:toggle_source(true) end),
202 bind.cmd({"viewsource!", "vs!"}, function (w) w:toggle_source() end),
205 function set_http_options(w)
206 local proxy = HTTPPROXY or os.getenv("http_proxy")
207 if proxy then w:set('proxy-uri', proxy) end
208 w:set('user-agent', 'luakit')
209 -- Uncomment the following options if you want to enable SSL certs validation.
210 -- w:set('ssl-ca-file', '/etc/certs/ca-certificates.crt')
211 -- w:set('ssl-strict', true)
214 -- Build and pack window widgets
215 function build_window()
216 -- Create a table for widgets and state variables for a window
217 local w = {
218 win = window(),
219 ebox = eventbox(),
220 layout = vbox(),
221 tabs = notebook(),
222 -- Tab bar widgets
223 tbar = {
224 layout = hbox(),
225 ebox = eventbox(),
226 titles = { },
228 -- Status bar widgets
229 sbar = {
230 layout = hbox(),
231 ebox = eventbox(),
232 -- Left aligned widgets
233 l = {
234 layout = hbox(),
235 ebox = eventbox(),
236 uri = label(),
237 loaded = label(),
239 -- Fills space between the left and right aligned widgets
240 filler = label(),
241 -- Right aligned widgets
242 r = {
243 layout = hbox(),
244 ebox = eventbox(),
245 buf = label(),
246 tabi = label(),
247 scroll = label(),
250 -- Input bar widgets
251 ibar = {
252 layout = hbox(),
253 ebox = eventbox(),
254 prompt = label(),
255 input = entry(),
259 -- Assemble window
260 w.ebox:set_child(w.layout)
261 w.win:set_child(w.ebox)
263 -- Pack tab bar
264 local t = w.tbar
265 t.ebox:set_child(t.layout, false, false, 0)
266 w.layout:pack_start(t.ebox, false, false, 0)
268 -- Pack notebook
269 w.layout:pack_start(w.tabs, true, true, 0)
271 -- Pack left-aligned statusbar elements
272 local l = w.sbar.l
273 l.layout:pack_start(l.uri, false, false, 0)
274 l.layout:pack_start(l.loaded, false, false, 0)
275 l.ebox:set_child(l.layout)
277 -- Pack right-aligned statusbar elements
278 local r = w.sbar.r
279 r.layout:pack_start(r.buf, false, false, 0)
280 r.layout:pack_start(r.tabi, false, false, 0)
281 r.layout:pack_start(r.scroll, false, false, 0)
282 r.ebox:set_child(r.layout)
284 -- Pack status bar elements
285 local s = w.sbar
286 s.layout:pack_start(l.ebox, false, false, 0)
287 s.layout:pack_start(s.filler, true, true, 0)
288 s.layout:pack_start(r.ebox, false, false, 0)
289 s.ebox:set_child(s.layout)
290 w.layout:pack_start(s.ebox, false, false, 0)
292 -- Pack input bar
293 local i = w.ibar
294 i.layout:pack_start(i.prompt, false, false, 0)
295 i.layout:pack_start(i.input, true, true, 0)
296 i.ebox:set_child(i.layout)
297 w.layout:pack_start(i.ebox, false, false, 0)
299 -- Other settings
300 i.input.show_frame = false
301 w.tabs.show_tabs = false
302 l.loaded:hide()
303 l.uri.selectable = true
305 return w
308 function attach_window_signals(w)
309 -- Attach notebook widget signals
310 w.tabs:add_signal("page-added", function (nbook, view, idx)
311 w:update_tab_count(idx)
312 w:update_tab_labels()
313 end)
315 w.tabs:add_signal("switch-page", function (nbook, view, idx)
316 w:update_tab_count(idx)
317 w:update_win_title(view)
318 w:update_uri(view)
319 w:update_progress(view)
320 w:update_tab_labels(idx)
321 end)
323 -- Attach window widget signals
324 w.win:add_signal("key-press", function (win, mods, key)
325 -- Reset command line completion
326 if w:get_mode() == "command" and key ~= "Tab" and w.compl_start then
327 w:update_uri()
328 w.compl_index = 0
331 if w:hit(mods, key) then
332 return true
334 end)
336 w.win:add_signal("mode-changed", function (win, mode)
337 local i, p = w.ibar.input, w.ibar.prompt
339 w:update_binds(mode)
340 w.cmd_hist_cursor = nil
342 -- Clear following hints if the user exits follow mode
343 if w.showing_hints then
344 w:eval_js("clear();");
345 w.showing_hints = false
348 -- If a user aborts a search return to the original position
349 if w.search_start_marker then
350 w:get_current():set_scroll_vert(w.search_start_marker)
351 w.search_start_marker = nil
354 if mode == "normal" then
355 p:hide()
356 i:hide()
357 elseif mode == "insert" then
358 i:hide()
359 i.text = ""
360 p.text = "-- INSERT --"
361 p:show()
362 elseif mode == "command" then
363 p:hide()
364 i.text = ":"
365 i:show()
366 i:focus()
367 i:set_position(-1)
368 elseif mode == "search" then
369 p:hide()
370 i:show()
371 elseif mode == "follow" then
372 w:eval_js_from_file(util.find_data("scripts/follow.js"))
373 w:eval_js("clear(); show_hints();")
374 w.showing_hints = true
375 p.text = "Follow:"
376 p:show()
377 i.text = ""
378 i:show()
379 i:focus()
380 i:set_position(-1)
381 else
382 w.ibar.prompt.text = ""
383 w.ibar.input.text = ""
385 end)
387 -- Attach inputbar widget signals
388 w.ibar.input:add_signal("changed", function()
389 local text = w.ibar.input.text
390 -- Auto-exit "command" mode if you backspace or delete the ":"
391 -- character at the start of the input box when in "command" mode.
392 if w:is_mode("command") and not string.match(text, "^:") then
393 w:set_mode()
394 elseif w:is_mode("search") then
395 if string.match(text, "^[\?\/]") then
396 w:search(string.sub(text, 2), (string.sub(text, 1, 1) == "/"))
397 else
398 w:clear_search()
399 w:set_mode()
401 elseif w:is_mode("follow") then
402 w:eval_js(string.format("update(%q)", w.ibar.input.text))
404 end)
406 w.ibar.input:add_signal("activate", function()
407 local text = w.ibar.input.text
408 if w:is_mode("command") then
409 w:cmd_hist_add(text)
410 w:match_cmd(string.sub(text, 2))
411 w:set_mode()
412 elseif w:is_mode("search") then
413 w:srch_hist_add(text)
414 w:search(string.sub(text, 2), string.sub(text, 1, 1) == "/")
415 -- User doesn't want to return to start position
416 w.search_start_marker = nil
417 w:set_mode()
418 w.ibar.prompt.text = util.escape(text)
419 w.ibar.prompt:show()
421 end)
424 -- Attach signal handlers to a new tab's webview
425 function attach_webview_signals(w, view)
426 view:add_signal("property::title", function (v)
427 w:update_tab_labels()
428 if w:is_current(v) then
429 w:update_win_title(v)
431 end)
433 view:add_signal("property::uri", function (v)
434 w:update_tab_labels()
435 if w:is_current(v) then
436 w:update_uri(v)
438 end)
440 view:add_signal("link-hover", function (v, link)
441 if w:is_current(v) and link then
442 w.sbar.l.uri.text = "Link: " .. util.escape(link)
444 end)
446 view:add_signal("link-unhover", function (v)
447 if w:is_current(v) then
448 w:update_uri(v)
450 end)
452 view:add_signal("form-active", function ()
453 w:set_mode("insert")
454 end)
456 view:add_signal("root-active", function ()
457 w:set_mode()
458 end)
460 view:add_signal("key-press", function ()
461 -- Only allow key press events to hit the webview if the user is in
462 -- "insert" mode.
463 if not w:is_mode("insert") then
464 return true
466 end)
468 view:add_signal("button-release", function (v, mods, button)
469 if w:hit(mods, button) then
470 return true
472 end)
474 -- Update progress widgets & set default mode on navigate
475 view:add_signal("load-status", function (v, status)
476 if w:is_current(v) then
477 w:update_progress(v)
478 if status == "provisional" then
479 w:set_mode()
482 end)
484 -- Domain properties
485 view:add_signal("load-status", function (v, status)
486 if status == "committed" then
487 local domain = string.match(v.uri, "^%a+://([^/]*)/?") or "other"
488 if string.match(domain, "^www.") then domain = string.sub(domain, 5) end
489 local props = util.table.join(domain_props.all or {}, domain_props[domain] or {})
490 for k, v in pairs(props) do
491 info("Domain prop: %s = %s (%s)", k, tostring(v), domain)
492 view:set_prop(k, v)
495 end)
497 -- 'link' contains the download link
498 -- 'mime' contains the mime type that is requested
499 -- return TRUE to accept or FALSE to reject
500 view:add_signal("mime-type-decision", function (v, link, mime)
501 info("Requested link: %s (%s)", link, mime)
502 -- i.e. block binary files like *.exe
503 --if mime == "application/octet-stream" then
504 -- return false
505 --end
506 end)
508 -- 'link' contains the download link
509 -- 'filename' contains the suggested filename (from server or webkit)
510 view:add_signal("download-request", function (v, link, filename)
511 if not filename then return end
512 -- Make download dir
513 os.execute(string.format("mkdir -p %q", DOWNLOAD_DIR))
514 local dl = DOWNLOAD_DIR .. "/" .. filename
515 local wget = string.format("wget -q %q -O %q", link, dl)
516 info("Launching: %s", wget)
517 luakit.spawn(wget)
518 end)
520 -- 'link' contains the download link
521 -- 'reason' contains the reason of the request (i.e. "link-clicked")
522 -- return TRUE to handle the request by yourself or FALSE to proceed
523 -- with default behaviour
524 view:add_signal("new-window-decision", function (v, link, reason)
525 info("New window decision: %s (%s)", link, reason)
526 if reason == "link-clicked" then
527 new_window({ link })
528 return true
530 w:new_tab(link)
531 end)
533 view:add_signal("create-web-view", function (v)
534 return w:new_tab()
535 end)
537 view:add_signal("property::progress", function (v)
538 if w:is_current(v) then
539 w:update_progress(v)
541 end)
543 view:add_signal("expose", function (v)
544 if w:is_current(v) then
545 w:update_scroll(v)
547 end)
550 -- Parses scroll amounts of the form:
551 -- Relative: "+20%", "-20%", "+20px", "-20px"
552 -- Absolute: 20, "20%", "20px"
553 -- And returns an absolute value.
554 function parse_scroll(current, max, value)
555 if string.match(value, "^%d+px$") then
556 return tonumber(string.match(value, "^(%d+)px$"))
557 elseif string.match(value, "^%d+%%$") then
558 return math.ceil(max * (tonumber(string.match(value, "^(%d+)%%$")) / 100))
559 elseif string.match(value, "^[\-\+]%d+px") then
560 return current + tonumber(string.match(value, "^([\-\+]%d+)px"))
561 elseif string.match(value, "^[\-\+]%d+%%$") then
562 return math.ceil(current + (max * (tonumber(string.match(value, "^([\-\+]%d+)%%$")) / 100)))
563 else
564 print("E: unable to parse scroll amount:", value)
568 -- Helper functions which operate on a windows widget structure
569 window_helpers = {
570 -- Return the widget in the currently active tab
571 get_current = function (w) return w.tabs:atindex(w.tabs:current()) end,
572 -- Check if given widget is the widget in the currently active tab
573 is_current = function (w, wi) return w.tabs:indexof(wi) == w.tabs:current() end,
575 -- Wrappers around the mode plugin
576 set_mode = function (w, name) mode.set(w.win, name) end,
577 get_mode = function (w) return mode.get(w.win) end,
578 is_mode = function (w, name) return name == w:get_mode() end,
579 is_any_mode = function (w, t, name) return util.table.hasitem(t, name or w:get_mode()) end,
581 -- Wrappers around the view:get_prop & view:set_prop methods
582 get = function (w, prop, view)
583 if not view then view = w:get_current() end
584 return view:get_prop(prop)
585 end,
587 set = function (w, prop, val, view)
588 if not view then view = w:get_current() end
589 view:set_prop(prop, val)
590 end,
592 get_tab_title = function (w, view)
593 if not view then view = w:get_current() end
594 return view:get_prop("title") or view.uri or "(Untitled)"
595 end,
597 navigate = function (w, uri, view)
598 local v = view or w:get_current()
599 if v then
600 v.uri = uri
601 else
602 return w:new_tab(uri)
604 end,
606 reload = function (w, view)
607 if not view then view = w:get_current() end
608 view:reload()
609 end,
611 new_tab = function (w, uri)
612 local view = webview()
613 w.tabs:append(view)
614 set_http_options(w)
615 attach_webview_signals(w, view)
616 if uri then view.uri = uri end
617 view.show_scrollbars = false
618 w:update_tab_count()
619 return view
620 end,
622 -- close the current tab
623 close_tab = function (w, view)
624 if not view then view = w:get_current() end
625 if not view then return end
626 w.tabs:remove(view)
627 view.uri = "about:blank"
628 view:destroy()
629 w:update_tab_count()
630 w:update_tab_labels()
631 end,
633 -- evaluate javascript code and return string result
634 eval_js = function (w, script, file, view)
635 if not view then view = w:get_current() end
636 return view:eval_js(script, file or "(buffer)")
637 end,
639 -- evaluate javascript code from file and return string result
640 eval_js_from_file = function (w, file, view)
641 local fh, err = io.open(file)
642 if not fh then return error(err) end
643 local script = fh:read("*a")
644 fh:close()
645 return w:eval_js(script, file, view)
646 end,
648 -- Wrapper around the bind plugin's hit method
649 hit = function (w, mods, key)
650 local caught, newbuf = bind.hit(w.binds or {}, mods, key, w.buffer, w:is_mode("normal"), w)
651 w.buffer = newbuf
652 w:update_buf()
653 return caught
654 end,
656 -- Wrapper around the bind plugin's match_cmd method
657 match_cmd = function (w, buffer)
658 return bind.match_cmd(commands, buffer, w)
659 end,
661 -- Toggle source view
662 toggle_source = function (w, show, view)
663 if not view then view = w:get_current() end
664 if show == nil then show = not view:get_view_source() end
665 view:set_view_source(show)
666 end,
668 -- enter command or characters into command line
669 enter_cmd = function (w, cmd)
670 local i = w.ibar.input
671 w:set_mode("command")
672 i.text = cmd
673 i:set_position(-1)
674 end,
676 -- insert a string into the command line at the current cursor position
677 insert_cmd = function (w, str)
678 if not str then return nil end
679 local i = w.ibar.input
680 local text = i.text
681 local pos = i:get_position()
682 local left, right = string.sub(text, 1, pos), string.sub(text, pos+1)
683 i.text = left .. str .. right
684 i:set_position(pos + #str + 1)
685 end,
687 -- search engine wrapper
688 websearch = function (w, args)
689 local sep = string.find(args, " ")
690 local engine = string.sub(args, 1, sep-1)
691 local search = string.sub(args, sep+1)
692 if not search_engines[engine] then
693 print("E: No matching search engine found:", engine)
694 return 0
696 local uri = string.gsub(search_engines[engine], "{%d}", search)
697 return w:navigate(uri)
698 end,
700 -- Command line completion of available commands
701 cmd_completion = function (w)
702 local i = w.ibar.input
703 local s = w.sbar.l.uri
704 local cmpl = {}
706 -- Get last completion (is reset on key press other than <Tab>)
707 if not w.compl_start or w.compl_index == 0 then
708 w.compl_start = "^" .. string.sub(i.text, 2)
709 w.compl_index = 1
712 -- Get suitable commands
713 for _, b in ipairs(commands) do
714 for _, c in pairs(b.commands) do
715 if c and string.match(c, w.compl_start) then
716 table.insert(cmpl, c)
721 table.sort(cmpl)
723 if #cmpl > 0 then
724 local text = ""
725 for index, comp in pairs(cmpl) do
726 if index == w.compl_index then
727 i.text = ":" .. comp .. " "
728 i:set_position(-1)
730 if text ~= "" then
731 text = text .. " | "
733 text = text .. comp
736 -- cycle through all possible completions
737 if w.compl_index == #cmpl then
738 w.compl_index = 1
739 else
740 w.compl_index = w.compl_index + 1
742 s.text = util.escape(text)
744 end,
746 del_word = function (w)
747 local i = w.ibar.input
748 local text = i.text
749 local pos = i:get_position()
750 if text and #text > 1 and pos > 1 then
751 local left, right = string.sub(text, 2, pos), string.sub(text, pos+1)
752 if not string.find(left, "%s") then
753 left = ""
754 elseif string.find(left, "%w+%s*$") then
755 left = string.sub(left, 0, string.find(left, "%w+%s*$") - 1)
756 elseif string.find(left, "%W+%s*$") then
757 left = string.sub(left, 0, string.find(left, "%W+%s*$") - 1)
759 i.text = string.sub(text, 1, 1) .. left .. right
760 i:set_position(#left + 2)
762 end,
764 del_line = function (w)
765 local i = w.ibar.input
766 if i.text ~= ":" then
767 i.text = ":"
768 i:set_position(-1)
770 end,
772 -- Search history adding
773 srch_hist_add = function (w, srch)
774 if not w.srch_hist then w.srch_hist = {} end
775 -- Check overflow
776 if #w.srch_hist > ((MAX_SRCH_HISTORY or 100) + 5) then
777 while #w.srch_hist > (MAX_SRCH_HISTORY or 100) do
778 table.remove(w.srch_hist, 1)
781 table.insert(w.srch_hist, srch)
782 end,
784 -- Search history traversing
785 srch_hist_prev = function (w)
786 if not w.srch_hist then w.srch_hist = {} end
787 if not w.srch_hist_cursor then
788 w.srch_hist_cursor = #w.srch_hist + 1
789 w.srch_hist_current = w.ibar.input.text
791 local c = w.srch_hist_cursor - 1
792 if w.srch_hist[c] then
793 w.srch_hist_cursor = c
794 w.ibar.input.text = w.srch_hist[c]
795 w.ibar.input:set_position(-1)
797 end,
799 srch_hist_next = function (w)
800 if not w.srch_hist then w.srch_hist = {} end
801 local c = (w.srch_hist_cursor or #w.srch_hist) + 1
802 if w.srch_hist[c] then
803 w.srch_hist_cursor = c
804 w.ibar.input.text = w.srch_hist[c]
805 w.ibar.input:set_position(-1)
806 elseif w.srch_hist_current then
807 w.srch_hist_cursor = nil
808 w.ibar.input.text = w.srch_hist_current
809 w.ibar.input:set_position(-1)
811 end,
813 -- Command history adding
814 cmd_hist_add = function (w, cmd)
815 if not w.cmd_hist then w.cmd_hist = {} end
816 -- Make sure history doesn't overflow
817 if #w.cmd_hist > ((MAX_CMD_HISTORY or 100) + 5) then
818 while #w.cmd_hist > (MAX_CMD_HISTORY or 100) do
819 table.remove(w.cmd_hist, 1)
822 table.insert(w.cmd_hist, cmd)
823 end,
825 -- Command history traversing
826 cmd_hist_prev = function (w)
827 if not w.cmd_hist then w.cmd_hist = {} end
828 if not w.cmd_hist_cursor then
829 w.cmd_hist_cursor = #w.cmd_hist + 1
830 w.cmd_hist_current = w.ibar.input.text
832 local c = w.cmd_hist_cursor - 1
833 if w.cmd_hist[c] then
834 w.cmd_hist_cursor = c
835 w.ibar.input.text = w.cmd_hist[c]
836 w.ibar.input:set_position(-1)
838 end,
840 cmd_hist_next = function (w)
841 if not w.cmd_hist then w.cmd_hist = {} end
842 local c = (w.cmd_hist_cursor or #w.cmd_hist) + 1
843 if w.cmd_hist[c] then
844 w.cmd_hist_cursor = c
845 w.ibar.input.text = w.cmd_hist[c]
846 w.ibar.input:set_position(-1)
847 elseif w.cmd_hist_current then
848 w.cmd_hist_cursor = nil
849 w.ibar.input.text = w.cmd_hist_current
850 w.ibar.input:set_position(-1)
852 end,
854 -- Searching functions
855 start_search = function (w, forward)
856 -- Clear previous search results
857 w:clear_search()
858 w:set_mode("search")
859 local i = w.ibar.input
860 if forward then
861 i.text = "/"
862 else
863 i.text = "?"
865 i:focus()
866 i:set_position(-1)
867 end,
869 search = function (w, text, forward)
870 local view = w:get_current()
871 local text = text or w.last_search
872 if forward == nil then forward = true end
873 local case_sensitive = false
874 local wrap = true
876 if not text or #text == 0 then
877 w:clear_search()
878 return nil
881 w.last_search = text
882 if w.searching_forward == nil then
883 w.searching_forward = forward
884 w.search_start_marker = view:get_scroll_vert()
885 else
886 -- Invert the direction if originally searching in reverse
887 forward = (w.searching_forward == forward)
890 view:search(text, case_sensitive, forward, wrap);
891 end,
893 clear_search = function (w)
894 w:get_current():clear_search()
895 -- Clear search state
896 w.last_search = nil
897 w.searching_forward = nil
898 w.search_start_marker = nil
899 end,
901 -- Webview scroll functions
902 scroll_vert = function (w, value, view)
903 if not view then view = w:get_current() end
904 local cur, max = view:get_scroll_vert()
905 if type(value) == "string" then
906 value = parse_scroll(cur, max, value)
908 view:set_scroll_vert(value)
909 end,
911 scroll_horiz = function (w, value, view)
912 if not view then view = w:get_current() end
913 local cur, max = view:get_scroll_horiz()
914 if type(value) == "string" then
915 value = parse_scroll(cur, max, value)
917 view:set_scroll_horiz(value)
918 end,
920 -- vertical scroll of a multiple of the view_size
921 scroll_page = function (w, value, view)
922 if not view then view = w:get_current() end
923 local cur, max, size = view:get_scroll_vert()
924 view:set_scroll_vert(cur + size * value)
925 end,
927 -- Tab traversing functions
928 next_tab = function (w, n)
929 w.tabs:switch((((n or 1) + w.tabs:current() -1) % w.tabs:count()) + 1)
930 end,
931 prev_tab = function (w, n)
932 w.tabs:switch(((w.tabs:current() - (n or 1) -1) % w.tabs:count()) + 1)
933 end,
934 goto_tab = function (w, n)
935 w.tabs:switch(n)
936 end,
938 -- History traversing functions
939 back = function (w, n, view)
940 (view or w:get_current()):go_back(n or 1)
941 end,
942 forward = function (w, n, view)
943 (view or w:get_current()):go_forward(n or 1)
944 end,
946 -- GUI content update functions
947 update_tab_count = function (w, i, t)
948 w.sbar.r.tabi.text = string.format("[%d/%d]", i or w.tabs:current(), t or w.tabs:count())
949 end,
951 update_win_title = function (w, view)
952 if not view then view = w:get_current() end
953 local title = view:get_prop("title")
954 local uri = view.uri
955 if not title and not uri then
956 w.win.title = "luakit"
957 else
958 w.win.title = (title or "luakit") .. " - " .. (uri or "about:blank")
960 end,
962 update_uri = function (w, view, uri)
963 if not view then view = w:get_current() end
964 w.sbar.l.uri.text = util.escape((uri or (view and view.uri) or "about:blank"))
965 end,
967 update_progress = function (w, view, p)
968 if not view then view = w:get_current() end
969 if not p then p = view:get_prop("progress") end
970 if not view:loading() or p == 1 then
971 w.sbar.l.loaded:hide()
972 else
973 w.sbar.l.loaded:show()
974 w.sbar.l.loaded.text = string.format("(%d%%)", p * 100)
976 end,
978 update_scroll = function (w, view)
979 if not view then view = w:get_current() end
980 local val, max = view:get_scroll_vert()
981 if max == 0 then val = "All"
982 elseif val == 0 then val = "Top"
983 elseif val == max then val = "Bot"
984 else val = string.format("%2d%%", (val/max) * 100)
986 w.sbar.r.scroll.text = val
987 end,
989 update_buf = function (w)
990 if w.buffer then
991 w.sbar.r.buf.text = util.escape(string.format(" %-3s", w.buffer))
992 w.sbar.r.buf:show()
993 else
994 w.sbar.r.buf:hide()
996 end,
998 update_binds = function (w, mode)
999 -- Generate the list of active key & buffer binds for this mode
1000 w.binds = util.table.join(mode_binds[mode], mode_binds.all)
1001 -- Clear & hide buffer
1002 w.buffer = nil
1003 w:update_buf()
1004 end,
1006 -- Tab label functions
1007 make_tab_label = function (w, pos)
1008 local t = {
1009 label = label(),
1010 sep = label(),
1011 ebox = eventbox(),
1012 layout = hbox(),
1014 t.label.font = theme.tablabel_font or theme.font
1015 t.layout:pack_start(t.label, true, true, 0)
1016 t.layout:pack_start(t.sep, false, false, 0)
1017 t.ebox:set_child(t.layout)
1018 t.ebox:add_signal("button-release", function (e, m, b)
1019 if b == 1 then
1020 w.tabs:switch(pos)
1021 return true
1022 elseif b == 2 then
1023 w:close_tab(w.tabs:atindex(pos))
1024 return true
1026 end)
1027 return t
1028 end,
1030 destroy_tab_label = function (w, t)
1031 if not t then t = table.remove(w.tbar.titles) end
1032 -- Destroy widgets without their own windows first (I.e. labels)
1033 for _, wi in ipairs{ t.label, t.sep} do wi:destroy() end
1034 for _, wi in ipairs{ t.ebox, t.layout} do wi:destroy() end
1035 end,
1037 update_tab_labels = function (w, current)
1038 local tb = w.tbar
1039 local count, current = w.tabs:count(), current or w.tabs:current()
1040 tb.ebox:hide()
1042 -- Leave the tablist hidden if there is only one tab open
1043 if count <= 1 then
1044 return nil
1047 if count ~= #tb.titles then
1048 -- Grow the number of labels
1049 while count > #tb.titles do
1050 local t = w:make_tab_label(#tb.titles + 1)
1051 tb.layout:pack_start(t.ebox, true, true, 0)
1052 table.insert(tb.titles, t)
1054 -- Prune number of labels
1055 while count < #tb.titles do
1056 w:destroy_tab_label()
1060 if count ~= 0 then
1061 for i = 1, count do
1062 local t = tb.titles[i]
1063 local title = " " ..i.. " "..w:get_tab_title(w.tabs:atindex(i))
1064 t.label.text = util.escape(string.format(theme.tablabel_format or "%s", title))
1065 w:apply_tablabel_theme(t, i == current)
1068 tb.ebox:show()
1069 end,
1071 -- Theme functions
1072 apply_tablabel_theme = function (w, t, selected, atheme)
1073 local theme = atheme or theme
1074 if selected then
1075 t.label.fg = theme.selected_tablabel_fg or theme.tablabel_fg or theme.fg
1076 t.ebox.bg = theme.selected_tablabel_bg or theme.tablabel_bg or theme.bg
1077 else
1078 t.label.fg = theme.tablabel_fg or theme.fg
1079 t.ebox.bg = theme.tablabel_bg or theme.bg
1081 end,
1083 apply_window_theme = function (w, atheme)
1084 local theme = atheme or theme
1085 local s, i, t = w.sbar, w.ibar, w.tbar
1086 local fg, bg, font = theme.fg, theme.bg, theme.font
1088 -- Set foregrounds
1089 for wi, v in pairs({
1090 [s.l.uri] = theme.uri_fg or theme.statusbar_fg or fg,
1091 [s.l.loaded] = theme.loaded_fg or theme.statusbar_fg or fg,
1092 [s.r.buf] = theme.buf_fg or theme.statusbar_fg or fg,
1093 [s.r.tabi] = theme.tabi_fg or theme.statusbar_fg or fg,
1094 [s.r.scroll] = theme.scroll_fg or theme.statusbar_fg or fg,
1095 [i.prompt] = theme.prompt_fg or theme.inputbar_fg or fg,
1096 [i.input] = theme.input_fg or theme.inputbar_fg or fg,
1097 }) do wi.fg = v end
1099 -- Set backgrounds
1100 for wi, v in pairs({
1101 [s.l.ebox] = theme.statusbar_bg or bg,
1102 [s.r.ebox] = theme.statusbar_bg or bg,
1103 [s.ebox] = theme.statusbar_bg or bg,
1104 [i.ebox] = theme.inputbar_bg or bg,
1105 [i.input] = theme.input_bg or theme.inputbar_bg or bg,
1106 }) do wi.bg = v end
1108 -- Set fonts
1109 for wi, v in pairs({
1110 [s.l.uri] = theme.uri_font or theme.statusbar_font or font,
1111 [s.l.loaded] = theme.loaded_font or theme.statusbar_font or font,
1112 [s.r.buf] = theme.buf_font or theme.statusbar_font or font,
1113 [s.r.tabi] = theme.tabi_font or theme.statusbar_font or font,
1114 [s.r.scroll] = theme.scroll_font or theme.statusbar_font or font,
1115 [i.prompt] = theme.prompt_font or theme.inputbar_font or font,
1116 [i.input] = theme.input_font or theme.inputbar_font or font,
1117 }) do wi.font = v end
1118 end,
1121 -- Create new window
1122 function new_window(uris)
1123 local w = build_window()
1125 -- Pack the window table full of the common helper functions
1126 for k, v in pairs(window_helpers) do w[k] = v end
1128 attach_window_signals(w)
1130 -- Apply window theme
1131 w:apply_window_theme()
1133 -- Populate notebook with tabs
1134 for _, uri in ipairs(uris or {}) do
1135 w:new_tab(uri)
1138 -- Make sure something is loaded
1139 if w.tabs:count() == 0 then
1140 w:new_tab(HOMEPAGE)
1143 -- Set initial mode
1144 w:set_mode()
1146 return w
1149 new_window(uris)
1151 -- vim: ft=lua:et:sw=4:ts=8:sts=4:tw=80