Fix window title updating
[luakit.git] / rc.lua
blobebe8d1e8619bc9b176a863b4d61aaf4900b4d618
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
17 -- Variable definitions
18 HOMEPAGE = "http://luakit.org/"
19 --HOMEPAGE = "http://github.com/mason-larobina/luakit"
20 SCROLL_STEP = 20
21 MAX_HISTORY = 100
23 -- Luakit theme
24 theme = theme or {
25 -- Default settings
26 font = "monospace normal 9",
27 fg = "#fff",
28 bg = "#000",
30 -- General settings
31 statusbar_fg = "#fff",
32 statusbar_bg = "#000",
33 inputbar_fg = "#000",
34 inputbar_bg = "#fff",
36 -- Specific settings
37 loaded_fg = "#33AADD",
38 tablabel_fg = "#999",
39 tablabel_bg = "#111",
40 selected_tablabel_fg = "#fff",
41 selected_tablabel_bg = "#000",
43 -- Enforce a minimum tab width of 30 characters to prevent longer tab
44 -- titles overshadowing small tab titles when things get crowded.
45 tablabel_format = "%-30s",
48 widget.add_signal("new", function(wi)
49 wi:add_signal("init", function(wi)
50 if wi.type == "window" then
51 wi:add_signal("destroy", function ()
52 -- Call the quit function if this was the last window left
53 if #luakit.windows == 0 then luakit.quit() end
54 end)
55 end
56 end)
57 end)
59 -- Add key bindings to be used across all windows
60 mode_binds = {
61 all = {
62 -- bind.key({Modifiers}, Key name, function (w, opts) .. end, opts),
63 bind.key({}, "Escape", function (w) w:set_mode() end),
64 bind.key({"Control"}, "[", function (w) w:set_mode() end),
66 normal = {
67 -- bind.key({Modifiers}, Key name, function (w, opts) .. end, opts),
68 bind.key({}, "Escape", function (w) w:set_mode() end),
69 bind.key({}, "i", function (w) w:set_mode("insert") end),
70 bind.key({}, ":", function (w) w:set_mode("command") end),
71 bind.key({}, "h", function (w) w:scroll_horiz("-"..SCROLL_STEP.."px") end),
72 bind.key({}, "j", function (w) w:scroll_vert ("+"..SCROLL_STEP.."px") end),
73 bind.key({}, "k", function (w) w:scroll_vert ("-"..SCROLL_STEP.."px") end),
74 bind.key({}, "l", function (w) w:scroll_horiz("+"..SCROLL_STEP.."px") end),
75 bind.key({}, "Left", function (w) w:scroll_horiz("-"..SCROLL_STEP.."px") end),
76 bind.key({}, "Down", function (w) w:scroll_vert ("+"..SCROLL_STEP.."px") end),
77 bind.key({}, "Up", function (w) w:scroll_vert ("-"..SCROLL_STEP.."px") end),
78 bind.key({}, "Right", function (w) w:scroll_horiz("+"..SCROLL_STEP.."px") end),
80 -- bind.buf(Pattern, function (w, buffer, opts) .. end, opts),
81 bind.buf("^[0-9]*H$", function (w, b) w:back (tonumber(string.match(b, "^(%d*)H$") or 1)) end),
82 bind.buf("^[0-9]*L$", function (w, b) w:forward(tonumber(string.match(b, "^(%d*)L$") or 1)) end),
83 bind.buf("^gg$", function (w) w:scroll_vert("0%") end),
84 bind.buf("^G$", function (w) w:scroll_vert("100%") end),
85 bind.buf("^[0-9]*gT$", function (w, b) w:prev_tab(tonumber(string.match(b, "^(%d*)gT$") or 1)) end),
86 bind.buf("^[0-9]*gt$", function (w, b) w:next_tab(tonumber(string.match(b, "^(%d*)gt$") or 1)) end),
87 bind.buf("^[\-\+]?[0-9]+[%%|G]$", function (w, b) w:scroll_vert(string.match(b, "^([\-\+]?%d+)[%%G]$") .. "%") end),
88 bind.buf("^gH$", function (w) w:new_tab(HOMEPAGE) end),
89 bind.buf("^gh$", function (w) w:navigate(HOMEPAGE) end),
90 bind.buf("^ZZ$", function (w) luakit.quit() end),
92 command = {
93 bind.key({}, "Up", function (w) w:cmd_hist_prev() end),
94 bind.key({}, "Down", function (w) w:cmd_hist_next() end),
96 insert = { },
99 -- Commands
100 commands = {
101 -- bind.cmd({Command, Alias1, ...}, function (w, arg, opts) .. end, opts),
102 bind.cmd({"open", "o"}, function (w, a) w:navigate(a) end),
103 bind.cmd({"tabopen", "t"}, function (w, a) w:new_tab(a) end),
104 bind.cmd({"back" }, function (w, a) w:back(tonumber(a) or 1) end),
105 bind.cmd({"forward", "f"}, function (w, a) w:forward(tonumber(a) or 1) end),
106 bind.cmd({"scroll" }, function (w, a) w:scroll_vert(a) end),
107 bind.cmd({"quit", "q"}, function (w) luakit.quit() end),
110 -- Build and pack window widgets
111 function build_window()
112 -- Create a table for widgets and state variables for a window
113 local w = {
114 win = window(),
115 ebox = eventbox(),
116 layout = vbox(),
117 tabs = notebook(),
118 -- Tab bar widgets
119 tbar = {
120 layout = hbox(),
121 ebox = eventbox(),
122 titles = { },
124 -- Status bar widgets
125 sbar = {
126 layout = hbox(),
127 ebox = eventbox(),
128 -- Left aligned widgets
129 l = {
130 layout = hbox(),
131 ebox = eventbox(),
132 uri = label(),
133 loaded = label(),
135 -- Fills space between the left and right aligned widgets
136 filler = label(),
137 -- Right aligned widgets
138 r = {
139 layout = hbox(),
140 ebox = eventbox(),
141 buf = label(),
142 tabi = label(),
143 scroll = label(),
146 -- Input bar widgets
147 ibar = {
148 layout = hbox(),
149 ebox = eventbox(),
150 prompt = label(),
151 input = entry(),
155 -- Assemble window
156 w.ebox:set_child(w.layout)
157 w.win:set_child(w.ebox)
159 -- Pack tab bar
160 local t = w.tbar
161 t.ebox:set_child(t.layout, false, false, 0)
162 w.layout:pack_start(t.ebox, false, false, 0)
164 -- Pack notebook
165 w.layout:pack_start(w.tabs, true, true, 0)
167 -- Pack left-aligned statusbar elements
168 local l = w.sbar.l
169 l.layout:pack_start(l.uri, false, false, 0)
170 l.layout:pack_start(l.loaded, false, false, 0)
171 l.ebox:set_child(l.layout)
173 -- Pack right-aligned statusbar elements
174 local r = w.sbar.r
175 r.layout:pack_start(r.buf, false, false, 0)
176 r.layout:pack_start(r.tabi, false, false, 0)
177 r.layout:pack_start(r.scroll, false, false, 0)
178 r.ebox:set_child(r.layout)
180 -- Pack status bar elements
181 local s = w.sbar
182 s.layout:pack_start(l.ebox, false, false, 0)
183 s.layout:pack_start(s.filler, true, true, 0)
184 s.layout:pack_start(r.ebox, false, false, 0)
185 s.ebox:set_child(s.layout)
186 w.layout:pack_start(s.ebox, false, false, 0)
188 -- Pack input bar
189 local i = w.ibar
190 i.layout:pack_start(i.prompt, false, false, 0)
191 i.layout:pack_start(i.input, true, true, 0)
192 i.ebox:set_child(i.layout)
193 w.layout:pack_start(i.ebox, false, false, 0)
195 -- Other settings
196 i.input.show_frame = false
197 w.tabs.show_tabs = false
198 l.loaded:hide()
199 l.uri.selectable = true
201 return w
204 function attach_window_signals(w)
205 -- Attach notebook widget signals
206 w.tabs:add_signal("page-added", function(nbook, view, idx)
207 w:update_tab_count(idx)
208 w:update_tab_labels()
209 end)
211 w.tabs:add_signal("switch-page", function(nbook, view, idx)
212 w:update_tab_count(idx)
213 w:update_win_title(view)
214 w:update_uri(view)
215 w:update_progress(view)
216 w:update_tab_labels(idx)
217 end)
219 -- Attach window widget signals
220 w.win:add_signal("key-press", function(win, mods, key)
221 if w:hit(mods, key) then
222 return true
224 end)
226 w.win:add_signal("mode-changed", function(win, mode)
227 w:update_binds(mode)
228 w.cmd_hist_cursor = nil
230 if mode == "normal" then
231 w.ibar.prompt:hide()
232 w.ibar.input:hide()
233 elseif mode == "insert" then
234 w.ibar.input:hide()
235 w.ibar.input.text = ""
236 w.ibar.prompt.text = "-- INSERT --"
237 w.ibar.prompt:show()
238 elseif mode == "command" then
239 w.ibar.prompt:hide()
240 w.ibar.input.text = ":"
241 w.ibar.input:show()
242 w.ibar.input:focus()
243 w.ibar.input:set_position(-1)
244 else
245 w.ibar.prompt.text = ""
246 w.ibar.input.text = ""
248 end)
250 -- Attach inputbar widget signals
251 w.ibar.input:add_signal("changed", function()
252 -- Auto-exit "command" mode if you backspace or delete the ":"
253 -- character at the start of the input box when in "command" mode.
254 if w:is_mode("command") and not string.match(w.ibar.input.text, "^:") then
255 w:set_mode()
257 end)
259 w.ibar.input:add_signal("activate", function()
260 local buffer = w.ibar.input.text
261 w:cmd_hist_add(buffer)
262 w:match_cmd(string.sub(buffer, 2))
263 w:set_mode()
264 end)
267 -- Attach signal handlers to a new tab's webview
268 function attach_webview_signals(w, view)
269 view:add_signal("title-changed", function (v)
270 w:update_tab_labels()
271 if w:is_current(v) then
272 w:update_win_title(v)
274 end)
276 view:add_signal("property::uri", function(v)
277 w:update_tab_labels()
278 if w:is_current(v) then
279 w:update_uri(v)
281 end)
283 view:add_signal("key-press", function ()
284 -- Only allow key press events to hit the webview if the user is in
285 -- "insert" mode.
286 if not w:is_mode("insert") then
287 return true
289 end)
291 view:add_signal("load-start", function (v)
292 if w:is_current(v) then
293 w:update_progress(v, 0)
294 w:set_mode()
296 end)
298 view:add_signal("progress-update", function (v)
299 if w:is_current(v) then
300 w:update_progress(v)
302 end)
304 view:add_signal("expose", function(v)
305 if w:is_current(v) then
306 w:update_scroll(v)
308 end)
311 -- Parses scroll amounts of the form:
312 -- Relative: "+20%", "-20%", "+20px", "-20px"
313 -- Absolute: 20, "20%", "20px"
314 -- And returns an absolute value.
315 function parse_scroll(current, max, value)
316 if string.match(value, "^%d+px$") then
317 return tonumber(string.match(value, "^(%d+)px$"))
318 elseif string.match(value, "^%d+%%$") then
319 return math.ceil(max * (tonumber(string.match(value, "^(%d+)%%$")) / 100))
320 elseif string.match(value, "^[\-\+]%d+px") then
321 return current + tonumber(string.match(value, "^([\-\+]%d+)px"))
322 elseif string.match(value, "^[\-\+]%d+%%$") then
323 return math.ceil(current + (max * (tonumber(string.match(value, "^([\-\+]%d+)%%$")) / 100)))
324 else
325 print("E: unable to parse scroll amount:", value)
329 -- Helper functions which operate on a windows widget structure
330 window_helpers = {
331 -- Return the widget in the currently active tab
332 get_current = function(w) return w.tabs:atindex(w.tabs:current()) end,
333 -- Check if given widget is the widget in the currently active tab
334 is_current = function(w, wi) return w.tabs:indexof(wi) == w.tabs:current() end,
336 -- Wrappers around the mode plugin
337 set_mode = function(w, name) mode.set(w.win, name) end,
338 get_mode = function(w) return mode.get(w.win) end,
339 is_mode = function(w, name) return name == w:get_mode() end,
340 is_any_mode = function(w, t, name) return util.table.hasitem(t, name or w:get_mode()) end,
342 -- Wrappers around the view:get_prop & view:set_prop methods
343 get = function (w, prop, view)
344 if not view then view = w:get_current() end
345 return view:get_prop(prop)
346 end,
348 set = function (w, prop, val, view)
349 if not view then view = w:get_current() end
350 view:set_prop(prop, val)
351 end,
353 get_tab_title = function (w, view)
354 if not view then view = w:get_current() end
355 return view:get_prop("title") or view.uri or "(Untitled)"
356 end,
358 navigate = function(w, uri, view)
359 (view or w:get_current()).uri = uri
360 end,
362 new_tab = function(w, uri)
363 local view = webview()
364 w.tabs:append(view)
365 attach_webview_signals(w, view)
366 if uri then view.uri = uri end
367 view.show_scrollbars = false
368 w:update_tab_count()
369 end,
371 -- Wrapper around the bind plugin's hit method
372 hit = function (w, mods, key)
373 local caught, newbuf = bind.hit(w.binds or {}, mods, key, w.buffer, w:is_mode("normal"), w)
374 w.buffer = newbuf
375 w:update_buf()
376 return caught
377 end,
379 -- Wrapper around the bind plugin's match_cmd method
380 match_cmd = function (w, buffer)
381 return bind.match_cmd(commands, buffer, w)
382 end,
384 -- Command history adding
385 cmd_hist_add = function(w, cmd)
386 if not w.cmd_hist then w.cmd_hist = {} end
387 -- Make sure history doesn't overflow
388 if #w.cmd_hist > ((MAX_HISTORY or 100) + 5) then
389 while #w.cmd_hist > (MAX_HISTORY or 100) do
390 table.remove(w.cmd_hist, 1)
393 table.insert(w.cmd_hist, cmd)
394 end,
396 -- Command history traversing
397 cmd_hist_prev = function(w)
398 if not w.cmd_hist then w.cmd_hist = {} end
399 if not w.cmd_hist_cursor then
400 w.cmd_hist_cursor = #w.cmd_hist + 1
401 w.cmd_hist_current = w.ibar.input.text
403 local c = w.cmd_hist_cursor - 1
404 if w.cmd_hist[c] then
405 w.cmd_hist_cursor = c
406 w.ibar.input.text = w.cmd_hist[c]
407 w.ibar.input:set_position(-1)
409 end,
411 cmd_hist_next = function(w)
412 if not w.cmd_hist then w.cmd_hist = {} end
413 local c = (w.cmd_hist_cursor or #w.cmd_hist) + 1
414 if w.cmd_hist[c] then
415 w.cmd_hist_cursor = c
416 w.ibar.input.text = w.cmd_hist[c]
417 w.ibar.input:set_position(-1)
418 elseif w.cmd_hist_current then
419 w.cmd_hist_cursor = nil
420 w.ibar.input.text = w.cmd_hist_current
421 w.ibar.input:set_position(-1)
423 end,
425 -- Webview scroll functions
426 scroll_vert = function(w, value, view)
427 if not view then view = w:get_current() end
428 local cur, max = view:get_scroll_vert()
429 if type(value) == "string" then
430 value = parse_scroll(cur, max, value)
432 view:set_scroll_vert(value)
433 end,
435 scroll_horiz = function(w, value)
436 if not view then view = w:get_current() end
437 local cur, max = view:get_scroll_horiz()
438 if type(value) == "string" then
439 value = parse_scroll(cur, max, value)
441 view:set_scroll_horiz(value)
442 end,
444 -- Tab traversing functions
445 next_tab = function(w, n)
446 w.tabs:switch((((n or 1) + w.tabs:current() -1) % w.tabs:count()) + 1)
447 end,
448 prev_tab = function(w, n)
449 w.tabs:switch(((w.tabs:current() - (n or 1) -1) % w.tabs:count()) + 1)
450 end,
451 goto_tab = function(w, n)
452 w.tabs:switch(n)
453 end,
455 -- History traversing functions
456 back = function(w, n, view)
457 (view or w:get_current()):go_back(n or 1)
458 end,
459 forward = function(w, n, view)
460 (view or w:get_current()):go_forward(n or 1)
461 end,
463 -- GUI content update functions
464 update_tab_count = function (w, i, t)
465 w.sbar.r.tabi.text = string.format("[%d/%d]", i or w.tabs:current(), t or w.tabs:count())
466 end,
468 update_win_title = function (w, view)
469 if not view then view = w:get_current() end
470 local title = view:get_prop("title")
471 local uri = view.uri
472 if not title and not uri then
473 w.win.title = "luakit"
474 else
475 w.win.title = (title or "luakit") .. " - " .. (uri or "about:blank")
477 end,
479 update_uri = function (w, view, uri)
480 if not view then view = w:get_current() end
481 w.sbar.l.uri.text = (uri or view.uri or "about:blank")
482 end,
484 update_progress = function (w, view, p)
485 if not view then view = w:get_current() end
486 if not p then p = view:get_prop("progress") end
487 if not view:loading() or p == 1 then
488 w.sbar.l.loaded:hide()
489 else
490 w.sbar.l.loaded:show()
491 w.sbar.l.loaded.text = string.format("(%d%%)", p * 100)
493 end,
495 update_scroll = function (w, view)
496 if not view then view = w:get_current() end
497 local val, max = view:get_scroll_vert()
498 if max == 0 then val = "All"
499 elseif val == 0 then val = "Top"
500 elseif val == max then val = "Bot"
501 else val = string.format("%2d%%", (val/max) * 100)
503 w.sbar.r.scroll.text = val
504 end,
506 update_buf = function (w)
507 if w.buffer then
508 w.sbar.r.buf.text = string.format(" %-3s", w.buffer)
509 w.sbar.r.buf:show()
510 else
511 w.sbar.r.buf:hide()
513 end,
515 update_binds = function (w, mode)
516 -- Generate the list of binds for this mode + all
517 w.binds = util.table.clone(mode_binds[mode])
518 for _, b in ipairs(mode_binds["all"]) do
519 table.insert(w.binds, b)
521 -- Clear & hide buffer
522 w.buffer = nil
523 w:update_buf()
524 end,
526 -- Tab label functions
527 make_tab_label = function (w, pos)
528 local t = {
529 label = label(),
530 sep = label(),
531 ebox = eventbox(),
532 layout = hbox(),
534 t.label.font = theme.tablabel_font or theme.font
535 t.layout:pack_start(t.label, true, true, 0)
536 t.layout:pack_start(t.sep, false, false, 0)
537 t.ebox:set_child(t.layout)
538 t.ebox:add_signal("clicked", function(e) w.tabs:switch(pos) end)
539 return t
540 end,
542 destroy_tab_label = function(w, t)
543 if not t then t = table.remove(w.tbar.titles) end
544 for _, wi in pairs(t) do
545 wi:destroy()
547 end,
549 update_tab_labels = function (w, current)
550 local tb = w.tbar
551 local count, current = w.tabs:count(), current or w.tabs:current()
553 if count ~= #tb.titles then
554 -- Grow the number of labels
555 while count > #tb.titles do
556 local t = w:make_tab_label(#tb.titles + 1)
557 tb.layout:pack_start(t.ebox, true, true, 0)
558 table.insert(tb.titles, t)
560 -- Prune number of labels
561 while count < #tb.titles do
562 w:destroy_tab_label()
566 if count ~= 0 then
567 for i = 1, count do
568 local t = tb.titles[i]
569 local title = " " ..i.. " "..w:get_tab_title(w.tabs:atindex(i))
570 t.label.text = string.format(theme.tablabel_format or "%s", title)
571 w:apply_tablabel_theme(t, i == current)
575 end,
577 -- Theme functions
578 apply_tablabel_theme = function (w, t, selected, atheme)
579 local theme = atheme or theme
580 if selected then
581 t.label.fg = theme.selected_tablabel_fg or theme.tablabel_fg or theme.fg
582 t.ebox.bg = theme.selected_tablabel_bg or theme.tablabel_bg or theme.bg
583 else
584 t.label.fg = theme.tablabel_fg or theme.fg
585 t.ebox.bg = theme.tablabel_bg or theme.bg
587 end,
589 apply_window_theme = function (w, atheme)
590 local theme = atheme or theme
591 local s, i, t = w.sbar, w.ibar, w.tbar
592 local fg, bg, font = theme.fg, theme.bg, theme.font
594 -- Set foregrounds
595 for wi, v in pairs({
596 [s.l.uri] = theme.uri_fg or theme.statusbar_fg or fg,
597 [s.l.loaded] = theme.loaded_fg or theme.statusbar_fg or fg,
598 [s.r.buf] = theme.buf_fg or theme.statusbar_fg or fg,
599 [s.r.tabi] = theme.tabi_fg or theme.statusbar_fg or fg,
600 [s.r.scroll] = theme.scroll_fg or theme.statusbar_fg or fg,
601 [i.prompt] = theme.prompt_fg or theme.inputbar_fg or fg,
602 [i.input] = theme.input_fg or theme.inputbar_fg or fg,
603 }) do wi.fg = v end
605 -- Set backgrounds
606 for wi, v in pairs({
607 [s.l.ebox] = theme.statusbar_bg or bg,
608 [s.r.ebox] = theme.statusbar_bg or bg,
609 [s.ebox] = theme.statusbar_bg or bg,
610 [i.ebox] = theme.inputbar_bg or bg,
611 [i.input] = theme.input_bg or theme.inputbar_bg or bg,
612 }) do wi.bg = v end
614 -- Set fonts
615 for wi, v in pairs({
616 [s.l.uri] = theme.uri_font or theme.statusbar_font or font,
617 [s.l.loaded] = theme.loaded_font or theme.statusbar_font or font,
618 [s.r.buf] = theme.buf_font or theme.statusbar_font or font,
619 [s.r.tabi] = theme.tabi_font or theme.statusbar_font or font,
620 [s.r.scroll] = theme.scroll_font or theme.statusbar_font or font,
621 [i.prompt] = theme.prompt_font or theme.inputbar_font or font,
622 [i.input] = theme.input_font or theme.inputbar_font or font,
623 }) do wi.font = v end
624 end,
627 -- Create new window
628 function new_window(uris)
629 local w = build_window()
631 -- Pack the window table full of the common helper functions
632 for k, v in pairs(window_helpers) do w[k] = v end
634 attach_window_signals(w)
636 -- Apply window theme
637 w:apply_window_theme()
639 -- Populate notebook with tabs
640 for _, uri in ipairs(uris or {}) do
641 w:new_tab(uri)
644 -- Make sure something is loaded
645 if w.tabs:count() == 0 then
646 w:new_tab(HOMEPAGE)
649 -- Set initial mode
650 w:set_mode()
652 return w
655 new_window(uris)