require 'glimmer-dsl-libui'
include Glimmer
window('hello world').show
Почти каждый из читателей Хабра настраивает домашнюю сеть, пишет скрипты для автоматизации умного дома, админит домашний сервер и т. д. Всё это практически «семейные обязанности» разработчика, как вынести мусор для семейного мужчины.
А как насчёт написать для своей семьи мобильное приложение? Это уже новый уровень.
▍ Софт для себя и семьи
Хотя Робин Слоан (Robian Sloan) вовсе не профессиональный программист, а успешный писатель-фантаст, его знаний хватило для написания простенького видеочата под iOS. В первый же день после выхода программы BoopSnoop в январе 2020 года её скачали четыре человека из разных часовых поясов (мама, папа, сестра и он сам). Все они до сих пор остаются счастливыми пользователями.
Программа создана по образцу Taptalk: при запуске приложения на экран выводится видеопоток с камеры и четыре иконки для пользователей, которым вы можете отправить фото или видео (мама, папа, сестра, группа для всех) одним «тапом» (нажатием), отсюда и название.
Интерфейс Taptalk
Как только убираете палец — сообщение отправляется, а программа закрывается, никакого редактирования и лишних действий. Даже история переписки не ведётся, всё максимально просто.
К сожалению, сервис Tapstack закрылся 15 ноября 2019 года (разработчики бесплатного приложения без рекламы никогда не предполагали, что оно станет настолько популярным). Это и стало для Робина причиной создания собственной альтернативы.
Он нашёл опенсорсный фреймворк SwiftyCam, на котором всё и сделал за неделю (плюс маленький инстанс S3 для хранения фото и видео, а также парочка лямбда-функций для обработки новых сообщений). Приложение BoopSnoop выглядит ещё проще, у него даже собственного интерфейса практически нет, видео всегда в полноэкранном режиме, только кнопка записи и цифра в правом верхнем углу (количество непросмотренных сообщений):
BoopSnoop
С помощью инструментов вроде Shoes! или Glimmer DSL (на КДПВ) написать GUI — дело нескольких минут для разработчика, не знакомого с графическим дизайном. Но в данном случае никакого особого GUI даже нет. Это так называемый ситуационный софт (situated software), то есть узкоспециализированное ПО с минимальным количеством пользователей. Оно создаётся для решения конкретной задачи и для конкретной группы людей.
Робин распространяет программу среди родственников как бесплатную бету через Apple TestFlight. Теоретически, можно выложить её в открытый доступ для всех. Но это создаёт кучу проблем. Что делать, если ей начнут пользоваться другие люди, сотни и тысячи людей? Начнут жаловаться на баги, присылать фича-реквесты и требуя устранить какие-то недоработки или архитектурные косяки. Как на них реагировать? Не хочется превращать хобби в работу. Тем более, родственников всё устраивает, а для них это и писалось в первую очередь. За два года Робин добавил в приложение только одну функцию, о которой попросила мама.
К приложению «для людей» выдвигается больше требований, чем к приложению «для себя». Как минимум, там надо внедрять систему аутентификации (логин-пароль), а здесь она необязательна. Надо придумывать интерфейс для управления списком контактов и т. д. Всё становится на порядок сложнее. Так что когда у вас спросят «Классная программка, как её скачать?», ответ будет однозначным: «Никак!»
Иногда разработка «для себя» постепенно перерастает в нечто большее. Так случилось с Михаилом Лапушкиным, который уже девять лет (с 2015 года) пишет нативный текстовый редактор под Mac. Когда-то давно он увидел iA Writer и захотел создать нечто такое же простое и элегантное.
Результатом его работы в погоне за абсолютным минимализмом стал редактор Paper, вполне успешное коммерческое приложение, которое можно установить из Mac App Store. Потом вышла версия под iOS.
Лапушкин выбрал пробную модель монетизации, когда платные функции можно попробовать в течение некоторого времени, а потом предлагалось купить версию Pro (In-App Purchase):
История разработки Paper (в двух частях) весьма увлекательна. Видно, что Михаил очень любит своё дело и погружен в него с головой. Наверное, такому увлечённому программисту помощь коллег может даже помешать.
▍ Соло-разработка
Многие программисты после корпоративной карьеры делятся опытом удачной соло-разработки. Пусть она не всегда приносит больше денег, но обычно делает человека счастливее.
Можно построить успешный бизнес на опенсорсе, то есть на своём личном проекте, который изначально рассматривался как хобби. Андрис Рейнман (Andris Reinman) участвует в опенсорсных проектах около 15 лет, а какое-то время назад он написал клиент EmailEngine Email API (почтовый клиент для приложений, а не для людей) для удобной интеграции электронной почты в любое приложение или сервис, чтобы клиенты не заморачивались настройкой SMTP и IMAP, а всё работало из коробки.
EmailEngine Email API
Программа с открытым исходным кодом сначала была доступна под свободной лицензией AGPL для установки на своём сервере, а лицензия MIT стоила €250 в год. Потом автор полностью перешёл на коммерческую лицензию — и в первый же месяц продал семь лицензий. Затем постепенно увеличивал стоимость до €495, €695, €795, €895 — а количество пользователей не уменьшалось. Текущая стоимость лицензии составляет €6100 в год, так что десятка или двух десятков клиентов вполне хватает эстонцу на жизнь.
Андрис говорит, что раньше был романтичным дураком, когда писал открытый код и даже отказывался от пожертвований со стороны крупных коммерческих пользователей, чтобы сохранить независимость и «не продаваться». С возрастом его взгляды на жизнь несколько изменились, особенно когда чужой стартап Nodemailer на его опенсорсном софте продался за $500 млн.
Оказывается, деньги — не такое уж и зло, как думают молодые энтузиасты. И только переход на коммерческую лицензию (пусть и с открытыми исходниками) обеспечил стабильный доход.
Или вот примерно такая же история. Программистка Элли Хакстейбл (Ellie Huxtable) тоже бросила работу ради своего опенсорсного проекта. Несколько лет она написала утилиту Atuin для синхронизации истории консольных команд, поиска и резервного копирования (поддерживаются Bash, ZSH, Fish и NuShell). История всех команд хранится в единой БД, с сохранением контекста запуска.
Оказалось, что это полезный софт для повышения продуктивности — и тысячи людей разделяют это мнение, судя по количеству звёздочек на Гитхабе.
Действительно, бывает позарез нужно найти команду, запущенную несколько месяцев или лет назад вместо того, чтобы заново её составлять методом проб и ошибок.
Поскольку программа оказалась востребованной, проект постепенно становился больше, чем хобби. Автор улучшала инфраструктуру (за донаты от спонсоров на GitHub), выступала на компьютерных конференциях, принимала PR'ы от десятков контрибьюторов.
Рост количества пользователей
Количество строк истории, которые пользователя загружали на сервер ежедневно
В конце концов она открыла коммерческую фирму вокруг этого продукта, уволилась с прежнего места работы в инфраструктурной компании PostHog — и полностью посвятила себя личному бизнесу. Программа по-прежнему распространяется с открытым исходным кодом, и её можно захостить на своём сервере, но за деньги предлагаются премиальные функции в облаке (плюс коммерческие лицензии для бизнеса).
Открытие своего бизнеса — это ещё и способ избежать профессионального выгорания, которое очень часто настигает мейнтейнеров опенсорса.
▍ Удовольствие, а не работа
В целом, ключевой вопрос, какую роль играет программирование в жизни — это работа или хобби? Ведь если рассматривать его главным образом как источник средств для существования, то становится очень сложно получать удовольствие от процесса. Это сродни тому, что практически невозможно искренне дружить со своим начальником. Или с человеком, который может вас уничтожить по щелчку пальцев. Просто
Поэтому для внутренней гармонии профессиональному разработчику полезно иметь некие хобби-проекты для души. Например, как antirez (Сальваторе Санфилиппо, автор СУБД Redis и, кстати, тоже писатель-фантаст) недавно написал минимальный концепт чат-сервера smallchat только для себя и нескольких друзей. Он постарался сделать минимально возможный чат-сервер на C, как на конкурсах минималистичного кода, в стиле IRC, и смог это сделать всего в 200 строках кода, если не считать пробелы и комментарии:
/* smallchat.c -- Read clients input, send to all the other connected clients.
*
* Copyright (c) 2023, Salvatore Sanfilippo <antirez at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the project name of nor the names of its contributors may be used
* to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/select.h>
#include <unistd.h>
#include "chatlib.h"
/* ============================ Data structures =================================
* The minimal stuff we can afford to have. This example must be simple
* even for people that don't know a lot of C.
* =========================================================================== */
#define MAX_CLIENTS 1000 // This is actually the higher file descriptor.
#define SERVER_PORT 7711
/* This structure represents a connected client. There is very little
* info about it: the socket descriptor and the nick name, if set, otherwise
* the first byte of the nickname is set to 0 if not set.
* The client can set its nickname with /nick <nickname> command. */
struct client {
int fd; // Client socket.
char *nick; // Nickname of the client.
};
/* This global structure encapsulates the global state of the chat. */
struct chatState {
int serversock; // Listening server socket.
int numclients; // Number of connected clients right now.
int maxclient; // The greatest 'clients' slot populated.
struct client *clients[MAX_CLIENTS]; // Clients are set in the corresponding
// slot of their socket descriptor.
};
struct chatState *Chat; // Initialized at startup.
/* ====================== Small chat core implementation ========================
* Here the idea is very simple: we accept new connections, read what clients
* write us and fan-out (that is, send-to-all) the message to everybody
* with the exception of the sender. And that is, of course, the most
* simple chat system ever possible.
* =========================================================================== */
/* Create a new client bound to 'fd'. This is called when a new client
* connects. As a side effect updates the global Chat state. */
struct client *createClient(int fd) {
char nick[32]; // Used to create an initial nick for the user.
int nicklen = snprintf(nick,sizeof(nick),"user:%d",fd);
struct client *c = chatMalloc(sizeof(*c));
socketSetNonBlockNoDelay(fd); // Pretend this will not fail.
c->fd = fd;
c->nick = chatMalloc(nicklen+1);
memcpy(c->nick,nick,nicklen);
assert(Chat->clients[c->fd] == NULL); // This should be available.
Chat->clients[c->fd] = c;
/* We need to update the max client set if needed. */
if (c->fd > Chat->maxclient) Chat->maxclient = c->fd;
Chat->numclients++;
return c;
}
/* Free a client, associated resources, and unbind it from the global
* state in Chat. */
void freeClient(struct client *c) {
free(c->nick);
close(c->fd);
Chat->clients[c->fd] = NULL;
Chat->numclients--;
if (Chat->maxclient == c->fd) {
/* Ooops, this was the max client set. Let's find what is
* the new highest slot used. */
int j;
for (j = Chat->maxclient-1; j >= 0; j--) {
if (Chat->clients[j] != NULL) {
Chat->maxclient = j;
break;
}
}
if (j == -1) Chat->maxclient = -1; // We no longer have clients.
}
free(c);
}
/* Allocate and init the global stuff. */
void initChat(void) {
Chat = chatMalloc(sizeof(*Chat));
memset(Chat,0,sizeof(*Chat));
/* No clients at startup, of course. */
Chat->maxclient = -1;
Chat->numclients = 0;
/* Create our listening socket, bound to the given port. This
* is where our clients will connect. */
Chat->serversock = createTCPServer(SERVER_PORT);
if (Chat->serversock == -1) {
perror("Creating listening socket");
exit(1);
}
}
/* Send the specified string to all connected clients but the one
* having as socket descriptor 'excluded'. If you want to send something
* to every client just set excluded to an impossible socket: -1. */
void sendMsgToAllClientsBut(int excluded, char *s, size_t len) {
for (int j = 0; j <= Chat->maxclient; j++) {
if (Chat->clients[j] == NULL ||
Chat->clients[j]->fd == excluded) continue;
/* Important: we don't do ANY BUFFERING. We just use the kernel
* socket buffers. If the content does not fit, we don't care.
* This is needed in order to keep this program simple. */
write(Chat->clients[j]->fd,s,len);
}
}
/* The main() function implements the main chat logic:
* 1. Accept new clients connections if any.
* 2. Check if any client sent us some new message.
* 3. Send the message to all the other clients. */
int main(void) {
initChat();
while(1) {
fd_set readfds;
struct timeval tv;
int retval;
FD_ZERO(&readfds);
/* When we want to be notified by select() that there is
* activity? If the listening socket has pending clients to accept
* or if any other client wrote anything. */
FD_SET(Chat->serversock, &readfds);
for (int j = 0; j <= Chat->maxclient; j++) {
if (Chat->clients[j]) FD_SET(j, &readfds);
}
/* Set a timeout for select(), see later why this may be useful
* in the future (not now). */
tv.tv_sec = 1; // 1 sec timeout
tv.tv_usec = 0;
/* Select wants as first argument the maximum file descriptor
* in use plus one. It can be either one of our clients or the
* server socket itself. */
int maxfd = Chat->maxclient;
if (maxfd < Chat->serversock) maxfd = Chat->serversock;
retval = select(maxfd+1, &readfds, NULL, NULL, &tv);
if (retval == -1) {
perror("select() error");
exit(1);
} else if (retval) {
/* If the listening socket is "readable", it actually means
* there are new clients connections pending to accept. */
if (FD_ISSET(Chat->serversock, &readfds)) {
int fd = acceptClient(Chat->serversock);
struct client *c = createClient(fd);
/* Send a welcome message. */
char *welcome_msg =
"Welcome to Simple Chat! "
"Use /nick <nick> to set your nick.n";
write(c->fd,welcome_msg,strlen(welcome_msg));
printf("Connected client fd=%dn", fd);
}
/* Here for each connected client, check if there are pending
* data the client sent us. */
char readbuf[256];
for (int j = 0; j <= Chat->maxclient; j++) {
if (Chat->clients[j] == NULL) continue;
if (FD_ISSET(j, &readfds)) {
/* Here we just hope that there is a well formed
* message waiting for us. But it is entirely possible
* that we read just half a message. In a normal program
* that is not designed to be that simple, we should try
* to buffer reads until the end-of-the-line is reached. */
int nread = read(j,readbuf,sizeof(readbuf)-1);
if (nread <= 0) {
/* Error or short read means that the socket
* was closed. */
printf("Disconnected client fd=%d, nick=%sn",
j, Chat->clients[j]->nick);
freeClient(Chat->clients[j]);
} else {
/* The client sent us a message. We need to
* relay this message to all the other clients
* in the chat. */
struct client *c = Chat->clients[j];
readbuf[nread] = 0;
/* If the user message starts with "/", we
* process it as a client command. So far
* only the /nick <newnick> command is implemented. */
if (readbuf[0] == '/') {
/* Remove any trailing newline. */
char *p;
p = strchr(readbuf,'r'); if (p) *p = 0;
p = strchr(readbuf,'n'); if (p) *p = 0;
/* Check for an argument of the command, after
* the space. */
char *arg = strchr(readbuf,' ');
if (arg) {
*arg = 0; /* Terminate command name. */
arg++; /* Argument is 1 byte after the space. */
}
if (!strcmp(readbuf,"/nick") && arg) {
free(c->nick);
int nicklen = strlen(arg);
c->nick = chatMalloc(nicklen+1);
memcpy(c->nick,arg,nicklen+1);
} else {
/* Unsupported command. Send an error. */
char *errmsg = "Unsupported commandn";
write(c->fd,errmsg,strlen(errmsg));
}
} else {
/* Create a message to send everybody (and show
* on the server console) in the form:
* nick> some message. */
char msg[256];
int msglen = snprintf(msg, sizeof(msg),
"%s> %s", c->nick, readbuf);
/* snprintf() return value may be larger than
* sizeof(msg) in case there is no room for the
* whole output. */
if (msglen >= (int)sizeof(msg))
msglen = sizeof(msg)-1;
printf("%s",msg);
/* Send it to all the other clients. */
sendMsgToAllClientsBut(j,msg,msglen);
}
}
}
}
} else {
/* Timeout occurred. We don't do anything right now, but in
* general this section can be used to wakeup periodically
* even if there is no clients activity. */
}
}
return 0;
}
Клиент smallchat такой же минималистичный.
И другие соло-разработчики тоже показывают примеры таких хобби-проектов, которые иногда сделаны чисто для себя, а иногда в демонстрационных целях для всех. Кто-то выкладывает эти проекты в открытый доступ, а кто-то не делает этого. Antirez сразу предупреждает о пулл-реквестах:
«Имейте в виду, что большинство PR на добавление функций будут отклонены. Cмысл этого репозитория в том, чтобы улучшать его шаг за шагом в следующих видеороликах [см. первый и второй эпизоды]. Мы будем делать рефакторинг во время живых сессий (или объяснять в видео необходимость рефакторинга), внедрять больше библиотек для улучшения внутренней работы программы (linenoise, rax и т. д.). Так что если вы хотите улучшить программу в качестве упражнения — вперёд! Отличная идея. Но я не буду добавлять новые фичи, так как смысл программы в постепенном развитии на живых сессиях».
Хобби-проекты напоминают, что программирование — это не только деньги, но ещё и очень интересное занятие. Мы бы с удовольствием занимались им бесплатно, если бы за это не платили.
Написание приложений для себя — это примерно как готовить дома. Вроде как и необязательно, но люди это делают для удовольствия. Иногда хочется выйти за рамки простого потребления и что-то создать своими руками. В целом, писать приложения чисто для себя на порядок проще и приятнее, чем для клиента или работодателя.
А если этот хобби-проект становится коммерческим и приносит какие-то деньги, тем лучше.
Автор: Анатолий Ализар