aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README1
-rw-r--r--TODO10
-rw-r--r--doc/sciteco.1.in8
-rw-r--r--src/core-commands.c23
-rw-r--r--src/interface-curses/interface.c26
-rw-r--r--src/interface-gtk/interface.c23
-rw-r--r--src/ring.c106
-rw-r--r--src/ring.h19
8 files changed, 194 insertions, 22 deletions
diff --git a/README b/README
index 7c798b8..4f3b7b5 100644
--- a/README
+++ b/README
@@ -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.
diff --git a/TODO b/TODO
index c56592d..ed3efda 100644
--- a/TODO
+++ b/TODO
@@ -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)
{
diff --git a/src/ring.c b/src/ring.c
index 0dbe911..e42ff14 100644
--- a/src/ring.c
+++ b/src/ring.c
@@ -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)");
diff --git a/src/ring.h b/src/ring.h
index 6466a69..1b1727e 100644
--- a/src/ring.h
+++ b/src/ring.h
@@ -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);