diff --git a/app/controllers/commands_controller.rb b/app/controllers/commands_controller.rb
new file mode 100644
index 000000000..143ff514d
--- /dev/null
+++ b/app/controllers/commands_controller.rb
@@ -0,0 +1,25 @@
+class CommandsController < ApplicationController
+ def create
+ command = parse_command(params[:command])
+
+ if command
+ result = command.execute
+
+ case result
+ when Command::Result::Redirection
+ redirect_to url_for(result.url)
+ else
+ redirect_back_or_to root_path
+ end
+ else
+ raise "Pending to handle invalid commands"
+ end
+ end
+
+ private
+ def parse_command(string)
+ Command::Parser.new(Current.user).parse(string).tap do |command|
+ Current.user.commands << command if command
+ end
+ end
+end
diff --git a/app/javascript/controllers/commands_controller.js b/app/javascript/controllers/commands_controller.js
new file mode 100644
index 000000000..98eaa7598
--- /dev/null
+++ b/app/javascript/controllers/commands_controller.js
@@ -0,0 +1,8 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = [ "input" ]
+
+ // Actions
+
+}
diff --git a/app/models/command.rb b/app/models/command.rb
new file mode 100644
index 000000000..fb8cfbd2c
--- /dev/null
+++ b/app/models/command.rb
@@ -0,0 +1,20 @@
+class Command < ApplicationRecord
+ include Rails.application.routes.url_helpers
+
+ belongs_to :user
+
+ def execute
+ end
+
+ def undo
+ end
+
+ def can_undo?
+ false
+ end
+
+ private
+ def redirect_to(...)
+ Command::Result::Redirection.new(...)
+ end
+end
diff --git a/app/models/command/go_to_card.rb b/app/models/command/go_to_card.rb
new file mode 100644
index 000000000..b3e3122c5
--- /dev/null
+++ b/app/models/command/go_to_card.rb
@@ -0,0 +1,24 @@
+class Command::GoToCard < Command
+ store_accessor :data, :card_id
+
+ def card=(card)
+ self.card_id = card.id
+ end
+
+ def execute
+ redirect_to card
+ end
+
+ def description
+ "Search '#{query}"
+ end
+
+ def to_command
+ query
+ end
+
+ private
+ def card
+ user.accessible_cards.find(card_id)
+ end
+end
diff --git a/app/models/command/parser.rb b/app/models/command/parser.rb
new file mode 100644
index 000000000..1f173c0cb
--- /dev/null
+++ b/app/models/command/parser.rb
@@ -0,0 +1,15 @@
+class Command::Parser
+ attr_reader :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ def parse(string)
+ if card = user.accessible_cards.find_by_id(string)
+ Command::GoToCard.new(card: card)
+ else
+ Command::Search.new(query: string)
+ end
+ end
+end
diff --git a/app/models/command/result/redirection.rb b/app/models/command/result/redirection.rb
new file mode 100644
index 000000000..3fd5b9b8d
--- /dev/null
+++ b/app/models/command/result/redirection.rb
@@ -0,0 +1 @@
+Command::Result::Redirection = Struct.new(:url)
diff --git a/app/models/command/search.rb b/app/models/command/search.rb
new file mode 100644
index 000000000..c8e383950
--- /dev/null
+++ b/app/models/command/search.rb
@@ -0,0 +1,15 @@
+class Command::Search < Command
+ store_accessor :data, :query
+
+ def execute
+ redirect_to cards_path("terms[]": query)
+ end
+
+ def description
+ "Search '#{query}"
+ end
+
+ def to_command
+ query
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 370aae05c..d68ccff46 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -15,6 +15,7 @@ class User < ApplicationRecord
has_many :closures, dependent: :nullify
has_many :pins, dependent: :destroy
has_many :pinned_cards, through: :pins, source: :card
+ has_many :commands, dependent: :destroy
normalizes :email_address, with: ->(value) { value.strip.downcase }
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index fd857fafe..804999cdb 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -50,7 +50,7 @@
<% end %>
diff --git a/app/views/my/commands/_input.html.erb b/app/views/my/commands/_input.html.erb
new file mode 100644
index 000000000..ae80a5402
--- /dev/null
+++ b/app/views/my/commands/_input.html.erb
@@ -0,0 +1,26 @@
+<%= tag.div class: "terminal full-width flex flex-column gap justify-end", data: {
+ controller: "toggle-class commands",
+ toggle_class_toggle_class: "terminal--open" } do %>
+
+
+ <%= form_tag commands_path,
+ class: "flex align-center gap-half",
+ data: { controller: "form" } do %>
+
+
+ <%= text_field_tag "command", nil,
+ autocomplete: "off",
+ autocorrect: "off",
+ autocapitalize: "off",
+ class: "terminal__input input fill-transparent unpad",
+ data: {
+ commands_target: "input",
+ action: "keydown.up->toggle-class#add:prevent keydown.down->toggle-class#remove:prevent"
+ },
+ placeholder: "Search or type commands…" %>
+ <% end %>
+<% end %>
+
diff --git a/app/views/terminals/show.html.erb b/app/views/terminals/show.html.erb
index 843d79039..aa2b19706 100644
--- a/app/views/terminals/show.html.erb
+++ b/app/views/terminals/show.html.erb
@@ -1,9 +1,9 @@
<%= turbo_frame_tag "terminal" do %>
-
+
<%= turbo_frame_tag "terminal_form" do %>
<%= link_to edit_terminal_path, class: "terminal__link flex align-center gap-half min-width", data: { action: "keydown.meta+shift+p@document->hotkey#click", controller: "hotkey" } do %>
Fizzy do >
@@ -11,4 +11,4 @@
<% end %>
<% end %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 762985595..733128e1e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -89,6 +89,8 @@ Rails.application.routes.draw do
end
end
+ resources :commands
+
namespace :my do
resources :pins
end
diff --git a/db/migrate/20250505123008_create_commands.rb b/db/migrate/20250505123008_create_commands.rb
new file mode 100644
index 000000000..61b3868eb
--- /dev/null
+++ b/db/migrate/20250505123008_create_commands.rb
@@ -0,0 +1,10 @@
+class CreateCommands < ActiveRecord::Migration[8.1]
+ def change
+ create_table :commands do |t|
+ t.references :user, null: false, foreign_key: true, index: true
+ t.json :data, default: {}
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index fd3176781..f64ff1720 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.1].define(version: 2025_04_29_162506) do
+ActiveRecord::Schema[8.1].define(version: 2025_05_05_123008) do
create_table "accesses", force: :cascade do |t|
t.integer "collection_id", null: false
t.datetime "created_at", null: false
@@ -159,6 +159,14 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_29_162506) do
t.index ["filter_id"], name: "index_collections_filters_on_filter_id"
end
+ create_table "commands", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.json "data", default: {}
+ t.datetime "updated_at", null: false
+ t.integer "user_id", null: false
+ t.index ["user_id"], name: "index_commands_on_user_id"
+ end
+
create_table "comments", force: :cascade do |t|
t.integer "card_id", null: false
t.datetime "created_at", null: false
@@ -280,7 +288,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_29_162506) do
t.datetime "created_at", null: false
t.string "title"
t.datetime "updated_at", null: false
- t.index ["title"], name: "index_tags_on_account_id_and_title", unique: true
end
create_table "users", force: :cascade do |t|
@@ -327,6 +334,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_29_162506) do
add_foreign_key "closures", "cards"
add_foreign_key "closures", "users"
add_foreign_key "collections", "workflows"
+ add_foreign_key "commands", "users"
add_foreign_key "comments", "cards"
add_foreign_key "events", "collections"
add_foreign_key "mentions", "users", column: "mentionee_id"
diff --git a/db/schema_cache.yml b/db/schema_cache.yml
index c196953f7..86315984a 100644
--- a/db/schema_cache.yml
+++ b/db/schema_cache.yml
@@ -80,7 +80,7 @@ columns:
default_function:
collation:
comment:
- - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: user_id
cast_type: *1
@@ -532,6 +532,29 @@ columns:
collections_filters:
- *22
- *18
+ commands:
+ - *5
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: data
+ cast_type: &25 !ruby/object:ActiveRecord::Type::Json
+ precision:
+ scale:
+ limit:
+ sql_type_metadata: &26 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: json
+ type: :json
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default: "{}"
+ default_function:
+ collation:
+ comment:
+ - *6
+ - *9
+ - *24
comments:
- *21
- *5
@@ -579,16 +602,8 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: particulars
- cast_type: &24 !ruby/object:ActiveRecord::Type::Json
- precision:
- scale:
- limit:
- sql_type_metadata: &25 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
- sql_type: json
- type: :json
- limit:
- precision:
- scale:
+ cast_type: *25
+ sql_type_metadata: *26
'null': true
default: "{}"
default_function:
@@ -601,8 +616,8 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: fields
- cast_type: *24
- sql_type_metadata: *25
+ cast_type: *25
+ sql_type_metadata: *26
'null': false
default: "{}"
default_function:
@@ -667,7 +682,7 @@ columns:
default_function:
collation:
comment:
- - &26 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: source_id
cast_type: *1
@@ -677,7 +692,7 @@ columns:
default_function:
collation:
comment:
- - &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: source_type
cast_type: *7
@@ -711,16 +726,16 @@ columns:
default_function:
collation:
comment:
- - *26
- *27
- - *9
- *28
+ - *9
+ - *24
pins:
- *21
- *5
- *6
- *9
- - *28
+ - *24
reactions:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
@@ -800,7 +815,7 @@ columns:
default_function:
collation:
comment:
- - *28
+ - *24
taggings:
- *21
- *5
@@ -862,7 +877,7 @@ columns:
- *5
- *6
- *9
- - *28
+ - *24
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: watching
@@ -921,6 +936,7 @@ primary_keys:
closures: id
collections: id
collections_filters:
+ commands: id
comments: id
creators_filters:
events: id
@@ -957,6 +973,7 @@ data_sources:
closures: true
collections: true
collections_filters: true
+ commands: true
comments: true
creators_filters: true
events: true
@@ -1450,6 +1467,23 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ commands:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: commands
+ name: index_commands_on_user_id
+ unique: false
+ columns:
+ - user_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
comments:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: comments
@@ -1905,23 +1939,7 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- tags:
- - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: tags
- name: index_tags_on_account_id_and_title
- unique: true
- columns:
- - title
- lengths: {}
- orders: {}
- opclasses: {}
- where:
- type:
- using:
- include:
- nulls_not_distinct:
- comment:
- valid: true
+ tags: []
users:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: users
@@ -2006,4 +2024,4 @@ indexes:
comment:
valid: true
workflows: []
-version: 20250429162506
+version: 20250505123008