diff options
| author | Robin Haberkorn <rhaberkorn@fmsbw.de> | 2025-12-17 01:17:11 +0100 |
|---|---|---|
| committer | Robin Haberkorn <rhaberkorn@fmsbw.de> | 2025-12-17 01:17:11 +0100 |
| commit | deed71ac895451041359d7b18e58eca0a0972bc3 (patch) | |
| tree | 2cce2c266b2f92fca45335c95f0a4b8f4945d31e /src | |
| parent | ad0780c7163c9673f89dc584d2a6096f317bec2b (diff) | |
implemented backup file mechanism
* The backup mechanism is supposed to guard against crashes of SciTECO and
unexpected program terminations (e.g. power cycling, etc.)
* In a given interval (no matter whether busy or idlying on the prompt)
SciTECO saves all modified buffers with the filename~ (like most other editors).
As an optimization files are not backed up if they have been backed up
previously to avoid pointless and possibly slow file system writes.
* While the backup mechanism exists outside of the usual undo-paradigm -
backup file creating is not bound to character input and it makes no sense
to restore the exact state of backup files - there are some interesting
interactions:
* When a buffer is dirtyfied or saved that was previously backed up, it must always
be reset to the DIRTY state on rubout, so backups are eventually recreated.
* When a buffer is dirtyfied first (was clean), the backup file must be
removed on rubout as well - we don't expect backup files for clean buffers.
* There is currently no automatic way to restore backup files.
This could potentially be done by opener.tes and session.tes in the future,
although you couldn't currently always get meaningful user feedback
(whether he wants to restore the file).
Perhaps we should at least log a message when detecting backup files that
are newer than the file that is being opened.
Diffstat (limited to 'src')
| -rw-r--r-- | src/core-commands.c | 23 | ||||
| -rw-r--r-- | src/interface-curses/interface.c | 26 | ||||
| -rw-r--r-- | src/interface-gtk/interface.c | 23 | ||||
| -rw-r--r-- | src/ring.c | 106 | ||||
| -rw-r--r-- | src/ring.h | 19 |
5 files changed, 180 insertions, 17 deletions
diff --git a/src/core-commands.c b/src/core-commands.c index e0b2f89..47809e2 100644 --- a/src/core-commands.c +++ b/src/core-commands.c @@ -2151,6 +2151,11 @@ teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error) * .IP 5: * Height of the command line view in lines. * Must not be smaller than 1. + * .IP 6: + * Backup interval in seconds or 0 if backups are disabled. + * After changing the interval, the new value may become + * active only after the previous interval expires. + * Backups are not created in batch mode. * . * .IP -1: * Type of the last mouse event (\fBread-only\fP). @@ -2204,7 +2209,8 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) EJ_MEMORY_LIMIT, EJ_INIT_COLOR, EJ_CARETX, - EJ_CMDLINE_HEIGHT + EJ_CMDLINE_HEIGHT, + EJ_BACKUP_INTERVAL }; static teco_int_t caret_x = 0; @@ -2255,6 +2261,17 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) teco_undo_guint(teco_cmdline.height) = value; break; + case EJ_BACKUP_INTERVAL: + if (value < 0 || value > G_MAXUINT) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid backup interval %" TECO_INT_FORMAT "s " + "for <EJ>", value); + return; + } + teco_undo_guint(teco_ring_backup_interval) = value; + /* FIXME: Perhaps signal the interface to reprogram timers */ + break; + default: g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Cannot set property %" TECO_INT_FORMAT " " @@ -2314,6 +2331,10 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) teco_expressions_push(teco_cmdline.height); break; + case EJ_BACKUP_INTERVAL: + teco_expressions_push(teco_ring_backup_interval); + break; + default: g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Invalid property %" TECO_INT_FORMAT " " diff --git a/src/interface-curses/interface.c b/src/interface-curses/interface.c index 8f41f2a..e03ba72 100644 --- a/src/interface-curses/interface.c +++ b/src/interface-curses/interface.c @@ -205,6 +205,9 @@ static struct { teco_string_t info_current; gboolean info_dirty; + /** timer to track the backup interval */ + GTimer *backup_timer; + WINDOW *msg_window; /** @@ -1217,7 +1220,7 @@ teco_interface_info_update_buffer(const teco_buffer_t *buffer) teco_string_clear(&teco_interface.info_current); teco_string_init(&teco_interface.info_current, filename, strlen(filename)); - teco_interface.info_dirty = buffer->dirty; + teco_interface.info_dirty = buffer->state > TECO_BUFFER_CLEAN; teco_interface.info_type = TECO_INFO_TYPE_BUFFER; /* NOTE: drawn in teco_interface_event_loop_iter() */ } @@ -2014,6 +2017,18 @@ teco_interface_blocking_getch(void) /* no special <CTRL/C> handling */ raw(); nodelay(teco_interface.input_pad, FALSE); + + /* + * Make sure we return when it's time to create backups. + */ + if (teco_ring_backup_interval != 0) { + if (G_UNLIKELY(!teco_interface.backup_timer)) + teco_interface.backup_timer = g_timer_new(); + gdouble elapsed = g_timer_elapsed(teco_interface.backup_timer, NULL); + wtimeout(teco_interface.input_pad, + MAX((gdouble)teco_ring_backup_interval - elapsed, 0)*1000); + } + /* * Memory limiting is stopped temporarily, since it might otherwise * constantly place 100% load on the CPU. @@ -2029,6 +2044,12 @@ teco_interface_blocking_getch(void) cbreak(); #endif + if (key == ERR && teco_ring_backup_interval != 0 && + g_timer_elapsed(teco_interface.backup_timer, NULL) >= teco_ring_backup_interval) { + teco_ring_backup(); + g_timer_start(teco_interface.backup_timer); + } + return key; } @@ -2290,4 +2311,7 @@ teco_interface_cleanup(void) if (teco_interface.pair_table) g_hash_table_destroy(teco_interface.pair_table); + + if (teco_interface.backup_timer) + g_timer_destroy(teco_interface.backup_timer); } diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c index 7aa9797..0eec55e 100644 --- a/src/interface-gtk/interface.c +++ b/src/interface-gtk/interface.c @@ -60,6 +60,7 @@ //#define DEBUG static gboolean teco_interface_busy_timeout_cb(gpointer user_data); +static gboolean teco_interface_backup_cb(gpointer user_data); static void teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data); static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation, @@ -573,8 +574,8 @@ teco_interface_info_update_buffer(const teco_buffer_t *buffer) teco_string_clear(&teco_interface.info_current); teco_string_init(&teco_interface.info_current, filename, strlen(filename)); - teco_interface.info_type = buffer->dirty ? TECO_INFO_TYPE_BUFFER_DIRTY - : TECO_INFO_TYPE_BUFFER; + teco_interface.info_type = buffer->state > TECO_BUFFER_CLEAN + ? TECO_INFO_TYPE_BUFFER_DIRTY : TECO_INFO_TYPE_BUFFER; } static GdkAtom @@ -1220,6 +1221,10 @@ teco_interface_event_loop(GError **error) g_unix_signal_add(SIGTERM, teco_interface_sigterm_handler, NULL); #endif + /* the interval might have been changed in the profile */ + g_timeout_add_seconds(teco_ring_backup_interval, + teco_interface_backup_cb, NULL); + /* don't limit while waiting for input as this might be a busy operation */ teco_memory_stop_limiting(); @@ -1278,6 +1283,20 @@ teco_interface_busy_timeout_cb(gpointer user_data) return G_SOURCE_REMOVE; } +static gboolean +teco_interface_backup_cb(gpointer user_data) +{ + teco_ring_backup(); + + /* + * The backup interval could have changed (6EJ). + * New intervals will not be effective immediately, though. + */ + g_timeout_add_seconds(teco_ring_backup_interval, + teco_interface_backup_cb, NULL); + return G_SOURCE_REMOVE; +} + static void teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data) { @@ -21,6 +21,7 @@ #include <glib.h> #include <glib/gprintf.h> +#include <glib/gstdio.h> #include <Scintilla.h> @@ -75,16 +76,21 @@ teco_buffer_undo_edit(teco_buffer_t *ctx) } /** @private @memberof teco_buffer_t */ +static inline gchar * +teco_buffer_get_backup(teco_buffer_t *ctx) +{ + return g_strconcat(ctx->filename, "~", NULL); +} + +/** @private @memberof teco_buffer_t */ static gboolean teco_buffer_load(teco_buffer_t *ctx, const gchar *filename, GError **error) { if (!teco_view_load(ctx->view, filename, TRUE, error)) return FALSE; -#if 0 /* NOTE: currently buffer cannot be dirty */ - undo__teco_interface_info_update_buffer(ctx); - teco_undo_gboolean(ctx->dirty) = FALSE; -#endif + /* currently buffer cannot be dirty */ + g_assert(ctx->state == TECO_BUFFER_CLEAN); teco_buffer_set_filename(ctx, filename); return TRUE; @@ -110,7 +116,13 @@ teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error) */ if (ctx == teco_ring_current && !teco_qreg_current) undo__teco_interface_info_update_buffer(ctx); - teco_undo_gboolean(ctx->dirty) = FALSE; + if (ctx->state == TECO_BUFFER_DIRTY_BACKEDUP) { + g_autofree gchar *filename_backup = teco_buffer_get_backup(ctx); + g_unlink(filename_backup); + /* on rubout, we do not restore the backup file */ + ctx->state = TECO_BUFFER_DIRTY; + } + teco_undo_guint(ctx->state) = TECO_BUFFER_CLEAN; /* * FIXME: necessary also if the filename was not specified but the file @@ -129,6 +141,11 @@ teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error) static inline void teco_buffer_free(teco_buffer_t *ctx) { + if (ctx->state == TECO_BUFFER_DIRTY_BACKEDUP) { + g_autofree gchar *filename_backup = teco_buffer_get_backup(ctx); + g_unlink(filename_backup); + } + teco_view_free(ctx->view); g_free(ctx->filename); g_free(ctx); @@ -222,15 +239,40 @@ teco_ring_find_by_id(teco_int_t id) return NULL; } +static void +teco_ring_undirtify(void) +{ + if (teco_ring_current->state == TECO_BUFFER_DIRTY_BACKEDUP) { + g_autofree gchar *filename_backup = teco_buffer_get_backup(teco_ring_current); + g_unlink(filename_backup); + } + + teco_ring_current->state = TECO_BUFFER_CLEAN; + teco_interface_info_update(teco_ring_current); +} + +TECO_DEFINE_UNDO_CALL(teco_ring_undirtify); + void teco_ring_dirtify(void) { - if (teco_qreg_current || teco_ring_current->dirty) + if (teco_qreg_current) return; - undo__teco_interface_info_update_buffer(teco_ring_current); - teco_undo_gboolean(teco_ring_current->dirty) = TRUE; - teco_interface_info_update(teco_ring_current); + teco_buffer_state_t old_state = teco_ring_current->state; + teco_ring_current->state = TECO_BUFFER_DIRTY; + switch (old_state) { + case TECO_BUFFER_CLEAN: + teco_interface_info_update(teco_ring_current); + undo__teco_ring_undirtify(); + break; + case TECO_BUFFER_DIRTY: + break; + case TECO_BUFFER_DIRTY_BACKEDUP: + /* set to TECO_BUFFER_DIRTY on rubout */ + teco_undo_guint(teco_ring_current->state); + break; + } } /** Get id of first dirty buffer, or otherwise 0 */ @@ -241,7 +283,7 @@ teco_ring_get_first_dirty(void) for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { teco_buffer_t *buffer = (teco_buffer_t *)cur; - if (buffer->dirty) + if (buffer->state > TECO_BUFFER_CLEAN) return id; id++; } @@ -255,13 +297,53 @@ teco_ring_save_all_dirty_buffers(GError **error) for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { teco_buffer_t *buffer = (teco_buffer_t *)cur; /* NOTE: Will fail for a dirty unnamed file */ - if (buffer->dirty && !teco_buffer_save(buffer, NULL, error)) + if (buffer->state > TECO_BUFFER_CLEAN && + !teco_buffer_save(buffer, NULL, error)) return FALSE; } return TRUE; } +/** + * Backup interval in seconds or 0 if disabled. + * It's not currently enforced in batch mode. + */ +guint teco_ring_backup_interval = 5*60; + +/** + * Back up all dirty buffers. + * + * Should be called by the interface every teco_ring_backup_interval seconds. + * This does not generate or expect undo tokens, so it can be called + * even when idlying. + */ +void +teco_ring_backup(void) +{ + g_assert(teco_ring_backup_interval > 0); + + for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { + teco_buffer_t *buffer = (teco_buffer_t *)cur; + /* + * Dirty unnamed buffers cannot be backed up. + * Already backed-up buffers don't have to be written again. + * FIXME: Perhaps they should be under ~/UNNAMED~? + */ + if (buffer->state != TECO_BUFFER_DIRTY || !buffer->filename) + continue; + + g_autofree gchar *filename_backup = teco_buffer_get_backup(buffer); + /* + * FIXME: Errors are silently ignored. + * Should we log warnings instead? + */ + teco_view_save(buffer->view, filename_backup, NULL); + + buffer->state = TECO_BUFFER_DIRTY_BACKEDUP; + } +} + gboolean teco_ring_edit_by_name(const gchar *filename, GError **error) { @@ -719,7 +801,7 @@ teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error) if (teco_machine_main_eval_colon(ctx) > 0) { if (!teco_buffer_save(buffer, NULL, error)) return; - } else if (!force && buffer->dirty) { + } else if (!force && buffer->state > TECO_BUFFER_CLEAN) { g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Buffer \"%s\" is dirty", buffer->filename ? : "(Unnamed)"); @@ -25,13 +25,26 @@ #include "parser.h" #include "list.h" +typedef enum { + TECO_BUFFER_CLEAN = 0, + /** buffer modified */ + TECO_BUFFER_DIRTY, + /** modified and backup already written */ + TECO_BUFFER_DIRTY_BACKEDUP +} teco_buffer_state_t; + typedef struct teco_buffer_t { teco_tailq_entry_t entry; teco_view_t *view; gchar *filename; - gboolean dirty; + + /** + * A teco_buffer_state_t. + * This is still a guint, so you can call teco_undo_guint(). + */ + guint state; } teco_buffer_t; /** @memberof teco_buffer_t */ @@ -70,6 +83,10 @@ void teco_ring_dirtify(void); guint teco_ring_get_first_dirty(void); gboolean teco_ring_save_all_dirty_buffers(GError **error); +extern guint teco_ring_backup_interval; + +void teco_ring_backup(void); + gboolean teco_ring_edit_by_name(const gchar *filename, GError **error); gboolean teco_ring_edit_by_id(teco_int_t id, GError **error); |
