diff options
| -rw-r--r-- | README | 1 | ||||
| -rw-r--r-- | TODO | 10 | ||||
| -rw-r--r-- | doc/sciteco.1.in | 8 | ||||
| -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 |
8 files changed, 194 insertions, 22 deletions
@@ -73,6 +73,7 @@ Features This makes it even harder to destroy work by accident than in most other editors. Rubbed out commands can be re-inserted (redo). +* Timing-based backup mechanism * Munging: Macros may be munged, that is executed in batch mode. In other words, SciTECO can be used for scripting. By default, a profile is munged. @@ -481,11 +481,11 @@ Features: * Touch restored save point files - should perhaps be configurable. This is important when working with Makefiles, as make looks at the modification times of files. - * There should really be a backup mechanism. It would be relatively - easy to implement portably, by using timeout() on Curses. - The Gtk version can simply use a glib timer. - Backup files should NOT be hidden and the timeout should be - configurable (EJ?). + * Could we somehow offer to open backup~ files? + opener.tes and session.tes operate in batch mode and interactively + asking what to do wouldn't always work in the GUI variants. + Also, we'd need a command to fetch the modification timestamp + as well. * Error handling in SciTECO macros: Allow throwing errors with e.g. [n]^F<description>^F where n is an error code, defaulting to 0 and description is the error string - there could be code-specific diff --git a/doc/sciteco.1.in b/doc/sciteco.1.in index 0ed9f85..0370c50 100644 --- a/doc/sciteco.1.in +++ b/doc/sciteco.1.in @@ -450,6 +450,14 @@ and opening files specified on the command line. .B $SCITECOPATH/*.tes Standard library macros. .TP +.SCITECO_TOPIC backup +.IB filename ~ +Backup file: +After a configurable backup interval (5 minutes by default) +\*(ST backs up all modified buffers, that have not been backed up +previously. +These files should be ignored by version control systems. +.TP .SCITECO_TOPIC savepoint .BI .teco- n - filename ~ Save point files created by \*(ST when saving files 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); |
