Column_header_color = {r=0.7, g=0.7, b=0.7}
Pane_title_color = {r=0.5, g=0.5, b=0.5}
Pane_title_background_color = {r=0, g=0, b=0, a=0.1}
Pane_background_color = {r=0.7, g=0.7, b=0.7, a=0.1}
Grab_background_color = {r=0.7, g=0.7, b=0.7}
Cursor_pane_background_color = {r=0.7, g=0.7, b=0, a=0.1}
Menu_background_color = {r=0.6, g=0.8, b=0.6}
Menu_border_color = {r=0.6, g=0.7, b=0.6}
Menu_command_color = {r=0.2, g=0.2, b=0.2}
Command_palette_background_color = Menu_background_color
Command_palette_border_color = Menu_border_color
Command_palette_command_color = Menu_command_color
Command_palette_alternatives_background_color = Menu_background_color
Command_palette_highlighted_alternative_background_color = {r=0.5, g=0.7, b=0.3}
Command_palette_alternatives_color = {r=0.3, g=0.5, b=0.3}
Crosslink_color={r=0, g=0.7, b=0.7}
Crosslink_background_color={r=0, g=0, b=0, a=0.1}
run = {}
profile = require 'profile'
Editor_state = {}
function run.initialize_globals()
Surface = {}
Links = {}
Panes_to_draw = {} Column_headers_to_draw = {}
Display_settings = {
state={id='normal'},
x=0, y=0, column_width=400,
show_debug=false,
palette=nil, }
Font_height = 20
Line_height = math.floor(Font_height*1.3)
Menu_status_bar_height = 5 + Line_height + 5
Column_header_height = 5 + Line_height + 5
Header_height = Menu_status_bar_height + Column_header_height
Padding_vertical = 20 Padding_horizontal = 20
Margin_above = 10
Margin_below = 10
Pan_step = 10
Pan = {}
Cursor_pane = {col=0, row=1} Grab_pane = nil
Directory = nil
Directory_error = nil
Cursor_time = 0
Error_log = edit.initialize_state(0, 0, Display_settings.column_width, Font_height, Line_height)
Error_log.id = 'errors'
Error_log.filename = nil Error_log.editable = false
Text.redraw_all(Error_log)
Current_error = nil
Current_error_time = nil
end
function run.initialize(arg)
log_new('run')
if Settings then
print('loading settings')
run.load_settings()
Directory = Settings.data_directory
else
print('no saved settings; initializing to defaults')
run.initialize_default_settings()
end
if #arg > 0 then
if not is_absolute_path(arg[1]) then
Directory_error = 'Please use an unambiguous absolute path for the notes directory.'
return
end
Directory = arg[1]
print('setting Directory from commandline: '..arg[1])
if Directory:sub(#Directory,#Directory) ~= '/' then
Directory = Directory..'/'
end
print('Directory is now '..Directory)
end
if Directory == nil then
print('setting Directory_error')
if Settings == nil then
Directory_error =
"Please run pensieve.love once from a terminal window and specify the\n"..
"location of your notes. The location will be remembered in future.\n"..
"Thank you! If all goes well, you won't see this message ever again."
else
Directory_error =
"Please perform a one-time migration for your notes:\n"..
"\n"..
"* (optional) Move any existing notes in "..App.save_dir.."data\n"..
" or similar locations to your preferred location.\n"..
"\n"..
"* (optional) Move any existing "..App.save_dir.."config\n"..
" file _inside_ your notes directory to preserve any prior state\n"..
" of your note-taking surface (open columns, etc.).\n"..
"\n"..
"* Please run pensieve.love once from a terminal window and specify the\n"..
" location of your notes as an absolute path. This location will be\n"..
" remembered in future.\n"..
"\n"..
"Thank you! If all goes well, you won't see this message ever again."
end
return
end
assert(Directory, 'no directory to browse notes in')
print('Directory initialized to '..Directory)
Error_log.filename = Directory..'errors'
run.load_more_settings_from_notes_directory()
Editor_state = nil
love.window.setTitle('pensieve.love')
if #arg > 1 then
print('ignoring commandline args after '..arg[1])
end
print_and_log('reading notes from '..Directory)
print_and_log('put any notes there (and make frequent backups)')
if Display_settings.column_width > App.screen.width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal then
Display_settings.column_width = math.max(200, App.screen.width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal)
end
Cursor_pane.col = math.min(Cursor_pane.col, #Surface)
if Cursor_pane.col >= 1 then
Cursor_pane.row = math.min(Cursor_pane.row, #Surface[Cursor_pane.col])
end
plan_draw()
if rawget(_G, 'jit') then
jit.off()
jit.flush()
end
end
function print_and_log(s)
print(s)
log(3, s)
end
function run.load_settings()
Display_settings.font = love.graphics.newFont(Settings.font_height)
_, _, App.screen.flags = App.screen.size()
App.screen.flags.resizable = true
App.screen.width, App.screen.height = Settings.width, Settings.height
App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
run.set_window_position_from_settings(Settings)
Font_height = Settings.font_height
Line_height = math.floor(Font_height*1.3)
love.graphics.setFont(Display_settings.font)
end
function run.load_more_settings_from_notes_directory()
local f = App.open_for_reading(Directory..'config')
if f then
local directory_settings = json.decode(f:read())
f:close()
Display_settings.column_width = directory_settings.column_width
for _,column_name in ipairs(directory_settings.columns) do
create_column(column_name)
end
Cursor_pane.col = directory_settings.cursor_col
Cursor_pane.row = directory_settings.cursor_row
Display_settings.x = directory_settings.surface_x
Display_settings.y = directory_settings.surface_y
else
command.recently_modified()
end
end
function run.set_window_position_from_settings(settings)
local os = love.system.getOS()
if os == 'Linux' then
App.screen.move(settings.x, settings.y-37, settings.displayindex)
else
App.screen.move(settings.x, settings.y, settings.displayindex)
end
end
function run.initialize_default_settings()
Display_settings.font = love.graphics.newFont(Font_height)
love.graphics.setFont(Display_settings.font)
run.initialize_window_geometry()
Display_settings.column_width = 40*Display_settings.font:getWidth('m')
end
function run.initialize_window_geometry()
App.screen.width, App.screen.height, App.screen.flags = App.screen.size()
App.screen.flags.resizable = true
App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
end
function run.resize(w, h)
App.screen.width, App.screen.height = w, h
if Directory_error then return end
plan_draw()
end
function load_pane(id)
local result = edit.initialize_state(0, 0, math.min(Display_settings.column_width, App.screen.width-Margin_right), love.graphics.getFont(), Font_height, Line_height)
result.id = id
result.filename = Directory..id
load_from_disk(result)
if Links[id] == nil then
Links[id] = load_links(id)
end
result.font_height = Font_height
result.line_height = Line_height
result.editable = false
edit.check_locs(result)
Text.redraw_all(result)
return result
end
function height(pane)
if pane._height == nil then
refresh_pane_height(pane)
end
return pane._height
end
function refresh_pane_height(pane)
local y = 0
if pane.title then
y = y + 5+Line_height+5
end
rehydrate_pane(pane)
for i=1,#pane.lines do
local line = pane.lines[i]
if line.mode == 'text' then
Text.populate_screen_line_starting_pos(pane, i)
y = y + Line_height*#pane.line_cache[i].screen_line_starting_pos
Text.clear_screen_line_cache(pane, i)
elseif line.mode == 'drawing' then
y = y + Drawing.pixels(line.h, Display_settings.column_width) + Drawing_padding_height
else
assert(false, ('unknown line mode %s'):format(line.mode))
end
end
if Links[pane.id] and not empty(Links[pane.id]) then
y = y + 5+Line_height+5 end
pane._height = y
end
function add_title(pane, title)
pane.title = title
pane._height = nil
end
function plan_draw()
Panes_to_draw = {}
Column_headers_to_draw = {}
local sx = Padding_horizontal + Margin_left
if Grab_pane then rehydrate_pane(Grab_pane) end
for column_index, column in ipairs(Surface) do
if should_show_column(sx) then
table.insert(Column_headers_to_draw, {name=('%d. %s'):format(column_index, column.name), x = sx-Display_settings.x})
local sy = Padding_vertical
for pane_index, pane in ipairs(column) do
if sy > Display_settings.y + App.screen.height - Header_height then
break
end
if should_show_pane(pane, sy) then
table.insert(Panes_to_draw, pane)
pane.column_index = column_index
pane.pane_index = pane_index
local y_offset = 0
local body_sy = sy
if column[pane_index].title then
body_sy = body_sy + 5+Line_height+5
end
if body_sy < Display_settings.y then
pane.screen_top1, y_offset = schema1_of_y(pane, Display_settings.y - body_sy)
pane.top = 0
else
pane.screen_top1 = {line=1, pos=1}
pane.top = body_sy - Display_settings.y
end
pane.top = Header_height + Margin_above + pane.top - y_offset
pane.left = sx - Display_settings.x
pane.right = pane.left + Display_settings.column_width
pane.width = pane.right - pane.left
else
pane.top = nil
end
sy = sy + Margin_above + height(pane) + Margin_below + Padding_vertical
end
else
for _, pane in ipairs(column) do
pane.top = nil
end
end
sx = sx + Margin_right + Display_settings.column_width + Padding_horizontal + Margin_left
end
end
function run.draw()
if Directory_error then
love.graphics.print(Directory_error, 50,50)
return
end
if Display_settings.state.id == 'normal' then
draw_normal_mode()
elseif Display_settings.state.id == 'search' then
draw_normal_mode()
Text.draw_search_bar(Display_settings.state, true)
elseif Display_settings.state.id == 'search_all' then
draw_normal_mode()
elseif Display_settings.state.id == 'searching_all' then
draw_normal_mode()
elseif Display_settings.state.id == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
pane.top = Header_height + Margin_above
pane.left = App.screen.width/2 - 20*pane.font:getWidth('m')
pane.right = App.screen.width/2 + 20*pane.font:getWidth('m')
pane.width = pane.right - pane.left
edit.draw(pane)
end
end
else
assert(false, ('pensieve is in an unknown state %s'):format(Display_settings.state.id))
end
if Grab_pane then
local old_top, old_left, old_right = Grab_pane.top, Grab_pane.left, Grab_pane.right
local old_screen_top = Grab_pane.screen_top1
Grab_pane.screen_top1 = {line=1, pos=1}
Grab_pane.top = App.screen.height - 10*Line_height
Grab_pane.left = App.screen.width - Display_settings.column_width - Margin_right - Padding_horizontal
Grab_pane.right = Grab_pane.left + Display_settings.column_width
Grab_pane.width = Grab_pane.right - Grab_pane.left
App.color(Grab_background_color)
love.graphics.rectangle('fill', Grab_pane.left-Margin_left,Grab_pane.top-Margin_above, Grab_pane.width+Margin_left+Margin_right, App.screen.height-Grab_pane.top+Margin_above)
edit.draw(Grab_pane)
Grab_pane.top, Grab_pane.left, Grab_pane.right = old_top, old_left, old_right
Grab_pane.screen_top1 = old_screen_top
end
draw_menu_bar()
if Display_settings.state.id == 'search_all' or Display_settings.state.id == 'searching_all' then
draw_command_palette_for_search_all()
else
if Display_settings.palette then
draw_command_palette()
else
show_error()
end
end
draw_debug()
end
function draw_normal_mode()
assert(Cursor_pane.col, 'no current column')
assert(Cursor_pane.row, 'no current row')
for _,pane in ipairs(Panes_to_draw) do
assert(pane.top, "pane has no top coordinate; there's likely a problem in plan_draw")
if pane.title and eq(pane.screen_top1, {line=1, pos=1}) then
draw_title(pane)
end
edit.draw(pane)
if pane_drew_to_bottom(pane) then
draw_links(pane)
end
if pane.column_index == Cursor_pane.col and pane.pane_index == Cursor_pane.row then
App.color(Cursor_pane_background_color)
if pane.editable and Surface.cursor_on_screen_check then
assert(pane.cursor_y, 'cursor went off screen; this should never happen')
Surface.cursor_on_screen_check = false
end
else
App.color(Pane_background_color)
end
love.graphics.rectangle('fill', pane.left-Margin_left,pane.top-Margin_above, pane.width+Margin_left+Margin_right, pane.bottom-pane.top+Margin_above+Margin_below)
end
for _,header in ipairs(Column_headers_to_draw) do
App.color(Column_header_color)
love.graphics.rectangle('fill', header.x - Margin_left, Menu_status_bar_height, Margin_left + Display_settings.column_width + Margin_right, Column_header_height)
App.color(Text_color)
love.graphics.print(header.name, header.x, Menu_status_bar_height+5)
end
end
function pane_drew_to_bottom(pane)
return pane.bottom < App.screen.height - Line_height
end
function should_show_column(sx)
return overlap(sx-Margin_left, sx+Display_settings.column_width+Margin_right, Display_settings.x, Display_settings.x + App.screen.width)
end
function should_show_pane(pane, sy)
return overlap(sy, sy + Margin_above + height(pane) + Margin_below, Display_settings.y, Display_settings.y + App.screen.height - Header_height)
end
function draw_title(pane)
assert(pane.title, 'pane has no title')
App.color(Pane_title_color)
App.screen.print(pane.title, pane.left, pane.top-Margin_above -5-Line_height)
App.color(Pane_title_background_color)
love.graphics.rectangle('fill', pane.left-Margin_left, pane.top-Margin_above-5-Line_height-5, Margin_left+Display_settings.column_width+Margin_right, 5+Line_height+5)
end
function draw_links(pane)
local links = Links[pane.id]
if links == nil then return end
if empty(links) then return end
local x = pane.left
for _,label in ipairs(Edge_list) do
if links[label] then
draw_link(pane.font, label, x, pane.bottom,
array.find(Non_unique_links, label) and #links[label])
end
x = x + pane.font:getWidth(label) + 10 + 10
end
for link,_ in pairs(links) do
if not Opposite[link] then
draw_link(pane.font, link, x, pane.bottom)
x = x + pane.font:getWidth(link) + 10 + 10
end
end
pane.bottom = pane.bottom + 5+Line_height+5
end
function draw_link(font, label, x,y, n)
if n and n > 1 then
local s
if n > 9 then
s = '*'
else
s = tostring(n)
end
label = ('%s (%s)'):format(label, s)
end
local padding = 10
local w = font:getWidth(label)+padding
App.color(Crosslink_color)
love.graphics.print(label, x, y+padding/2)
App.color(Crosslink_background_color)
love.graphics.rectangle('fill', x-padding/2, y+3, w, 2+Line_height+2)
end
function overlap(lo1,hi1, lo2,hi2)
if lo1 <= lo2 and hi1 > lo2 then
return true
end
if lo1 < hi2 and hi1 >= hi2 then
return true
end
return lo1 >= lo2 and hi1 <= hi2
end
function run.update(dt)
if Directory_error then return end
update_footprint()
Cursor_time = Cursor_time + dt
if App.mouse_y() < Header_height then
love.mouse.setCursor(love.mouse.getSystemCursor('arrow'))
elseif in_pane(App.mouse_x(), App.mouse_y()) then
love.mouse.setCursor(love.mouse.getSystemCursor('arrow'))
else
love.mouse.setCursor(love.mouse.getSystemCursor('hand'))
end
if Pan.x then
Display_settings.x = math.max(Pan.x-App.mouse_x(), 0)
Display_settings.y = math.max(Pan.y-(App.mouse_y()-Header_height), 0)
end
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
edit.update(pane, dt)
end
end
if not Display_settings.palette and (Display_settings.state.id == 'normal' or Display_settings.state.id == 'search') and App.mouse_down(1) then
plan_draw()
end
if Display_settings.state.id == 'searching_all' then
resume_search_all()
end
if Current_error then
if App.get_time() - Current_error_time > 3 then
Current_error = nil
Current_error_time = nil
end
end
end
function in_pane(x,y)
local sx,sy = to_surface(x,y)
local x = Padding_horizontal
for column_idx, column in ipairs(Surface) do
if sx < x then
return false
end
if sx < x + Margin_left + Display_settings.column_width + Margin_right then
local y = Padding_vertical
for pane_idx, pane in ipairs(column) do
if sy < y then
return false
end
if sy < y + Margin_above + height(pane) + Margin_below then
return true
end
y = y + Margin_above + height(pane) + Margin_below + Padding_vertical
end
end
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
end
return false
end
function to_pane(sx,sy)
local x = Padding_horizontal
for column_idx, column in ipairs(Surface) do
if sx < x then
return nil
end
if sx < x + Margin_left + Display_settings.column_width + Margin_right then
local y = Padding_vertical
for pane_idx, pane in ipairs(column) do
if sy < y then
return nil
end
if sy < y + Margin_above + height(pane) + Margin_below then
return {col=column_idx, row=pane_idx}
end
y = y + Margin_above + height(pane) + Margin_below + Padding_vertical
end
end
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
end
return nil
end
function to_surface(x, y)
return x+Display_settings.x, y+Display_settings.y-Header_height
end
function run.quit()
if Directory_error then return end
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
stop_editing(pane)
end
end
end
function run.settings()
assert(Directory_error == nil,
"tried to save settings when we couldn't determine the directory to browse notes in")
local column_names = {}
for _,column in ipairs(Surface) do
table.insert(column_names, column.name)
end
local status = App.write_file(Directory..'config',
json.encode({
columns=column_names,
column_width=Display_settings.column_width,
cursor_col=Cursor_pane.col,
cursor_row=Cursor_pane.row,
surface_x=Display_settings.x,
surface_y=Display_settings.y,
}))
assert(status, 'failed to write settings')
if Settings == nil then Settings = {} end
Settings.x, Settings.y, Settings.displayindex = App.screen.position()
return {
x=Settings.x, y=Settings.y, displayindex=Settings.displayindex,
width=App.screen.width, height=App.screen.height,
font_height=Font_height,
data_directory=Directory,
}
end
function run.mouse_press(x,y, mouse_button)
if Directory_error then return end
Cursor_time = 0 love.keyboard.setTextInput(true) clear_selections()
if Display_settings.state.id == 'normal' or Display_settings.state.id == 'search' or Display_settings.state.id == 'search_all' or Display_settings.state.id == 'searching_all' then
mouse_press_in_normal_mode(x,y, mouse_button)
elseif Display_settings.state.id == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.mouse_press(pane, x,y, mouse_button)
end
end
else
assert(false, ('pensieve is in an unknown state %s'):format(Display_settings.state.id))
end
end
function clear_selections()
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane.selection1 = {}
end
end
end
function mouse_press_in_normal_mode(x,y, mouse_button)
Pan = {}
if y < Header_height then
return
end
local sx,sy = to_surface(x,y)
if in_pane(x,y) then
Cursor_pane = to_pane(sx,sy)
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.mouse_press(pane, x,y, mouse_button)
pane._height = nil
end
end
else
Pan = {x=sx, y=sy}
end
end
function run.mouse_release(x,y, mouse_button)
if Directory_error then return end
Cursor_time = 0 if in_pane(x,y) or Display_settings.state.id == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.mouse_release(pane, x,y, mouse_button)
end
end
end
Pan = {}
end
function run.mouse_wheel_move(dx,dy)
if Directory_error then return end
Cursor_time = 0 if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if pane.editable then
if pane.cursor_x then
edit.mouse_wheel_move(pane, dx,dy)
plan_draw()
return
end
end
end
end
if App.shift_down() then
if dx == 0 then
dx,dy = dy,dx
end
end
if dy > 0 then
for i=1,math.floor(dy) do
pan_up()
end
plan_draw()
elseif dy < 0 then
for i=1,math.floor(-dy) do
pan_down()
end
plan_draw()
end
if dx > 0 then
for i=1,math.floor(dx) do
pan_left()
end
plan_draw()
elseif dx < 0 then
for i=1,math.floor(-dx) do
pan_right()
end
plan_draw()
end
end
function run.text_input(t)
if Directory_error then return end
Cursor_time = 0 if Display_settings.palette then
Display_settings.palette.command = Display_settings.palette.command..t
Display_settings.palette.alternative_index = 1
Display_settings.palette.candidates = candidates()
elseif Display_settings.state.id == 'normal' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if not pane.editable then
if t == 'X' then
command.wider_columns()
return
elseif t == 'x' then
command.narrower_columns()
return
end
else
if pane.cursor_x and pane.cursor_x >= 0 and pane.cursor_x < App.screen.width then
if pane.cursor_y and pane.cursor_y >= Header_height and pane.cursor_y < App.screen.height then
edit.text_input(pane, t)
bring_cursor_of_cursor_pane_in_view('down')
pane._height = nil
plan_draw()
end
end
end
end
end
elseif Display_settings.state.id == 'search' then
Display_settings.state.search_term = Display_settings.state.search_term..t
clear_selections()
Display_settings.x = Display_settings.state.search_backup_x
Display_settings.y = Display_settings.state.search_backup_y
Cursor_pane = deepcopy(Display_settings.state.search_backup_cursor_pane)
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
elseif Display_settings.state.id == 'search_all' then
Display_settings.state.search_all_query = Display_settings.state.search_all_query..t
elseif Display_settings.state.id == 'searching_all' then
elseif Display_settings.state.id == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if pane.editable then
edit.text_input(pane, t)
end
end
end
else
assert(false, ('pensieve is in an unknown state %s'):format(Display_settings.state.id))
end
end
function run.keychord_press(chord, key)
if Directory_error then return end
if App.run_tests == nil then
log(2, os.date('%Y/%m/%d/%H-%M-%S')..' app.keychord_press '..chord..' '..key)
end
Cursor_time = 0 if chord == 'C-=' then
update_font_settings(Font_height+2)
elseif chord == 'C--' then
update_font_settings(Font_height-2)
elseif chord == 'C-0' then
update_font_settings(20)
elseif Display_settings.palette then
keychord_press_on_command_palette(chord, key)
elseif Display_settings.state.id == 'normal' then
if chord == 'C-return' then
Display_settings.palette = {command='', alternative_index=1, candidates = initial_candidates()}
elseif chord == 'C-f' then
command.commence_find_on_surface()
elseif Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if pane.editable then
if chord == 'C-e' then
command.exit_editing()
elseif pane.cursor_x == nil then
assert(pane.cursor_y == nil, 'cursor x is not set but y is set')
panning_keychord_press(chord, key)
plan_draw()
else
local old_top = {line=pane.screen_top1.line, pos=pane.screen_top1.pos}
edit.keychord_press(pane, chord, key)
if chord == 'pagedown' or chord == 'S-pagedown' then
pan_surface_to_screen_top_of_cursor_pane()
elseif chord == 'backspace' or chord == 'up' or chord == 'left' or chord == 'pageup' then
bring_cursor_of_cursor_pane_in_view('up')
else
bring_cursor_of_cursor_pane_in_view('down')
end
pane._height = nil
plan_draw()
end
else
keychord_press_in_normal_mode_with_immutable_pane(pane, chord, key)
plan_draw()
end
end
end
elseif Display_settings.state.id == 'search' then
keychord_press_in_search_mode(chord, key)
elseif Display_settings.state.id == 'search_all' then
keychord_press_in_search_all_mode(chord, key)
elseif Display_settings.state.id == 'searching_all' then
interrupt_search_all()
elseif Display_settings.state.id == 'maximize' then
if chord == 'C-return' then
Display_settings.palette = {command='', alternative_index=1, candidates = initial_candidates()}
else
keychord_press_in_maximize_mode(chord, key)
end
else
assert(false, ('pensieve is in an unknown state %s'):format(Display_settings.state.id))
end
end
function update_font_settings(font_height)
local column_width_in_ems = Display_settings.column_width / Display_settings.font:getWidth('m')
Font_height = font_height
Display_settings.font = love.graphics.newFont(Font_height)
Line_height = math.floor(font_height*1.3)
Menu_status_bar_height = 5 + Line_height + 5
Column_header_height = 5 + Line_height + 5
Header_height = Menu_status_bar_height + Column_header_height
Display_settings.column_width = column_width_in_ems*Display_settings.font:getWidth('m')
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane.font = Display_settings.font
pane.font_height = Font_height
pane.line_height = Line_height
pane.left = 0
pane.right = Display_settings.column_width
pane.width = pane.right - pane.left
end
end
clear_all_pane_heights()
plan_draw()
end
function search_next()
if #Display_settings.state.search_term == 0 then return end
if Cursor_pane.col < 1 then return end
clear_all_search_terms()
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
local old_cursor_in_cursor_pane = {line=pane.cursor1.line, pos=pane.cursor1.pos}
if search_next_in_pane(pane) then
return
end
pane.cursor1 = old_cursor_in_cursor_pane
for current_pane_index=Cursor_pane.row+1,#Surface[Cursor_pane.col] do
local pane = Surface[Cursor_pane.col][current_pane_index]
pane.cursor1 = {line=1, pos=1}
edit.check_locs(pane)
pane.screen_top1 = {line=1, pos=1}
if search_next_in_pane(pane) then
Cursor_pane.row = current_pane_index
return
end
end
local current_column_index = 1 + Cursor_pane.col%#Surface while true do
for current_pane_index,pane in ipairs(Surface[current_column_index]) do
pane.cursor1 = {line=1, pos=1}
edit.check_locs(pane)
pane.screen_top1 = {line=1, pos=1}
if search_next_in_pane(pane) then
Cursor_pane = {col=current_column_index, row=current_pane_index}
return
end
end
current_column_index = 1 + current_column_index%#Surface if current_column_index == Cursor_pane.col then
break
end
end
for current_pane_index=1,Cursor_pane.row-1 do
local pane = Surface[Cursor_pane.col][current_pane_index]
if search_next_in_pane(pane) then
Cursor_pane.row = current_pane_index
return
end
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
local old_cursor = pane.cursor1
pane.cursor1 = {line=1, pos=1}
edit.check_locs(pane)
pane.screen_top1 = {line=1, pos=1}
if search_next_in_pane(pane) then
if Text.lt1(pane.cursor1, old_cursor) then
return
end
end
pane.cursor1 = old_cursor_in_cursor_pane
end
function search_next_in_pane(pane)
pane.search_term = Display_settings.state.search_term
pane.search_backup = {cursor={line=pane.cursor1.line, pos=pane.cursor1.pos}, screen_top={line=pane.screen_top1.line, pos=pane.screen_top1.pos}}
for i=1,#pane.lines do
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
end
if Text.search_next(pane) then
if Text.le1(pane.search_backup.cursor, pane.cursor1) then
return true
end
end
pane.search_term = nil
pane.cursor1.line = pane.search_backup.cursor.line
pane.cursor1.pos = pane.search_backup.cursor.pos
pane.screen_top1.line = pane.search_backup.screen_top.line
pane.screen_top1.pos = pane.search_backup.screen_top.pos
pane.search_backup = nil
end
function search_previous()
if #Display_settings.state.search_term == 0 then return end
if Cursor_pane.col < 1 then return end
clear_all_search_terms()
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
local old_cursor_in_cursor_pane = {line=pane.cursor1.line, pos=pane.cursor1.pos}
if search_previous_in_pane(pane) then
return
end
pane.cursor1 = old_cursor_in_cursor_pane
for current_pane_index=Cursor_pane.row-1,1,-1 do
local pane = Surface[Cursor_pane.col][current_pane_index]
pane.cursor1 = edit.final_cursor(pane)
if search_previous_in_pane(pane) then
Cursor_pane.row = current_pane_index
return
end
end
local current_column_index = 1 + (Cursor_pane.col-2)%#Surface while true do
for current_pane_index = #Surface[current_column_index],1,-1 do
local pane = Surface[current_column_index][current_pane_index]
pane.cursor1 = edit.final_cursor(pane)
if search_previous_in_pane(pane) then
Cursor_pane = {col=current_column_index, row=current_pane_index}
return
end
end
current_column_index = 1 + (current_column_index-2)%#Surface if current_column_index == Cursor_pane.col then
break
end
end
for current_pane_index=#Surface[Cursor_pane.col],Cursor_pane.row+1,-1 do
local pane = Surface[Cursor_pane.col][current_pane_index]
if search_previous_in_pane(pane) then
Cursor_pane.row = current_pane_index
return
end
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
local old_cursor = pane.cursor1
pane.cursor1 = edit.final_cursor(pane)
if search_previous_in_pane(pane) then
if Text.lt1(old_cursor, pane.cursor1) then
return
end
end
pane.cursor1 = old_cursor_in_cursor_pane
end
function search_previous_in_pane(pane)
pane.search_term = Display_settings.state.search_term
pane.search_backup = {cursor={line=pane.cursor1.line, pos=pane.cursor1.pos}, screen_top={line=pane.screen_top1.line, pos=pane.screen_top1.pos}}
for i=1,#pane.lines do
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
end
if Text.search_previous(pane) then
if Text.lt1(pane.cursor1, pane.search_backup.cursor) then
return true
end
end
pane.search_term = nil
pane.cursor1.line = pane.search_backup.cursor.line
pane.cursor1.pos = pane.search_backup.cursor.pos
pane.screen_top1.line = pane.search_backup.screen_top.line
pane.screen_top1.pos = pane.search_backup.screen_top.pos
pane.search_backup = nil
end
function pan_surface_to_screen_top_of_cursor_pane()
if Cursor_pane.col < 1 then
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
Display_settings.y = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.screen_top1)
end
function bring_cursor_of_cursor_pane_in_view(dir)
if Cursor_pane.col < 1 then
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
local left_edge_sx = left_edge_sx(Cursor_pane.col)
local cursor_sx = left_edge_sx + Text.x_of_schema1(pane, pane.cursor1)
local vertically_ok = cursor_sx > Display_settings.x and cursor_sx < Display_settings.x + App.screen.width - pane.font:getWidth('m')
local cursor_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.cursor1)
local horizontally_ok = cursor_sy > Display_settings.y and cursor_sy < Display_settings.y + App.screen.height - Header_height - 2*Line_height if vertically_ok and horizontally_ok then
return
end
if dir == 'up' then
if not vertically_ok then
Display_settings.x = left_edge_sx - Margin_left - Padding_horizontal
end
if not horizontally_ok then
Display_settings.y = cursor_sy - 3*Line_height
end
elseif dir == 'down' then
if not vertically_ok then
Display_settings.x = left_edge_sx + Display_settings.column_width + Margin_right + Padding_horizontal - App.screen.width
end
if not horizontally_ok then
Display_settings.y = cursor_sy + Text.search_bar_height(pane) - (App.screen.height - Header_height)
Display_settings.y = Display_settings.y + Line_height
assert(App.screen.height - (cursor_sy-Display_settings.y) > 1.5*Line_height, 'ugh, ancient bug is back: panning the viewport when cursor falls off')
end
else
assert(false, ('unknown dir %s'):format(dir))
end
Display_settings.x = math.max(Display_settings.x, 0)
Display_settings.y = math.max(Display_settings.y, 0)
end
function clear_all_search_terms()
for col,column in ipairs(Surface) do
for row,pane in ipairs(column) do
pane.search_term = nil
end
end
end
function keychord_press_in_maximize_mode(chord, key)
if Cursor_pane.col < 1 then
print_and_log('keychord_press (maximized): no current note to edit')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
print_and_log('keychord_press (maximized): no current note to edit')
return
end
if pane.editable then
if chord == 'C-e' then
command.exit_editing()
else
edit.keychord_press(pane, chord, key)
end
else
if chord == 'C-e' then
command.edit_note()
elseif chord == 'C-c' then
edit.keychord_press(pane, chord, key)
end
end
end
function keychord_press_in_normal_mode_with_immutable_pane(pane, chord, key)
local left_sx = left_edge_sx(Cursor_pane.col)
if not should_show_column(left_sx) then
panning_keychord_press(chord, key)
return
end
local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)
if not should_show_pane(pane, up_sy) then
panning_keychord_press(chord, key)
return
end
if chord == 'C-e' then
command.edit_note()
profile.start()
elseif chord == 'C-c' then
edit.keychord_press(pane, chord, key)
else
panning_keychord_press(chord, key)
end
end
function y_of_schema1(pane, loc)
local result = 0
if pane.title then
result = result + 5+Line_height+5
end
result = result + Margin_above
if loc.line == 1 and loc.pos == 1 then
return result
end
for i=1,loc.line-1 do
Text.populate_screen_line_starting_pos(pane, i)
result = result + line_height(pane, i, pane.left, pane.right)
end
if pane.lines[loc.line].mode == 'text' then
Text.populate_screen_line_starting_pos(pane, loc.line)
for i,screen_line_starting_pos in ipairs(pane.line_cache[loc.line].screen_line_starting_pos) do
if screen_line_starting_pos >= loc.pos then
break
end
result = result + Line_height
end
end
return result
end
function keychord_press_in_search_mode(chord, key)
if chord == 'escape' then
clear_all_search_terms()
dehydrate_all_panes()
Display_settings.x = Display_settings.state.search_backup_x
Display_settings.y = Display_settings.state.search_backup_y
Cursor_pane = deepcopy(Display_settings.state.search_backup_cursor_pane)
Display_settings.state = {id='normal'}
plan_draw()
elseif chord == 'return' then
Display_settings.state = {id='normal'}
clear_all_search_terms()
dehydrate_all_panes()
elseif chord == 'backspace' then
local len = utf8.len(Display_settings.state.search_term)
local byte_offset = Text.offset(Display_settings.state.search_term, len)
Display_settings.state.search_term = string.sub(Display_settings.state.search_term, 1, byte_offset-1)
clear_selections()
Display_settings.x = Display_settings.state.search_backup_x
Display_settings.y = Display_settings.state.search_backup_y
Cursor_pane = deepcopy(Display_settings.state.search_backup_cursor_pane)
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
elseif chord == 'C-v' then
Display_settings.state.search_term = Display_settings.state.search_term..App.get_clipboard()
clear_selections()
Display_settings.x = Display_settings.state.search_backup_x
Display_settings.y = Display_settings.state.search_backup_y
Cursor_pane = deepcopy(Display_settings.state.search_backup_cursor_pane)
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
elseif chord == 'up' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
search_previous()
bring_cursor_of_cursor_pane_in_view('up')
Surface.cursor_on_screen_check = true
plan_draw()
end
end
elseif chord == 'down' then
if #Display_settings.state.search_term > 0 and Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
Text.right(pane)
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
end
end
elseif chord == 'C-c' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.keychord_press(pane, chord, key)
end
end
end
end
function keychord_press_in_search_all_mode(chord, key)
if chord == 'escape' then
Display_settings.state = {id='normal'}
elseif chord == 'return' then
finalize_search_all_pane()
add_search_all_pane_to_right_of_cursor()
Display_settings.state.id = 'searching_all'
plan_draw()
elseif chord == 'backspace' then
local len = utf8.len(Display_settings.state.search_all_query)
local byte_offset = Text.offset(Display_settings.state.search_all_query, len)
Display_settings.state.search_all_query = string.sub(Display_settings.state.search_all_query, 1, byte_offset-1)
elseif chord == 'C-v' then
Display_settings.state.search_all_query = Display_settings.state.search_all_query..App.get_clipboard()
end
end
function schema1_of_y(pane, y)
assert(y >= 0, 'something is at negative y on the surface')
local y_offset = y
for i=1,#pane.lines do
Text.populate_screen_line_starting_pos(pane, i)
local height = line_height(pane, i, pane.left, pane.right)
if y_offset < height then
local line = pane.lines[i]
if line.mode ~= 'text' then
return {line=i, pos=1}, y_offset
else
local nlines = math.floor(y_offset/pane.line_height)
assert(nlines >= 0 and nlines < #pane.line_cache[i].screen_line_starting_pos, 'error in mapping y coordinate to schema-1')
local pos = pane.line_cache[i].screen_line_starting_pos[nlines+1] y_offset = y_offset - nlines*pane.line_height
return {line=i, pos=pos}, y_offset
end
end
y_offset = y_offset - height
end
return {line=#pane.lines+1, pos=1}, y_offset
end
function line_height(pane, line_index, left, right)
local line = pane.lines[line_index]
local line_cache = pane.line_cache[line_index]
if line.mode == 'text' then
return pane.line_height*#line_cache.screen_line_starting_pos
else
return Drawing.pixels(line.h, right-left) + Drawing_padding_height
end
end
function stop_editing_all()
local edit_count = 0
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
if pane.editable then
stop_editing(pane)
edit_count = edit_count+1
end
end
end
assert(edit_count <= 1, 'multiple panes were editable')
end
function stop_editing(pane)
log(2, 'stop_editing: '..pane.id)
edit.quit(pane)
for rel,x in pairs(Links[pane.id]) do
process_all_links(x, function(target_id)
log(2, 'stop_editing '..pane.id..': saving links for '..target_id)
if Links[target_id] then
save_links(target_id)
end
end)
end
if Display_settings.state.id ~= 'maximize' then
refresh_panes(pane)
end
pane.editable = false
end
function panning_keychord_press(chord, key)
if chord == 'up' then
pan_up()
elseif chord == 'down' then
pan_down()
elseif chord == 'left' then
pan_left()
elseif chord == 'right' then
pan_right()
elseif chord == 'pageup' or chord == 'S-up' then
Display_settings.y = math.max(Display_settings.y - App.screen.height + Line_height*2, 0)
local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)
local up_py = up_sy - Display_settings.y
if up_py > 2/3*App.screen.height then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
elseif chord == 'pagedown' or chord == 'S-down' then
local visible_column_max_y = most(column_height, visible_columns())
if visible_column_max_y - Display_settings.y > App.screen.height then
Display_settings.y = Display_settings.y + App.screen.height - Line_height*2
end
local down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)
local down_px = down_sx - Display_settings.y
if down_px < App.screen.height/3 then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
elseif chord == 'S-left' then
Display_settings.x = math.max(Display_settings.x - Margin_left - Display_settings.column_width - Margin_right - Padding_horizontal, 0)
local left_sx = left_edge_sx(Cursor_pane.col)
local left_px = left_sx - Display_settings.x
if left_px > App.screen.width - Margin_right - Display_settings.column_width/2 then
Cursor_pane.col = math.min(#Surface, col(Display_settings.x + App.screen.width - Margin_right - Display_settings.column_width/2))
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)
end
elseif chord == 'S-right' then
if Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) then
Display_settings.x = Display_settings.x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
local right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_width
local right_px = right_sx - Display_settings.x
if right_px < Margin_left + Display_settings.column_width/2 then
Cursor_pane.col = math.min(#Surface, col(Display_settings.x + Margin_left + Display_settings.column_width/2))
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)
end
end
elseif chord == 'C-down' then
command.down_one_pane()
elseif chord == 'C-up' then
command.up_one_pane()
elseif chord == 'C-end' then
command.bottom_pane_of_column()
elseif chord == 'C-home' then
command.top_pane_of_column()
end
end
function pan_up()
Display_settings.y = math.max(Display_settings.y - Pan_step, 0)
local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)
local up_py = up_sy - Display_settings.y
if up_py > 2/3*App.screen.height then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
end
function pan_down()
local visible_column_max_y = most(column_height, visible_columns())
if visible_column_max_y - Display_settings.y > App.screen.height/2 then
Display_settings.y = Display_settings.y + Pan_step
end
local down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)
local down_px = down_sx - Display_settings.y
if down_px < App.screen.height/3 then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
end
function pan_left()
Display_settings.x = math.max(Display_settings.x - Pan_step, 0)
local left_sx = left_edge_sx(Cursor_pane.col)
local left_px = left_sx - Display_settings.x
if left_px > App.screen.width - Margin_right - Display_settings.column_width/2 then
Cursor_pane.col = math.min(#Surface, col(Display_settings.x + App.screen.width - Margin_right - Display_settings.column_width/2))
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)
end
end
function pan_right()
if Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) then
Display_settings.x = Display_settings.x + Pan_step
end
local right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_width
local right_px = right_sx - Display_settings.x
if right_px < Margin_left + Display_settings.column_width/2 then
Cursor_pane.col = math.min(#Surface, col(Display_settings.x + Margin_left + Display_settings.column_width/2))
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)
end
end
function visible_columns()
local result = {}
local col = col(Display_settings.x)
local x = left_edge_sx(col) - Display_settings.x
while col <= #Surface do
x = x + Padding_horizontal
table.insert(result, col)
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
if x > App.screen.width then
break
end
col = col+1
end
return result
end
function refresh_panes(pane)
for x,col in ipairs(Surface) do
for y,p in ipairs(col) do
if p.id == pane.id then
if p ~= pane then
Surface[x][y] = load_pane(pane.id)
end
end
end
end
plan_draw()
end
function dehydrate_all_panes()
for x,col in ipairs(Surface) do
for y,p in ipairs(col) do
p._height = nil
p.line_cache = {}
end
end
plan_draw()
end
function rehydrate_pane(pane)
for i=1,#pane.lines do
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
Text.clear_screen_line_cache(pane, i)
end
end
function run.key_release(key, scancode)
if Directory_error then return end
Cursor_time = 0 if Cursor_pane.col < 1 then
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
edit.key_release(pane, key, scancode)
end
end
function clear_all_pane_heights()
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane._height = nil
end
end
end
function col(x)
return 1 + math.floor(x / (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right))
end
function left_edge_sx(col)
return (col-1)*(Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) + Padding_horizontal + Margin_left
end
function row(col, y)
local sy = Padding_vertical
for i,pane in ipairs(Surface[col]) do
local next_sy = sy + Margin_above + height(pane) + Margin_below + Padding_vertical
if next_sy > y then
return i
end
sy = next_sy
end
return #Surface[col]
end
function up_edge_sy(col, row)
local result = Padding_vertical
for i=1,row-1 do
local pane = Surface[col][i]
result = result + Margin_above + height(pane) + Margin_below + Padding_vertical
end
return result
end
function down_edge_sx(col, row)
local result = Padding_vertical
for i=1,row do
local pane = Surface[col][i]
result = result + Margin_above + height(pane) + Margin_below + Padding_vertical
end
return result - Padding_vertical
end
function column_height(col)
local result = Padding_vertical
for pane_index, pane in ipairs(Surface[col]) do
result = result + Margin_above + height(pane) + Margin_below + Padding_vertical
end
return result
end
function most(f, arr)
local result = nil
for _,x in ipairs(arr) do
local curr = f(x)
if result == nil or result < curr then
result = curr
end
end
return result
end
function width(s)
return love.graphics.getFont():getWidth(s)
end
function num_panes()
local result = 0
for _,column in ipairs(Surface) do
result = result+#column
end
return result
end