change how we provide the notes directory

Before:
- notes directory is saveDir/data by default
  where saveDir = love.filesystem.getSaveDirectory()
- providing a commandline arg of x makes the notes directory saveDir/data.x

Now:
- there is no default directory; you get an error message instead
  telling you what to do.
- the commandline arg is the (preferably absolute) path of the notes
  directory you want to load

Unchanged: once you set the notes directory it gets remembered for
future runs.

Now the Readme is a lot simpler.

I tried to make migrating simpler, but this is complicated enough (see
Manual_tests.md)
This commit is contained in:
Kartik K. Agaram 2023-09-08 14:16:50 -07:00
parent f1b6580dc2
commit 209dc38a9b
6 changed files with 168 additions and 76 deletions

View File

@ -12,8 +12,25 @@ Initializing settings:
- start out running the note-taking app, move window, press ctrl+e twice; window is running note-taking app in same position+dimensions
- start out editing source, move window, press ctrl+e twice; window is editing source in same position+dimensions
- no log file; switching to source works
- start out with a non-default data set; default config file and data directory don't get clobbered
- start out with a non-default data set and switch to editor and press ctrl+k to clear logs; default config file and data directory don't get clobbered
Load directory:
- clear save dir, run, you get an error message telling you to specify a directory for notes
- quit works
- create an empty config file, run, you get an error message telling you to migrate your existing notes directory
- quit works
- clear save dir, run with a dir argument that doesn't exist; app creates dir and opens up with no data
- quit works
- on quit, app creates 'config' file within the dir
- clear save dir, run with a dir argument that exists and contains no 'config'; app opens up
- quit works
- on quit, app creates 'config' file within the dir
- run with a dir argument of '/' (that you don't have access to)
?? app should either throw an error or save data you put in _somewhere_
- run app from terminal with a dir argument; the app uses settings from 'config' inside that directory
- quit works
- run app from terminal with a dir argument, hit C-w; the app switches to source editor
- app has a config with a `data_directory` key, run app from terminal with a dir argument; app opens up with new data directory
- on quit, app saves the new data directory to the `data_directory` key
Code loading:
* run love with directory; note-taking app runs

View File

@ -36,34 +36,13 @@ Install [LÖVE](https://love2d.org). It's just a 5MB download, open-source and
extremely well-behaved. I'll assume below that you can invoke it using the
`love` command, but that might vary depending on your OS.
To try out pensieve.love with some sample data, run the following command from
this repo's top-level directory:
The first time you run pensieve.love, do so from a terminal and pass in the
directory where you want to store your notes.
```
love . sample
```
(The repo contains a 'data.sample/' directory for trying out with.)
This will read the notes from the `data.sample/` directory in this repo to
give you a flavor for what the app is like.
While the sample gives you stuff to browse, don't use it to write any real
notes. LÖVE limits what directories apps can list files from (which is great
for privacy), but has some confusing rules around them. Each app can only
write to files under a single directory, the app's _save directory_
([love.filesystem.getSaveDirectory()](https://love2d.org/wiki/love.filesystem.getSaveDirectory)).
In addition, reads will also fall back to read from the repo directory,
enabling the sample experience. However, edits you make will not modify the
`data.sample/` directory in this repo. Your notes will fragment between two
directories, which can get confusing.
To create your own directory of notes, run without an argument:
```
love .
```
pensieve.love will then create a `data/` directory in the app's save directory
as above. Confusing location, but everything will be in one place.
From here on out, restarting pensieve.love will open the same directory.
Run again from a terminal if you ever want to switch to a new directory.
## Modifying the app

View File

@ -394,7 +394,7 @@ end
function file_candidates(prefix)
--? print('-- '..prefix)
local info = love.filesystem.getInfo(Directory..prefix)
local info = nativefs.getInfo(Directory..prefix)
--? print(info)
--? if info then
--? print(info.type)
@ -416,7 +416,7 @@ function file_candidates(prefix)
path = path..visible_dir
end
--? print('path:', path)
local files = love.filesystem.getDirectoryItems(path)
local files = App.files(path)
--? print(#files, 'files')
local base
if info and info.type == 'directory' then
@ -431,7 +431,7 @@ function reorder(dir, files)
local result = {}
local info = {}
for _,file in ipairs(files) do
info[file] = love.filesystem.getInfo(dir..'/'..file)
info[file] = nativefs.getInfo(dir..'/'..file)
end
-- files before directories
for _,file in ipairs(files) do
@ -534,7 +534,7 @@ function run_command(cmd, args)
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(function_name))
print_and_log(('run_command: not implemented yet: %s'):format(cmd))
end
end
@ -772,7 +772,8 @@ end
function populate_recently_modified_column(column)
local filenames = {}
for line in love.filesystem.lines(Directory..'recent') do
local f = App.open_for_reading(Directory..'recent')
for line in f:lines() do
table.insert(filenames, line)
end
local done, ndone = {}, 0
@ -858,7 +859,7 @@ function finalize_search_all_pane()
end
local id = id_for_search_all_pane(Display_settings.search_all_query)
--? print(id)
love.filesystem.remove(Directory..id)
App.remove(Directory..id)
Display_settings.search_all_pane.id = id
Display_settings.search_all_pane.filename = Directory..id
Display_settings.editable = false
@ -879,13 +880,13 @@ function resume_search_all()
if Display_settings.search_all_state == nil then
Display_settings.search_all_progress_indicator = 'initialized top-level files'
Display_settings.search_all_state = {
top_level_files = love.filesystem.getDirectoryItems(Directory),
top_level_files = App.files(Directory),
top_level_file_index = 1,
}
elseif Display_settings.search_all_state.top_level_file_index then
local current_filename = Display_settings.search_all_state.top_level_files[Display_settings.search_all_state.top_level_file_index]
Display_settings.search_all_progress_indicator = current_filename
local info = love.filesystem.getInfo(Directory..current_filename)
local info = nativefs.getInfo(Directory..current_filename)
if info.type == 'file' then
search_in_file(current_filename)
end
@ -903,10 +904,10 @@ function resume_search_all()
Display_settings.search_all_progress_indicator = Display_settings.search_all_state.date
local old_year = Display_settings.search_all_state.year
local date_dir = Directory..Display_settings.search_all_state.date
local info = love.filesystem.getInfo(date_dir)
local info = nativefs.getInfo(date_dir)
if info then
if info.type == 'directory' then
local filenames = love.filesystem.getDirectoryItems(date_dir)
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).
@ -921,7 +922,7 @@ function resume_search_all()
Display_settings.search_all_state.year = os.date('%Y', Display_settings.search_all_state.time)
Display_settings.search_all_state.date = os.date('%Y/%m/%d/', Display_settings.search_all_state.time)
if old_year ~= Display_settings.search_all_state.year then
local previous_year_info = love.filesystem.getInfo(Directory..Display_settings.search_all_state.year)
local previous_year_info = nativefs.getInfo(Directory..Display_settings.search_all_state.year)
if previous_year_info == nil then
Display_settings.search_all_state = nil
Display_settings.mode = 'normal'
@ -934,14 +935,15 @@ end
function search_in_file(filename)
--? print('searching '..filename..' for '..Display_settings.search_all_query)
local contents = love.filesystem.read(Directory..filename)
local f = App.open_for_reading(Directory..filename)
local contents = f:read()
if contents == nil then
error('no contents in '..filename)
end
if match_all(contents, Display_settings.search_all_terms) then
local id = id_for_search_all_pane(Display_settings.search_all_query)
local outfilename = Directory..id
local success, errmsg = love.filesystem.append(outfilename, '[['..filename..']]\n')
local success, errmsg = append_to_file(outfilename, '[['..filename..']]\n')
if not success then error(errmsg) end
local index = 0
while true do
@ -952,7 +954,7 @@ function search_in_file(filename)
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 = love.filesystem.append(outfilename, '...'..snippet..'...\n\n')
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)
@ -975,7 +977,7 @@ function interrupt_search_all()
if Display_settings.search_all_state then
local id = id_for_search_all_pane(Display_settings.search_all_query)
local outfilename = Directory..id
local success, errmsg = love.filesystem.append(outfilename, 'interrupted at '..Display_settings.search_all_progress_indicator..'\n')
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)
@ -1523,16 +1525,16 @@ function command.delete_note()
return
end
-- delete from disk
love.filesystem.remove(Directory..pane.id)
App.remove(Directory..pane.id)
-- delete from recently modified
local filenames = {}
for line in love.filesystem.lines(Directory..'recent') do
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
local f = love.filesystem.newFile(Directory..'recent')
f:open('w')
local f = App.open_for_writing(Directory..'recent')
for _,filename in ipairs(filenames) do
f:write(filename)
f:write('\n')
@ -1603,7 +1605,7 @@ 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 = love.filesystem.createDirectory(Directory..dirname(id))
local status = App.mkdir(Directory..dirname(id))
assert(status)
Links[id] = {}
local pane = load_pane(id)

View File

@ -1,6 +1,6 @@
-- primitives for saving to file and loading from file
function file_exists(filename)
local infile = App.open_for_reading(App.save_dir..filename)
local infile = App.open_for_reading(filename)
if infile then
infile:close()
return true
@ -10,7 +10,7 @@ function file_exists(filename)
end
function load_from_disk(State)
local infile = App.open_for_reading(App.save_dir..State.filename)
local infile = App.open_for_reading(State.filename)
State.lines = load_from_file(infile)
if infile then infile:close() end
end
@ -38,7 +38,7 @@ end
function save_to_disk(State)
-- save the payload
log(2, 'save_to_disk: '..State.id)
local outfile = App.open_for_writing(App.save_dir..State.filename)
local outfile = App.open_for_writing(State.filename)
if not outfile then
error('failed to write to "'..State.filename..'"')
end
@ -62,7 +62,7 @@ function save_to_disk(State)
-- update recent
if not State.recent_updated then
if State.id then
local f, err = io.open(App.save_dir..Directory..'recent', 'a')
local f, err = io.open(Directory..'recent', 'a')
if not f then
error(err)
end
@ -148,7 +148,7 @@ end
function load_links(id)
print(id)
local infile = App.open_for_reading(App.save_dir..Directory..id..'.json')
local infile = App.open_for_reading(Directory..id..'.json')
if not infile then
return {}
end
@ -156,13 +156,13 @@ function load_links(id)
end
function save_links(id)
local links_filename = App.save_dir..Directory..id..'.json'
local status = love.filesystem.createDirectory(dirname(links_filename))
local links_filename = Directory..id..'.json'
local status = App.mkdir(dirname(links_filename))
assert(status)
log(2, 'save_links: '..id)
if empty(Links[id]) then
print_and_log('save_links: no links; getting rid of .json if it exists')
love.filesystem.remove(links_filename)
App.remove(links_filename)
return
end
local outfile = App.open_for_writing(links_filename)
@ -240,6 +240,21 @@ function load_drawing_from_array(iter, a, i)
return i, drawing
end
function append_to_file(filename, contents)
local f, err = App.open_for_reading(filename)
if not f then
return nil, err
end
local old_contents = f:read()
f:close()
f, err = App.open_for_writing(filename)
if not f then
return nil, err
end
f:write(old_contents..contents)
f:close()
end
function is_absolute_path(path)
local os_path_separator = package.config:sub(1,1)
if os_path_separator == '/' then

View File

@ -181,6 +181,7 @@ function App.keychord_press(chord, key)
if chord == 'C-w' then
-- carefully save settings
if Current_app == 'run' then
if Directory_error then return end -- make sure we never ever save 'run' settings if we failed to load Directory. All bets are off. Perform no operations that might persist past restart. Debugging errors in Directory_error code will be more inconvenient, but so it goes.
local source_settings = Settings.source
Settings = run.settings()
Settings.source = source_settings
@ -279,6 +280,8 @@ end
function love.quit()
if Current_app == 'run' then
if Directory_error then return end
if Settings == nil then Settings = {} end
local source_settings = Settings.source
Settings = run.settings()
Settings.source = source_settings

116
run.lua
View File

@ -108,7 +108,8 @@ function run.initialize_globals()
Grab_pane = nil
-- where we store our notes (pane id is also a relative path under there)
Directory = 'data/'
Directory = nil
Directory_error = nil -- Any error encountered while determining Directory. If this is ever set, the app will do nothing but display it. (We can't rely on 'error' for that because we don't want to open up the sources in that situation.)
-- blinking cursor
Cursor_time = 0
@ -116,7 +117,7 @@ function run.initialize_globals()
-- a read-only buffer for errors
Error_log = edit.initialize_state(0, 0, Display_settings.column_width, Font_height, Line_height)
Error_log.id = 'errors'
Error_log.filename = Directory..'errors'
Error_log.filename = nil -- Will live within Directory. Don't forget to set this once Directory is initialized.
Error_log.editable = false
Text.redraw_all(Error_log)
Current_error = nil
@ -127,11 +128,56 @@ 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
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)
print('Directory initialized to '..Directory)
Error_log.filename = Directory..'errors'
run.load_more_settings_from_notes_directory()
Editor_state = nil -- not used outside editor tests
@ -145,7 +191,7 @@ function run.initialize(arg)
print('ignoring commandline args after '..arg[1])
end
print_and_log('reading notes from '..App.save_dir..Directory)
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
@ -180,14 +226,25 @@ function run.load_settings()
Font_height = Settings.font_height
Line_height = math.floor(Font_height*1.3)
love.graphics.setFont(love.graphics.newFont(Font_height))
Display_settings.column_width = Settings.column_width
for _,column_name in ipairs(Settings.columns) do
create_column(column_name)
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
-- initialize surface with a single column
command.recently_modified()
end
Cursor_pane.col = Settings.cursor_col
Cursor_pane.row = Settings.cursor_row
Display_settings.x = Settings.surface_x
Display_settings.y = Settings.surface_y
end
function run.set_window_position_from_settings(settings)
@ -204,8 +261,6 @@ function run.initialize_default_settings()
love.graphics.setFont(love.graphics.newFont(Font_height))
run.initialize_window_geometry()
Display_settings.column_width = 40*App.width('m')
-- initialize surface with a single column
command.recently_modified()
end
function run.initialize_window_geometry()
@ -225,6 +280,7 @@ end
function run.resize(w, h)
App.screen.width, App.screen.height = w, h
if Directory_error then return end
--? print('resize:', App.screen.width, App.screen.height)
plan_draw()
end
@ -347,6 +403,10 @@ function plan_draw()
end
function run.draw()
if Directory_error then
love.graphics.print(Directory_error, 50,50)
return
end
--? print('draw', Display_settings.y)
if Display_settings.mode == 'normal' then
draw_normal_mode()
@ -511,6 +571,7 @@ function overlap(lo1,hi1, lo2,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
@ -600,6 +661,7 @@ function to_surface(x, y)
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
@ -609,26 +671,35 @@ function run.quit()
end
function run.settings()
if Settings == nil then Settings = {} end
Settings.x, Settings.y, Settings.displayindex = App.screen.position()
assert(Directory_error == nil) -- this will go into an infinite loop if handle_error ever tries to save settings
-- side effect: save notes-related settings inside Directory
local column_names = {}
for _,column in ipairs(Surface) do
table.insert(column_names, column.name)
end
local f = App.open_for_writing(Directory..'config')
assert(f)
f:write(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,
}))
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,
column_width=Display_settings.column_width,
surface_x=Display_settings.x,
surface_y=Display_settings.y,
cursor_col=Cursor_pane.col,
cursor_row=Cursor_pane.row,
columns=column_names,
data_directory=Directory,
}
end
function run.mouse_press(x,y, mouse_button)
if Directory_error then return end
--? print('app mouse pressed', x,y)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
clear_selections()
@ -677,6 +748,7 @@ function mouse_press_in_normal_mode(x,y, mouse_button)
end
function run.mouse_release(x,y, mouse_button)
if Directory_error then return end
--? print('app mouse released')
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if in_pane(x,y) or Display_settings.mode == 'maximize' then
@ -691,6 +763,7 @@ function run.mouse_release(x,y, mouse_button)
end
function run.mouse_wheel_move(dx,dy)
if Directory_error then return end
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
@ -735,6 +808,7 @@ function run.mouse_wheel_move(dx,dy)
end
function run.text_input(t)
if Directory_error then return end
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
--? print('textinput', t)
-- hotkeys operating on the cursor pane
@ -802,6 +876,7 @@ function run.text_input(t)
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
@ -1559,6 +1634,7 @@ function rehydrate_pane(pane)
end
function run.key_release(key, scancode)
if Directory_error then return end
--? print('key release', key)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Cursor_pane.col < 1 then