Pular para o conteúdo principal

Thiago Lages de Alencar

The Elm Architecture

(MVU, Model-View-Update)

A arquitetura se resume a 3 partes:

  • Model: Conjuntos de dados sobre o estado da aplicação
  • View: Converte os dados do model para algo visual
  • Update: Reage a atualizações para atualizar os dados do model

Seja qual for a sua aplicação (CLI, TUI, GUI, ...), ela é responsável por receber o que o usuário deve visualizar e por notificar atualizações vindas do usuário:

MVU

Bubbletea

É responsável por rodar em loop a arquitetura Elm. Todo o código para isto se resume a:

package main

import tea "github.com/charmbracelet/bubbletea"

func main() {
program := tea.NewProgram(
MyStruct{} // Here you pass your model.
)

_, err := program.Run()

if err != nil {
print(err.Error())
}
}

Entenda que Model é terminologia para representar uma coleção de dados e é por isto que Bubbletea utiliza structs para representar models.

Models Structure

Models dentro do Bubbletea devem seguir a seguinte estrutura:

type MyStruct struct {
// Fields.
}

func (m MyStruct) Init() tea.Cmd {
// Run on initialization.
}

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Process a message.
}

func (m MyStruct) View() string {
// Write to screen.
}

Podemos ver que é dividido em 4 partes:

  • Uma struct responsável por representar o Model da arquitetura Elm
  • Um método Init() único da estrutura bubbletea
  • Um método Update() responsável por representar o Update da arquitetura Elm
  • Um método View() responsável por representar o View da arquitetura Elm
info

Em muitos tutoriais, pessoas também incluem uma função responsável por criar o model e configura-lo com os padrões desejados:

func newMyStruct() MyStruct {
return MyStruct{
// Set default fields values here
}
}

func main() {
program := tea.NewProgram(newMyStruct()) // Use here
}

Keys

Teclas pressionadas no teclado vão para o mesmo lugar que todas interações (Update()), lá resta a você reconhecer como sendo um pressionamento de tecla.

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
}

return m, nil
}

No exemplo podemos ver que possuimos um switch para nos ajudar a identificar o tipo da mensagem, isso é necessário pois a mensagem pode ter qualquer tipo (não necessariamente uma mensagem de pressionamento de tecla).

Mensagens de teclas dentro do Bubbletea podem ser convertido para strings, o que nos da uma string de fácil entedimento porém nem sempre uma ação está ligada a apenas um atalho, então pode ser útil sabermos como ligar diversos atalhos a uma ação:

var quit = key.NewBinding(
key.WithKeys("ctrl+c", "ctrl+q", "q")
)

// ...

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, quit):
return m, tea.Quit
}
}

return m, nil
}
tip

Em vez de criar uma variável para cada atalho, você pode agrupar todas em uma que simbolize todo o mapeamento dos atalhos:

var keyMap struct {
Up key.Binding
Down key.Binding
Quit key.Binding
}

var keys = keyMap{
Up: key.NewBinding(
key.WithKeys("up", "w"),
),
Down: key.NewBinding(
key.WithKeys("down", "s"),
),
Quit: key.NewBinding(
key.WithKeys("ctrl+c", "ctrl+q", "q"),
),
}

// ...

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, keys.Quit):
return m, tea.Quit
case key.Matches(msg, keys.Down):
// Do something
case key.Matches(msg, keys.Up):
// Do something
}

return m, nil
}

Bubbles

É uma coleção de models com parte da lógica já preparada.

Olhando a assinatura das funções e structs do spinner, podemos ver que ele implementa:

  • Model: type Model struct
  • Update: func (m Model) Update(msg tea.Msg) (Model, tea.Cmd)
  • View: func (m Model) View() string

Ele implementa quase todas as partes necessárias para ser executado diretamente no Bubbletea, a única parte que está faltando é Init(). Por que isso?

Acontece que spinner não tem intenção de ser um modelo perfeito para ser utilizado diretamente pelo Bubbletea, pois não faz sentido criar um programa Bubbletea que apenas exibe um spinner.

Porém podemos criar nosso próprio model que utiliza este spinner:

type MyStruct struct {
spinner spinner.Model
}

func newSpinner() MyStruct {
return MyStruct{
spinner: spinner.New(),
}
}

func (m MyStruct) Init() tea.Cmd {
return m.spinner.Tick // spinner requires this to start
}

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd

switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
default:
m.spinner, cmd = m.spinner.Update(msg)
}

return m, cmd
}

func (m MyStruct) View() string {
return m.spinner.View()
}

func main() {
program := tea.NewProgram(
MyStruct{
spinner: spinner.New(),
}
)

_, err := program.Run()

if err != nil {
print(err.Error())
}
}

Note como basicamente reutilizamos tudo do spinner para fazer nosso model:

  • Nossa struct é o spinner sozinho
  • Nossa View() apenas chama a View() do spinner
  • Nosso Update() chama Update() do spinner e detecta se o atalho para sair foi pressionado

Como podemos ver os componentes do Bubbles foram feitos para serem utilizados pelos seus models (e não pelo Bubbletea diretamente). Imagine o susto de alguém executando o spinner diretamente e ele nunca fechando pois ninguém processa ctrl+c para sair.

References

Thiago Lages de Alencar

1 - Performance

Mude o modo de renderização para Compatibility, não é como se você fosse precisar de qualquer renderização além do básico para desenhar uma janela na tela.

Compatibility

Ative Application > Run > Low Processor Mode para que tenha um delay entre as renderizações da janela e apenas renderizar se alguma mudança for detectada. Em jogos a tela muda constantemente, então esse tipo de delay e validação só atrapalham mas como estamos falando de GUI que altera bem menos, isto ajuda muito.

Low Processor Mode

2 - Window Title Bar

Por padrão o nosso sistema operacional nos providência o gráfico básico de uma janela e nos deixa responsável por desenhar o conteúdo dentro dela.

Not borderless

O lado positivo é que isto nos providência o básico de uma janela, como aqueles 3 botões no topo da direita (minimizar, máximizar, fechar, ...).

Porém note que, dependendo do sistema operacional, mais opções podem estar disponíveis! Se eu clicar com o botão direito na title bar do topo, podemos ver mais ações:

Not borderless again

Se ativarmos Display > Window > Size > Borderless, o sistema operacional deixará de adicionar a title bar no topo:

Borderless

Basicamente ele está assumindo que você mesmo irá desenhar a title bar no topo caso queira (normalmente em jogos isto não faz sentido).

info

Borderless ou não, ainda se trata de uma janela no seu sistema operacional então alguns atalhos podem continuar funcionando (Super + Up/Down/Left/Right, Alt + Space).

3 - Multiple Windows

Janelas abertas são tratadas como processos filhos, ou seja, o encerramento de uma janela pai irá encerrar os filhos.

Caso queiramos ter múltiplas janelas idênticas, igual a editores de textos e navegadores, precisamos ter certeza que a janela principal (processo inicial) não possa ser encerrado da maneira padrão (clickando no botão de fechar).

Podemos resolver isto escondendo a janela principal e apenas exibindo as subwindows.

Ative Display > Window > Size > Transparent para que o fundo cinza padrão não seja renderizado durante a execução.

Transparent background

note

Acredito que a cor padrão do canvas é preto, por isto deixar de pintar vai deixar a janela preta

Ative Display > Window > Per Pixel Transparency > Allowed para que o fundo realmente seja transparente (caso contrário vai ficar o canvas preto).

Per Pixel Transparency

warning

Existe uma configuração que eu ainda não entendi a necessidade: Rendering > Viewport > Transparent Background.

Mas a documentação menciona ela como necessária.

Como visto na sessão anterior...

Ative Display > Window > Size > Borderless para o sistema operacional deixará de adicionar a title bar no topo.

Invisible

Embora ela esteja transparente, ela ainda é uma janela como as outras. Podemos conferir que ela ainda aparece quando apertando Alt+Tab (ou apenas apertando Alt no Ubuntu).

Window invisible but exist

Agora nós temos que tratar inputs!

Primeiro podemos notar que se está janela for posta na frente de outra, ela não irá deixar de consumir os seus clicks (mesmo que você queira selecionar algo na janela de trás).

Para resolver isto podemos alterar janela raiz (criada quando seu programa inicia) para repassar adiante clicks do mouse.

func _ready() -> void:
get_window().mouse_passthrough = true

Segundo podemos notar que ela ainda está processando teclas (pode ser selecionada pelo Alt + Tab, fechada por Alt + F4, maximizada com Super + Up, etc).

Ative Display > Window > Size > No Focus para que ela não possa ser focada (até por atalhos).

Process invisible

Lembre que fechar o processo pai fecha todos os filhos, porém fechar todos os filhos não fecha o pai.

Isto quer dizer que o processo pai continua rodando mesmo se o usuário fechar todas as janelas filhas. Agora o usuário apenas conseguiria encerrar o programa pelo "gerenciador de tarefas" ou terminal.

Para tratar isto podemos ligar um signal a um método responsável por notar quando a quantidade de filhos mudar e encerrar o programa se necessário.

Signal child_order_changed

func _on_child_order_changed() -> void:
if get_child_count() == 0:
get_tree().quit()
note

Isto é apenas uma maneira de tratar!

Nós poderiamos checar a cada frame se todas as janelas foram fechadas, poderiamos fazer os filhos avisarem o pai quando fossem encerrados, etc.

Agora precisamos entender que cada janela aberta é uma subwindow. Existem dois tipos de subwindows:

  1. Subwindows
    • Quando sua janela pede ao sistema operacional para criar uma janela filha dela
    • Sua janela filha vai possuir a title bar padrão de janelas
  2. Embed subwindows
    • Quando sua janela simula outra janela dentro dela mesmo
    • Isto impossibilita ela de ser mover para fora da janela pai

Se estamos tratando de uma aplicação que possui múltiplas janelas, precisamos que ela se mova para fora da janela pai. Caso contrário isso ocorreria:

Subwindow + Half subwindow

A janela 2 está saindo do limite da janela pai.

Poderiamos inicializar a janela pai maximizada para evitar isto porém outros problemas iriam aparecer, por exemplo: Janela pai ignorar clicks e teclas, tornando impossível interagir com as janelas simuladas nele.

Desative Display > Window > Subwindows > Embed Subwindows para que as subwindows sejam tratadas como janelas reais pelo sistema operacional (em vez de simuladas pela janela pai).

Subwindows

Mas se quisermos ter uma title bar de janela única para as nossas janelas? Podemos fazer o mesmo que fizemos com janela principal, torna-la borderless.

subwindows borderless

Dentro das propriedades da Janela, ative Flags > Borderless.

subwindows borderless activate

Agora nós seriamos responsáveis por criar a title bar no topo da janela. Desta maneira poderiamos fazer uma title bar única igual ao Google Chrome ou Steam!

4 - Custom Title Bar

Ter uma title bar própria é relativamente raro hoje em dia, pois muitas vezes requer reinventar a roda sem trazer benifícios reais.

Mas isto não quer dizer que nenhuma aplicação faz isto:
Custom Title Bars
(Steam, GNOME Files, Google Chrome)

Note que as 3 aplicações aproveitaram o espaço para providênciar mais informações e funcionalidades ao usuário. Porém nós vamos focar em pelo menos reproduzir o básico:

  1. Exibir titulo
  2. Providênciar botões de minimizar, maximizar e fechar
  3. Double click maximizar
  4. Arrastar a title bar deve mover a janela
  5. Redimensionar janela se arrastar as bordas

Depois disso você deve ser capaz de adicionar ou remover mais utilidades conforme a sua vontade.

warning

Estarei partindo do princípio que queremos customizar uma title bar na janela principal, por isto o código utiliza get_window(), mas adaptações podem ser necessárias caso esteja tratando subwindows.

Exibir Titulo

Basta utilizar o node Label.

Minimize, Maximize, Close Buttons

Basta utilizar 3 nodes Button tratando o signal pressed:

func _on_minimize_pressed() -> void:
get_window().mode = Window.MODE_MINIMIZED


func _on_maximize_pressed() -> void:
if get_window().mode == Window.MODE_MAXIMIZED:
get_window().mode = Window.MODE_WINDOWED
else:
get_window().mode = Window.MODE_MAXIMIZED


func _on_close_pressed() -> void:
get_tree().quit()
subwindows tip

É importante tratar o signal close_requested vindo da janela, pois é por ele que você recebe notificações que o usuário tentou fechar de outras maneiras (taskbar do windows, etc).

Double Click Maximize

Container não possui signal para isto diretamente porém podemos utilizar o signal mais geral gui_input.

func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_title_bar_mouse_button(event)


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
_on_title_bar_double_click()


func _on_title_bar_double_click() -> void:
match get_window().mode:
Window.MODE_MAXIMIZED:
get_window().mode = Window.MODE_WINDOWED
_:
get_window().mode = Window.MODE_MAXIMIZED

Já estamos dividindo em funções menores pois os passos seguintes irão adicionar mais funcionalidades nestas funções gerais.

Drag Window

A princípio, arrastar a janela pode ser resumido em saber duas coisas:

  • Saber se o click do mouse está sendo pressionado
  • Onde que o click estava quando começou
var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_title_bar_mouse_button(event)
elif event is InputEventMouseMotion:
_on_title_bar_mouse_motion(event)


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
_on_title_bar_double_click()
elif event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_title_bar_dragging = true
_title_bar_dragging_start = get_global_mouse_position()
elif event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
_title_bar_dragging = false


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(_event: InputEventMouseMotion) -> void:
if _title_bar_dragging:
_on_title_bar_dragged()


func _on_title_bar_dragged() -> void:
match get_window().mode:
Window.MODE_WINDOWED:
get_window().position += get_global_mouse_position() as Vector2i - _title_bar_dragging_start
subwindows tip

Primeiro: Talvez seja bom mover para o centro da tela a janela pois a posição poder não estar correta durante a inicialização (bug?):

func _ready() -> void:
get_window().move_to_center()

Segundo: Talvez seja necessário utilizar get_local_mouse_position() em vez de get_global_mouse_position() pois deve ser necessário o canvas da própria subwindow.

Esse foi apenas o essencial sobre arrastar, agora podemos pensar em implementar detalhes sobre a ação de arrastar janelas.

Por exemplo: Quando o usuário tentar arrastar uma janela máximizada, ela automaticamente sai do máximizado e se posiciona para que o mouse esteja proporcionalmente na posição correta.

var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i

var _title_bar_dragging_adjustment: float = 0


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
...


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
...


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(event: InputEventMouseMotion) -> void:
...


func _on_title_bar_dragged() -> void:
match get_window().mode:
Window.MODE_WINDOWED:
get_window().position += get_global_mouse_position() as Vector2i - _title_bar_dragging_start
Window.MODE_MAXIMIZED:
_title_bar_dragging_adjustment = get_global_mouse_position().x / get_window().size.x
get_window().mode = Window.MODE_WINDOWED


func _on_resized() -> void:
if _title_bar_dragging_adjustment != 0:
get_window().position += (get_global_mouse_position() as Vector2i)
get_window().position.x -= get_window().size.x * _title_bar_dragging_adjustment
_title_bar_dragging_start = get_global_mouse_position()
_title_bar_dragging_adjustment = 0

Resize Window

Redimensionar pode ser facilmente implementado se utilizarmos o node MarginContainer que nos permite adicionar bordas às laterais, estas serão nossas bordas que devem reagir ao mouse.

Nodes do tipo Control possuem lógica para lidar com inputs do mouse, eles podem consumir ou passar ao node de cima as input do mouse.

Isso quer dizer que qualquer input do mouse na nossa janela (que não tiver sido consumida) chegará ao nossoMarginContainer. Isto não é o que queremos, para nós só é interessante que chegue inputs interagindo com a borda do nosso container.

Podemos resolver isto parando o consumo de inputs no container logo abaixo do MarginContainer:

Margin

Agora temos certeza que interações vindo do signal gui_input são interações diretas com o MarginContainer.

enum Margin {
NONE,
TOP,
RIGHT,
BOTTOM,
LEFT,
TOP_RIGHT,
TOP_LEFT,
BOTTOM_RIGHT,
BOTTOM_LEFT,
}

var _margin_dragging: bool = false

var _margin_dragging_edge_start: Vector2i

var _margin_selected: Margin

var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i

var _title_bar_dragging_adjustment: float = 0

func _get_current_margin() -> Margin:
var margin: Margin = Margin.NONE

if get_global_mouse_position().x < get_theme_constant("margin_left"):
margin = Margin.LEFT
elif get_global_mouse_position().x > size.x - get_theme_constant("margin_right"):
margin = Margin.RIGHT

if get_global_mouse_position().y < get_theme_constant("margin_top"):
match margin:
Margin.LEFT:
return Margin.TOP_LEFT
Margin.NONE:
return Margin.TOP
Margin.RIGHT:
return Margin.TOP_RIGHT
elif get_global_mouse_position().y > size.y - get_theme_constant("margin_bottom"):
match margin:
Margin.LEFT:
return Margin.BOTTOM_LEFT
Margin.NONE:
return Margin.BOTTOM
Margin.RIGHT:
return Margin.BOTTOM_RIGHT

return margin


func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_mouse_button(event)
elif event is InputEventMouseMotion:
_on_mouse_motion(event)


func _on_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_margin_dragging = true
_margin_selected = _get_current_margin()
_margin_dragging_edge_start = get_window().position + get_window().size
elif event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
_margin_dragging = false


func _on_mouse_motion(_event: InputEventMouseMotion) -> void:
if _margin_dragging:
_on_dragged()
else:
_on_hover()


func _on_dragged() -> void:
if get_window().mode != Window.MODE_WINDOWED:
return

var mouse_position: Vector2i = get_global_mouse_position()

match _margin_selected:
Margin.TOP:
get_window().position.y += mouse_position.y # TODO: Fixing moving window
get_window().size.y = _margin_dragging_edge_start.y - get_window().position.y
Margin.RIGHT:
get_window().size.x = mouse_position.x
Margin.BOTTOM:
get_window().size.y = mouse_position.y
Margin.LEFT:
get_window().position.x += mouse_position.x # TODO: Fixing moving window
get_window().size.x = _margin_dragging_edge_start.x - get_window().position.x
Margin.TOP_RIGHT:
get_window().position.y += mouse_position.y # Top
get_window().size = Vector2i(
mouse_position.x, # Right
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.TOP_LEFT:
get_window().position = Vector2i(
get_window().position.x + mouse_position.x, # Left,
get_window().position.y + mouse_position.y, # Top
)

get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.BOTTOM_RIGHT:
get_window().size = Vector2i(
mouse_position.x, # Right
mouse_position.y, # Bottom
)
Margin.BOTTOM_LEFT:
get_window().position.x += mouse_position.x # Left
get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
mouse_position.y, # Bottom
)


func _on_hover() -> void:
match _get_current_margin():
Margin.TOP:
mouse_default_cursor_shape = Control.CURSOR_VSIZE
Margin.RIGHT:
mouse_default_cursor_shape = Control.CURSOR_HSIZE
Margin.BOTTOM:
mouse_default_cursor_shape = Control.CURSOR_VSIZE
Margin.LEFT:
mouse_default_cursor_shape = Control.CURSOR_HSIZE
Margin.TOP_RIGHT:
mouse_default_cursor_shape = Control.CURSOR_BDIAGSIZE
Margin.TOP_LEFT:
mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE
Margin.BOTTOM_RIGHT:
mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE
Margin.BOTTOM_LEFT:
mouse_default_cursor_shape = Control.CURSOR_BDIAGSIZE


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
...


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
...


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(_event: InputEventMouseMotion) -> void:
...


func _on_title_bar_dragged() -> void:
...


func _on_resized() -> void:
...

Dentro das funções novas, muitas possuem a mesma lógica utilizada para arrastar janela. Porém duas possuem lógica nova: _get_current_margin() e _on_dragged()

A primeira é responsável por identificar a borda a qual o mouse se encontra (varias validações para identificar a posição do mouse em relação as bordas).

A segunda é a lógica de redimensionar, para resolver ela é recomendado primeiro resolver a lógica para cima, direita, baixo e esquerda (as diagonais são combinações das lógicas das outras).

note

Redimensionar uma janela inclui redimensionar os items dentro dela, isso pode ser um tanto quanto custoso de se fazer todas as frames.

Eu penso em testar redimensionar de tempos em tempos e apenas se tiver algum redimensionamento pendente 🤔.

references

Thiago Lages de Alencar

Crawler: Responsável por navegar entre websites utilizando links encontrados neles mesmos
Scraper: Responsável por extrair informações importantes dos websites navegados

Crawling é essêncial para scraping, pois você não tem como conseguir extrair informações de um site sem navegar para ele antes.
Scraping não é essêncial para crawling, pois os dados do site podem ser irrelevantes para você.

Por exemplo:

  • Google utiliza um crawler para navegar a internet e indexar páginas delas, porém não extrai as informações dos websites em si
  • OpenAI utiliza scraper para pegar os videos do youtube e utilizar na inteligência artificial deles

Se eu tivesse que separar crawlers em categorias, seria:

  • Browser: Utilizando um browser real para crawlear
  • Raw: Simulando um browser atráves de requisições pela internet

Hoje em dia simular um navegador é incrivelmente difícil então a maneira raw é bem menos potente e fornece muito menos funcionalidades.

Browser

Existem 3 ferramentas famosas de automação de navegadores:

É importante notar que elas todas se declaram para usos de testes, porém ainda assim são muito utilizadas para web scraping.

Selenium

Criado em ~2004 mas que continua a oferecer suporte para todos os navegadores e diversas linguagens (não necessariamente bem).

Muito do seu estilo vem do fato de ter sido criado utilizando Java e depois adaptado para outras linguagens.

javascript
import { Browser, Builder, By } from "selenium-webdriver";

const driver = await new Builder().forBrowser(Browser.CHROME).build()
await driver.get('https://www.etiquetaunica.com.br/')
await driver.manage().setTimeouts({implicit: 1000});

let hamburguerButton = await driver.findElement(By.xpath('//*[@id="headerWrapper"]/div[1]/button[1]'))
await hamburguerButton.click()

let brandButton = await driver.findElement(By.xpath('//*[@id="menu"]/div/div[2]/ul/li[7]/a'))
await brandButton.click()

Puppeteer

Criado pela Google em ~2017 para fornecer automação ao Google Chrome utilizando JavaScript.

Note que o locator favorito deles envolve utilizar CSS.

javascript
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({headless: false})
const page = await browser.newPage()

await page.setViewport({ width: 1600, height: 1024 })
await page.goto('https://gringa.com.br/')
await page.locator('#section-header > div > div.Header__FlexItem.Header__FlexItem--fill.Header__FlexItem_left--mobile > nav > ul > li:nth-child(3) > a').click()

Playwright

Criado pela Microsoft em ~2020 para fornecer automação em diversos navegador. O estilo é bem parecido ao do Puppeteer pois boa parte dos desenvolvedores vieram do Puppeteer 🤣.

A linguagem primária de programação dele é JavaScript, porém fornece suporte a diversas outras (não necessariamente bem).

javascript
import { chromium, devices } from "playwright";

const browser = await chromium.launch({ channel: 'chrome', headless: false })
const context = await browser.newContext(devices['Desktop Chrome'])
const page = await context.newPage()

await page.goto('https://canseivendi.com.br/')
await page.getByRole('link', {name: 'Marcas'}).click()

Raw

Neste caso o mais importante é você possuir uma boa quantidade de bibliotecas que o ajudem a realizar a tarefa! O básico é conseguir requisitar a página na internet e parsear o conteúdo HTML.

python
import httpx
from parsel import Selector

USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"

response = httpx.get(
"https://canseivendi.com.br/bolsas",
headers={"user-agent": USER_AGENT},
)

selector = Selector(text=response.text)
names = selector.xpath('//div[@class="name"]/a/text()').getall()
links = selector.xpath('//div[@class="name"]/a/@href').getall()
prices = selector.xpath('//div[@class="cash"]/text()').getall()

Neste exemplo escolhi utilizar a biblioteca httpx (criada por Encode) e parsel (criado por Scrapy), mas fica a sua escolha as bibliotecas para as tarefas.

note

Navegadores fazem muito mais que apenas uma requisição! Uma página leva uma reação em cadeia de requisições por conteúdos delas.

Por exemplo, ao receber uma página HTML e o navegador identificar uma imagem nela (<img src="photo.jpg">), ele precisa fazer uma requisição dessa imagem ao site.

Agora imagina que isto acontece para diversos tipos de conteúdos da página:

  • Imagens: <img src="myimage.jpg">
  • Audio: <audio></audio>
  • Videos: <video></video>
  • CSS: <link rel="stylesheet" href="mystyle.css">
  • JavaScript: <script src="myscripts.js"></script>
  • Iframe: <iframe src="url"></iframe>

Cat and Mouse Game

Ter os dados do seu site scrapeado por bots não é algo bom, pois eles geram grande tráfego e nenhum lucro (não estou falando de scalping).

Por isto é normal ver websites tentando identificar bots para bloquea-los e bots fingindo serem usuários normais do dia a dia.

Acontece que muitas vezes isso envolve simular comportamentos de um usuário e simular um navegador, onde ambos não são tarefas fáceis.

Aqui uma lista pequena de coisas a se pensar:

  • Simular Navegador
    • Construir Headers
    • Analisar HTML
    • Executar JavaScript
    • Variar fingerprint
  • Simular Usuário
    • Resolver Captchas
    • Movimento do mouse
    • Velocidade digitando
  • Após ser bloqueado
    • Alterar comportamento/tática
      • Para não ser bloqueado novamente
    • Utilizar Proxy

Uma da melhor maneira de saber como atacar é sabendo como os sites se protegem... O que é algo que eu tenho pouco conhecimento então vou terminar aqui 🤣.

References

Thiago Lages de Alencar
note

Pesquisa limitada pelo fato de Twitter ainda estar bloqueado no Brasil.

Aproveitando que eu estava olhando o drama em cima de Godot e assistindo Ace Attorney... Resolvi brincar com o site https://objection.lol.

Video

Details

Twitter

Discord

Godot Foundation

Website

jun-17

jul-08

oct-11

Fork

SMS

Docs

Twitter++

BlueSky

Google

Discord++

Youtube

Thiago Lages de Alencar

Está é uma lista com minha opnião sobre diversas ferramentas de terminal.
Aqueles marcados com estrela (⭐) são ainda utilizados por mim.

CLI

  • nala
    • Substitui: apt
    • Uso diário: Alto
    • Nota: Torna muito melhor a visualização e entendimento da instalação de programas
  • duf
    • Substitui: df
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois acabo olhando em GUIs essas informações
  • dust
    • Substitui: du
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois acabo olhando em GUIs essas informações
  • fastfetch
    • Substitui: neofetch
    • Uso diário: Baixo
    • Nota: Providência mais informação e o projeto não foi abandonado
  • jq
    • Substitui: ---
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois trato JSON por código

TUI

  • bottom
    • Substitui: htop, top
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois é muito chato ter que lembrar de todos os atalhos, acabo voltando para htop
  • superfile
    • Substitui: ---
    • Uso diário: Baixo
    • Nota: Nunca uso pois dificilmente tenho necessidade de usar file manager no terminal
  • yazi
    • Substitui: ---
    • Uso diário: Baixo
    • Nota: Nunca uso pois dificilmente tenho necessidade de usar file manager no terminal
  • micro
    • Substitui: nano
    • Uso diário: Alto
    • Nota: Muito mais parecido com um editor de texto do dia a dia

Shells

  • fish
    • Substitui: bash
    • Nota 1: Providência uma ótima experiência logo de cara
    • Nota 2: Sintaxe melhor que bash porém ainda não da vontade de aprender
  • nushell
    • Substitui: bash, fish
    • Nota 1: Providência uma ótima experiência logo de cara
    • Nota 2: Sintaxe muito mais agradável
    • Nota 3: Remove a necessidade de possuir jq
    • Nota 4: Remove a necessidade de possuir curl pois possui o comando http

Em shells "uso diário" é 100% de quando você utilizar o shell, então sempre é alto.

Prompt

  • Starship
    • Opnião 1: Permite grande costumização do prompt com facilidade, tornando o prompt mais agradável
    • Opnião 2: Quando lidando com Git possue um peso considerativo

Em prompts "subsititui" é sempre sobre substituir o padrão.
Em prompts "uso diário" é 100% de quando você utilizar o shell, então sempre é alto.

References

Thiago Lages de Alencar

Imagem mostrando camada do App, OS e Hardwares

Quando você quer ler o arquivo, você pede ao sistema operacional pelo conteúdo do arquivo.

Quando você quer escrever no arquivo, você pede ao sistema operacional para inserir o conteúdo no arquivo.

É importante saber que o sistema operacional toma diversos cuidados para que desenvolvedores não acessem diretamente o hardware, ou seja, por baixo dos panos você está pedindo para o sistema operacional ler/escrever.

  • C
    • fgets()
    • fwrite()
  • Python
    • file.read()
    • file.write()
  • Rust
    • file.read_to_string()
    • file.write_all()
  • Go
    • os.ReadFile()
    • os.WriteFile()

Veremos como garantir a segurança de um arquivo quando se tem múltiplos processos querendo altera-lo.

Process

A função utilizada para se criar processos é fork(), está função faz com que o atual processo crie um processo filho quase idêntico e executando o mesmo código que o pai.

Olhe este código que printa duas vezes "Hi":

#include <stdio.h>
#include <unistd.h>

int main() {
fork();
printf("Hi\n");
}

Se você executa-lo irá notar que o filho é tão igual ao pai que ele continua exatamente do mesmo local que o pai se encontrava (logo após fork() retornar um valor). Se tivessemos variáveis, poderiamos ver que até o valor delas são idênticos ao do pai.

No entanto, precisamos de uma maneira de reconhecer quem é o pai e filho, caso contrário este código executária exatamente a mesma coisa para ambos (não seria nada produtivo). Acontece que a função fork() retorna um valor e este valor é utilizado para sabermos se estamos no pai ou no filho.

#include <stdio.h>
#include <unistd.h>

int main() {
int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
printf("I'm the child process\n");
} else {
printf("I'm the parent process\n");
}
}

A função fork() vai retornar ao pai o PID do filho (ou -1 em caso de error).
A função fork() vai retornar ao filho zero.

tip

Normalmente o código do pai e filho são inseridos em funções em vez de deixar tudo dentro de um if/else.

if(pid == 0) {
child_code();
} else {
parent_code();
}

Ou se utiliza funções exec para transformar completamente o código executado naquele processo.

Problem

Imagem ilustrativa do app 1 lendo do arquivo, app 2 lendo do arquivo, app 1 escrevendo no arquivo, app 2 escrevendo no arquivo

Quando dois processos interagem com o mesmo arquivo, pode acontecer da informação ser preenchida incorretamente? Afinal, precisamos primeiramente descobrir se isso é possível ou não de acontecer.

Como o escalonamento pode ser imprevisivel, uma maneira de testar se durante a interação com um arquivo houve troca de processo é repetindo a ação diversas vezes e ver se pelo menos uma vez ocorreu.

O seguinte código irá ser executado para o processo pai e filho:

int count = 0;
FILE* file = fopen("example.txt", "w+");

while(count < 10000) {
int i;

fseek(file, 0, SEEK_SET);
fscanf(file, "%d", &i);
fseek(file, 0, SEEK_SET);
fprintf(file, "%d ", ++i);

count++;
}

fclose(file);

O código irá garantir que o cursor está no início do arquivo, ler o atual número do arquivo, mover o cursor para o início do arquivo e sobreescrever o número.

note
fprintf(file, "%d     ", ++i);

Por que inserir espaço após o número? Foi uma maneira de evitar que o número de ambos processos se misturem.

Por exemplo: Processo 1 escreve 5000 e processo 2 escreve 9, o arquivo irá conter "9000" pois o 9 foi escrito em cima do 5.

Agora só precisamos adicionar a lógica de criar processo vista anteriormente:

#include <stdio.h>
#include <unistd.h>

void code() {
int count = 0;
FILE* file = fopen("example.txt", "w+");

while(count < 10000) {
int i;

fseek(file, 0, SEEK_SET);
fscanf(file, "%d", &i);
fseek(file, 0, SEEK_SET);
fprintf(file, "%d ", ++i);

count++;
}

fclose(file);
}

int main() {
FILE* file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

Quando executei este código para 10 iterações, o valor final do arquivo foi 20.
Quando executei este código para 1000 iterações, o valor final do arquivo foi 1000.
Quando executei este código para 10000 iterações, o valor final do arquivo foi 10015.

O que somos capaz de deduzir com isto?

  • O resultado é imprevisível pois não temos controle de quando o escalonador vai trocar os processos
  • Dependendo do volume de iterações e da máquina do usuário, um processo pode ou não conseguir fazer a tarefa antes do escalonador trocar o processo
  • Se houver troca durante uma tarefa, pode corromper o resultado do arquivo

Quais as chances disto acontecer? Depende do software, pois existem arquivos que a chance de dois softwares interagirem ao mesmo tempo é 0%.

Locks

Acontece que existe mais de uma maneira de aplicar locks no Linux.

Uma pessoa bem surpresa

  • flock
    • file lock
    • Origem do sistema operacional BSD
  • lockf
    • lock file
    • POSIX
    • É uma versão simplificada do fcntl "Advisory record locking"
  • fcntl "Advisory record locking"
    • file control
    • POSIX
    • Uma função capaz de fazer diversas operações sobre file descriptors
      • Uma delas é utilizar "Advisory record locking"
  • fcntl "Open file description locks (non-POSIX)"
    • file control
    • Linux specific
      • Existem propostas para ser adicionado ao padrões POSIX
    • Uma função capaz de fazer diversas operações sobre file descriptors
      • Uma delas é utilizar "Open file description locks (non-POSIX)"

Se quiser saber mais sobre cada um, é bom ler o post no blog: https://gavv.net/articles/file-locks/
Incrivel como um blog de 8 anos atrás me ajudou mais do que pesquisas no Google.

flock

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

while (count < 10000) {
int i;

flock(fd, LOCK_EX);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
flock(fd, LOCK_UN);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

lockf

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

while (count < 10000) {
int i;

lockf(fd, F_LOCK, 0);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
lockf(fd, F_ULOCK, 0);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

fcntl - "Advisory record locking"

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
struct flock fl;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;

while (count < 10000) {
int i;

fl.l_type = F_WRLCK;
fcntl(fd, F_SETLKW, &fl);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &fl);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

References

Extra

Por diversão, eu experimentei replicar o mesmo problema em outras linguagens.

package main

import (
"fmt"
"os"
"os/exec"
)

func code() {
var count int = 0
file, _ := os.OpenFile("example.txt", os.O_RDWR, 0666)

for count < 10000 {
var i int

file.Seek(0, 0)
fmt.Fscanf(file, "%d", &i)
file.Seek(0, 0)
fmt.Fprintf(file, "%d ", i+1)

count++
}

file.Close()
}

func main() {
if len(os.Args) >= 2 {
code()
fmt.Println("Child finished")
} else {
os.WriteFile("example.txt", []byte{'0'}, 0600)

command := exec.Command("/usr/bin/go", "run", "main.go", "fork")
command.Start()
code()
command.Wait()

fmt.Println("Parent finished")
}
}

Thiago Lages de Alencar

My english is trash :v

Remember the old days where we would immerse ourselfs while reading books?

I don't... I suck at books.

Person laughing

But you know the feeling, right? We just have to change books for movies/games/sports...

Pretty sure that after the title and text above, you can guess everything that I'm going to write about (if you reflect deep enough, you don't even have to read this post...).

Storytelling

What do theaters, movie theaters and libraries have in common? When you make noise, people go "Shh".

Person making &quot;shhhh!&quot;

I'm joking... They are storytelling places.

Where you go to read, watch, see, or hear a predetermined story. The author tries to build a path for how you should feel in that storytelling (sometimes is not even close).

Video games are storytelling? If you put a story in it... Yes! They may not be the best storytelling because it's hard to control what players feel, but that's why people insert cinematics in games.

Cinematics are a way of telling a story without letting you ruin everything (I'm being dramatic). Just think about what stupid thing players could if you let them free in the cinematic (probably throw themselves from a high place... I don't know).

People love to test the boundaries of the games or try to find something that the author didn't count on.

Anyway, I'm getting off track. It's harder than movies and books because it's not just storytelling, but it's still possible to do a great job at it.

info

"It's not just storytelling"

Not saying that storytelling is easy, just that when you have to count everything that the player can do... Telling a good story becomes difficult.

Think about a great book or movie that somebody attempted to transform into a game... It's really hard to tell the same story. The developer may want them to feel like a great hero, but maybe they are pissed after dying 20 times to the same boss.

¯\_(ツ)_/¯

Immerse our selfs

Remember when I joked about places where people "Shh" you? I wasn't joking.

Person watching a monitor/tv

Is kind of obvious that people "Shh" in those places because it's really hard to immerse in something, but it's very easy to lose focus.

When I say "immerse", think about the situation where you are so focused on something that you ignore somebody calling your name.

Obviously, is important for storytelling games (except comedy, which is a free ticket to "f*ck it" town), but it's also important for competitive games! You don't have to immerse yourself in feelings but in the game mechanics (thinking about everything that others players can do).

That's probably why we get more angry or sad after losing a competitive game where we invest ourselfs to win. Over time, we learn to manage these feelings better, but it's still not a happy feeling.

note

"I don't get sad or angry"

Err... Not even fustrated? Are you sure that you were invested in the game? Maybe you just didn't care about the game.

Attention problem

Attention is a limited resource... We can't make 20 tasks at the same time and get the same quality of making one with all our attention (exceptions exist, but I don't think you are one of them).

Person being distracted
(It was supposed to be someone getting distracted and dying in the game...)

A classic example is your parents trying to talk to you while you are playing. In this cases, you attempt to multitask between playing and answering them (if you are like me, you are probably dead in the game).

In case of games/movies/books storytelling the same can happen... How can you get in the horror game mood of "I'm going to die, I want to run" if you have to stop to do something for your parents just before the scary moment?

note

Stop blaming the player, is also devs fault.

You want me to blame developers? Okay, it's also the developers fault for not making great storytelling that locks you into the screen... But this post is more about our problem, I can't just start badmouthing developers now (this post would get BIG and I don't have time to complain about everyone (I actually have... I'm unemployed... Sad...)).

Streamers

Streamer playing

"Making games for streamers seems hard as f***"

How do you capture the attention of someone who may stop to read others people messages and thinking about an answer to them?

How do you reproduce into another the feeling that you had when children where you would play in front of TV without noticing your parents calling you?

How do you tell about the struggles of your character if the other person is busy reflecting their life to other people?

Streamers are not "just" players, they are entertainment too! This means that they need to share their attention with their viewers.

  • Look at the chat to check if the stream didn't go offline
  • Interact with the chat to show that you cares about them
  • Thanks the subscrpitions and donations
  • Express what is on their mind while playing because the viewers want to know

I'm not here to blame anyone, just pointing the difficulties.

Compared to other options for playing games:

  • Playing the game alone in the room
    • You don't have to say a word
  • Recording a video for Youtube
    • People normally want to know what is on your mind
    • I didn't reflect enough on this so I don't have another point... Sorry Youtubers

Conclusion

I don't have any...

I don't know how developers attack this... They invest less in long immersive moments? They do more short content? They always assume that the player is a streamer? I mean, you can't just ignore the fact that a streamer could badmouth your game (this can affect sales, right?).

note

It's always good to see that streamers know that this is something that can happen.

https://youtu.be/5LtQAcGxQlM?t=193

Thiago Lages de Alencar
warning

Este post parte do princípio que o leitor viu o post anterior!

Além disto este post for reescrito pois o formato não estava do meu agrado (igual ao post anterior).

O repositório com exemplos continua vivo em:
https://github.com/thiagola92/learning-authentication

A lógica básica é a mesma que o post anterior, porém usando o email no lugar do username.

UI de login com email e password

O que temos de extra é que podemos utilizar o email do usuário para nos dar mais confiança que a pessoa acessando é o dono da conta.

Register

Client

Segue exatamente o mesmo passo a passo do post anterior (utilizando o email em vez de usuário).

Server

Segue exatamente o mesmo passo a passo do post anterior (utilizando o email em vez de usuário) porém não cadastramos o usuário ao final!

Carta representando um email

O motivo é bem simples: O email fornecido é realmente da pessoa que cadastrou a conta?

Imagina se nós terminassemos o cadastro acreditando naquele email... Dia seguinte aquele email poderia receber emails nosso, achar super esquisito e nos marcar como spam. Se isso acontecesse muito o Google iria acabar marcando nosso email corporativo como spam!

Para resolver esse problema basta cobrarmos uma prova que a pessoa é a dona do email. Podemos fazer isso enviando um email para ela e esperando que ela nos diga algo que possue no email.

note

Partindo do princípio que a pessoa dona do email é a única que deveria ter acesso ao email.

O modo clássico é gerar um código no servidor e enviar para o email da pessoa este código, agora ela precisa acessar o email e nos dizer o código para provar que ela conseguiu o código certo.

Visão geral do processo

Email é bom pois nós não precisamos ser dependentes de terceiros para utilizar (Gmail/Outlook/etc).

import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["Subject"] = "Confirmation code"
message["From"] = "noreply@yourwebsite.com"
message["To"] = "user@example.com"
message.set_content("Your confirmation code is 123")

# I'm running my own email server for tests
with smtplib.SMTP("localhost", 8025) as s:
s.send_message(message)
info

Utilizei a biblioteca aiosmtpd para criar o server de test:
python -m aiosmtpd -n

Se tudo foi feito corretamente, todo email novo deve aparecer lá no formato:

---------- MESSAGE FOLLOWS ----------
Subject: Confirmation code
From: noreply@yourwebsite.com
To: user@example.com
Content: Your confirmation code is 123
X-Peer: ('127.0.0.1', 52020)

------------ END MESSAGE ------------

Porém o exemplo acima é inseguro, pois não utiliza a SLL (que ajuda a criar uma camada de segurança entre nós e o serviço). Hoje em dia isso é obrigatório para interagir com quase todos os serviços de email (eles não vão aceitar emails sem essa segurança).

import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["Subject"] = "Confirmation code"
message["From"] = "noreply@yourwebsite.com"
message["To"] = "user@example.com"
message.set_content("Your confirmation code is 123")

with smtplib.SMTP("localhost", 8024) as s:
s.starttls()
s.send_message(message)
info

Levantei outro server de email e nele estou utilizando TLS.

Criei meu certificado auto-assinado:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'

E utilizei ele durante a inicialização do server:
python -m aiosmtpd -n --tlscert cert.pem --tlskey key.pem -l localhost:8024

Então é isso? Podemos mandar email para provedores como Gmail? Nope.

Rosto chorando

Deve ser bem óbvio que embora este código funcione localmente, ele jamais passaria por qualquer segurança do nível de aplicação.

Ele possui TLS para proteger contra man-in-the-middle mas como que o provedor vai saber que nós somos os donos daquele email? Se nós botarmos noreply@gmail.com, nós temos que provar que temos acesso à aquela conta no Gmail.

A maneira clássica é logar na conta.

import smtplib
from email.message import EmailMessage

# Removed "From" because providers will use the email used in s.login()
message = EmailMessage()
message["Subject"] = "Confirmation code"
message["To"] = "user@example.com"
message.set_content("Your confirmation code is 123")

with smtplib.SMTP("localhost", 8024) as s:
s.starttls()
s.login("noreply@gmail.com", "password")
s.send_message(message)

Este código não vai funcionar!

Hoje em dia já sabemos que usuário e senha sozinhos não providênciam uma segurança forte durante a autenticação, por isso que Google nos cobra outros métodos de MFA para provar que nós somos nós mesmos durante os logins (telefone, passkeys, authenticators, ...).

Google requer que você explicitamente adicione uma senha para aquele "app" e mesmo assim ele não recomenda fazer isto!
Link para a resposta no StackOverflow

Bem, vamos parar por aqui pois eu apenas tenho interesse em ver o conceito de segurança com o email (não quero ensinar a se autenticar em diversos provedores com email).

Client 2

Existe duas opções aqui, a primeira é o server ter armazenado numa tabela temporária seu email, código enviado por email e hash da senha.

emailcodehash
thiagola92@email.com48935e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
darklord@email.com38922d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25
juninho@email.com0283b7e94be513e96e8c45cd23d162275e5a12ebde9100a425c4ebcdd7fa4dcd897c

Neste caso basta o usuário enviar o código para o server validar a conta.

import httpx
from urllib.parse import urlencode

body = urlencode({"code": code})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/register/code", headers=headers, content=body)
note

Embora eu tenha feito como um requisição POST, não existe nenhuma obrigação de ser assim.

Uma maneira mais familiar é o usuário receber uma URL com o código/token para o usuário acessar e isto seria o suficiente para confirmar.

http://127.0.0.1:8000/register/{code}

A outra é ele não ter armazenado o hash e só esperar que você envie a senha novamente.

emailcode
thiagola92@email.com4893
darklord@email.com3892
juninho@email.com0283
import httpx
from urllib.parse import urlencode

body = urlencode({"email": email, "password": password, "code": code})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/register/code", headers=headers, content=body)
note

Em ambas as tabelas é normal de ter a data de criação do código, para que ele não fique válido para sempre (não queremos que ninguém chute todas as opções de código).

Server 2

Segue exatamente o mesmo passo a passo do post anterior (utilizando o email em vez de usuário).

Recovery

Client

A grande vantagem de ter email é adicionar um ponto de recuperação de senha.

Usuário se perguntando qual a senha

Dado que o usuário esqueceu a senha, ele só precisa requisitar o resete de senha da conta relacionada a aquele email.

import httpx
from urllib.parse import urlencode

body = urlencode({"email": email})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/recovery", headers=headers, content=body)

Como estamos falando do caso em que o usuário não lembra a senha, não podemos cobrar nenhuma autenticação... Em outras palavras, qualquer pessoa má intencionada pode ficar requisitando e resta ao server tomar cuidados para elas não conseguirem acesso.

Server

É a mesma ideia do login, queremos confirmar que é o dono da conta então mandamos para o email dele um código/token para ele utilizar na alteração da senha atual dele.

Lembrando que é importante botarmos tempo para o usuário fazer essa alteração e limite de tentativas, pois não queremos que usuários má intencionados usem força bruta para descobrir o código/token.

Client 2

Basicamente idêntico ao registrar, porém temos que fornecer a nova senha. Isto pode ser feito tudo em uma requisição ou em duas (acessar o URL com token e inserir a nova senha em um form).

Server 2

Valide o código/token!

Carimbo

Cada tentativa errada é um sinal de perigo, por isso é importante lembrar:

  • Invalidar o código/token depois de certo tempo
    • Por isto alguns tokens são grandes, pois nunca é possível chutar todas as possibilidades antes do tempo esgotar
  • Contar o número de tentativas de acertar o código/token para um email
    • Importante invalidar o código depois de certo número de tentativas, não queremos X máquinas tentando acertar o código

References

Thiago Lages de Alencar
warning

Eu não estava feliz que o post era muito "aqui o código de X/Y/Z", pois gerava paredão de texto. E quando escrevo estes posts, a intenção é reforçar conhecimento para no futuro eu conseguir voltar e relembrar do assunto (e não do código em específico).

O repositório com exemplos continua vivo em:
https://github.com/thiagola92/learning-authentication

Username com password é uma das maneiras mais velhas de se criar autenticação no seu sistema.

UI para login

Nós confiaremos que aquele usuário é o dono da conta se ele souber a senha que está relacionada àquele username. Note que falei "confiaremos" pois nunca podemos ter 100% de certeza, só estamos tentando reduzir a possibilidade de ser alguém indesejado na conta.

Register

Client

UI para registrar

Para entender como o registro de novos usuários funciona, podemos olhar como formulários em HTML funcionam.

<form action="./register">
</form>

Formulários coletam todos os dados dos elementos <input> dentro deles e enviam para o endereço especificado no atributo action. Tenha em mente que enviar quer dizer "mandar para aquele endpoint um request HTTP".

note

Caso você já tenha feito uma API REST em qualquer linguagem, você pode começar a notar detalhes familiares. Acontece que os navegadores não fazem nenhuma mágica, eles enviam o mesmo tipo de request que você está acostumado a lidar no backend de APIs REST.

Como eu estou escrevendo isto ao mesmo tempo que testo o código, eu vou trocar o endpoint para a minha API (http://127.0.0.1:8000/register).

<form action="http://127.0.0.1:8000/register">
</form>

Nós precisamos adicionar o input do tipo submit pois ele é utilizado para engatilhar o envio.

<form action="http://127.0.0.1:8000/register">
<input type="submit" value="register">
</form>

Se você clicar no botão de registrar, você mandara um request GET para o endpoint register, com nenhuma informação pois não existe nenhum campo input que segure informação.

No nosso caso precisamos de um campo para o username e outro para o password:

<form action="http://127.0.0.1:8000/register">
<input type="text" name="username" value="username"><br>
<input type="password" name="password" value="password"><br>
<input type="submit" value="register">
</form>

Note que utilizamos os tipos text e password apenas para dar o comportamento correto no navegador. O importante mesmo é o atributo name pois ele define o nome a qual o valor vai estar relacionado quando enviado.

Se clicarmos no botão de registrar, enviaremos as informações na URL:
/register?username=username&password=password

Por padrão o formulário envia um request GET, o que é ótimo se você quiser compartilhar URL com alguém ou salvar no favoritos.

Porém não é nada seguro quando estamos falando de informação sensível como a senha do usuário! Neste caso queremos enviar no corpo do request POST (onde não fica visível a qualquer pessoa olhando a tela do seu computador).

Podemos especificar o método utilizado no request atráves do atributo method:

<form action="http://127.0.0.1:8000/register" method="post">
<input type="text" name="username" value="username"><br>
<input type="password" name="password" value="password"><br>
<input type="submit" value="register">
</form>

Agora ao clicar no botão de registrar, enviaremos as informações no corpo do request e a URL para qual você vai ser direcionado não vai conter seus dados (/register).

Corpo do request: username=username&password=password

note

Note que no final das contas é o mesmo formato porém em lugares diferentes.

Se você tiver uma API, conseguirá ver que ambos possuem o campo content-type da requisição com o valor application/x-www-form-urlencoded.

Tudo isto poderia ser reproduzido em python com:

import httpx
from urllib.parse import urlencode

body = urlencode({"username": username, "password": password})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/register", headers=headers, content=body)
note

Eu continuarei mostrando o código Python equivalente ao que seria feito pelo navegador/server pois acho que da uma visão lógica de como as coisas são feitas por baixo dos panos.

Server

Okay, seu server recebeu a requisição de cadastro do usuário. O que fazer agora? Validações!

Robo lendo um papel

O conteúdo da requisição está no formato esperado?

Podemos verificar se o campo content-type está com application/x-www-form-urlencoded.

if request.headers.get("Content-Type") != "application/x-www-form-urlencoded":
raise Exception("Invalid body format")

Claro que isso não impede o usuário de formatar o conteúdo incorretamente, porém agora podemos assumir que ele errou o formato e enviarmos uma mensagem de erro coerente com o problema.

try:
body = await request.body()
body = body.decode()
fields = parse_qs(body)
except ValueError:
raise Exception("Body incorrectly formatted")

E se o usuário esquecer um dos campos? Sim, precisamos validar isto também.

if "username" not in fields or "password" not in fields:
return Exception("Missing username or password")

E se o usuário tiver caracters inválido?
E se a senha tiver caracters inválido?
E se o usuário já existir no banco?
...

Acho que você já entendeu que validação é importante.
Agora vamos falar de storage!

Database

Como se armazena o username?
Igual a qualquer outro campo texto...

Como se armazena a senha?
Não se armazena senha...

Pode parecer estranho a primeira vista mas não precisamos armazenar a senha para conferir se alguém nos deus a senha correta.

Existem funções hash criptográficas que produzem saídas com propriedades que nos ajudam a conferir se a senha de um usuário está correta.

Propriedades que nos interessão nessas funções:

  • Dada uma entrada de bytes, sempre produz a mesma saída de bytes
    • Nada de especial aqui, apenas está garantindo que não é afetado por outro fatores aleatórios (tempo, temperatura, etc)
  • Não existe função que reverte a operação
    • Em outras palavras, tendo a saída de bytes da função você não consegue saber a entrada que foi dada para a função hash (sem ir chutando todas as possibilidades)
  • Qualquer mudança na entrada de bytes gera uma saída de bytes muito diferente
    • A ideia é que as pessoas não devem saber que as entradas são parecidas a partir da saída

Diversas funções hash criptográficas existem, cada uma com o próprio algoritmo.
No nosso caso vamos utilizar o algoritmo sha256 para os exemplos!

>>> sha256(entrada)
saida
note

Segurança é algo que muda com o tempo, então funções hash criptográficas seguras de antigamente podem já não ser mais seguras.

Estou usando a função hash que usa o algoritmo sha256 apenas de exemplo, não estou considerando se é segura ou não para o ano atual.

entradasaída
abcba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
abda52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
ABCb5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78
123a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
  • Se você der a mesma entrada, vai receber a mesma saída
  • Olhando a saída você não sabe a entrada
  • A mudanaça de um bit entre "abc" e "abd" mudou totalmente a saída

Não sei se ficou claro, mas isso é perfeito para podermos conferir se alguém acertou a senha.

Assim que um usuário registra no serviço e nos da senha, podemos calcular o hash da senha (saída da função hash para a senha) para armazenar.

>>> sha256(b"password").hexdigest()
'5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'

>>> sha256(b"mega_password").hexdigest()
'2d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25'

>>> sha256(b"senha").hexdigest()
'b7e94be513e96e8c45cd23d162275e5a12ebde9100a425c4ebcdd7fa4dcd897c'
usernamehash
thiagola925e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
darklord2d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25
juninhob7e94be513e96e8c45cd23d162275e5a12ebde9100a425c4ebcdd7fa4dcd897c

Quando alguém for logar no nosso serviço, a pessoa vai inserir a senha e nós vamos conferir se o hash dessa senha é igual ao que temos no banco.

  • Se for igual, a pessoa sabe a senha e nós podemos autorizar o acesso ao serviço
  • Se não for igual, a pessoa não sabe a senha e nós devemos negar o acesso ao serviço

Qual a vantagem de armazenar assim?
Se algum hacker acessar nosso banco, ele não vai conseguir saber a senha das pessoas.

Mesmo que a pessoa saiba que meu hash é:
5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
Se ela tentar o hash como senha, o hash do hash vai ser totalmente diferente.

>>> sha256(b"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8").hexdigest()
'113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3'

Então é isso? Temos que armazenar o hash da senha? Nope.

Face saying nope

O que acontece quando dois usuários possuem a mesma senha?

usernamehash
thiagola925e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
darklord2d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25
juninhob7e94be513e96e8c45cd23d162275e5a12ebde9100a425c4ebcdd7fa4dcd897c
mmiguel2d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25

Isso quer dizer que se um hacker advinhar a senha de mmiguel, ele também sabe a senha de darklord.

Para previnir isso é normal adicionar salt às senhas, ou seja, concatenar bytes extras na senha antes de fazer o hash.

Assim que o usuário criar a conta, criaremos um salt para ele com uma função que gera X bytes aleatórios (os.urandom(X)):

# No caso vamos gerar 10 bytes aleatórios.
salt = os.urandom(10)
usernamesalt
thiagola92cea3a49482094b096acb
darklordb9f195b7ce55a51fdc0d
juninho885cfd855add75c08a7a
mmiguel10cc25d0c8adcbc44e9a

Iremos inserir na função hash a concatenação de senha + salt.

>>> sha256(b"password" + b"cea3a49482094b096acb").hexdigest()
'a15760e9639bc64e8908d5d2109900dcbc508e6fc5fa7b161e5149fcdaa04eee'

>>> sha256(b"mega_password" + b"b9f195b7ce55a51fdc0d").hexdigest()
'8c7044c8e42efebbf7d19027db0a00c8d2242197a0e001d5d0d977cccfaa020c'

>>> sha256(b"senha" + b"885cfd855add75c08a7a").hexdigest()
'f51f77c4575127dbf8906c60b7b1c80e01772be217c8a1c9df68e4f527b7f8eb'

>>> sha256(b"mega_password" + b"10cc25d0c8adcbc44e9a").hexdigest()
'bbb70b912376b0c266d90cd3ebd1ffa99f6c439a50ed6481bb809b99fd93d300'
usernamesalthash
thiagola92cea3a49482094b096acba15760e9639bc64e8908d5d2109900dcbc508e6fc5fa7b161e5149fcdaa04eee
darklordb9f195b7ce55a51fdc0d8c7044c8e42efebbf7d19027db0a00c8d2242197a0e001d5d0d977cccfaa020c
juninho885cfd855add75c08a7af51f77c4575127dbf8906c60b7b1c80e01772be217c8a1c9df68e4f527b7f8eb
mmiguel10cc25d0c8adcbc44e9abbb70b912376b0c266d90cd3ebd1ffa99f6c439a50ed6481bb809b99fd93d300

Agora se um hacker tentar todas as possibilidades de senhas para o mmiguel e um dia acertar, ele jamais vai ser que é a mesma senha para darklord.

E se o salt gerado for o mesmo para ambos?

Uma maneira de solucionar isto é verificar se já existe alguém com o mesmo hash e salt.
Caso sim, gere um novo salt...

warning

Minha intenção aqui era explicar o conceito.

É importante olhar a documentação da sua linguagem/biblioteca para saber a melhor função hash a se usar!

Por exemplo, python já disponibiliza uma função que já concatena senha e salt, e depois faz o hash (hashlib.scrypt()). Então não existe necessidade de fazer cada etapa dita neste post.

Login

Client

UI para login

Quando falando de website, a maneira para logar é a mesma de se registrar:

import httpx
from urllib.parse import urlencode

body = urlencode({"username": username, "password": password})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/login", headers=headers, content=body)

E o server responde enviando cookies para que o browser use nas próximas requisições de conteúdo.

Note que esse request utiliza o body para enviar username e password, o que deixa o body ocupado por dados relacionados ao login (ao menos que você queira fazer uma bagunça e misturar login com pedido de dados).

Outra maneira conhecida por APIs é passar a autentição no headers pelo campo Authorization:

import httpx
import base64

credentials = f"{username}:{password}"
credentials = credentials.encode()
credentials = base64.b64encode(credentials)
credentials = credentials.decode()

response = httpx.get("http://127.0.0.1:8000/", headers={"Authorization": f"Basic {credentials}"})

Note que nesse modelo nós botamos como valor do Authroization o esquema e depois a credencial:
<scheme> <credential>

No nosso caso o esquema é Basic, onde quer dizer que vamos passar as credenciais em base 64 e concatenadas com :.
<username>:<password>

A vantagem desta maneira é que em uma requisição nós já conseguiriamos nos autenticar e aplicar a operação desejada na API.

note

Essa maneira não é incentivada pois passar em toda requisição username e password é arriscado, basta uma pessoa má intencionado conseguir acesso a uma mensagem para obter sua senha.

Passar em uma requisição para obter um token/cookie é válido, pois toda requisição seguinte nós utilizariamos esse token/cookie para sermos validados no server.

Server

Okay, seu servidor recebeu o request de logar... O que fazer? Validações! (novamente)

Robo lendo um papel com os olhos estressados

Se estivermos falando da mesma maneira utilizada por websites, então as validações são bem parecidas com as de registrar!

if request.headers.get("Content-Type") != "application/x-www-form-urlencoded":
raise Exception("Invalid body format")
try:
body = await request.body()
body = body.decode()
fields = parse_qs(body)
except ValueError:
raise Exception("Body incorrectly formatted")
if "username" not in fields or "password" not in fields:
return Exception("Missing username or password")

Se estivermos falando da maneira das APIs, temos que adaptar ao outro local onde o username e password estão mas nada complicado!

if not request.headers.get("Authorization"):
raise Exception("Missing authorization")
auth = conn.headers["Authorization"]
scheme, credentials = auth.split()

if scheme.lower() != "basic":
raise Exception("Incorrect authorization protocol")
try:
credentials = base64.b64decode(credentials)
credentials = credentials.decode()
username, _, password = credentials.partition(":")
except ValueError:
raise Exception("Credentials incorrectly formatted")

Chegou a hora da verdade.
Quem mandou a requisição é o usuário verdadeiro da conta?

Pessoa com o lazer de uma arma na cabeça

Bem... Ele não vai ser o usuário verdadeiro se o usuário nem existir né?

Podemos verificar isso tentando pegar do banco o salt e hash.

salt, hash = get_user_auth(username)
if not salt or not hash:
raise Exception("Invalid user")

Agora vem a parte fácil... Validar se a senha que ele passou está certa! Basta calcular o hash usando a senha que nos passaram.

if sha256(password + salt).hexdigest() != hash:
raise Exception("Wrong password")

Fim!

Principalmente porque não pretendo cobrir a fundo assuntos como cookies e tokens. Mas tudo coberto já da um ótimo início para entender autenticação/login/cadastro/etc.

References

Thiago Lages de Alencar

Esse post é meio que uma compilação do que eu entendi de cada assunto após de horas lendo na internet e perguntando para meu pai.
Em outras palavras: Pode ter informação incorreta!

TTY

Teletype
(https://en.wikipedia.org/wiki/Teleprinter)

TTY é um comando em linux para saber o nome do terminal o qual a input está diretamente conectada.

$ tty
/dev/pts/1

$ echo "example" | tty
not a tty

No primeiro exemplo, o comando tty veio como input diretamente do terminal.

No segundo exemplo, o comando tty recebeu a input example do comando anterior (não de um terminal).

Fim! Pode ir para a próxima sessão, ao menos que você queira saber o que diabos é uma teletype.

Rosto curioso

Já notou que muitas coisas no computador possuem o nome de objetos que existem fora do computador?

Acontece que o nome é dado baseado nestes objetos para ajudar usuários a entenderem melhor o uso deles no computador! Por exemplo:

  • file
  • folder
  • trash can
  • window

O mesmo vale para TTY, onde o nome veio de teletypes. Infelizmente não é um nome que ajude muito pois computadores já subsittuiram o uso delas então esse nome não ajuda ninguém a saber do que o comando se trata 🤣.

O que são teletypes?
Entenda que elas são uma junção de typewriters e telegraph key.
O primeiro utilizado para escrever em papel e o segundo utilizado para enviar morse code a distância.

Morse code era muito utilizado como uma forma de comunicação binária (som curto/longo) via cabo. Porém para isso acontecer era necessário duas pessoas treinadas em morse code (uma para enviar e outra para receber).

mensagem <=> tradutor <=> morse code <=> transmissão

Com a chegada das teletypes, os tradutores foram substituidos por estas máquinas que eram capaz de traduzir e ler morse code.

mensagem <=> teletype <=> morse code <=> transmissão

Computadores da época utilizavam paper tape como forma de armazenamento de dados e monitores não eram algo acessível. Teletypes viram está oportunidade para preencher outro meio de comunicação (dado que computadores também utilizam binário).

mensagem <=> teletype <=> binário <=> computador

Este video mostra uma teletype recebendo e enviando dados de/a um terminal:
https://www.youtube.com/watch?v=S81GyMKH7zw

Por isto que o termo TTY era utilizado para referência aparelhos enviando/recebendo (input/output) mensagem do computador.

Algumas linguagens até incluem código para fazer essa verificação:

  • C
    • #include <unistd.h>

      isatty(fildes);
  • Python
    • import os

      os.isatty(fd)
  • NodeJS
    • tty.isatty(fd)

Onde a funcionalidade das funções é identificar se a input/ouput está vinculada a um aparelho (device).

warning

Preste bem atenção que sua input pode estar ligada ou não a um aparelho E sua output pode estar ligada ou não a um aparelho.
Um deles estar ligado não quer dizer que ambos estão.

As funções recebem um file descriptor e dizem se ele está ou não linkado a um aparelho.
Você poderia passar STDIN, STDOUT ou até STDERR para a função analisar.

Esse video cobre bem o assunto: https://www.youtube.com/watch?v=SYwbEcNrcjI

Se realmente quiser saber detalhes sobre TTY, existe este blog cheio de informações (que eu não li):
https://www.linusakesson.net/programming/tty/

TTY

Terminal

(https://en.wikipedia.org/wiki/Computer_terminal)

Teletypes originalmente eram conhecidas como "hard-copy terminals" por usarem papel, mas com a vinda de telas nós formamos uma nova ideia de terminal nas nossas cabeças (a tela preta).

Terminal não possui armazenamento de dados, da mesma maneira que teletypes apenas eram responsáveis por ler e escrever do computador, ou seja, a lógica ainda estava no computador. Alguns terminais possuiam um pouco de lógica neles porém nada comparado ao computador.

Se você viu o video da sessão anterior então já deve ter ganhado uma ideia do que é um terminal, pois nele é mostrado uma teletype lendo e escrevendo para um terminal.
Mas caso queira outro video mostrando melhor um terminal:
https://www.youtube.com/watch?v=UNdu0YQfvF0

Terminal

Terminal Emulator

(https://en.wikipedia.org/wiki/Terminal_emulator)

Hoje em dia usamos o termo terminal para representarmos emuladores de terminais.

Diferentemente de terminais, estes estão fortemente ligados a computador e não são máquinas separadas da lógica. Basicamente estamos falando da janela que finge ser um terminal (GNOME terminal).

Terminal Emulator

Shell

(https://en.wikipedia.org/wiki/Shell_script)

Um programa responsável por ficar em loop esperando comandos do usuário para serem executados.

Comandos podem ser:

  • Programas
    • echo
    • ls
    • touch
    • mkdir
    • Buscados em lugares pré definidos (/bin, /usr/bin, ...)
      • Use echo $PATH para ver a lista de lugares a se olhar
  • Comandos do próprio shell
    • type
    • which
    • help
    • man
    • Estes existem dentro do shell e não precisam ser buscados.
  • Shell functions
  • Aliases
    • Comandos definidos por nós, construido de outros comandos

Existem variações e alternativas de shell:

Shell

CLI

Command-line interface
(https://en.wikipedia.org/wiki/Command-line_interface)

É uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa.

Está interface se basea no usuário passar flags e mais informações em conjunto ao comando, dessa maneira mudando o comportamento do commando. Por exemplo, o programa ls disponibiliza diversas flags para alterar o comportamento:

  • ls
    • Lista tudo no diretório atual mas ignora os começando com .
  • ls -a
    • Lista tudo no diretório atual e não ignora os começando com .
  • ls -l
    • Lista tudo no diretório atual mas com mais detalhes

Fique bem claro que é o programa te dando opções de como interagir com ele, não o shell ou terminal, então resta ao programa implementar comportamentos para certas flags.

note

É muito comum programas oferecerem detalhes sobre as flags quando utilizando a flag --help (ls --help).

CLI

TUI

Terminal user interface
(https://en.wikipedia.org/wiki/Text-based_user_interface)

Novamente é uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa. Porém está foca em dar uma interação mais visual e continua.

Diferente de CLI's onde toda a interação começa e termina em um comando só, TUI's continuam esperando mais interações do usuário até um dos dois decidirem terminar.

Um exemplo bem comum é top que providência uma visão dos programas/processos/threads em execução do sistema, uma vez inicializado ele esperar por mais interações do usuário. Se você apertar q ele termina, se você apertar h ele fornece a lista de comandos, etc.

Note que a TUI's ainda podem providênciar flags para alterar o comportamento (top --help).

TUI

GUI

Graphical user interface
(https://en.wikipedia.org/wiki/Graphical_user_interface)

Novamente é uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa. Porém não está limitada a usar texto para a visualização, pois tem a capacidade de desenhar na tela.

Hoje em dia é o meio mais popular de se usar uma aplicação, quando se abre VSCode, Google Chrome, Discord... Todos são GUI's pois utilizaram a capacidade de desenhar para dar uma interface ao usuário.

Mesmo programas focados em GUI's podem aceitar flags (VSCode: code --help).

GUI

References