1
0
mirror of https://github.com/Tha14/toxic.git synced 2024-11-26 15:53:26 +01:00

Started adding support for popup notifications and adjustments to new core

This commit is contained in:
mannol 2014-07-27 01:49:59 +02:00
parent 02b192d6ee
commit 18a6f621f0
12 changed files with 162 additions and 81 deletions

View File

@ -78,7 +78,7 @@ struct _ASettings {
ToxAv *av;
ToxAvCodecSettings cs;
ToxAvCSettings cs;
Call calls[MAX_CALLS];
} ASettins;
@ -107,6 +107,7 @@ ToxAv *init_audio(ToxWindow *self, Tox *tox)
{
ASettins.cs = av_DefaultSettings;
ASettins.cs.max_video_height = ASettins.cs.max_video_width = 0;
ASettins.cs.audio_channels = 1;
ASettins.errors = ae_None;
@ -173,8 +174,11 @@ void read_device_callback (const int16_t* captured, uint32_t size, void* data)
void write_device_callback(ToxAv* av, int32_t call_index, int16_t* data, int size)
{
if (ASettins.calls[call_index].ttas)
write_out(ASettins.calls[call_index].out_idx, data, size, 1);
if (call_index >= 0 && ASettins.calls[call_index].ttas) {
ToxAvCSettings csettings = ASettins.cs;
toxav_get_peer_csettings(av, call_index, 0, &csettings);
write_out(ASettins.calls[call_index].out_idx, data, size, csettings.audio_channels);
}
}
int start_transmission(ToxWindow *self)
@ -182,7 +186,7 @@ int start_transmission(ToxWindow *self)
if ( !ASettins.av || self->call_idx == -1 ) return -1;
/* Don't provide support for video */
if ( 0 != toxav_prepare_transmission(ASettins.av, self->call_idx, &ASettins.cs, 0) ) {
if ( 0 != toxav_prepare_transmission(ASettins.av, self->call_idx, av_jbufdc, av_VADd, 0) ) {
line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Could not prepare transmission");
}
@ -192,8 +196,11 @@ int start_transmission(ToxWindow *self)
set_call(&ASettins.calls[self->call_idx], _True);
if ( open_primary_device(input, &ASettins.calls[self->call_idx].in_idx,
av_DefaultSettings.audio_sample_rate, av_DefaultSettings.audio_frame_duration) != de_None )
ToxAvCSettings csettings;
toxav_get_peer_csettings(ASettins.av, self->call_idx, 0, &csettings);
if ( open_primary_device(input, &ASettins.calls[self->call_idx].in_idx,
csettings.audio_sample_rate, csettings.audio_frame_duration, csettings.audio_channels) != de_None )
line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to open input device!");
if ( register_device_callback(self->call_idx, ASettins.calls[self->call_idx].in_idx,
@ -202,7 +209,7 @@ int start_transmission(ToxWindow *self)
line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to register input handler!");
if ( open_primary_device(output, &ASettins.calls[self->call_idx].out_idx,
av_DefaultSettings.audio_sample_rate, av_DefaultSettings.audio_frame_duration) != de_None ) {
csettings.audio_sample_rate, csettings.audio_frame_duration, csettings.audio_channels) != de_None ) {
line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to open output device!");
ASettins.calls[self->call_idx].has_output = 0;
}
@ -345,7 +352,7 @@ void cmd_call(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MA
goto on_error;
}
ToxAvError error = toxav_call(ASettins.av, &self->call_idx, self->num, TypeAudio, 30);
ToxAvError error = toxav_call(ASettins.av, &self->call_idx, self->num, &ASettins.cs, 30);
if ( error != ErrorNone ) {
if ( error == ErrorAlreadyInCall ) error_str = "Already in a call!";
@ -376,7 +383,7 @@ void cmd_answer(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[
goto on_error;
}
ToxAvError error = toxav_answer(ASettins.av, self->call_idx, TypeAudio);
ToxAvError error = toxav_answer(ASettins.av, self->call_idx, &ASettins.cs);
if ( error != ErrorNone ) {
if ( error == ErrorInvalidState ) error_str = "Cannot answer in invalid state!";
@ -615,19 +622,23 @@ void cmd_ccur_device(WINDOW * window, ToxWindow * self, Tox *m, int argc, char (
if ( self->call_idx > -1) {
Call* this_call = &ASettins.calls[self->call_idx];
if (this_call->ttas) {
ToxAvCSettings csettings;
toxav_get_peer_csettings(ASettins.av, self->call_idx, 0, &csettings);
if (type == output) {
pthread_mutex_lock(&this_call->mutex);
close_device(output, this_call->out_idx);
this_call->has_output = open_device(output, selection, &this_call->out_idx,
av_DefaultSettings.audio_sample_rate, av_DefaultSettings.audio_frame_duration)
csettings.audio_sample_rate, csettings.audio_frame_duration, csettings.audio_channels)
== de_None ? 1 : 0;
pthread_mutex_unlock(&this_call->mutex);
}
else {
/* TODO: check for failure */
close_device(input, this_call->in_idx);
open_device(input, selection, &this_call->in_idx,
av_DefaultSettings.audio_sample_rate, av_DefaultSettings.audio_frame_duration);
open_device(input, selection, &this_call->in_idx, csettings.audio_sample_rate,
csettings.audio_frame_duration, csettings.audio_channels);
/* Set VAD as true for all; TODO: Make it more dynamic */
register_device_callback(self->call_idx, this_call->in_idx, read_device_callback, &self->call_idx, _True);
}

View File

@ -765,7 +765,7 @@ static void chat_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr)
ctx->start = wlen < x2 ? 0 : wlen - x2 + 1;
}
} else {
beep();
notify(self, error, 0);
}
} else if (key == '\n') {

View File

@ -63,6 +63,7 @@ typedef struct _Device {
pthread_mutex_t mutex[1];
uint32_t sample_rate;
uint32_t frame_duration;
int32_t sound_mode;
#ifdef _AUDIO
float VAD_treshold; /* 40 is usually recommended value */
#endif
@ -194,19 +195,21 @@ DeviceError set_primary_device(DeviceType type, int32_t selection)
return de_None;
}
DeviceError open_primary_device(DeviceType type, uint32_t* device_idx, uint32_t sample_rate, uint32_t frame_duration)
DeviceError open_primary_device(DeviceType type, uint32_t* device_idx, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels)
{
return open_device(type, primary_device[type], device_idx, sample_rate, frame_duration);
return open_device(type, primary_device[type], device_idx, sample_rate, frame_duration, channels);
}
// TODO: generate buffers separately
DeviceError open_device(DeviceType type, int32_t selection, uint32_t* device_idx, uint32_t sample_rate, uint32_t frame_duration)
DeviceError open_device(DeviceType type, int32_t selection, uint32_t* device_idx, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels)
{
if (size[type] <= selection || selection < 0) return de_InvalidSelection;
if (channels != 1 && channels != 2) return de_UnsupportedMode;
lock;
const uint32_t frame_size = (sample_rate * frame_duration / 1000);
uint32_t i;
@ -220,6 +223,7 @@ DeviceError open_device(DeviceType type, int32_t selection, uint32_t* device_idx
device->sample_rate = sample_rate;
device->frame_duration = frame_duration;
device->sound_mode = channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16;
for (i = 0; i < *device_idx; i ++) { /* Check if any previous has the same selection */
if ( running[type][i]->selection == selection ) {
@ -238,7 +242,7 @@ DeviceError open_device(DeviceType type, int32_t selection, uint32_t* device_idx
if (type == input) {
device->dhndl = alcCaptureOpenDevice(devices_names[type][selection],
sample_rate, AL_FORMAT_MONO16, frame_size * 2);
sample_rate, device->sound_mode, frame_size * 2);
#ifdef _AUDIO
device->VAD_treshold = user_settings_->VAD_treshold;
#endif
@ -263,7 +267,7 @@ DeviceError open_device(DeviceType type, int32_t selection, uint32_t* device_idx
memset(zeros, 0, frame_size*2);
for ( i =0; i < OPENAL_BUFS; ++i) {
alBufferData(device->buffers[i], AL_FORMAT_MONO16, zeros, frame_size*2, sample_rate);
alBufferData(device->buffers[i], device->sound_mode, zeros, frame_size*2, sample_rate);
}
alSourceQueueBuffers(device->source, OPENAL_BUFS, device->buffers);
@ -372,7 +376,7 @@ inline__ DeviceError write_out(uint32_t device_idx, int16_t* data, uint32_t leng
}
alBufferData(bufid, AL_FORMAT_MONO16, data, lenght * 2 * channels, device->sample_rate);
alBufferData(bufid, device->sound_mode, data, lenght * 2 * channels, device->sample_rate);
alSourceQueueBuffers(device->source, 1, &bufid);
ALint state;
@ -415,7 +419,7 @@ void* thread_poll (void* arg) // TODO: maybe use thread for every input source
}
Device* device = running[input][i];
int16_t frame[4096];
int16_t frame[16000];
alcCaptureSamples(device->dhndl, frame, f_size);
if ( device->muted

View File

@ -50,7 +50,8 @@ typedef enum DeviceError {
de_AllDevicesBusy = -5,
de_DeviceNotActive = -6,
de_BufferError = -7,
de_AlError = -8,
de_UnsupportedMode = -8,
de_AlError = -9,
} DeviceError;
typedef void (*DataHandleCallback) (const int16_t*, uint32_t size, void* data);
@ -76,9 +77,9 @@ DeviceError device_set_VAD_treshold(uint32_t device_idx, float value);
#endif
DeviceError set_primary_device(DeviceType type, int32_t selection);
DeviceError open_primary_device(DeviceType type, uint32_t* device_idx, uint32_t sample_rate, uint32_t frame_duration);
DeviceError open_primary_device(DeviceType type, uint32_t* device_idx, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels);
/* Start device */
DeviceError open_device(DeviceType type, int32_t selection, uint32_t* device_idx, uint32_t sample_rate, uint32_t frame_duration);
DeviceError open_device(DeviceType type, int32_t selection, uint32_t* device_idx, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels);
/* Stop device */
DeviceError close_device(DeviceType type, uint32_t device_idx);

View File

@ -363,10 +363,10 @@ static void groupchat_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr)
ctx->start = wlen < x2 ? 0 : wlen - x2 + 1;
}
} else {
beep();
notify(self, error, 0);
}
} else {
beep();
notify(self, error, 0);
}
} else if (key == T_KEY_C_RB) { /* Scroll peerlist up and down one position */
int L = y2 - CHATBOX_HEIGHT - SDBAR_OFST;

View File

@ -31,6 +31,7 @@
#include "misc_tools.h"
#include "toxic_strings.h"
#include "line_info.h"
#include "notify.h"
/* add a char to input field and buffer */
void input_new_char(ToxWindow *self, wint_t key, int x, int y, int mx_x, int mx_y)
@ -41,12 +42,12 @@ void input_new_char(ToxWindow *self, wint_t key, int x, int y, int mx_x, int mx_
/* this is the only place we need to do this check */
if (cur_len == -1) {
beep();
notify(self, error, 0);
return;
}
if (add_char_to_buf(ctx, key) == -1) {
beep();
notify(self, error, 0);
return;
}
@ -62,7 +63,7 @@ static void input_backspace(ToxWindow *self, int x, int mx_x)
ChatContext *ctx = self->chatwin;
if (del_char_buf_bck(ctx) == -1) {
beep();
notify(self, error, 0);
return;
}
@ -79,21 +80,21 @@ static void input_backspace(ToxWindow *self, int x, int mx_x)
static void input_delete(ToxWindow *self)
{
if (del_char_buf_frnt(self->chatwin) == -1)
beep();
notify(self, error, 0);
}
/* deletes entire line before cursor from input field and buffer */
static void input_discard(ToxWindow *self)
{
if (discard_buf(self->chatwin) == -1)
beep();
notify(self, error, 0);
}
/* deletes entire line after cursor from input field and buffer */
static void input_kill(ChatContext *ctx)
{
if (kill_buf(ctx) == -1)
beep();
notify(NULL, error, NT_ALWAYS);
}
static void input_yank(ToxWindow *self, int x, int mx_x)
@ -101,7 +102,7 @@ static void input_yank(ToxWindow *self, int x, int mx_x)
ChatContext *ctx = self->chatwin;
if (yank_buf(ctx) == -1) {
beep();
notify(self, error, 0);
return;
}

View File

@ -30,6 +30,7 @@
#include "line_info.h"
#include "groupchat.h"
#include "settings.h"
#include "notify.h"
extern struct user_settings *user_settings_;
@ -414,14 +415,14 @@ static void line_info_scroll_up(struct history *hst)
{
if (hst->line_start->prev)
hst->line_start = hst->line_start->prev;
else beep();
else notify(NULL, error, NT_ALWAYS);
}
static void line_info_scroll_down(struct history *hst)
{
if (hst->line_start->next)
hst->line_start = hst->line_start->next;
else beep();
else notify(NULL, error, NT_ALWAYS);
}
static void line_info_page_up(ToxWindow *self, struct history *hst)

View File

@ -28,34 +28,42 @@
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdarg.h>
#include <time.h>
#include <sys/stat.h>
#ifdef __APPLE__
#include <OpenAL/al.h>
#include <OpenAL/alc.h>
#ifdef _SOUND_NOTIFY
#include <OpenAL/alut.h> /* Is this good? */
#ifdef _AUDIO
#ifdef __APPLE__
#include <OpenAL/al.h>
#include <OpenAL/alc.h>
#ifdef _SOUND_NOTIFY
#include <OpenAL/alut.h> /* Is this good? */
#endif
#else
#include <AL/al.h>
#include <AL/alc.h>
#ifdef _SOUND_NOTIFY
#include <AL/alut.h> /* freealut packet */
#endif
#endif
#else
#include <AL/al.h>
#include <AL/alc.h>
#ifdef _SOUND_NOTIFY
#include <AL/alut.h> /* freealut packet */
#endif
#endif
#endif /* _AUDIO */
#ifdef _X11
#include <X11/Xlib.h>
#endif /* _X11 */
#ifdef _BOX_NOTIFY
#include <libnotify/notify.h>
#endif
#define SOUNDS_SIZE 10
#define ACTIVE_SOUNDS_MAX 50
#define ACTIVE_NOTIFS_MAX 50
extern struct user_settings *user_settings_;
struct _Control {
time_t cooldown;
time_t notif_timeout;
unsigned long this_window;
#ifdef _X11
Display *display;
@ -69,14 +77,18 @@ struct _Control {
#endif /* _SOUND_NOTIFY */
} Control = {0};
struct _ActiveNotifications {
#ifdef _SOUND_NOTIFY
struct _ActiveSounds {
uint32_t source;
uint32_t buffer;
_Bool active;
_Bool looping;
} actives[ACTIVE_SOUNDS_MAX] = {{0}};
#endif
#ifdef _BOX_NOTIFY
NotifyNotification* box;
#endif
} actives[ACTIVE_NOTIFS_MAX] = {{0}};
/**********************************************************************************/
/**********************************************************************************/
/**********************************************************************************/
@ -112,20 +124,20 @@ void graceful_clear()
int i;
pthread_mutex_lock(Control.poll_mutex);
while (1) {
for (i = 0; i < ACTIVE_SOUNDS_MAX; i ++) {
for (i = 0; i < ACTIVE_NOTIFS_MAX; i ++) {
if (actives[i].active) {
if ( actives[i].looping ) {
stop_sound(i);
} else {
if (!is_playing(actives[i].source))
memset(&actives[i], 0, sizeof(struct _ActiveSounds));
memset(&actives[i], 0, sizeof(struct _ActiveNotifications));
else break;
}
}
}
if (i == ACTIVE_SOUNDS_MAX) {
if (i == ACTIVE_NOTIFS_MAX) {
pthread_mutex_unlock(Control.poll_mutex);
return;
}
@ -140,15 +152,19 @@ void* do_playing(void* _p)
int i;
while(Control.poll_active) {
pthread_mutex_lock(Control.poll_mutex);
for (i = 0; i < ACTIVE_SOUNDS_MAX; i ++) {
if (actives[i].active && !actives[i].looping) {
for (i = 0; i < ACTIVE_NOTIFS_MAX; i ++) {
if (actives[i].active && !actives[i].looping
#ifdef _BOX_NOTIFY
&& !actives[i].box
#endif
) {
if (!is_playing(actives[i].source)) {
/* Close */
alSourceStop(actives[i].source);
alDeleteSources(1, &actives[i].source);
alDeleteBuffers(1,&actives[i].buffer);
memset(&actives[i], 0, sizeof(struct _ActiveSounds));
memset(&actives[i], 0, sizeof(struct _ActiveNotifications));
}
}
}
@ -163,8 +179,8 @@ int play_source(uint32_t source, uint32_t buffer, _Bool looping)
{
pthread_mutex_lock(Control.poll_mutex);
int i = 0;
for (; i < ACTIVE_SOUNDS_MAX && actives[i].active; i ++);
if ( i == ACTIVE_SOUNDS_MAX ) {
for (; i < ACTIVE_NOTIFS_MAX && actives[i].active; i ++);
if ( i == ACTIVE_NOTIFS_MAX ) {
pthread_mutex_unlock(Control.poll_mutex);
return -1; /* Full */
}
@ -189,11 +205,11 @@ int play_source(uint32_t source, uint32_t buffer, _Bool looping)
/* Opens primary device */
int init_notify(int login_cooldown)
int init_notify(int login_cooldown, int notification_timeout)
{
#ifdef _SOUND_NOTIFY
alutInitWithoutContext(NULL, NULL);
if (open_primary_device(output, &Control.device_idx, 48000, 20) != de_None)
if (open_primary_device(output, &Control.device_idx, 48000, 20, 1) != de_None)
return -1;
pthread_mutex_init(Control.poll_mutex, NULL);
@ -214,6 +230,10 @@ int init_notify(int login_cooldown)
#endif /* _X11 */
#ifdef _BOX_NOTIFY
notify_init("toxic");
#endif
Control.notif_timeout = notification_timeout;
return 1;
}
@ -230,6 +250,11 @@ void terminate_notify()
close_device(output, Control.device_idx);
alutExit();
#endif /* _SOUND_NOTIFY */
#ifdef _BOX_NOTIFY
notify_uninit();
#endif
}
#ifdef _SOUND_NOTIFY
@ -271,7 +296,7 @@ int play_sound_internal(Notification what, _Bool loop)
int play_notify_sound(Notification notif, uint64_t flags)
{
int rc = 0;
int rc = -1;
if (flags & NT_BEEP) beep();
else if (notif != silent) {
@ -287,12 +312,12 @@ int play_notify_sound(Notification notif, uint64_t flags)
void stop_sound(int sound)
{
if (sound >= 0 && sound < ACTIVE_SOUNDS_MAX && actives[sound].looping && actives[sound].active) {
if (sound >= 0 && sound < ACTIVE_NOTIFS_MAX && actives[sound].looping && actives[sound].active ) {
alSourcei(actives[sound].source, AL_LOOPING, false);
alSourceStop(actives[sound].source);
alDeleteSources(1, &actives[sound].source);
alDeleteBuffers(1,&actives[sound].buffer);
memset(&actives[sound], 0, sizeof(struct _ActiveSounds));
memset(&actives[sound], 0, sizeof(struct _ActiveNotifications));
}
}
#endif
@ -330,9 +355,50 @@ int notify(ToxWindow* self, Notification notif, uint64_t flags)
else if (flags & NT_ALWAYS)
rc = m_play_sound(notif, flags);
if (flags & NT_NOTIFWND) {
/* TODO: pop notify window */
}
return rc;
}
int box_notify(ToxWindow* self, Notification notif, uint64_t flags, char* title, char* format, ...)
{
#ifdef _BOX_NOTIFY
int id = notify(self, notif, flags);
if (id == -1) { /* Could not play */
pthread_mutex_lock(Control.poll_mutex);
for (id = 0; id < ACTIVE_NOTIFS_MAX && actives[id].active; id ++);
if ( id == ACTIVE_NOTIFS_MAX ) {
pthread_mutex_unlock(Control.poll_mutex);
return -1; /* Full */
}
pthread_mutex_unlock(Control.poll_mutex);
}
char title_compact [24] = {'\0'};
strncpy(title_compact, title, 24);
if (strlen(title) > 23) strcpy(title_compact + 20, "...");
char msg_compact [128] = {'\0'};
va_list __ARGS__;
va_start (__ARGS__, format);
snprintf (msg_compact, 127, format, __ARGS__);
va_end (__ARGS__);
if (strlen(msg_compact) > 124) strcpy(msg_compact + 124, "...");
actives[id].active = 1;
actives[id].box = notify_notification_new(title_compact, msg_compact, NULL);
notify_notification_set_timeout(actives[id].box, Control.notif_timeout);
notify_notification_show(actives[id].box, NULL);
return id;
#else
return notify(self, notif, flags);
#endif
}
int box_notify_append(int id, char* format, ...)
{
return 0;
}

View File

@ -60,10 +60,12 @@ typedef enum _Flags {
NT_ALWAYS = 1 << 8, /* Force sound to play */
} Flags;
int init_notify(int login_cooldown);
int init_notify(int login_cooldown, int notification_timeout);
void terminate_notify();
int notify(ToxWindow* self, Notification notif, uint64_t flags);
int box_notify(ToxWindow* self, Notification notif, uint64_t flags, char* title, char* format, ...);
int box_notify_append(int id, char* format, ...);
#ifdef _SOUND_NOTIFY
int set_sound(Notification sound, const char* value);

View File

@ -185,10 +185,10 @@ static void prompt_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr)
ctx->start = wlen < x2 ? 0 : wlen - x2 + 1;
}
} else {
beep();
notify(self, error, 0);
}
} else {
beep();
notify(self, error, 0);
}
} else if (key == '\n') {
rm_trailing_spaces_buf(ctx);
@ -309,21 +309,13 @@ static void prompt_onConnectionChange(ToxWindow *self, Tox *m, int32_t friendnum
line_info_add(self, timefrmt, nick, NULL, CONNECTION, 0, GREEN, msg);
write_to_log(msg, nick, ctx->log, true);
#ifdef _SOUND_NOTIFY
notify(self, user_log_in, NT_WNDALERT_2 | NT_NOTIFWND | NT_RESTOL);
#else
notify(self, silent, NT_WNDALERT_2 | NT_NOTIFWND | NT_RESTOL);
#endif /* _SOUND_NOTIFY */
} else {
msg = "has gone offline";
line_info_add(self, timefrmt, nick, NULL, CONNECTION, 0, RED, msg);
write_to_log(msg, nick, ctx->log, true);
#ifdef _SOUND_NOTIFY
notify(self, user_log_out, NT_WNDALERT_2 | NT_NOTIFWND | NT_RESTOL);
#else
notify(self, silent, NT_WNDALERT_2 | NT_NOTIFWND | NT_RESTOL);
#endif /* _SOUND_NOTIFY */
}
}

View File

@ -635,12 +635,14 @@ int main(int argc, char *argv[])
#endif /* _AUDIO */
init_notify(60);
init_notify(60, 3000);
#ifdef _SOUND_NOTIFY
notify(prompt, self_log_in, 0);
// notify(prompt, self_log_in, 0);
#endif /* _SOUND_NOTIFY */
box_notify(prompt, self_log_in, 0, "Top Kike", "Oy VEY! It's just like anotha shoah!!!");
const char *msg;
if (config_err) {

View File

@ -28,6 +28,7 @@
#include "windows.h"
#include "misc_tools.h"
#include "toxic_strings.h"
#include "notify.h"
/* Adds char to line at pos. Return 0 on success, -1 if line buffer is full */
int add_char_to_buf(ChatContext *ctx, wint_t ch)
@ -191,7 +192,7 @@ void fetch_hist_item(ChatContext *ctx, int key_dir)
if (key_dir == KEY_UP) {
if (--ctx->hst_pos < 0) {
ctx->hst_pos = 0;
beep();
notify(NULL, error, NT_ALWAYS);
}
} else {
if (++ctx->hst_pos >= ctx->hst_tot) {