Mi aplicación de Notas con Python3 y GTK

Holas, como habrán notado nuestros lectores los ejemplos mostrados en los posts que se han publicado están hechos sobre un sistema GNU/Linux, de hecho soy un usuario de este sistema operativo desde hace mucho tiempo, además siempre me ha gustado tener mis sistemas relativamente actualizados, por lo que desde hace 5 años mas o menos utilizo openSUSE en su versión Tumbleweed (Rolling Release -> Actualización continua) con el escritorio KDE Plasma 5 y ocasionalmente XFCE 4.14, utilizar un sistema que tiene alguna actualización casi todos los días es muy interesante y tiene muchas ventajas relacionadas a parches de seguridad, funciones nuevas, etc. Pero trae consigo algunos inconvenientes, tales como una rotura del driver de la tarjeta NVIDIA por alguna actualización del kernel Linux o que falle alguna cosa como VLC, en su mayoría han sido problemitas muy simples de arreglar que no toman mas de 5 minutos o simplemente esperar unas horas a que una siguiente actualización lo arregle. El problema viene cuando ese sistema operativo es tu sistema principal, es decir con el trabajas a diario, puesto que en estas condiciones no puedes permitirte tomar tiempo de tu horario laboral para poder arreglar un problema en tu sistema, personalmente suelo arreglar dichos inconvenientes durante la noche o después de terminar mi jornada laboral, ya que la mayoría de esos "pequeños" errores no se han metido con mi flujo de trabajo diario.

Pero en los últimos 2 meses he tenido un error muy dificil de corregir completamente (relacionado a la latencia del gestor de ventanas Kwin) por que se lanza de manera aleatoria y hace que todo el escritorio termine congelado, algo que se soluciona momentáneamente con una ejecución de un comando en consola TTY que no toma mas de 15 segundos, pero se ha vuelto molesto tener que hacerlo de 3 a 6 veces diarias, por lo que decidí volver a las viejas andanzas en instalar el gestor de ventas openbox con todas las cosas que necesito para poder trabajar y me he dado cuenta que he terminado con un sistema que me agrada, se actualiza y es muy muy estable, pero (siempre el gran pero) no tengo una aplicación para notas o como algunos lo conocen sticky notes, las que hay disponibles consumen recursos innecesariamente o no cumplen con las necesidades que tengo como usuario, por lo que decidí desarrollar una aplicación lo mas simple posible y que cumpla con dichas necesidades, en este post explicaré lo que hice y como funciona

La necesidad

No suelo, o para ser más exactos no solía utilizar aplicaciones de notas puesto que siempre me parecieron excesivamente llamativas y con un potencial muy alto de hacerme perder la concentración debido a los colores chillones con los que se suelen presentar y su estilo caótico en el cual se lanzan sobre el escritorio, en mi teléfono si suelo tener algunas notas muchas veces de series que quiero ver o de cosas que leí de manera frugal pero me interesaron y las quiero leer mas a profundidad, pero no siempre estoy con mi teléfono en la mano por lo que algunas de las notas las pasé a mi laptop a mano por que la aplicación de mi teléfono (OmniNotes) no tiene sincronización y encontré en GNU/Linux una aplicación de XFCE llamada Xfce4-Notes que me pareció muy buena por que puedo quitarle los colores chillones, puede ser sticky y aparecer en cualquier workspace u escritorio virtual en el que me encuentre trabajando y lo que más me llamó la atención es que puede crear tabs para manterner las notas con un cierto orden, la utilicé durante un tiempo pero en algunas ocasiones mi laptop empezaba a comsumir muchos recursos al poner dicha aplicación al inicio así que en mi escritorio KDE empecé a utilizar una aplicación similar hasta que me cansé de los errores y congelamientos aleatorios del escritorio y se dió el cambio a openbox.

Por lo tanto la necesidad que tengo para una aplicación de notas son:

  • que pueda ser sticky
  • que no tenga colores chillones (mientras mas se asemeje al tema de escritorio mejor)
  • que solo sea una aplicación y no un montón de notas por todas partes
  • que tenga pestañas para tener las notas por categoría
  • que guarde las notas en texto plano por si me las quiero llevar a otro computador

Dicho esto no encontré ninguna aplicación que cumpliera con esas 5 premisas, así que me dispuse a crear una aplicación que cumpliera ese propósito.

Interfaz gráfica y lenguaje

Como actualmente me encuentro utilizando el ya mencionado openbox casi todas mis aplicaciones a excepción de VLC, Clementine y un par mas, son aplicaciones escritas con la librería de graficos GTK, así que siguiendo esa línea mi aplicación de notas utiliza la misma librería, para el lenguaje me he decidido por Python3 puesto que es mas simple que C/C++ y me permite crear algo pequeño como si de un script se tratara.

Código

Si llegaron hasta aquí les agradezco haber leído un pedazo de historía (y desahogo personal) antes de comenzar con lo que a muchos nos llama la intención, el código, sin más preambulos les dejo el pequeño código de mi aplicación de notas.


#!/usr/bin/env python3
"""
Description: Simple GTK3 notes App
Author: Daniel Córdova A.
Github : @danesc87
Released under GPLv3
"""

import os
import sys
import gi

# Necesita GTK version 3
gi.require_version('Gtk', '3.0')

from gi.repository import Gtk
from gi.repository.Gdk import Screen
# Tag para el titulo que va en texto plano y representa un Tab en la aplicacion
TITLE_TAG = '###'

# Ventana principal que hereda de Gtk.Window
class NoteBookWindow(Gtk.Window):
    def __init__(self):
        # Iniciamos la ventana
        Gtk.Window.__init__(self, title='NoteBook')
        # Añadimos propiedades a la venta
        self.set_window_properties()
        # Creamos una malla donde van a ir las notas y botones
        notebook_grid = Gtk.Grid()
        # Añadimos la malla a la venta principal
        self.add(notebook_grid)
        # Creamos un tipo Notebook que nos permite trabajar con pestañas
        self.notebook = Gtk.Notebook(vexpand=True, hexpand=True)
        # Ponemos las pestañas en la parte inferior
        self.notebook.set_tab_pos(3)
        # Creamos los botones
        buttons_box = self.create_buttons_box_and_return()
        # Añadimos el notebook a la malla
        notebook_grid.attach(self.notebook, 0, 0, 1, 1)
        # Añadimos los botones a la malla
        notebook_grid.attach(buttons_box, 0, 1, 1, 1)

    def set_window_properties(self):
        self.set_border_width(5)
        self.set_default_size(300, 500)
        # Evita que la aplicación aparezca en el
        # panel de linux que estemos usando
        self.set_skip_taskbar_hint(True)
        # Se mantiene siempre sobre las demas ventanas
        self.set_keep_above(True)
        # No tiene bordes de ventana
        self.set_decorated(False)
        # Obtenemos la pantalla
        screen = Screen.get_default()
        # Con la pantalla obtenemos la resolución para
        # poner la aplicación en la parte inferior derecha
        self.move(screen.get_width(), screen.get_height())

    def create_buttons_box_and_return(self):
        # Creamos una caja para los botones
        button_box = Gtk.Box()
        notebook_page_add_button = Gtk.Button.new_with_label('+')
        notebook_page_remove_button = Gtk.Button.new_with_label('-')
        close_button = Gtk.Button.new_with_label('Save')
        # Añadimos a los botones comportamientos asociados al evento "click"
        notebook_page_add_button.connect('clicked', self.add_notebook_page)
        notebook_page_remove_button.connect('clicked', self.remove_notebook_page)
        close_button.connect('clicked', self.on_quit)
        # Añadimos los botones a la caja de botones
        button_box.pack_start(notebook_page_add_button, True, True, 0)
        button_box.pack_start(notebook_page_remove_button, True, True, 0)
        button_box.pack_start(close_button, True, True, 0)
        return button_box

    # Crea una pagina de notebook para cada dato
    # en el mapa de datos obtenido del archivo
    def create_notebooks(self, data):
        for title, value in data.items():
            self.notebook.append_page(NoteBookPage(title, value), Gtk.Label(label=title))

    # Obtiene la data y titulo de todas las
    # paginas del notebook para su posterior guardado
    def get_notebook_data(self):
        notebook_data = ''
        for page_number in range(self.notebook.get_n_pages()):
            notebook_data += self.notebook.get_nth_page(page_number).get_notebook_page_data_and_title()
        return notebook_data

    # Permite añadir una pestaña mas a nuestras notas
    def add_notebook_page(self, _):
        new_tab_window = NewTabWindow(self)
        response = new_tab_window.run()
        # La pestaña no tiene nombre al inicio por que
        # espera que el nombre venga en un evento
        tab_name = ''

        if response == Gtk.ResponseType.OK:
            tab_name = new_tab_window.get_tab_name()
        new_tab_window.destroy()

        if tab_name is not None or tab_name != '':
            self.notebook.append_page(NoteBookPage(tab_name, ''), Gtk.Label(label=tab_name))
            self.notebook.get_nth_page(self.notebook.get_n_pages()-1).show_all()

    # Permite borrar una pestaña con sus notas
    def remove_notebook_page(self, _):
        self.notebook.remove_page(self.notebook.get_current_page())

    # Permite cerra la aplicacion cuando se solicita
    def on_quit(self, _):
        self.destroy()

# Crea un pequeño dialogo que pide el nombre para nuestra nueva pestaña
class NewTabWindow(Gtk.Dialog):
    def __init__(self, parent):
        Gtk.Dialog.__init__(self, title='Tab Name', transient_for=parent, flags=0)
        self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
        box = self.get_content_area()
        self.entry = Gtk.Entry()
        box.add(self.entry)
        self.show_all()

    def get_tab_name(self):
        return self.entry.get_text()

# Página del NoteBook, en concreto es una caja donde va a ir la data
class NoteBookPage(Gtk.Box):
    def __init__(self, name, data):
        Gtk.Box.__init__(self, name=name, vexpand=True, hexpand=True)
        self.set_border_width(5)
        self.tex_view = NoteBookPageData(data)
        self.add(self.tex_view)

    # Retorna la data del bufer en el formato deseado para el texto plano
    def get_notebook_page_data_and_title(self):
        return TITLE_TAG + ' ' + self.get_name() + '\n' + self.tex_view.get_text_view_data() + '\n'

# TextView y Bufer donde la data va a ser mostrada y/o editada
class NoteBookPageData(Gtk.ScrolledWindow):
    def __init__(self, data):
        self.text_buffer = None
        self.text_view = None
        Gtk.ScrolledWindow.__init__(self, vexpand=True, hexpand=True)
        self.set_border_width(5)
        self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.load_text_buffer(data)
        self.load_text_view()
        self.add(self.text_view)

    # Obtenemos el bufer con la data en su interior
    def load_text_buffer(self, data):
        self.text_buffer = Gtk.TextBuffer()
        end_iter = self.text_buffer.get_end_iter()
        self.text_buffer.insert(end_iter, data)

    # Cargamos el texview que permite ver/editar con los datos del bufer
    def load_text_view(self):
        self.text_view = Gtk.TextView(buffer=self.text_buffer, vexpand=True, hexpand=True)
        # Permite hacer "Wrapping" para que no exista scroll horizontal
        self.text_view.set_wrap_mode(Gtk.WrapMode.WORD)

    # Permite obtener la data que se encuentra en el bufer de una página
    def get_text_view_data(self):
        start_iter = self.text_buffer.get_start_iter()
        end_iter = self.text_buffer.get_end_iter()
        text_view_data = self.text_buffer.get_text(start_iter, end_iter, True)

        if len(text_view_data) > 1 and text_view_data[-1] != '\n':
            text_view_data += '\n'
        return text_view_data

# Clase que permite manejar el archivo de texto plano
class FileHandler(object):

    def __init__(self, file_path):
        self.data = {}
        self.file_path = file_path

    def read_data(self):
        title = ''
        paged_data = ''
        # Trata de obtener el archivo con permisos lectura/escritura
        try:
            file = open(self.file_path, 'r+')
        except FileNotFoundError:
            # Si no existe crea uno nuevo
            file = open(self.file_path, 'w+')

        # Lee el archivo de texto plano y pone los datos en un mapa
        # cuyo titulo tiene el tag mencionado como constante
        with file as opened_file:
            for line in opened_file:
                data = line.strip().split('\n', 1)[0]
                if line == '\n':
                    continue
                if TITLE_TAG in line:
                    paged_data = ''
                    title = data.split(TITLE_TAG, 1)[1].strip()
                else:
                    paged_data += data.strip() + '\n'
                self.data[title] = paged_data
        file.close()

    def get_file_data(self):
        return self.data

    # Permite escribir datos en el archivo
    def write_data(self, data):
        file = open(self.file_path, 'w')
        file.write(data)
        file.close()

# Clase que maneja toda la aplicación
class NoteBook(object):
    def __init__(self, file_path):
        # Crea una instancia del manejador de archivos 
        # con el "path" del archivo de texto plano
        self.file_handler = FileHandler(file_path)
        # Lee los datos del archivo
        self.file_handler.read_data()
        # Crea una instancia de la ventana
        self.win = NoteBookWindow()
        # Crea las paginas del NoteBook con los datos
        self.win.create_notebooks(self.file_handler.get_file_data())
        # Añade un comportamiento al evento "destroy"
        self.win.connect('destroy', self.on_quit)
        # Muestra la aplicacion
        self.win.show_all()
        Gtk.main()

    # En el evento destroy "llamado por el boton save"
    # se obtiene los datos de los buferes de todas
    # las paginas y los guarda en el archivo
    def on_quit(self, _):
        self.file_handler.write_data(self.win.get_notebook_data())
        Gtk.main_quit()

if __name__ == '__main__':
    # Se espera que el único argumento que se
    # envía a la aplicación sea el path del archivo de notas
    if len(sys.argv) > 1:
        app = NoteBook(os.path.abspath(sys.argv[1]))
    else:
        print("This application takes full file path of notes")

Ejecución y uso

Para ejecutar la aplicación de notas se lo puede hacer de la siguiente manera

python3 notes.py /home/{usuario}/{path-archivo-notas}

La aplicación se ejecutará y nos mostrará las notas de la siguiente manera

notes app

notes with tab name

Añadiendo nuestra apalicación al systray

Bueno no tiene mucho sentido tener que ejecutar todo el tiempo desde la terminal nuestra aplicación de notas, así que agregué un botón con el icono de notas a mi panel y le asigné un script en bash que ejecuta/cierra la aplicación de notas al hacer click, cuyo contenido es el siguiente:


#!/bin/sh

# Author : Daniel Córdova A.
# Github : @danesc87
# Released under GPLv3

# Path donde se ejecuta este script, mismo path del archivo python
SCRIPT_PATH=${0%/*}
# El path de mis notas
NOTES_FILE_PATH="$HOME/.local/share/notes"
# Nombre de la aplicacion
NOTES_APP_NAME='notes.py'
# Buscamos si el proceso ya existe
NOTES_RUNNING=$(ps aux | grep -i "$NOTES_APP_NAME" | grep -E 'python3')

if [[ -z "$NOTES_RUNNING" ]]; then
    # Si no existe el proceso ejecutamos la aplicación
    exec python3 "$SCRIPT_PATH/$NOTES_APP_NAME" "$NOTES_FILE_PATH" > /dev/null &
else
    # Si ya existe cerramos la aplicación
    kill -9 $(echo "$NOTES_RUNNING" | awk '{print $2}')
fi

Por lo tanto se verá de la siguiente manera:

systray notes

Ejemplo del archivo de notas

Se preguntarán que tipo de archivo creará el programa en python que tiene algo llamado TITLE_TAG='###', pues aquí les dejo un ejemplo

### Miscelanea
Pruebas de notas

### Revisar
Systray en python

### Trabajo
Cosas de trabajo

Aún se ecuenta en fase 'alpha' aunque funcional, probablemente le añada mas cosas como la capacidad de configurarlo desde un archivo, hacer algunas mejoras, etc. Pero con el resultado que he obtenido estoy muy contento.

PS: La parte sticky aún no está desarrollada, por lo cual está puesta una regla en mi gestor de ventanas, para que cuando se abra la aplicación se vea en todas las areas de trabajo

El contenido de esta aplicación está en mis dotfiles:
https://github.com/danesc87/dotfiles/

Dentro de la carpeta .config/custom_scripts

Sin más que decirles, les mando un saludo

Happy Hacking!!

You may also like...