techmeet.love/commands.lua

1737 lines
55 KiB
Lua

Recently_modified_lookback_window = 100 -- how many notes to populate the 'recently modified' column with
-- keep sync'd with Edge_list
Opposite = {
next='previous',
previous='next',
side='previous', -- experiment; you can unroll from a side thread and lose the current note
child='parent',
parent='child',
cross='cross',
before='after',
after='before',
link='backlink',
backlink='link',
}
-- keep sync'd with Opposite
Edge_list = {'previous', 'next', 'side', 'child', 'parent', 'cross', 'before', 'after', 'link', 'backlink'}
-- Most link types/labels are unique; a node can have only one link with each type.
-- However there are some exceptions.
-- The opposite of a unique link (e.g. parent) might be non-unique (e.g. child).
Non_unique_links = {'side', 'child', 'cross', 'before', 'after'}
-- Candidate commands to show in in the command palette in different contexts.
-- Ideally we'd have rules for:
-- contexts to show a command at
-- order in which to show commands for each context
-- But I don't want to design that, so I'm just going to embrace a
-- combinatorial explosion of duplication for a while.
Commands = {
normal={
'capture',
'edit note at cursor (ctrl+e)',
'maximize note',
'close column surrounding cursor',
'add (___) (create immediately link)',
'step (___) (open link in new column)',
'extract (open note in new column)',
'unroll (___) (repeatedly step from cursor; unique only)',
'append (___) (repeatedly step, then add; unique only)',
'neighbors (open all links in new column)',
'down one pane (ctrl+down)',
'up one pane (ctrl+up)',
'top pane of column (ctrl+home)',
'bottom pane of column (ctrl+end)',
'left one column (ctrl+left)',
'right one column (ctrl+right)',
'grab (temporary second cursor for some commands)',
'ungrab (clear second cursor)',
'link (___) (to second cursor)',
'copy id (of current node to clipboard)',
'rename link ___ (to) ___ (some other label)',
'clear link ___ (use with care! ignores opposite link)',
'move column ___ (after given index; 1 by default)',
'wider columns (X)',
'narrower columns (x)',
'recently modified',
'errors',
'open file ___',
'find on surface (ctrl+f)',
'search (all notes)',
'reload all from disk',
'delete note at cursor from disk (if possible)',
'copy selection to clipboard (ctrl+c)',
'debug stats (toggle)',
'snapshot summary of memory use to disk',
},
editable={
'exit editing (ctrl+e)',
'find in note (ctrl+f)',
'copy selection to clipboard (ctrl+c)',
'cut selection to clipboard (ctrl+x)',
'paste from clipboard (ctrl+v)',
'undo (ctrl+z)',
'redo (ctrl+y)',
'cursor to next word (alt+right arrow)',
'cursor to previous word (alt+left arrow)',
'capture',
'maximize note',
'close column surrounding cursor',
'add (___) (create immediately link)',
'step (___) (open link in new column)',
'unroll (___) (repeatedly step from cursor; unique only)',
'append (___) (repeatedly step, then add; unique only)',
'neighbors (open all links in new column)',
'down one pane (ctrl+down)',
'up one pane (ctrl+up)',
'top pane of column (ctrl+home)',
'bottom pane of column (ctrl+end)',
'wider columns (X)',
'narrower columns (x)',
'recently modified',
'reload all from disk',
'snapshot summary of memory use to disk',
},
maximized={
'back to surface',
'edit note (ctrl+e)',
'add (___) (create immediately link)',
'step (___) (open link in new column)',
'append (___) (repeatedly step, then add; unique only)',
'copy selection to clipboard (ctrl+c)',
},
maximized_editable={
'exit editing (ctrl+e)',
'back to surface',
'find in note (ctrl+f)',
'add (___) (create immediately link)',
'step (___) (open link in new column)',
'append (___) (repeatedly step, then add; unique only)',
'copy selection to clipboard (ctrl+c)',
'cut selection to clipboard (ctrl+x)',
'paste from clipboard (ctrl+v)',
'undo (ctrl+z)',
'redo (ctrl+y)',
'cursor to next word (alt+right arrow)',
'cursor to previous word (alt+left arrow)',
},
}
-- We incrementally create the menu based on context. Menu_cursor tracks how
-- far along the screen width we've gotten.
Menu_cursor = 0
Palette_cursor = {y=0, x=0}
Palette_alternatives_height = 5 -- number of rows of options to show
function draw_menu_bar()
if App.run_tests then return end -- disable in tests
App.color(Menu_background_color)
love.graphics.rectangle('fill', 0,0, App.screen.width, Menu_status_bar_height)
App.color(Menu_border_color)
love.graphics.rectangle('line', 0,0, App.screen.width, Menu_status_bar_height)
if Display_settings.palette then
-- TODO: continue to put shortcuts on the menu bar, enter commands/search strings one row down
return
end
App.color(Menu_command_color)
Menu_cursor = 5
add_hotkey_to_menu('ctrl+enter: search commands...')
App.color(Menu_border_color)
love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)
if Display_settings.mode == 'search' then
add_hotkey_to_menu('esc: cancel')
add_hotkey_to_menu('up: next match')
add_hotkey_to_menu('down: previous match')
add_hotkey_to_menu('ctrl+v: paste')
return
end
if Cursor_pane.col >= 1 then
--? print(Cursor_pane.col, Cursor_pane.row, #Surface)
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if Display_settings.mode == 'normal' then
if not pane.editable then
local left_sx = left_edge_sx(Cursor_pane.col)
local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)
if should_show_column(left_sx) and should_show_pane(pane, up_sy) then
add_hotkey_to_menu('ctrl+e: edit')
add_hotkey_to_menu('ctrl+f: find on surface')
add_panning_hotkeys_to_menu()
end
add_hotkey_to_menu('x/X: narrower/wider columns')
else
if pane.cursor_x == nil then
add_panning_hotkeys_to_menu()
else
assert(pane.cursor_y, 'cursor fell off viewport')
add_hotkey_to_menu('ctrl+e: stop editing')
add_hotkey_to_menu('ctrl+h on drawing: help')
add_hotkey_to_menu('ctrl+f: find')
add_hotkey_to_menu('alt+left alt+right: prev/next word')
add_hotkey_to_menu('ctrl+z ctrl+y: undo/redo')
add_hotkey_to_menu('ctrl+x ctrl+c ctrl+v: cut/copy/paste')
end
end
end
end
end
add_hotkey_to_menu('ctrl+= ctrl+- ctrl+0: zoom')
end
function add_panning_hotkeys_to_menu()
add_hotkey_to_menu('arrows shift+arrows ctrl+up/down: pan')
end
function add_hotkey_to_menu(s)
local width = Display_settings.font:getWidth(s)
if Menu_cursor > App.screen.width - 30 then
return
end
App.color(Menu_command_color)
App.screen.print(s, Menu_cursor,5)
Menu_cursor = Menu_cursor + width + 30
end
function keychord_press_on_command_palette(chord, key)
if chord == 'escape' then
-- forget text for next command
Display_settings.palette = nil
elseif chord == 'backspace' then
local len = utf8.len(Display_settings.palette.command)
local byte_offset = Text.offset(Display_settings.palette.command, len)
Display_settings.palette.command = string.sub(Display_settings.palette.command, 1, byte_offset-1)
Display_settings.palette.alternative_index = 1
Display_settings.palette.candidates = candidates()
elseif chord == 'tab' then
-- select top candidate, but don't submit
local p = Display_settings.palette
p.command = command_string(p.candidates[p.alternative_index])
elseif chord == 'C-v' then
local p = Display_settings.palette
p.command = p.command..App.get_clipboard()
p.candidates = candidates()
elseif chord == 'return' then
-- submit selected candidate
local p = Display_settings.palette
local candidates = Display_settings.palette.candidates
if #p.candidates > 0 then
if file_exists(Directory..p.candidates[p.alternative_index]) then
command.open_file_in_next_column(p.candidates[p.alternative_index])
else
run_command(command_string(p.candidates[p.alternative_index]))
end
else
-- try to run the command as if it contains args
run_command_with_args(p.command)
end
-- forget text for next command
Display_settings.palette = nil
-- clean up some columns if possible
if Cursor_pane.col < 45 then
while #Surface > 50 do
print_and_log('keychord_press (palette) return: dropping '..Surface[#Surface].name)
table.remove(Surface)
end
end
elseif chord == 'up' then
if Display_settings.palette.alternative_index > 1 then
Display_settings.palette.alternative_index = Display_settings.palette.alternative_index-1
end
elseif chord == 'down' then
if Display_settings.palette.alternative_index < #Display_settings.palette.candidates then
Display_settings.palette.alternative_index = Display_settings.palette.alternative_index+1
end
elseif chord == 'left' then
if Display_settings.palette.alternative_index > Palette_alternatives_height then
Display_settings.palette.alternative_index = Display_settings.palette.alternative_index-Palette_alternatives_height
end
elseif chord == 'right' then
if Display_settings.palette.alternative_index <= #Display_settings.palette.candidates-Palette_alternatives_height then
Display_settings.palette.alternative_index = Display_settings.palette.alternative_index+Palette_alternatives_height
end
end
end
function command_string(s)
local result, _ = s:gsub(' %(.*', ''):gsub(' _.*', '')
return result
end
function draw_command_palette()
-- background
App.color(Command_palette_background_color)
love.graphics.rectangle('fill', 0,0, App.screen.width, Menu_status_bar_height)
App.color(Command_palette_border_color)
love.graphics.rectangle('line', 0,0, App.screen.width, Menu_status_bar_height)
-- input box
App.color(Command_palette_command_color)
draw_palette_input(5, 5)
-- alternatives
App.color(Command_palette_alternatives_background_color)
love.graphics.rectangle('fill', 0, Menu_status_bar_height, App.screen.width, 5+Palette_alternatives_height*Line_height+5)
App.color(Command_palette_border_color)
love.graphics.rectangle('line', 0, Menu_status_bar_height, App.screen.width, 5+Palette_alternatives_height*Line_height+5)
Palette_cursor = {y=Menu_status_bar_height+5, x=5, nextx=5}
for i,cmd in ipairs(Display_settings.palette.candidates) do
add_command_to_palette(cmd, i == Display_settings.palette.alternative_index)
end
end
function draw_command_palette_for_search_all()
-- background
App.color(Command_palette_background_color)
love.graphics.rectangle('fill', 0,0, App.screen.width, Menu_status_bar_height)
App.color(Command_palette_border_color)
love.graphics.rectangle('line', 0,0, App.screen.width, Menu_status_bar_height)
-- input box
App.color(Command_palette_command_color)
local x = 5
local y = 5
love.graphics.print(Display_settings.search_all_query, x,y)
if Display_settings.mode == 'search_all' then
-- draw cursor
x = x+Display_settings.font:getWidth(Display_settings.search_all_query)
draw_cursor(x, y)
elseif Display_settings.mode == 'searching_all' then
-- show progress
App.color(Command_palette_alternatives_background_color)
love.graphics.rectangle('fill', 0, Menu_status_bar_height, App.screen.width, 5+Line_height+5)
App.color(Command_palette_border_color)
love.graphics.rectangle('line', 0, Menu_status_bar_height, App.screen.width, 5+Line_height+5)
App.screen.print(Display_settings.search_all_progress_indicator, --[[x]] 5, --[[y]] Menu_status_bar_height+5)
end
end
function add_command_to_palette(s, cursor_highlight)
local width = Display_settings.font:getWidth(s)
if Palette_cursor.x + width/2 > App.screen.width - 5 then
return
end
if cursor_highlight then
App.color(Command_palette_highlighted_alternative_background_color)
else
App.color(Command_palette_alternatives_background_color)
end
love.graphics.rectangle('fill', Palette_cursor.x-5, Palette_cursor.y, width+10, Line_height)
App.color(Command_palette_alternatives_color)
App.screen.print(s, Palette_cursor.x, Palette_cursor.y)
Palette_cursor.nextx = math.max(Palette_cursor.nextx, Palette_cursor.x+width+10)
Palette_cursor.y = Palette_cursor.y + Line_height
if Palette_cursor.y >= Menu_status_bar_height + 5+Palette_alternatives_height*Line_height then
App.color(Command_palette_border_color)
love.graphics.line(Palette_cursor.nextx, Menu_status_bar_height+2, Palette_cursor.nextx, Menu_status_bar_height + 5+5*Line_height+5)
Palette_cursor.x = Palette_cursor.nextx + 5
Palette_cursor.y = Menu_status_bar_height+5
end
end
function draw_palette_input(x, y)
love.graphics.print(Display_settings.palette.command, x,y)
x = x+Display_settings.font:getWidth(Display_settings.palette.command)
draw_cursor(x, y)
end
function draw_cursor(x, y)
-- blink every 0.5s
if math.floor(Cursor_time*2)%2 == 0 then
App.color(Cursor_color)
love.graphics.rectangle('fill', x,y, 3,Line_height)
end
end
function candidates()
-- slight context-sensitive tweaks
local candidates = initial_candidates()
if Display_settings.palette.command == '' then
return candidates
elseif Display_settings.palette.command:sub(1,1) == '/' then
return {}
else
local results = filter_candidates(candidates, Display_settings.palette.command)
if Display_settings.mode == 'normal' then
append(results, file_candidates(Display_settings.palette.command))
end
return results
end
end
function initial_candidates()
if Cursor_pane.col < 1 then
return Commands.normal
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return Commands.normal
end
if Display_settings.mode == 'normal' then
if not pane.editable then
return Commands.normal
else
return Commands.editable
end
elseif Display_settings.mode == 'maximize' then
if not pane.editable then
return Commands.maximized
else
return Commands.maximized_editable
end
end
end
function filter_candidates(candidates, prefix)
local result = {}
for _,cand in ipairs(candidates) do
if cand:find(prefix, 1, --[[literal pattern]] true) == 1 then
table.insert(result, cand)
end
end
return result
end
function file_candidates(prefix)
--? print('-- '..prefix)
local info = App.file_info(Directory..prefix)
--? print(info)
--? if info then
--? print(info.type)
--? end
if info and info.type == 'file' then
return {prefix}
end
local path = Directory
local visible_dir = ''
if info and info.type == 'directory' then
if prefix:sub(#prefix) == '/' then
visible_dir = prefix
else
visible_dir = prefix..'/'
end
path = path..visible_dir
elseif prefix:find('/') then
visible_dir = dirname(prefix)
path = path..visible_dir
end
--? print('path:', path)
local files = App.files(path)
--? print(#files, 'files')
local base
if info and info.type == 'directory' then
base = ''
else
base = basename(prefix)
end
return concat_all(visible_dir, filter_candidates(reorder(path, files), base))
end
function reorder(dir, files)
local result = {}
local info = {}
for _,file in ipairs(files) do
info[file] = App.file_info(dir..'/'..file)
end
-- files before directories
for _,file in ipairs(files) do
if info[file].type ~= 'directory' then
table.insert(result, file)
end
end
for _,file in ipairs(files) do
if info[file].type == 'directory' then
table.insert(result, file..'/')
end
end
return result
end
function run_command(cmd, args)
if cmd == 'capture' then
command.capture()
elseif cmd == 'maximize note' then
command.maximize_note()
elseif cmd == 'back to surface' then
command.back_to_surface()
elseif cmd == 'edit note' or cmd == 'edit note at cursor' then
command.edit_note()
elseif cmd == 'exit editing' then
command.exit_editing()
elseif cmd == 'close column surrounding cursor' then
command.close_column_surrounding_cursor()
elseif cmd == 'grab' then
command.grab()
elseif cmd == 'ungrab' then
command.ungrab()
elseif cmd == 'link' then
command.link(args)
elseif cmd == 'copy id' then
command.copy_id_to_clipboard()
elseif cmd == 'rename link' then
command.rename_link(args)
elseif cmd == 'clear link' then
command.clear_link(args)
elseif cmd == 'add' then
command.add_note(args)
elseif cmd == 'step' then
command.step(args)
elseif cmd == 'extract' then
command.extract()
elseif cmd == 'unroll' then
command.unroll(args)
elseif cmd == 'append' then
command.append_note(args)
elseif cmd == 'neighbors' then
command.neighbors()
elseif cmd == 'down one pane' then
command.down_one_pane()
elseif cmd == 'up one pane' then
command.up_one_pane()
elseif cmd == 'top pane of column' then
command.top_pane_of_column()
elseif cmd == 'bottom pane of column' then
command.bottom_pane_of_column()
elseif cmd == 'left one column' then
command.left_one_column()
elseif cmd == 'right one column' then
command.right_one_column()
elseif cmd == 'move column' then
command.move_column(args)
elseif cmd == 'wider columns' then
command.wider_columns()
elseif cmd == 'narrower columns' then
command.narrower_columns()
elseif cmd == 'recently modified' then
command.recently_modified()
elseif cmd == 'errors' then
command.errors()
elseif cmd == 'open file' then
command.open_file_in_next_column(args)
elseif cmd == 'find on surface' then
command.commence_find_on_surface()
elseif cmd == 'search' then
command.commence_search_in_disk()
elseif cmd == 'reload all from disk' then
command.reload_all()
elseif cmd == 'delete note at cursor from disk' then
command.delete_note()
elseif cmd == 'debug stats' then
Display_settings.show_debug = not Display_settings.show_debug
elseif cmd == 'snapshot summary of memory use to disk' then
command.snapshot_memory()
-- editing
elseif cmd == 'find in note' then
command.send_key_to_current_pane('C-f', 'f')
elseif cmd == 'copy selection to clipboard' then
command.send_key_to_current_pane('C-c', 'c')
elseif cmd == 'cut selection to clipboard' then
command.send_key_to_current_pane('C-x', 'x')
elseif cmd == 'paste from clipboard' then
command.send_key_to_current_pane('C-v', 'v')
elseif cmd == 'undo' then
command.send_key_to_current_pane('C-z', 'z')
elseif cmd == 'redo' then
command.send_key_to_current_pane('C-y', 'y')
elseif cmd == 'cursor to next word' then
command.send_key_to_current_pane('M-right', 'right')
elseif cmd == 'cursor to previous word' then
command.send_key_to_current_pane('M-left', 'left')
else
print_and_log(('run_command: not implemented yet: %s'):format(cmd))
end
end
function run_command_with_args(cmd_with_args)
for _,cand in ipairs(initial_candidates()) do
cand = command_string(cand)
local found_offset = cmd_with_args:find(cand, 1, --[[literal pattern]] true)
if found_offset == 1 then
local pivot = #cand+1
if cmd_with_args:sub(pivot, pivot) == ' ' then
run_command(cand, trim(cmd_with_args:sub(pivot)))
end
return
end
end
end
-- commands that create columns also need to be recreatable from a title
-- I'm assuming that special columns have multiple words, and single-word
-- columns are always filenames. techmeet.love won't ever create filenames
-- containing spaces.
function create_column(column_name)
if file_exists(Directory..column_name) then
local column = {name=column_name}
local pane = load_pane(column_name)
table.insert(column, pane)
table.insert(Surface, column)
elseif not column_name:find(' ') then
-- File not found
--
-- It makes me nervous to silently drop errors, but at this point there's
-- really nothing actionable someone can do in response to an error.
--
-- Deeper issue: no way yet to communicate errors in the UI.
--
-- Philosophical question: what does crash-only mean if you ever run into
-- data loss? There's a hard tension between resilience and silent failures.
--
-- For now I'm going to rely on all my protections against data loss
-- elsewhere. Lines.love has never lost my data in several months of use.
--
-- While data loss seems unlikely, there _is_ a legitimate way you can end
-- up with a filename that doesn't exist: start a capture, then change
-- your mind and never type anything into it. It will continue to show as
-- a column on the surface, but there's no file backing it. You can still
-- edit it later and create a file for it. But if you just quit, the
-- column will silently disappear after restart.
print_and_log('create_column: file not found: '..column_name)
else
-- delegate to one of various helpers based on the column name
local column = {name=column_name}
populate_column(column)
if #column == 0 then
-- Something has changed from underneath us, likely between restarts;
-- assume we already printed out an error.
return
end
table.insert(Surface, column)
end
end
function populate_column(column)
if column.name == 'recently modified' then
populate_recently_modified_column(column)
elseif column.name == 'errors' then
populate_errors_column(column)
elseif string.match(column.name, '%S+ from %S+') then
local rel, start_id = string.match(column.name, '(%S+) from (%S+)')
populate_unroll_column(column, start_id, rel)
elseif string.match(column.name, 'neighbors of %S+') then
local start_id = string.match(column.name, 'neighbors of (%S+)')
populate_neighbors_column(column, start_id)
elseif string.match(column.name, '%S+ of %S+') then
local rel, start_id = string.match(column.name, '(%S+) of (%S+)')
populate_step_column(column, start_id, rel)
elseif string.match(column.name, 'search: .+') then
--? print('column name', column.name)
local search_all_query = string.match(column.name, 'search: (.+)')
--? print('search term', search_all_query)
populate_search_all_column(column, search_all_query)
else
error("don't know how to populate column \""..column.name.."\"")
end
end
command = {}
function command.capture()
local pane = new_pane()
local column = {name=pane.id}
table.insert(column, pane)
table.insert(Surface, Cursor_pane.col+1, column)
Cursor_pane.col = Cursor_pane.col+1
Cursor_pane.row = 1
bring_cursor_column_on_screen()
stop_editing_all()
pane.editable = true
command.maximize_note()
end
function command.maximize_note()
Display_settings.mode = 'maximize'
end
function command.back_to_surface()
Display_settings.mode = 'normal'
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
refresh_pane_height(pane)
plan_draw()
end
end
end
function command.close_column_surrounding_cursor()
stop_editing_all()
table.remove(Surface, Cursor_pane.col)
if Cursor_pane.col > 1 then
Cursor_pane.col = Cursor_pane.col - 1
Cursor_pane.row = 1
end
bring_cursor_column_on_screen()
plan_draw()
end
function command.edit_note()
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
assert(not pane.editable, 'pane already editable')
stop_editing_all()
pane.recent_updated = false
pane.editable = true
if Text.lt1(pane.cursor1, pane.screen_top1) then
pane.cursor1 = {line=pane.screen_top1.line, pos=pane.screen_top1.pos}
end
end
function command.exit_editing()
assert(Cursor_pane.col >= 1, 'no current pane')
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
assert(pane, 'no current pane')
assert(pane.editable, 'current pane not editable')
stop_editing(pane)
end
function command.down_one_pane()
if Cursor_pane.row < #Surface[Cursor_pane.col] then
Cursor_pane.row = Cursor_pane.row + 1
end
Display_settings.y = up_edge_sy(Cursor_pane.col, Cursor_pane.row) - Padding_vertical
plan_draw()
end
function command.up_one_pane()
if Cursor_pane.row > 1 then
Cursor_pane.row = Cursor_pane.row - 1
end
Display_settings.y = up_edge_sy(Cursor_pane.col, Cursor_pane.row) - Padding_vertical
plan_draw()
end
function command.bottom_pane_of_column()
if Cursor_pane.row < #Surface[Cursor_pane.col] then
Cursor_pane.row = #Surface[Cursor_pane.col]
end
Display_settings.y = up_edge_sy(Cursor_pane.col, Cursor_pane.row) - Padding_vertical
plan_draw()
end
function command.top_pane_of_column()
if Cursor_pane.row > 1 then
Cursor_pane.row = 1
end
Display_settings.y = up_edge_sy(Cursor_pane.col, Cursor_pane.row) - Padding_vertical
plan_draw()
end
-- imperfect commands for left/right navigation
-- these can be improved
function command.right_one_column()
if Cursor_pane.col >= #Surface then
return
end
Cursor_pane.col = Cursor_pane.col + 1
Cursor_pane.row = 1 -- always bounce back to top of column
Display_settings.y = 0
local xlo = left_edge_sx(Cursor_pane.col) - Margin_left - Padding_horizontal
Display_settings.x = math.max(xlo, Display_settings.x)
local xmax = left_edge_sx(Cursor_pane.col+1) - App.screen.width
Display_settings.x = math.min(xmax, Display_settings.x)
Display_settings.x = math.max(0, Display_settings.x)
plan_draw()
end
function command.left_one_column()
if Cursor_pane.col <= 1 then
return
end
Cursor_pane.col = Cursor_pane.col - 1
Cursor_pane.row = 1 -- always bounce back to top of column
Display_settings.y = 0
local xlo = left_edge_sx(Cursor_pane.col) - Margin_left - Padding_horizontal
Display_settings.x = math.min(xlo, Display_settings.x)
plan_draw()
end
-- move column to _after_ index
function command.move_column(index)
if index == nil then index = 1 end
if Cursor_pane.col < 1 then
add_error('no current column')
return
end
local column = Surface[Cursor_pane.col]
table.remove(Surface, Cursor_pane.col)
table.insert(Surface, index+1, column)
Cursor_pane.col = index+1
bring_cursor_column_on_screen()
bring_cursor_of_cursor_pane_in_view('down')
plan_draw()
end
function command.wider_columns()
Display_settings.column_width = Display_settings.column_width + 5*Display_settings.font:getWidth('m')
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane.left = 0
pane.right = Display_settings.column_width
end
end
clear_all_pane_heights()
plan_draw()
end
function command.narrower_columns()
Display_settings.column_width = Display_settings.column_width - 5*Display_settings.font:getWidth('m')
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane.left = 0
pane.right = Display_settings.column_width
end
end
clear_all_pane_heights()
plan_draw()
end
function command.recently_modified()
if not file_exists(Directory..'recent') then
return
end
local column = {name='recently modified'}
populate_recently_modified_column(column)
add_column_to_right_of_cursor(column)
plan_draw()
end
function populate_recently_modified_column(column)
local filenames = {}
local f = App.open_for_reading(Directory..'recent')
for line in f:lines() do
table.insert(filenames, line)
end
f:close()
local done, ndone = {}, 0
for i=#filenames,1,-1 do
local filename = filenames[i]
if ndone >= Recently_modified_lookback_window then break end
if not done[filename] then
done[filename] = true
ndone = ndone+1
--? print('loading', filename)
local pane = load_pane(filename)
table.insert(column, pane)
end
end
end
function command.errors()
local column = {name='errors'}
populate_errors_column(column)
add_column_to_right_of_cursor(column)
plan_draw()
end
function populate_errors_column(column)
-- TODO: we might run into some bugs if we have multiple error panes visible
-- on the surface; unlike everything else these currently alias.
local pane = Error_log
pane.font_height = Font_height
pane.line_height = Line_height
pane.editable = false
edit.check_locs(pane)
pane.title = '(do not edit)'
Text.redraw_all(pane)
table.insert(column, pane)
end
function command.open_file_in_next_column(filename)
local column = {name=filename}
local pane = load_pane(filename)
table.insert(column, pane)
add_column_to_right_of_cursor(column)
plan_draw()
end
function command.commence_find_on_surface()
-- save some state
clear_selections()
Display_settings.search_backup_x = Display_settings.x
Display_settings.search_backup_y = Display_settings.y
Display_settings.search_backup_cursor_pane = {row=Cursor_pane.row, col=Cursor_pane.col}
-- prepare to pass Display_settings to Text.draw_search_bar
--? print('entering search mode')
Display_settings.mode = 'search'
Display_settings.search_term = ''
Display_settings.line_height = Line_height
end
function command.commence_search_in_disk()
Display_settings.mode = 'search_all'
Display_settings.search_all_pane = initialize_search_all_pane()
Display_settings.search_all_query = ''
Display_settings.search_all_progress_indicator = 'starting search...'
end
-- search panes are opposites of regular panes
-- regular pane: pass in id, load from disk, may be edited
-- search pane: create without id, initialize id after search term is typed in, create empty file, slowly append to disk, may not be edited
function initialize_search_all_pane()
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.font_height = Font_height
result.line_height = Line_height
result.editable = false
return result
end
function finalize_search_all_pane()
if Display_settings.search_all_query:sub(1,1) == '"' then
-- support only a single quoted phrase by itself
assert(Display_settings.search_all_query:sub(#Display_settings.search_all_query) == '"', 'you can search for strings in quotes, but only one of them by itself')
Display_settings.search_all_terms = {Display_settings.search_all_query:sub(2, #Display_settings.search_all_query-1)}
else
Display_settings.search_all_terms = split(Display_settings.search_all_query)
end
local id = 'search'
--? print(id)
App.remove(Directory..id)
Display_settings.search_all_pane.id = id
Display_settings.search_all_pane.filename = Directory..id
Display_settings.editable = false
end
function add_search_all_pane_to_right_of_cursor()
local column = {name='search: '..Display_settings.search_all_query}
table.insert(column, Display_settings.search_all_pane)
add_column_to_right_of_cursor(column)
end
function resume_search_all()
-- make a little more progress towards searching the whole disk
if Display_settings.search_all_progress == nil then
Display_settings.search_all_progress_indicator = 'initialized top-level files'
Display_settings.search_all_progress = {
top_level_files = App.files(Directory),
top_level_file_index = 1,
}
elseif Display_settings.search_all_progress.top_level_file_index then
local current_filename = Display_settings.search_all_progress.top_level_files[Display_settings.search_all_progress.top_level_file_index]
Display_settings.search_all_progress_indicator = current_filename
if current_filename ~= 'search' and current_filename ~= 'config' and current_filename ~= 'recent' then -- ignore some housekeeping files for pensieve.love
local info = App.file_info(Directory..current_filename)
if info.type == 'file' then
search_in_file(current_filename)
end
end
Display_settings.search_all_progress.top_level_file_index = Display_settings.search_all_progress.top_level_file_index+1
if Display_settings.search_all_progress.top_level_file_index > #Display_settings.search_all_progress.top_level_files then
Display_settings.search_all_progress.top_level_file_index = nil
Display_settings.search_all_progress.top_level_files = nil
Display_settings.search_all_progress.time = os.time()
Display_settings.search_all_progress.year = os.date('%Y', Display_settings.search_all_progress.time)
Display_settings.search_all_progress.date = os.date('%Y/%m/%d/', Display_settings.search_all_progress.time)
end
elseif Display_settings.search_all_progress.date then
-- search one day's directory per frame
-- stop when a whole year is missing
Display_settings.search_all_progress_indicator = Display_settings.search_all_progress.date
local old_year = Display_settings.search_all_progress.year
local date_dir = Directory..Display_settings.search_all_progress.date
local info = App.file_info(date_dir)
if info then
if info.type == 'directory' then
local filenames = App.files(date_dir)
for _,filename in ipairs(filenames) do
-- hack: to speed up search, only search files created/managed by Pensieve
-- I often have other stuff here that's a lot larger (email).
if filename:match('^%d%d%-%d%d%-%d%d$') then
--? print(date_dir..filename)
search_in_file(Display_settings.search_all_progress.date..filename)
end
end
end
end
Display_settings.search_all_progress.time = Display_settings.search_all_progress.time - 24*60*60
Display_settings.search_all_progress.year = os.date('%Y', Display_settings.search_all_progress.time)
Display_settings.search_all_progress.date = os.date('%Y/%m/%d/', Display_settings.search_all_progress.time)
if old_year ~= Display_settings.search_all_progress.year then
local previous_year_info = App.file_info(Directory..Display_settings.search_all_progress.year)
if previous_year_info == nil then
Display_settings.search_all_progress = nil
Display_settings.mode = 'normal'
end
end
else
assert(false, 'error in search state machine')
end
end
function search_in_file(filename)
--? print('searching '..filename..' for '..Display_settings.search_all_query)
local contents, err = App.read_file(Directory..filename)
if err then
error(err)
end
if contents == nil then
error('no contents in '..filename)
end
if match_all(contents, Display_settings.search_all_terms) then
local outfilename = Directory..'search'
local success, errmsg = append_to_file(outfilename, '[['..filename..']]\n')
if not success then error(errmsg) end
local index = 0
while true do
index = find_any(contents, Display_settings.search_all_terms, index+1)
if index == nil then
break
end
local start_offset = find_previous_byte(contents, '\n', index)
local end_offset = contents:find('\n', index, --[[literal]] true)
local snippet = contents:sub(start_offset, end_offset)
local success, errmsg = append_to_file(outfilename, '...'..snippet..'...\n\n')
if not success then error(errmsg) end
end
load_from_disk(Display_settings.search_all_pane)
Text.redraw_all(Display_settings.search_all_pane)
refresh_pane_height(Display_settings.search_all_pane)
end
end
function find_previous_byte(s, b, index)
while index > 1 do
if s:sub(index, index) == b then
break
end
index = index-1
end
return index
end
function interrupt_search_all()
if Display_settings.search_all_progress then
local outfilename = Directory..'search'
local success, errmsg = append_to_file(outfilename, 'interrupted at '..Display_settings.search_all_progress_indicator..'\n')
if not success then error(errmsg) end
load_from_disk(Display_settings.search_all_pane)
Text.redraw_all(Display_settings.search_all_pane)
end
Display_settings.search_all_progress = nil
Display_settings.mode = 'normal'
end
function populate_search_all_column(column, search_all_query)
table.insert(column, load_pane('search'))
end
function command.reload_all()
local column_names = {}
for _,column in ipairs(Surface) do
table.insert(column_names, column.name)
end
Surface = {}
local old_viewport = {x=Display_settings.x, y=Display_settings.y}
local old_cursor_pane = Cursor_pane
Cursor_pane = {col=0,row=1}
for _,column_name in ipairs(column_names) do
create_column(column_name)
end
Cursor_pane = old_cursor_pane
-- something's moving us around
Display_settings.x = old_viewport.x
Display_settings.y = old_viewport.y
plan_draw()
end
function command.add_note(rel)
if rel == nil then
rel = 'next'
end
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
if Links[pane.id][rel] and not array.find(Non_unique_links, rel) then
add_error(('%s already has a %s note'):format(pane.id, rel))
return
end
stop_editing_all() -- save any edits
if #Surface[Cursor_pane.col] == 1 and not array.find(Non_unique_links, rel) then
-- column has a single note; turn it into unroll
Surface[Cursor_pane.col] = {name=('%s from %s'):format(rel, pane.id)}
populate_unroll_column(Surface[Cursor_pane.col], pane.id, rel)
end
local new_pane = new_pane()
new_pane.editable = true
-- connect up links
add_link(pane.id, rel, new_pane.id)
add_link(new_pane.id, Opposite[rel], pane.id)
if string.match(Surface[Cursor_pane.col].name, rel..' from %S+') then
-- we're unrolling along the same rel; just append to it
-- (we couldn't be inserting in the middle if we didn't return earlier in
-- the function)
table.insert(Surface[Cursor_pane.col], new_pane)
Cursor_pane.row = #Surface[Cursor_pane.col]
add_title(new_pane, ('%d/%d'):format(Cursor_pane.row, Cursor_pane.row))
refresh_pane_height(pane) -- just in case this is the first link
bring_cursor_column_on_screen()
bring_cursor_of_cursor_pane_in_view('down')
else
local column = {name=new_pane.id}
table.insert(column, new_pane)
add_column_to_right_of_cursor(column)
refresh_pane_height(pane) -- just in case this is the first link
bring_cursor_of_cursor_pane_in_view('up')
end
plan_draw()
end
function command.grab()
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
Grab_pane = pane
end
end
end
function command.ungrab()
Grab_pane = nil
end
function command.link(rel)
if Grab_pane == nil then
add_error('link: needs something to be in the secondary "grab" cursor but found nothing')
return
end
if rel == nil then
rel = 'next'
end
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
if not can_add_link(pane.id, rel, Grab_pane.id) then
return
end
if not can_add_link(Grab_pane.id, Opposite[rel], pane.id) then
return
end
-- connect up links
add_link(pane.id, rel, Grab_pane.id)
add_link(Grab_pane.id, Opposite[rel], pane.id)
schedule_save(pane)
stop_editing(pane)
refresh_pane_height(pane) -- just in case this is the first link
refresh_pane_height(Grab_pane) -- just in case this is the first link
Grab_pane = nil
plan_draw()
end
-- Can we add a link called 'rel' from src to target?
function can_add_link(src, rel, target)
print_and_log(('checking before adding link labeled %s from %s to %s'):format(rel, src, target))
if array.find(Non_unique_links, rel) then
-- check if already present
if Links[src][rel] then
for _,id in ipairs(Links[src][rel]) do
if id == target then
add_error(('%s is already a %s of %s'):format(target, rel, src))
return false
end
end
end
else
-- check for conflict
if Links[src][rel] then
add_error(('%s already has a %s note'):format(src, rel))
return false
end
end
return true
end
function add_link(src, rel, target)
print_and_log(('adding link labeled %s from %s to %s'):format(rel, src, target))
if array.find(Non_unique_links, rel) then
if Links[src][rel] == nil then
Links[src][rel] = {target}
else
table.insert(Links[src][rel], target)
end
else
print_and_log('unique link')
Links[src][rel] = target
end
end
function links_state(id)
local result = {}
table.insert(result, id..' -- ')
if Links[id] then
for rel,val in pairs(Links[id]) do
table.insert(result, rel)
table.insert(result, ':')
if type(val) == 'table' then
table.insert(result, '[')
for _,dest in ipairs(val) do
table.insert(result, dest)
table.insert(result, ' ')
end
table.insert(result, ']')
else
table.insert(result, val)
end
table.insert(result, '|')
end
end
return table.concat(result)
end
function remove_link(src, rel, target)
--? print(('removing %s of %s, to %s'):format(rel, src, target))
if array.find(Non_unique_links, rel) then
--? print(('%s is non-unique'):format(rel))
local arr = Links[src][rel]
assert(arr, 'found no link')
assert(type(arr) == 'table', 'links not arranged in a table')
local pos = array.find(arr, target)
--? print(('contains %s at index %s'):format(target, pos))
assert(pos, "couldn't find link to remove in links table")
table.remove(arr, pos)
if #arr == 0 then
Links[src][rel] = nil
end
else
assert(Links[src][rel] == target, 'link at this rel is not the target; giving up')
Links[src][rel] = nil
end
end
function command.copy_id_to_clipboard()
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
App.set_clipboard(pane.id)
end
function command.rename_link(args)
local from, to = args:match('(%w+)%s+to%s+(%w+)')
if from == nil then
from, to = args:match('(%w+)%s+(%w+)')
end
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
local target
if array.find(Non_unique_links, from) then
if #Links[pane.id][from] <= 0 then
add_error(('no %s links'):format(from))
return
end
if #Links[pane.id][from] > 1 then
add_error(('multiple %s links; not sure which one you mean'):format(from))
return
end
target = Links[pane.id][from][1]
else
target = Links[pane.id][from]
end
print_and_log(('renaming link %s of %s to %s'):format(from, pane, to))
if Links[pane.id][to] and not array.find(Non_unique_links, to) then
add_error(('%s already has a %s note'):format(pane.id, to))
return
end
-- forwards direction
remove_link(pane.id, from, target)
add_link(pane.id, to, target)
if Opposite[to] ~= Opposite[from] then
remove_link(target, Opposite[from], pane.id)
add_link(target, Opposite[to], pane.id)
end
schedule_save(pane)
stop_editing(pane)
end
function command.clear_link(rel)
if rel == nil then
add_error('specify a link to clear')
return
end
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
if Links[pane.id][rel] == nil then
add_error(('%s has no %s note'):format(pane.id, rel))
return
end
if type(Links[pane.id][rel]) == 'table' then
if #Links[pane.id][rel] > 1 then
add_error(('%s is a non-unique link; clearing all %d such links'):format(rel, #Links[pane.id][rel]))
end
end
print_and_log(('clearing link %s of %s (used to point to %s)'):format(rel, pane.id, Links[pane.id][rel]))
Links[pane.id][rel] = nil
schedule_save(pane)
stop_editing(pane)
refresh_pane_height(pane) -- just in case this is the final link
plan_draw()
end
function command.step(rel)
if rel == nil then
rel = 'next'
end
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
if Links[pane.id][rel] == nil then
add_error(('%s has no %s note'):format(pane.id, rel))
return
end
if array.find(Non_unique_links, rel) then
local column = {name=(('%s of %s'):format(rel, pane.id))}
populate_step_column(column, pane.id, rel)
add_column_to_right_of_cursor(column)
plan_draw()
else
command.open_file_in_next_column(Links[pane.id][rel])
end
end
function populate_step_column(column, start_id, rel)
if Links[start_id] == nil then
Links[start_id] = load_links(start_id)
end
if Links[start_id][rel] == nil then
return
end
local n = #Links[start_id][rel]
for i, id in ipairs(Links[start_id][rel]) do
local pane = load_pane(id)
add_title(pane, ('%d/%d'):format(i, n))
table.insert(column, pane)
end
end
function command.extract(rel)
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
command.open_file_in_next_column(pane.id)
end
function command.unroll(rel)
if rel == nil then
rel = 'next'
end
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
local column = {name=('%s from %s'):format(rel, pane.id)}
populate_unroll_column(column, pane.id, rel)
if #column == 0 then
return
end
if #Surface[Cursor_pane.col] == 1 then
assert(Cursor_pane.row == 1, "couldn't set current pane after unrolling")
stop_editing(pane) -- save any edits before we blow it away
Surface[Cursor_pane.col] = column
else
table.insert(Surface, Cursor_pane.col+1, column)
Cursor_pane.col = Cursor_pane.col+1
Cursor_pane.row = 1
end
bring_cursor_column_on_screen()
plan_draw()
end
function populate_unroll_column(column, id, rel)
if Opposite[rel] == rel then
add_error(("link type %s is undirected and can't be unrolled"):format(rel))
return
end
if array.find(Non_unique_links, rel) then
add_error(("link type %s is not unique and can't be unrolled"):format(rel))
return
end
-- back out to start of chain
while true do
if Links[id] == nil then
Links[id] = load_links(id)
end
if Links[id][Opposite[rel]] == nil then
break
end
id = Links[id][Opposite[rel]]
end
-- unroll from start
local curr = id
local n=0
while curr do
if Links[curr] == nil then
Links[curr] = load_links(curr)
end
curr = Links[curr][rel]
n = n+1
end
curr = id
local i=1
while curr do
local pane = load_pane(curr)
add_title(pane, ('%d/%d'):format(i, n))
table.insert(column, pane)
curr = Links[curr][rel]
i = i+1
end
end
function command.append_note(rel)
if rel == nil then
rel = 'next'
end
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
if array.find(Non_unique_links, rel) then
add_error(('%s is non-unique; which direction should I append?'):format(rel))
return
end
local curr_id = pane.id
while true do
if Links[curr_id] == nil then
Links[curr_id] = load_links(curr_id)
end
local next_id = Links[curr_id][rel]
if next_id == nil then
break
end
curr_id = next_id
end
local new_pane = new_pane()
stop_editing_all()
new_pane.editable = true
Links[curr_id][rel] = new_pane.id
Links[new_pane.id][Opposite[rel]] = curr_id
if #Surface[Cursor_pane.col] == 1 then
-- column has a single note; turn it into unroll
Surface[Cursor_pane.col] = {name=('%s from %s'):format(rel, pane.id)}
populate_unroll_column(Surface[Cursor_pane.col], pane.id, rel) -- invalidates new_pane
Cursor_pane.row = #Surface[Cursor_pane.col]
Surface[Cursor_pane.col][Cursor_pane.row].editable = true
bring_cursor_column_on_screen()
bring_cursor_of_cursor_pane_in_view('down')
elseif string.match(Surface[Cursor_pane.col].name, rel..' from %S+') then
-- we're unrolling along the same rel; just append to it
-- (we couldn't be inserting in the middle if we didn't return earlier in
-- the function)
table.insert(Surface[Cursor_pane.col], new_pane)
Cursor_pane.row = #Surface[Cursor_pane.col]
add_title(new_pane, ('%d/%d'):format(Cursor_pane.row, Cursor_pane.row))
refresh_pane_height(pane) -- just in case this is the first link
bring_cursor_column_on_screen()
bring_cursor_of_cursor_pane_in_view('down')
else
local column = {name=new_pane.id}
table.insert(column, new_pane)
add_column_to_right_of_cursor(column)
bring_cursor_of_cursor_pane_in_view('up')
end
plan_draw()
end
function command.neighbors()
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
stop_editing(pane) -- save any edits before we blow it away
local column = {name=('neighbors of %s'):format(pane.id)}
populate_neighbors_column(column, pane.id)
if #Surface[Cursor_pane.col] == 1 then
assert(Cursor_pane.row == 1, "couldn't repurpose column for neighbors")
Surface[Cursor_pane.col] = column
else
table.insert(Surface, Cursor_pane.col+1, column)
Cursor_pane.col = Cursor_pane.col+1
Cursor_pane.row = 1
end
bring_cursor_column_on_screen()
plan_draw()
end
function populate_neighbors_column(column, start_id)
table.insert(column, load_pane(start_id))
for rel,x in pairs(Links[start_id]) do
process_all_links(x, function(target)
local pane = load_pane(target)
add_title(pane, rel)
table.insert(column, pane)
end)
end
end
-- links might contain either a single target or a list of them
function process_all_links(x, fn)
if type(x) == 'string' then
fn(x)
else
for _,target in ipairs(x) do
fn(target)
end
end
end
function command.delete_note()
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
if #Links[pane.id] > 0 then
add_error('pane has links; giving up')
return
end
-- TODO: test harness support for file ops below
if App.run_tests then
return
end
-- delete from disk
App.remove(Directory..pane.id)
-- delete from recently modified
local filenames = {}
local f = App.open_for_reading(Directory..'recent')
for line in f:lines() do
if line ~= pane.id then
table.insert(filenames, line)
end
end
f:close()
local f = App.open_for_writing(Directory..'recent')
for _,filename in ipairs(filenames) do
f:write(filename)
f:write('\n')
end
f:close()
-- Delete any columns dedicated to just this note, and update cursor pane if necessary.
local delete_cursor_column = Surface[Cursor_pane.col].name == pane.id
for i=#Surface,Cursor_pane.col+1,-1 do
local column = Surface[i]
if column.name == pane.id then
table.remove(Surface, i)
end
end
local num_deleted = 0
for i=Cursor_pane.col,1,-1 do
local column = Surface[i]
if column.name == pane.id then
table.remove(Surface, i)
num_deleted = num_deleted+1
end
end
if num_deleted > 0 then
Cursor_pane.col = Cursor_pane.col - num_deleted
if delete_cursor_column then
Cursor_pane.row = 1
end
else
assert(not delete_cursor_column, "failed to delete note's column")
end
--
while Cursor_pane.row > #Surface[Cursor_pane.col] do
Cursor_pane.row = #Surface[Cursor_pane.col]
if Cursor_pane.row == 0 then
Cursor_pane.col = Cursor_pane.col - 1
Cursor_pane.row = 1
end
end
--
command.reload_all()
end
function command.snapshot_memory()
-- load library on demand
if mri == nil then
mri = require('MemoryReferenceInfo')
end
collectgarbage('collect')
print(collectgarbage('count'))
mri.m_cMethods.DumpMemorySnapshot('./', 'mem', -1)
add_error('snapshot successfully dumped')
end
function command.send_key_to_current_pane(chord, key)
if Cursor_pane.col < 1 then
add_error('no current note')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
add_error('no current note')
return
end
edit.keychord_press(pane, chord, key)
end
-- return a new pane with a unique filename
function new_pane()
local t = os.time()
local id = os.date('%Y/%m/%d/%H-%M-%S', t)
print_and_log('new_pane: creating directory '..Directory..dirname(id))
local status = App.mkdir(Directory..dirname(id))
assert(status, "failed to create directory for note")
Links[id] = {}
local pane = load_pane(id)
if not file_exists(pane.filename) then
table.insert(pane.lines, 1, {data=os.date('%Y-%m-%d %H:%M:%S %Z', t), mode='text'})
pane.cursor1 = {line=2, pos=1}
Text.redraw_all(pane)
end
return pane
end
function add_column_to_right_of_cursor(column)
table.insert(Surface, Cursor_pane.col+1, column)
Cursor_pane.col = Cursor_pane.col+1
Cursor_pane.row = 1
bring_cursor_column_on_screen()
end
function bring_cursor_column_on_screen()
local col_sx = left_edge_sx(Cursor_pane.col)
if col_sx < Display_settings.x or col_sx > Display_settings.x + App.screen.width - Display_settings.column_width then
Display_settings.x = math.max(0, col_sx + Display_settings.column_width + Margin_right + Padding_horizontal - App.screen.width)
Display_settings.y = 0
end
end
function emit_links_in_json_in_consistent_order(outfile, links)
local first_written = false
outfile:write('{')
for _,label in pairs(Edge_list) do
if links[label] then
if first_written then
outfile:write(',')
else
first_written = true
end
outfile:write(json.encode(label)..':'..json.encode(links[label]))
end
end
-- links we don't know about, just in case
for rel,target in pairs(links) do
if Opposite[rel] == nil then
if first_written then
outfile:write(',')
else
first_written = true
end
outfile:write(json.encode(rel)..':'..json.encode(target))
end
end
outfile:write('}')
end
function concat_all(dir, files)
if dir == '' then return files end
for i,file in ipairs(files) do
files[i] = dir..file
end
return files
end
function append(arr, b)
for _,x in ipairs(b) do
table.insert(arr, x)
end
end
function split(s)
local result = {}
for sub in s:gmatch("%S+") do
table.insert(result, sub)
end
return result
end
function match_all(s, subs)
for _,sub in ipairs(subs) do
if s:find(sub, 1, --[[literal pattern]] true) == nil then
return false
end
end
return true
end
function find_any(s, subs, start)
local result = nil
for _,sub in ipairs(subs) do
local i = s:find(sub, start, --[[literal pattern]] true)
if i then
if result == nil then
result = i
elseif i < result then
result = i
end
end
end
return result
end