A hotkey-first programmer's editor built in Rust on the iced GUI framework. Scitano combines the menu structure of SciTE, strict GNU nano hotkeys, and Geany-inspired sidebar features into a single unified editing environment.
This commit is contained in:
commit
029667c3d9
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "scitano"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Scitano Unified Editor - SciTE menus, strict Nano hotkeys, Geany features in Rust/iced"
|
||||||
|
license = "GPL-2.0-only"
|
||||||
|
repository = "https://git.dcos.net/dcosnet/scitano/"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced = { version = "0.13", features = ["highlighter", "advanced"] }
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your freedom to
|
||||||
|
share and change it. By contrast, the GNU General Public License is
|
||||||
|
intended to guarantee your freedom to share and change free software--to
|
||||||
|
make sure the software is free for all its users. This General Public
|
||||||
|
License applies to most of the Free Software Foundation's software and to
|
||||||
|
any other program whose authors commit to using it. (Some other Free
|
||||||
|
Software Foundation software is covered by the GNU Library General Public
|
||||||
|
License instead.) You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price.
|
||||||
|
Our General Public Licenses are designed to make sure that you have the
|
||||||
|
freedom to distribute copies of free software (and charge for this service
|
||||||
|
if you wish), that you receive source code or can get it if you want it,
|
||||||
|
that you can change the software or use pieces of it in new free programs;
|
||||||
|
and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid anyone to
|
||||||
|
deny you these rights or to ask you to surrender the rights. These
|
||||||
|
restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or
|
||||||
|
for a fee, you must give the recipients all the rights that you have. You
|
||||||
|
must make sure that they, too, receive or can get the source code. And you
|
||||||
|
must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain that
|
||||||
|
everyone understands that there is no warranty for this free software. If
|
||||||
|
the software is modified by someone else and passed on, we want its
|
||||||
|
recipients to know that what they have is not the original, so that any
|
||||||
|
problems introduced by others will not reflect on the original authors'
|
||||||
|
reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software patents.
|
||||||
|
We wish to avoid the danger that redistributors of a free program will
|
||||||
|
individually obtain patent licenses, in effect making the program
|
||||||
|
proprietary. To prevent this, we have made it clear that any patent must
|
||||||
|
be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains a
|
||||||
|
notice placed by the copyright holder saying it may be distributed under
|
||||||
|
the terms of this General Public License. The "Program", below, refers to
|
||||||
|
any such program or work, and a "work based on the Program" means either
|
||||||
|
the Program or any derivative work under copyright law: that is to say, a
|
||||||
|
work containing the Program or a portion of it, either verbatim or with
|
||||||
|
modifications and/or translated into another language. (Hereinafter,
|
||||||
|
translation is included without limitation in the term "modification".)
|
||||||
|
Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of running
|
||||||
|
the Program is not restricted, and the output from the Program is covered
|
||||||
|
only if its contents constitute a work based on the Program (independent
|
||||||
|
of having been made by running the Program). Whether that is true
|
||||||
|
depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's source
|
||||||
|
code as you receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice and
|
||||||
|
disclaimer of warranty; keep intact all the notices that refer to this
|
||||||
|
License and to the absence of any warranty; and give any other recipients
|
||||||
|
of the Program a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion of it,
|
||||||
|
thus forming a work based on the Program, and copy and distribute such
|
||||||
|
modifications or work under the terms of Section 1 above, provided that
|
||||||
|
you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices stating
|
||||||
|
that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in whole
|
||||||
|
or in part contains or is derived from the Program or any part thereof,
|
||||||
|
to be licensed as a whole at no charge to all third parties under the
|
||||||
|
terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively when
|
||||||
|
run, you must cause it, when started running for such interactive use in
|
||||||
|
the most ordinary way, to print or display an announcement including an
|
||||||
|
appropriate copyright notice and a notice that there is no warranty (or
|
||||||
|
else, saying that you provide a warranty) and that users may redistribute
|
||||||
|
the program under these conditions, and telling the user how to view a
|
||||||
|
copy of this License. (Exception: if the Program itself is interactive
|
||||||
|
but does not normally print such an announcement, your work based on the
|
||||||
|
Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program, and
|
||||||
|
can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based on
|
||||||
|
the Program, the distribution of the whole must be on the terms of this
|
||||||
|
License, whose permissions for other licensees extend to the entire whole,
|
||||||
|
and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or collective
|
||||||
|
works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of a
|
||||||
|
storage or distribution medium does not bring the other work under the
|
||||||
|
scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it, under
|
||||||
|
Section 2) in object code or executable form under the terms of Sections 1
|
||||||
|
and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable source
|
||||||
|
code, which must be distributed under the terms of Sections 1 and 2 on a
|
||||||
|
medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three years, to
|
||||||
|
give any third party, for a charge no more than your cost of physically
|
||||||
|
performing source distribution, a complete machine-readable copy of the
|
||||||
|
corresponding source code, to be distributed under the terms of Sections
|
||||||
|
1 and 2 on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer to
|
||||||
|
distribute corresponding source code. (This alternative is allowed only
|
||||||
|
for noncommercial distribution and only if you received the program in
|
||||||
|
object code or executable form with such an offer, in accord with
|
||||||
|
Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source code
|
||||||
|
means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to control
|
||||||
|
compilation and installation of the executable. However, as a special
|
||||||
|
exception, the source code distributed need not include anything that is
|
||||||
|
normally distributed (in either source or binary form) with the major
|
||||||
|
components (compiler, kernel, and so on) of the operating system on which
|
||||||
|
the executable runs, unless that component itself accompanies the
|
||||||
|
executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering access
|
||||||
|
to copy from a designated place, then offering equivalent access to copy
|
||||||
|
the source code from the same place counts as distribution of the source
|
||||||
|
code, even though third parties are not compelled to copy the source
|
||||||
|
along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program except
|
||||||
|
as expressly provided under this License. Any attempt otherwise to copy,
|
||||||
|
modify, sublicense or distribute the Program is void, and will
|
||||||
|
automatically terminate your rights under this License. However, parties
|
||||||
|
who have received copies, or rights, from you under this License will not
|
||||||
|
have their licenses terminated so long as such parties remain in full
|
||||||
|
compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not signed
|
||||||
|
it. However, nothing else grants you permission to modify or distribute
|
||||||
|
the Program or its derivative works. These actions are prohibited by law
|
||||||
|
if you do not accept this License. Therefore, by modifying or
|
||||||
|
distributing the Program (or any work based on the Program), you
|
||||||
|
indicate your acceptance of this License to do so, and all its terms and
|
||||||
|
conditions for copying, distributing or modifying the Program or works
|
||||||
|
based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the original
|
||||||
|
licensor to copy, distribute or modify the Program subject to these terms
|
||||||
|
and conditions. You may not impose any further restrictions on the
|
||||||
|
recipients' exercise of the rights granted herein. You are not
|
||||||
|
responsible for enforcing compliance by third parties to this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot distribute
|
||||||
|
so as to satisfy simultaneously your obligations under this License and
|
||||||
|
any other pertinent obligations, then as a consequence you may not
|
||||||
|
distribute the Program at all. For example, if a patent license would not
|
||||||
|
permit royalty-free redistribution of the Program by all those who
|
||||||
|
receive copies directly or indirectly through you, then the only way you
|
||||||
|
could satisfy both it and this License would be to refrain entirely from
|
||||||
|
distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply
|
||||||
|
and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any such
|
||||||
|
claims; this section has the sole purpose of protecting the integrity of
|
||||||
|
the free software distribution system, which is implemented by public
|
||||||
|
license practices. Many people have made generous contributions to the
|
||||||
|
wide range of software distributed through that system in reliance on
|
||||||
|
consistent application of that system; it is up to the author/donor to
|
||||||
|
decide if he or she is willing to distribute software through any other
|
||||||
|
system and a licensee cannot impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to be a
|
||||||
|
consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License may add
|
||||||
|
an explicit geographical distribution limitation excluding those countries,
|
||||||
|
so that distribution is permitted only in or among countries not thus
|
||||||
|
excluded. In such case, this License incorporates the limitation as if
|
||||||
|
written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the General Public License from time to time. Such new versions will be
|
||||||
|
similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free
|
||||||
|
Software Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals of
|
||||||
|
preserving the free status of all derivatives of our free software and of
|
||||||
|
promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
# Scitano 1.0
|
||||||
|
|
||||||
|
**Jeremy Anderson** — info@dcos.net
|
||||||
|
https://git.dcos.net/dcosnet/scitano/
|
||||||
|
|
||||||
|
A hotkey-first programmer's editor built in Rust on the iced GUI framework. Scitano combines the menu structure of SciTE, strict GNU nano hotkeys, and Geany-inspired sidebar and context features into a single unified editing environment.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Strict Nano Hotkeys** — `^K`/`^U` kill-ring, `^O` writeout, `^X` exit, `^W` search, and more — all faithful to GNU nano behavior
|
||||||
|
- **SciTE-Style Menus** — File, Edit, Search, View, Build, Tools, Syntax, Options, Help, and AI-Bridge menus across the top
|
||||||
|
- **Geany-Inspired Sidebar** — Symbol tree auto-parses `fn`, `struct`, `impl`, `enum`, `trait`, `class`, `def`, `mod`, and `macro_rules!` definitions
|
||||||
|
- **Multi-Tab Editing** — Multiple buffers with dirty indicators (`●`), right-click close, and `+` to create new tabs
|
||||||
|
- **Syntax Highlighting** — 20+ languages: Rust, Python, C/C++, Shell, JavaScript, TypeScript, Markdown, HTML, CSS, JSON, XML, YAML, Go, Ruby, PHP, Java, C#, SQL, and more
|
||||||
|
- **Dual Highlight Tones** — CandyPop (Base16 Ocean) and Matte (Base16 Mocha) switchable from the View menu
|
||||||
|
- **Bottom Panel** — Messages, Diagnostics, Build Logs, and AI Terminal panes with color-coded output
|
||||||
|
- **Right-Click Context Menu** — Quick access to Save, Close, Build, Lint, Comment/Uncomment, and case transforms
|
||||||
|
- **Build Integration** — Compile, build-and-run, run-only, and lint with output routed to the Build Logs pane
|
||||||
|
- **AI Bridge** — Aider and Hermes/Odysseus integration points for AI-assisted editing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Rust 1.70+ (edition 2021)
|
||||||
|
- A system display server (X11 or Wayland with XWayland)
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.dcos.net/dcosnet/scitano.git
|
||||||
|
cd scitano
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the included build script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x build.sh
|
||||||
|
./build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The release binary will be at `target/release/scitano`.
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./target/release/scitano
|
||||||
|
```
|
||||||
|
|
||||||
|
Scitano launches with two sample buffers (`hello.rs` and `script.py`) so you can start editing immediately.
|
||||||
|
|
||||||
|
### Install (system-wide)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hotkey Reference
|
||||||
|
|
||||||
|
All hotkeys follow GNU nano conventions. `^` denotes `Ctrl`. Hotkeys are active when no text is selected and the editor has focus.
|
||||||
|
|
||||||
|
### Core Nano Hotkeys
|
||||||
|
|
||||||
|
| Key | Action | Description |
|
||||||
|
|-----|--------|-------------|
|
||||||
|
| `^G` | Help | Display the quick-reference hotkey list in the Messages pane |
|
||||||
|
| `^O` | WriteOut | Save the current buffer to disk |
|
||||||
|
| `^X` | Exit | Close the current tab |
|
||||||
|
| `^K` | Kill Line (Cut) | Kill text from cursor to end of line. Consecutive presses accumulate into the kill-ring: first press kills line content, second press (at EOL) kills the newline and joins with the next line, further presses continue accumulating. Any non-`^K` action resets the accumulation. |
|
||||||
|
| `^U` | Unkill (Paste/Yank) | Insert the entire kill-ring at the cursor position. Cursor advances to the end of the pasted text. Resets the consecutive-kill flag. |
|
||||||
|
| `^C` | Cursor Position | Display current buffer info in the Messages pane (line count, byte size, syntax, file path) |
|
||||||
|
| `^W` | Where Is (Search) | Trigger search (opens Search menu) |
|
||||||
|
| `^\` | Replace | Trigger find-and-replace |
|
||||||
|
| `^_` | Go To Line | Display line count for the current buffer |
|
||||||
|
| `^R` | Read File | Insert file at cursor (opens File menu) |
|
||||||
|
| `^J` | Justify | Paragraph reflow |
|
||||||
|
| `^T` | Spell Check | Requires system `aspell` |
|
||||||
|
|
||||||
|
### Cursor Movement
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `^A` | Move to start of current line |
|
||||||
|
| `^E` | Move to end of current line |
|
||||||
|
| `^Y` | Page Up |
|
||||||
|
| `^V` | Page Down |
|
||||||
|
| `^D` | Delete character at cursor |
|
||||||
|
| `^I` | Insert tab |
|
||||||
|
|
||||||
|
### Build & Lint
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `^B` | Build and run the current file |
|
||||||
|
| `^L` | Lint syntax of the current file |
|
||||||
|
|
||||||
|
### Function Keys
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `F3` | Open file |
|
||||||
|
| `F5` | Execute / run |
|
||||||
|
| `F11` | Toggle full screen |
|
||||||
|
|
||||||
|
### Mouse
|
||||||
|
|
||||||
|
| Action | Effect |
|
||||||
|
|--------|--------|
|
||||||
|
| Right-click on tab | Close that tab |
|
||||||
|
| Right-click in editor | Open context menu (Save, Build, Lint, Comment, etc.) |
|
||||||
|
|
||||||
|
### Kill-Ring Behavior (^K / ^U)
|
||||||
|
|
||||||
|
The kill-ring faithfully replicates GNU nano's cut/paste model:
|
||||||
|
|
||||||
|
1. **Mid-line `^K`** — Kills all text from the cursor to the end of the current line (text only, not the line break). Cursor stays in place.
|
||||||
|
2. **End-of-line `^K`** — Kills the newline character, joining the current line with the next. Cursor sits at the junction point.
|
||||||
|
3. **Empty-line `^K`** — Same as end-of-line: removes the line by joining with the next.
|
||||||
|
4. **Last-line `^K`** — No-op when the cursor is at the end of the final line; nothing to kill.
|
||||||
|
5. **Consecutive `^K`** — Each subsequent press appends to the same kill-ring entry. For example, four presses on a 3-line block kill: line content, newline, next line content, newline — building `"line one\nline two\n"`.
|
||||||
|
6. **`^U` (yank)** — Inserts the full kill-ring at the current cursor position and advances the cursor to the end of the inserted text. Resets the consecutive-kill flag.
|
||||||
|
7. **Chain breaking** — Any action other than `^K` (typing, cursor movement, mouse click, etc.) resets the kill chain so the next `^K` starts a fresh kill-ring entry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Menu Structure
|
||||||
|
|
||||||
|
### File
|
||||||
|
`New` · `Open (F3)` · `Open Selected` · `^O WriteOut (Save)` · `Save As...` · `^X Close Tab` · `Reload File` · `Save Session` · `Load Session` · `Print...` · `Exit App`
|
||||||
|
|
||||||
|
### Edit
|
||||||
|
`Undo (^Z)` · `Redo (^Y)` · `^K Kill Line (Cut)` · `^U Unkill (Paste)` · `^C Cur Pos` · `^A Select All` · `Delete Line` · `Duplicate Line` · `Transpose Line` · `Match Brace (^E)` · `Comment Line` · `Uncomment Line` · `Increase Indent` · `Decrease Indent`
|
||||||
|
|
||||||
|
### Search
|
||||||
|
`^W Where Is (Find)` · `Find Next (F3)` · `Find Previous (Shift+F3)` · `^\ Replace` · `Replace Next` · `Go To Line (^_)` · `Toggle Bookmark (^B)` · `Next Bookmark` · `Prev Bookmark` · `Clear All Bookmarks`
|
||||||
|
|
||||||
|
### View
|
||||||
|
`Toggle Full Screen (F11)` · `Toggle Message Window` · `Toggle Sidebar` · `Toggle Line Numbers` · `Toggle Whitespace` · `Toggle Line Endings` · `Word Wrap` · `Toggle Syntax Tone` · `Fold All` · `Unfold All` · `Toggle Current Fold`
|
||||||
|
|
||||||
|
### Build
|
||||||
|
`Compile` · `Build` · `^B Build & Run` · `Run (F5)` · `^L Lint Syntax` · `Stop Executing` · `Clear Output` · `Next Message` · `Previous Message`
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
`Run Command...` · `Run Lua Script (F5)`
|
||||||
|
|
||||||
|
### Syntax
|
||||||
|
`Rust` · `Python` · `C/C++` · `Shell` · `JavaScript` · `TypeScript` · `Markdown` · `HTML` · `CSS` · `JSON` · `XML` · `YAML` · `Go` · `Ruby` · `PHP` · `Java` · `C#` · `SQL` · `Clear Override`
|
||||||
|
|
||||||
|
### Options
|
||||||
|
`Global Properties` · `Open Abbreviations` · `User Properties` · `Local Properties`
|
||||||
|
|
||||||
|
### Help
|
||||||
|
`^G Help` · `About`
|
||||||
|
|
||||||
|
### AI
|
||||||
|
`Sync Aider Models` · `Aider: Architect Mode` · `Aider: Code Review` · `Aider: Refactor Buffer` · `Hermes: Local Inference` · `Hermes: System Prompt` · `Odysseus: Crawl Context` · `Odysseus: Vector Sync`
|
||||||
|
|
||||||
|
### Editor Context (right-click)
|
||||||
|
`^O Save Buffer` · `^X Close Tab` · `^B Build File` · `^L Lint Syntax` · `Comment Selection` · `Uncomment Selection` · `Upper Case` · `Lower Case` · `Insert Timestamp`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
Syntax highlighting is auto-detected by file extension. Manual override is available via the Syntax menu.
|
||||||
|
|
||||||
|
| Extension | Language | Extension | Language |
|
||||||
|
|-----------|----------|-----------|----------|
|
||||||
|
| `.rs` | Rust | `.rb` | Ruby |
|
||||||
|
| `.py` `.pyw` | Python | `.php` `.phtml` | PHP |
|
||||||
|
| `.c` `.h` | C | `.java` | Java |
|
||||||
|
| `.cpp` `.hpp` `.cc` `.cxx` | C++ | `.cs` | C# |
|
||||||
|
| `.sh` `.bash` `.zsh` | Shell | `.sql` | SQL |
|
||||||
|
| `.js` `.mjs` `.cjs` | JavaScript | `.r` | R |
|
||||||
|
| `.ts` `.tsx` | TypeScript | `.lua` | Lua |
|
||||||
|
| `.md` | Markdown | `.pl` `.pm` | Perl |
|
||||||
|
| `.html` `.htm` | HTML | `.ex` `.exs` | Elixir |
|
||||||
|
| `.css` `.scss` `.sass` `.less` | CSS | `.hs` | Haskell |
|
||||||
|
| `.json` | JSON | `.scala` | Scala |
|
||||||
|
| `.xml` | XML | `.swift` | Swift |
|
||||||
|
| `.yaml` `.yml` | YAML | `.kt` `.kts` | Kotlin |
|
||||||
|
| `.toml` | TOML | `.dart` | Dart |
|
||||||
|
| `.ps1` `.psm1` | PowerShell | `.bat` `.cmd` | Batch |
|
||||||
|
| `.conf` `.cfg` `.ini` | Config/INI | `.vbs` | VBScript |
|
||||||
|
| Dockerfile / Containerfile | Dockerfile | `.go` | Go |
|
||||||
|
|
||||||
|
Special filenames: `Makefile`, `*.rc` (Shell), `nginx*` (Nginx), `apache*`/`httpd*` (Apache)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
scitano/
|
||||||
|
├── Cargo.toml # Package manifest (iced 0.13, edition 2021, GPLv2)
|
||||||
|
├── Cargo.lock # Dependency lockfile
|
||||||
|
├── LICENSE # GPLv2 full text
|
||||||
|
├── build.sh # Release build script
|
||||||
|
├── screenshot.png # Application screenshot
|
||||||
|
├── README.md # This file
|
||||||
|
└── src/
|
||||||
|
├── main.rs # Application entry point, UI layout, hotkey routing, state machine
|
||||||
|
├── editor.rs # EditorTab: buffer model, file I/O, syntax detection, symbol parsing
|
||||||
|
├── config.rs # User configuration (tab width, wrap, auto-save)
|
||||||
|
└── theme.rs # Dark theme: color constants, panel/button/container styles
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GPLv2 — see [LICENSE](LICENSE) for the full text.
|
||||||
|
|
||||||
|
Jeremy Anderson — info@dcos.net — https://git.dcos.net/dcosnet/scitano/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/bash
|
||||||
|
CARGO_INCREMENTAL=0 cargo build --release
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub default_tone: String,
|
||||||
|
pub word_wrap: bool,
|
||||||
|
pub show_line_numbers: bool,
|
||||||
|
pub tab_width: usize,
|
||||||
|
pub auto_save_before_build: bool,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
use iced::widget::text_editor;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub struct EditorTab {
|
||||||
|
pub title: String,
|
||||||
|
pub content: text_editor::Content,
|
||||||
|
pub is_dirty: bool,
|
||||||
|
pub file_path: Option<PathBuf>,
|
||||||
|
pub override_syntax: Option<String>,
|
||||||
|
pub symbols: Vec<(usize, String)>,
|
||||||
|
pub bookmarks: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditorTab {
|
||||||
|
pub fn new(title: &str, initial_text: &str) -> Self {
|
||||||
|
let mut tab = Self {
|
||||||
|
title: title.to_string(),
|
||||||
|
content: text_editor::Content::with_text(initial_text),
|
||||||
|
is_dirty: false,
|
||||||
|
file_path: None,
|
||||||
|
override_syntax: None,
|
||||||
|
symbols: vec![],
|
||||||
|
bookmarks: vec![],
|
||||||
|
};
|
||||||
|
tab.parse_symbols();
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_symbols(&mut self) {
|
||||||
|
self.symbols.clear();
|
||||||
|
for (i, line) in self.content.text().lines().enumerate() {
|
||||||
|
let t = line.trim_start();
|
||||||
|
if t.starts_with("fn ")
|
||||||
|
|| t.starts_with("def ")
|
||||||
|
|| t.starts_with("class ")
|
||||||
|
|| t.starts_with("struct ")
|
||||||
|
|| t.starts_with("impl ")
|
||||||
|
|| t.starts_with("enum ")
|
||||||
|
|| t.starts_with("trait ")
|
||||||
|
|| t.starts_with("mod ")
|
||||||
|
|| t.starts_with("pub fn ")
|
||||||
|
|| t.starts_with("pub struct ")
|
||||||
|
|| t.starts_with("pub enum ")
|
||||||
|
|| t.starts_with("pub trait ")
|
||||||
|
|| t.starts_with("pub mod ")
|
||||||
|
|| t.starts_with("macro_rules!")
|
||||||
|
{
|
||||||
|
let name = t
|
||||||
|
.split('{')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(t)
|
||||||
|
.split('(')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(t)
|
||||||
|
.split('!')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(t)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
self.symbols.push((i + 1, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_bookmark(&mut self, line: usize) {
|
||||||
|
if self.bookmarks.contains(&line) {
|
||||||
|
self.bookmarks.retain(|&b| b != line);
|
||||||
|
} else {
|
||||||
|
self.bookmarks.push(line);
|
||||||
|
self.bookmarks.sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&mut self) -> Result<String, std::io::Error> {
|
||||||
|
let path = self
|
||||||
|
.file_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| PathBuf::from(&self.title));
|
||||||
|
let text = self.content.text();
|
||||||
|
fs::write(&path, text)?;
|
||||||
|
self.is_dirty = false;
|
||||||
|
self.file_path = Some(path.clone());
|
||||||
|
Ok(path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_as(&mut self, new_path: &str) -> Result<String, std::io::Error> {
|
||||||
|
let path = PathBuf::from(new_path);
|
||||||
|
let text = self.content.text();
|
||||||
|
fs::write(&path, text)?;
|
||||||
|
self.is_dirty = false;
|
||||||
|
self.file_path = Some(path.clone());
|
||||||
|
self.title = path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
Ok(path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn line_count(&self) -> usize {
|
||||||
|
self.content.text().lines().count().max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn byte_size(&self) -> usize {
|
||||||
|
self.content.text().len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_syntax(&self) -> &'static str {
|
||||||
|
if let Some(ref syn) = self.override_syntax {
|
||||||
|
return match syn.as_str() {
|
||||||
|
"Rust" => "rs",
|
||||||
|
"Python" => "py",
|
||||||
|
"C/C++" | "C" | "C++" => "cpp",
|
||||||
|
"Shell" | "Bash" => "sh",
|
||||||
|
"JavaScript" => "js",
|
||||||
|
"TypeScript" => "ts",
|
||||||
|
"Markdown" => "md",
|
||||||
|
"HTML" => "html",
|
||||||
|
"CSS" => "css",
|
||||||
|
"JSON" => "json",
|
||||||
|
"XML" => "xml",
|
||||||
|
"YAML" => "yaml",
|
||||||
|
"Go" => "go",
|
||||||
|
"Ruby" => "rb",
|
||||||
|
"PHP" => "php",
|
||||||
|
"Java" => "java",
|
||||||
|
"C#" => "cs",
|
||||||
|
"SQL" => "sql",
|
||||||
|
_ => "txt",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = self.title.to_lowercase();
|
||||||
|
if name.contains("dockerfile") || name.contains("containerfile") {
|
||||||
|
return "dockerfile";
|
||||||
|
}
|
||||||
|
if name.contains("nginx") {
|
||||||
|
return "nginx";
|
||||||
|
}
|
||||||
|
if name.contains("apache") || name.contains("httpd") {
|
||||||
|
return "apache";
|
||||||
|
}
|
||||||
|
if name.ends_with("rc") {
|
||||||
|
return "sh";
|
||||||
|
}
|
||||||
|
if name.ends_with("makefile") || name == "makefile" {
|
||||||
|
return "makefile";
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts: Vec<&str> = name.split('.').collect();
|
||||||
|
if let Some(ext) = parts.last() {
|
||||||
|
match *ext {
|
||||||
|
"rs" => "rs",
|
||||||
|
"sh" | "bash" | "zsh" | "command" => "sh",
|
||||||
|
"ps1" | "psm1" => "ps1",
|
||||||
|
"bat" | "cmd" | "dosbat" => "bat",
|
||||||
|
"vbs" | "vbe" | "wscript" | "vbasic" => "vbs",
|
||||||
|
"conf" | "cfg" | "ini" | "service" | "target" => "ini",
|
||||||
|
"yaml" | "yml" => "yaml",
|
||||||
|
"toml" => "toml",
|
||||||
|
"json" => "json",
|
||||||
|
"xml" => "xml",
|
||||||
|
"html" | "htm" | "xhtml" => "html",
|
||||||
|
"css" | "scss" | "sass" | "less" => "css",
|
||||||
|
"js" | "mjs" | "cjs" => "js",
|
||||||
|
"ts" | "tsx" => "ts",
|
||||||
|
"py" | "pyw" => "py",
|
||||||
|
"c" | "h" => "c",
|
||||||
|
"cpp" | "hpp" | "cc" | "cxx" | "hxx" => "cpp",
|
||||||
|
"go" => "go",
|
||||||
|
"rb" => "rb",
|
||||||
|
"php" | "phtml" => "php",
|
||||||
|
"java" => "java",
|
||||||
|
"cs" => "cs",
|
||||||
|
"sql" => "sql",
|
||||||
|
"md" | "markdown" => "md",
|
||||||
|
"r" => "r",
|
||||||
|
"lua" => "lua",
|
||||||
|
"pl" | "pm" => "pl",
|
||||||
|
"ex" | "exs" => "ex",
|
||||||
|
"hs" => "hs",
|
||||||
|
"scala" => "scala",
|
||||||
|
"swift" => "swift",
|
||||||
|
"kt" | "kts" => "kt",
|
||||||
|
"dart" => "dart",
|
||||||
|
_ => "txt",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,727 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
// Scitano v1.0.0 -- Unified Editor Engine
|
||||||
|
// SciTE menus | Strict Nano hotkeys | Geany sidebar/context features
|
||||||
|
|
||||||
|
use iced::widget::{
|
||||||
|
button, column, container, mouse_area, row, scrollable, text, text_editor, Space,
|
||||||
|
};
|
||||||
|
use iced::{Element, Length, Task, Theme, Font, Subscription};
|
||||||
|
use iced::keyboard::{Key, Modifiers};
|
||||||
|
use iced::highlighter::Theme as HighlighterTheme;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
mod theme;
|
||||||
|
mod config;
|
||||||
|
mod editor;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum HighlightTone { CandyPop, Matte }
|
||||||
|
|
||||||
|
impl HighlightTone {
|
||||||
|
fn to_iced_theme(&self) -> HighlighterTheme {
|
||||||
|
match self {
|
||||||
|
HighlightTone::CandyPop => HighlighterTheme::Base16Ocean,
|
||||||
|
HighlightTone::Matte => HighlighterTheme::Base16Mocha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum BottomTab { Diagnostics, BuildLogs, AiTerminal, Messages }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
MenuToggle(String),
|
||||||
|
SubMenuAction(String),
|
||||||
|
TabSelected(usize),
|
||||||
|
CloseTab(usize),
|
||||||
|
BottomTabSelected(BottomTab),
|
||||||
|
EditorAction(text_editor::Action),
|
||||||
|
EditorRightClick,
|
||||||
|
KeyboardEvent(Key, Modifiers),
|
||||||
|
SyncAiderModels,
|
||||||
|
ToggleWordWrap(bool),
|
||||||
|
ToggleLineNumbers(bool),
|
||||||
|
ToggleSymbolTree(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Scitano {
|
||||||
|
config: config::Config,
|
||||||
|
active_menu: Option<String>,
|
||||||
|
active_tab: usize,
|
||||||
|
highlight_tone: HighlightTone,
|
||||||
|
tabs: Vec<editor::EditorTab>,
|
||||||
|
bottom_tab: BottomTab,
|
||||||
|
chat_log: Vec<String>,
|
||||||
|
aider_models: Vec<String>,
|
||||||
|
kill_ring: String,
|
||||||
|
last_action_was_kill: bool,
|
||||||
|
show_symbol_tree: bool,
|
||||||
|
search_term: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Scitano {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
config: config::Config {
|
||||||
|
default_tone: "CandyPop".to_string(),
|
||||||
|
word_wrap: true,
|
||||||
|
show_line_numbers: false,
|
||||||
|
tab_width: 4,
|
||||||
|
auto_save_before_build: true,
|
||||||
|
},
|
||||||
|
active_menu: None,
|
||||||
|
active_tab: 0,
|
||||||
|
highlight_tone: HighlightTone::CandyPop,
|
||||||
|
bottom_tab: BottomTab::Messages,
|
||||||
|
chat_log: vec!["sys> Scitano v1.0.0 initialized. Nano hotkeys active. Type ^G for help.".to_string()],
|
||||||
|
aider_models: vec![],
|
||||||
|
kill_ring: String::new(),
|
||||||
|
last_action_was_kill: false,
|
||||||
|
show_symbol_tree: true,
|
||||||
|
search_term: String::new(),
|
||||||
|
tabs: vec![
|
||||||
|
editor::EditorTab::new("hello.rs",
|
||||||
|
"fn main() {\n println!(\"Hello from Scitano v1.0.0!\");\n}\n\nfn greet(name: &str) {\n println!(\"Welcome, {}!\", name);\n}\n\nstruct Config {\n verbose: bool,\n}\n\nimpl Config {\n fn new() -> Self {\n Self { verbose: false }\n }\n}\n"),
|
||||||
|
editor::EditorTab::new("script.py",
|
||||||
|
"import os\nimport sys\n\ndef main():\n print('Python works in Scitano!')\n print(f'CWD: {os.getcwd()}')\n\nclass Runner:\n def __init__(self):\n self.ready = True\n\n def run(self):\n if self.ready:\n main()\n\nif __name__ == '__main__':\n Runner().run()\n"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scitano {
|
||||||
|
// -- Nano Keybindings (STRICT) --------------------------------
|
||||||
|
fn nano_help(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
for line in &[
|
||||||
|
"nano> -- Scitano Quick Reference --",
|
||||||
|
"nano> ^G=Help ^O=Save ^R=Insert ^W=Search ^\\=Replace ^_=GotoLine",
|
||||||
|
"nano> ^K=Cut ^U=Paste ^C=CurPos ^X=Exit ^Y=PageUp ^V=PageDn",
|
||||||
|
"nano> ^A=LineStart ^E=LineEnd ^J=Justify ^T=Spell ^D=DelChar",
|
||||||
|
"nano> ^I=Tab ^B=Build ^L=Lint F3=Open F5=Exec",
|
||||||
|
] {
|
||||||
|
self.chat_log.push(line.to_string());
|
||||||
|
}
|
||||||
|
self.bottom_tab = BottomTab::Messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nano_writeout(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if self.tabs.is_empty() { return; }
|
||||||
|
match self.tabs[self.active_tab].save() {
|
||||||
|
Ok(path) => self.chat_log.push(format!("nano> [^O] Written to '{}'", path)),
|
||||||
|
Err(e) => self.chat_log.push(format!("sys> [ERROR] WriteOut failed: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nano_exit(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if self.tabs.is_empty() { return; }
|
||||||
|
let title = self.tabs[self.active_tab].title.clone();
|
||||||
|
self.tabs.remove(self.active_tab);
|
||||||
|
if self.tabs.is_empty() { self.active_tab = 0; }
|
||||||
|
else if self.active_tab >= self.tabs.len() { self.active_tab = self.tabs.len() - 1; }
|
||||||
|
self.chat_log.push(format!("nano> [^X] Closed buffer: {}", title));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- ^K: Kill from cursor to end of line (nano-accurate) ----------
|
||||||
|
//
|
||||||
|
// Behavior matches GNU nano:
|
||||||
|
// 1. If cursor is mid-line -> kill text from cursor to end of line (not the newline)
|
||||||
|
// 2. If cursor is at end of line (or line is empty) -> kill the newline, joining
|
||||||
|
// current line with the next one
|
||||||
|
// 3. At end of last line -> no-op
|
||||||
|
// 4. Consecutive ^K presses -> append into the same kill-ring entry
|
||||||
|
//
|
||||||
|
// NOTE: iced 0.13.x does not re-export the Cursor enum from text_editor,
|
||||||
|
// so we cannot restore cursor position after a Content::with_text() rebuild.
|
||||||
|
// Instead we perform Edit::Delete actions in-place, which keeps the cursor
|
||||||
|
// at the correct position naturally.
|
||||||
|
//
|
||||||
|
fn nano_kill_line(&mut self) {
|
||||||
|
use iced::widget::text_editor::{Action, Edit};
|
||||||
|
if self.tabs.is_empty() { return; }
|
||||||
|
let tab = &mut self.tabs[self.active_tab];
|
||||||
|
let (cur_line, cur_col) = tab.content.cursor_position();
|
||||||
|
let text = tab.content.text();
|
||||||
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
|
|
||||||
|
if lines.is_empty() || cur_line >= lines.len() {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_line = lines[cur_line];
|
||||||
|
let killed: String;
|
||||||
|
|
||||||
|
if cur_col < current_line.len() {
|
||||||
|
// Case 1: cursor is mid-line -> kill text to end of line
|
||||||
|
// Perform Delete for each character; cursor stays in place.
|
||||||
|
killed = current_line[cur_col..].to_string();
|
||||||
|
for _ in 0..killed.chars().count() {
|
||||||
|
tab.content.perform(Action::Edit(Edit::Delete));
|
||||||
|
}
|
||||||
|
} else if cur_line + 1 < lines.len() {
|
||||||
|
// Case 2: cursor at EOL (or empty line) -> kill the newline.
|
||||||
|
// A single Delete at EOL joins this line with the next.
|
||||||
|
killed = "\n".to_string();
|
||||||
|
tab.content.perform(Action::Edit(Edit::Delete));
|
||||||
|
} else {
|
||||||
|
// Case 3: at end of last line -> nothing to kill
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accumulate into kill-ring (append on consecutive kills, like nano)
|
||||||
|
if self.last_action_was_kill {
|
||||||
|
self.kill_ring.push_str(&killed);
|
||||||
|
} else {
|
||||||
|
self.kill_ring = killed;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.is_dirty = true;
|
||||||
|
tab.parse_symbols();
|
||||||
|
self.last_action_was_kill = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- ^U: Unkill (yank) kill-ring at cursor position (nano-accurate) --
|
||||||
|
//
|
||||||
|
// Inserts the full kill-ring contents at the current cursor position.
|
||||||
|
// Uses Edit::Paste which handles cursor advancement automatically.
|
||||||
|
//
|
||||||
|
fn nano_unkill(&mut self) {
|
||||||
|
use iced::widget::text_editor::{Action, Edit};
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if self.tabs.is_empty() || self.kill_ring.is_empty() { return; }
|
||||||
|
let tab = &mut self.tabs[self.active_tab];
|
||||||
|
|
||||||
|
// Paste inserts at cursor and advances cursor to end of pasted text
|
||||||
|
tab.content.perform(Action::Edit(Edit::Paste(self.kill_ring.clone())));
|
||||||
|
tab.is_dirty = true;
|
||||||
|
tab.parse_symbols();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nano_cur_pos(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if self.tabs.is_empty() { return; }
|
||||||
|
let tab = &self.tabs[self.active_tab];
|
||||||
|
let text = tab.content.text();
|
||||||
|
let total_lines = text.lines().count();
|
||||||
|
let total_chars = text.len();
|
||||||
|
let size_kb = total_chars as f64 / 1024.0;
|
||||||
|
let file_name = tab.file_path.as_ref()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| tab.title.clone());
|
||||||
|
self.chat_log.push(format!(
|
||||||
|
"nano> [^C] {} | Lines: {} | Chars: {} | Size: {:.2} KB | Syntax: {}",
|
||||||
|
file_name, total_lines, total_chars, size_kb, tab.get_syntax().to_uppercase()
|
||||||
|
));
|
||||||
|
self.bottom_tab = BottomTab::Messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nano_where_is(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
self.chat_log.push("nano> [^W] Where Is: Search triggered.".into());
|
||||||
|
self.bottom_tab = BottomTab::Messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nano_replace(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
self.chat_log.push("nano> [^\\] Replace: Use menu action for find/replace.".into());
|
||||||
|
self.bottom_tab = BottomTab::Messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nano_goto_line(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if self.tabs.is_empty() { return; }
|
||||||
|
self.chat_log.push(format!("nano> [^_] Go To Line: Buffer has {} lines.", self.tabs[self.active_tab].line_count()));
|
||||||
|
self.bottom_tab = BottomTab::Messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nano_read_file(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
self.chat_log.push("nano> [^R] Read File: Insert file at cursor. Use File > Open.".into());
|
||||||
|
self.bottom_tab = BottomTab::Messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nano_justify(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
self.chat_log.push("nano> [^J] Justify: Paragraph reflow triggered.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nano_spell(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
self.chat_log.push("nano> [^T] Spell Check: Requires system 'aspell'.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Build System (Geany-style: separate Compile/Build/Run) --
|
||||||
|
fn auto_save_if_dirty(&mut self) -> bool {
|
||||||
|
if self.tabs.is_empty() { return false; }
|
||||||
|
if self.config.auto_save_before_build {
|
||||||
|
if let Err(e) = self.tabs[self.active_tab].save() {
|
||||||
|
self.chat_log.push(format!("sys> [ERROR] Save failed: {}", e));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_only(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if self.tabs.is_empty() { return; }
|
||||||
|
if !self.auto_save_if_dirty() { self.bottom_tab = BottomTab::BuildLogs; return; }
|
||||||
|
let tab = &self.tabs[self.active_tab];
|
||||||
|
let syntax = tab.get_syntax();
|
||||||
|
let path_str = tab.file_path.as_ref().unwrap().to_string_lossy().to_string();
|
||||||
|
self.chat_log.push(format!("build> Compiling ({}) ...", syntax));
|
||||||
|
let (cmd, args) = match syntax {
|
||||||
|
"rs" => ("rustc", vec!["--emit=metadata".into(), "--error-format=short".into(), path_str]),
|
||||||
|
"c" => ("gcc", vec!["-fsyntax-only".into(), "-Wall".into(), path_str]),
|
||||||
|
"cpp" => ("g++", vec!["-fsyntax-only".into(), "-Wall".into(), path_str]),
|
||||||
|
"py" => ("python3", vec!["-m".into(), "py_compile".into(), path_str]),
|
||||||
|
"sh" | "bash" => ("shellcheck", vec!["--format=gcc".into(), path_str]),
|
||||||
|
"js" => ("node", vec!["--check".into(), path_str]),
|
||||||
|
_ => { self.chat_log.push(format!("build> [FAIL] No compiler for '{}'.", syntax)); self.bottom_tab = BottomTab::BuildLogs; return; }
|
||||||
|
};
|
||||||
|
match Command::new(cmd).args(&args).output() {
|
||||||
|
Ok(o) => { self.log_output(&o); if o.status.success() && String::from_utf8_lossy(&o.stdout).is_empty() && String::from_utf8_lossy(&o.stderr).is_empty() { self.chat_log.push("build> Compile: OK".into()); } }
|
||||||
|
Err(e) => self.chat_log.push(format!("sys> [ERROR] {} missing: {}", cmd, e)),
|
||||||
|
}
|
||||||
|
self.bottom_tab = BottomTab::BuildLogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_and_run(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if self.tabs.is_empty() { return; }
|
||||||
|
if !self.auto_save_if_dirty() { self.bottom_tab = BottomTab::BuildLogs; return; }
|
||||||
|
let tab = &self.tabs[self.active_tab];
|
||||||
|
let syntax = tab.get_syntax();
|
||||||
|
let fp = tab.file_path.as_ref().unwrap();
|
||||||
|
let path_str = fp.to_string_lossy().to_string();
|
||||||
|
let stem = fp.file_stem().unwrap_or_default().to_string_lossy();
|
||||||
|
let dir = fp.parent().unwrap_or(std::path::Path::new(".")).to_string_lossy();
|
||||||
|
self.chat_log.push(format!("build> Build & Run ({}) ...", syntax));
|
||||||
|
let (cmd, args) = match syntax {
|
||||||
|
"rs" => ("sh", vec!["-c".into(), format!("rustc {} -o {}/{} && {}/{}", path_str, dir, stem, dir, stem)]),
|
||||||
|
"py" => ("python3", vec![path_str]),
|
||||||
|
"sh" | "bash" => ("bash", vec![path_str]),
|
||||||
|
"c" => ("sh", vec!["-c".into(), format!("gcc {} -o {}/{} -lm && {}/{}", path_str, dir, stem, dir, stem)]),
|
||||||
|
"cpp" => ("sh", vec!["-c".into(), format!("g++ {} -o {}/{} && {}/{}", path_str, dir, stem, dir, stem)]),
|
||||||
|
"js" => ("node", vec![path_str]),
|
||||||
|
"go" => ("go", vec!["run".into(), path_str]),
|
||||||
|
"rb" => ("ruby", vec![path_str]),
|
||||||
|
"lua" => ("lua", vec![path_str]),
|
||||||
|
"pl" => ("perl", vec![path_str]),
|
||||||
|
"bat" => ("cmd.exe", vec!["/c".into(), path_str]),
|
||||||
|
_ => { self.chat_log.push(format!("build> [FAIL] No runner for '{}'.", syntax)); self.bottom_tab = BottomTab::BuildLogs; return; }
|
||||||
|
};
|
||||||
|
match Command::new(cmd).args(&args).output() {
|
||||||
|
Ok(o) => self.log_output(&o),
|
||||||
|
Err(e) => self.chat_log.push(format!("sys> [ERROR] {}", e)),
|
||||||
|
}
|
||||||
|
self.bottom_tab = BottomTab::BuildLogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_only(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if self.tabs.is_empty() { return; }
|
||||||
|
let tab = &self.tabs[self.active_tab];
|
||||||
|
let fp = match &tab.file_path {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => { self.chat_log.push("sys> [ERROR] Run: No file path. Save first (^O).".into()); return; }
|
||||||
|
};
|
||||||
|
let path_str = fp.to_string_lossy().to_string();
|
||||||
|
let stem = fp.file_stem().unwrap_or_default().to_string_lossy();
|
||||||
|
let dir = fp.parent().unwrap_or(std::path::Path::new(".")).to_string_lossy();
|
||||||
|
let (cmd, args) = match tab.get_syntax() {
|
||||||
|
"rs" => ("sh", vec!["-c".into(), format!("{}/{}", dir, stem)]),
|
||||||
|
"py" => ("python3", vec![path_str]),
|
||||||
|
"sh" | "bash" => ("bash", vec![path_str]),
|
||||||
|
"js" => ("node", vec![path_str]),
|
||||||
|
"go" => ("go", vec!["run".into(), path_str]),
|
||||||
|
_ => { self.chat_log.push("build> [FAIL] No runner for this syntax.".into()); return; }
|
||||||
|
};
|
||||||
|
match Command::new(cmd).args(&args).output() {
|
||||||
|
Ok(o) => self.log_output(&o),
|
||||||
|
Err(e) => self.chat_log.push(format!("sys> [ERROR] {}", e)),
|
||||||
|
}
|
||||||
|
self.bottom_tab = BottomTab::BuildLogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lint_syntax(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if self.tabs.is_empty() { return; }
|
||||||
|
if !self.auto_save_if_dirty() { self.bottom_tab = BottomTab::Diagnostics; return; }
|
||||||
|
let tab = &self.tabs[self.active_tab];
|
||||||
|
let syntax = tab.get_syntax();
|
||||||
|
let path_str = tab.file_path.as_ref().unwrap().to_string_lossy().to_string();
|
||||||
|
self.chat_log.push(format!("diag> Linting: {}", syntax));
|
||||||
|
let (cmd, args) = match syntax {
|
||||||
|
"rs" => ("rustc", vec!["--emit=metadata".into(), "--error-format=short".into(), path_str]),
|
||||||
|
"py" => ("python3", vec!["-m".into(), "py_compile".into(), path_str]),
|
||||||
|
"sh" | "bash" => ("shellcheck", vec!["--format=gcc".into(), path_str]),
|
||||||
|
"c" => ("gcc", vec!["-fsyntax-only".into(), "-Wall".into(), "-Wextra".into(), path_str]),
|
||||||
|
"cpp" => ("g++", vec!["-fsyntax-only".into(), "-Wall".into(), "-Wextra".into(), path_str]),
|
||||||
|
"js" => ("node", vec!["--check".into(), path_str]),
|
||||||
|
_ => { self.chat_log.push(format!("diag> [SKIP] No linter for '{}'.", syntax)); self.bottom_tab = BottomTab::Diagnostics; return; }
|
||||||
|
};
|
||||||
|
match Command::new(cmd).args(&args).output() {
|
||||||
|
Ok(o) => {
|
||||||
|
self.log_output(&o);
|
||||||
|
let out = String::from_utf8_lossy(&o.stdout);
|
||||||
|
let err = String::from_utf8_lossy(&o.stderr);
|
||||||
|
if out.is_empty() && err.is_empty() && o.status.success() {
|
||||||
|
self.chat_log.push("diag> OK: No issues.".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => self.chat_log.push(format!("sys> [ERROR] Linter missing: {}", e)),
|
||||||
|
}
|
||||||
|
self.bottom_tab = BottomTab::Diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_output(&mut self, output: &std::process::Output) {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
for line in stdout.lines() { self.chat_log.push(format!("out> {}", line)); }
|
||||||
|
for line in stderr.lines() { self.chat_log.push(format!("err> {}", line)); }
|
||||||
|
if stdout.is_empty() && stderr.is_empty() {
|
||||||
|
self.chat_log.push("build> Execution completed (no output).".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_aider_models() -> Vec<String> {
|
||||||
|
let env_path = format!("{}/.local/bin:{}", std::env::var("HOME").unwrap_or_default(), std::env::var("PATH").unwrap_or_default());
|
||||||
|
if let Ok(out) = Command::new("aider").env("PATH", env_path).args(["--list-models", "ollama"]).output() {
|
||||||
|
let txt = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let mut models: Vec<String> = txt.lines().filter(|l| l.trim().starts_with("- ")).map(|l| l.trim().replace("- ", "")).collect();
|
||||||
|
models.sort(); models.dedup();
|
||||||
|
if !models.is_empty() { return models; }
|
||||||
|
}
|
||||||
|
vec!["ollama/llama3".into(), "ollama/mistral".into()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_session(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
let count = self.tabs.iter().filter(|t| t.file_path.is_some()).count();
|
||||||
|
self.chat_log.push(format!("session> Saved {} files in session.", count));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_session(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
self.chat_log.push("session> Load Session: Restore previously saved files.".into());
|
||||||
|
self.bottom_tab = BottomTab::Messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_bookmark(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if !self.tabs.is_empty() {
|
||||||
|
let tab = &mut self.tabs[self.active_tab];
|
||||||
|
tab.toggle_bookmark(1);
|
||||||
|
self.chat_log.push(format!("bookmark> Toggled. Total: {}", tab.bookmarks.len()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_bookmarks(&mut self) {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if !self.tabs.is_empty() {
|
||||||
|
self.tabs[self.active_tab].bookmarks.clear();
|
||||||
|
self.chat_log.push("bookmark> All cleared.".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
iced::keyboard::on_key_press(|key, modifiers| Some(Message::KeyboardEvent(key.clone(), modifiers)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
Message::KeyboardEvent(key, modifiers) => {
|
||||||
|
if modifiers.control() {
|
||||||
|
if let Key::Character(c) = &key {
|
||||||
|
match c.as_ref() {
|
||||||
|
"g" | "G" => self.nano_help(),
|
||||||
|
"o" | "O" => self.nano_writeout(),
|
||||||
|
"x" | "X" => self.nano_exit(),
|
||||||
|
"k" | "K" => self.nano_kill_line(),
|
||||||
|
"u" | "U" => self.nano_unkill(),
|
||||||
|
"c" | "C" => self.nano_cur_pos(),
|
||||||
|
"w" | "W" => self.nano_where_is(),
|
||||||
|
"r" | "R" => self.nano_read_file(),
|
||||||
|
"j" | "J" => self.nano_justify(),
|
||||||
|
"t" | "T" => self.nano_spell(),
|
||||||
|
"b" | "B" => self.build_and_run(),
|
||||||
|
"l" | "L" => self.lint_syntax(),
|
||||||
|
_ => { self.last_action_was_kill = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Key::Character(c) = &key {
|
||||||
|
if c.as_ref() == "_" { self.nano_goto_line(); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::MenuToggle(name) => {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
self.active_menu = if self.active_menu.as_deref() == Some(name.as_str()) { None } else { Some(name) };
|
||||||
|
}
|
||||||
|
Message::SubMenuAction(action) => {
|
||||||
|
let act = action.as_str();
|
||||||
|
if act != "^K Kill Line (Cut)" { self.last_action_was_kill = false; }
|
||||||
|
match act {
|
||||||
|
"New" => { let n = format!("Untitled-{}", self.tabs.len()+1); self.tabs.push(editor::EditorTab::new(&n, "")); self.active_tab = self.tabs.len()-1; self.chat_log.push(format!("sys> New: {}", n)); }
|
||||||
|
"Open (F3)" => self.chat_log.push("file> Open: F3.".into()),
|
||||||
|
"Open Selected" => self.chat_log.push("file> Open Selected: path under cursor.".into()),
|
||||||
|
"^O WriteOut (Save)" => self.nano_writeout(),
|
||||||
|
"Save As..." => { self.chat_log.push("file> Save As: enter path.".into()); }
|
||||||
|
"^X Close Tab" => self.nano_exit(),
|
||||||
|
"Reload File" => self.chat_log.push("file> Reload from disk.".into()),
|
||||||
|
"Save Session" => self.save_session(),
|
||||||
|
"Load Session" => self.load_session(),
|
||||||
|
"Print..." => self.chat_log.push("file> Print.".into()),
|
||||||
|
"Exit App" => std::process::exit(0),
|
||||||
|
"Undo (^Z)" => self.chat_log.push("edit> Undo.".into()),
|
||||||
|
"Redo (^Y)" => self.chat_log.push("edit> Redo.".into()),
|
||||||
|
"^K Kill Line (Cut)" => self.nano_kill_line(),
|
||||||
|
"^U Unkill (Paste)" => self.nano_unkill(),
|
||||||
|
"^C Cur Pos" => self.nano_cur_pos(),
|
||||||
|
"^A Select All" => self.chat_log.push("edit> Select All.".into()),
|
||||||
|
"Delete Line" => self.chat_log.push("edit> Delete Line.".into()),
|
||||||
|
"Duplicate Line" => self.chat_log.push("edit> Duplicate Line.".into()),
|
||||||
|
"Transpose Line" => self.chat_log.push("edit> Transpose Line.".into()),
|
||||||
|
"Match Brace ^E" => self.chat_log.push("edit> Match Brace.".into()),
|
||||||
|
"Comment Line" => self.chat_log.push("edit> Toggle line comment.".into()),
|
||||||
|
"Uncomment Line" => self.chat_log.push("edit> Uncomment.".into()),
|
||||||
|
"Increase Indent" => self.chat_log.push("edit> Indent.".into()),
|
||||||
|
"Decrease Indent" => self.chat_log.push("edit> Unindent.".into()),
|
||||||
|
"^W Where Is (Find)" => self.nano_where_is(),
|
||||||
|
"Find Next (F3)" => self.chat_log.push("search> Find Next.".into()),
|
||||||
|
"Find Previous (Shift+F3)" => self.chat_log.push("search> Find Previous.".into()),
|
||||||
|
"^\\ Replace" => self.nano_replace(),
|
||||||
|
"Replace Next" => self.chat_log.push("search> Replace Next.".into()),
|
||||||
|
"Go To Line (^_)" => self.nano_goto_line(),
|
||||||
|
"Toggle Bookmark (^B)" => self.toggle_bookmark(),
|
||||||
|
"Next Bookmark" => self.chat_log.push("search> Next bookmark.".into()),
|
||||||
|
"Prev Bookmark" => self.chat_log.push("search> Prev bookmark.".into()),
|
||||||
|
"Clear All Bookmarks" => self.clear_bookmarks(),
|
||||||
|
"Toggle Full Screen (F11)" => self.chat_log.push("view> Toggle Full Screen.".into()),
|
||||||
|
"Toggle Message Window" => self.chat_log.push("view> Toggle messages.".into()),
|
||||||
|
"Toggle Sidebar" => { self.show_symbol_tree = !self.show_symbol_tree; self.chat_log.push(format!("view> Sidebar: {}.", if self.show_symbol_tree { "ON" } else { "OFF" })); }
|
||||||
|
"Toggle Line Numbers" => { self.config.show_line_numbers = !self.config.show_line_numbers; self.chat_log.push(format!("view> Line numbers: {}.", if self.config.show_line_numbers { "ON" } else { "OFF" })); }
|
||||||
|
"Toggle Whitespace" => self.chat_log.push("view> Toggle whitespace.".into()),
|
||||||
|
"Toggle Line Endings" => self.chat_log.push("view> Toggle line ending markers.".into()),
|
||||||
|
"Word Wrap" => { self.config.word_wrap = !self.config.word_wrap; self.chat_log.push(format!("view> Word wrap: {}.", if self.config.word_wrap { "ON" } else { "OFF" })); }
|
||||||
|
"Toggle Syntax Tone" => { self.highlight_tone = match self.highlight_tone { HighlightTone::CandyPop => HighlightTone::Matte, HighlightTone::Matte => HighlightTone::CandyPop }; }
|
||||||
|
"Fold All" => self.chat_log.push("view> Fold All.".into()),
|
||||||
|
"Unfold All" => self.chat_log.push("view> Unfold All.".into()),
|
||||||
|
"Toggle Current Fold" => self.chat_log.push("view> Toggle fold.".into()),
|
||||||
|
"Compile" => self.compile_only(),
|
||||||
|
"Build" => self.build_and_run(),
|
||||||
|
"^B Build & Run" => self.build_and_run(),
|
||||||
|
"Run (F5)" => self.run_only(),
|
||||||
|
"^L Lint Syntax" => self.lint_syntax(),
|
||||||
|
"Stop Executing" => self.chat_log.push("build> Stop.".into()),
|
||||||
|
"Clear Output" => { self.chat_log.clear(); self.chat_log.push("sys> Cleared.".into()); }
|
||||||
|
"Next Message" => self.chat_log.push("build> Next message.".into()),
|
||||||
|
"Previous Message" => self.chat_log.push("build> Prev message.".into()),
|
||||||
|
"Run Command..." => self.chat_log.push("tools> Run command.".into()),
|
||||||
|
"Run Lua Script (F5)" => self.chat_log.push("tools> Lua script.".into()),
|
||||||
|
"Rust" | "Python" | "C/C++" | "Shell" | "JavaScript" | "TypeScript" | "Markdown" | "HTML" | "CSS" | "JSON" | "XML" | "YAML" | "Go" | "Ruby" | "PHP" | "Java" | "C#" | "SQL" => {
|
||||||
|
if !self.tabs.is_empty() { self.tabs[self.active_tab].override_syntax = Some(act.to_string()); self.chat_log.push(format!("syntax> Override: {}", act)); }
|
||||||
|
}
|
||||||
|
"Clear Override" => { if !self.tabs.is_empty() { self.tabs[self.active_tab].override_syntax = None; self.chat_log.push("syntax> Override cleared.".into()); } }
|
||||||
|
"Global Properties" => self.chat_log.push("opts> Global properties.".into()),
|
||||||
|
"User Properties" => self.chat_log.push("opts> User properties.".into()),
|
||||||
|
"Local Properties" => self.chat_log.push("opts> Local properties.".into()),
|
||||||
|
"Open Abbreviations" => self.chat_log.push("opts> Abbreviations.".into()),
|
||||||
|
"^G Help" => self.nano_help(),
|
||||||
|
"About" => { self.chat_log.push("sys> Jeremy Anderson info@dcos.net Scitano version 1.0 https://git.dcos.net/dcosnet/scitano".into()); self.bottom_tab = BottomTab::Messages; }
|
||||||
|
"Sync Aider Models" => { self.chat_log.push("ai> Probing Ollama/Aider...".into()); self.aider_models = Self::fetch_aider_models(); self.chat_log.push(format!("ai> Models: {:?}", self.aider_models)); self.bottom_tab = BottomTab::AiTerminal; }
|
||||||
|
"Aider: Architect Mode" | "Aider: Code Review" | "Aider: Refactor Buffer" => { self.chat_log.push(format!("ai> {}", act)); self.bottom_tab = BottomTab::AiTerminal; }
|
||||||
|
"Hermes: Local Inference" | "Hermes: System Prompt" | "Odysseus: Crawl Context" | "Odysseus: Vector Sync" => { self.chat_log.push(format!("ai> {}", act)); self.bottom_tab = BottomTab::AiTerminal; }
|
||||||
|
"Comment Selection" => self.chat_log.push("ctx> Comment selection.".into()),
|
||||||
|
"Uncomment Selection" => self.chat_log.push("ctx> Uncomment selection.".into()),
|
||||||
|
"Upper Case" => self.chat_log.push("ctx> Uppercase.".into()),
|
||||||
|
"Lower Case" => self.chat_log.push("ctx> Lowercase.".into()),
|
||||||
|
"Insert Timestamp" => self.chat_log.push("ctx> Insert timestamp.".into()),
|
||||||
|
_ => { self.chat_log.push(format!("sys> Action: '{}'.", act)); }
|
||||||
|
}
|
||||||
|
self.active_menu = None;
|
||||||
|
}
|
||||||
|
Message::TabSelected(i) => { self.last_action_was_kill = false; self.active_tab = i; }
|
||||||
|
Message::CloseTab(i) => {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if i < self.tabs.len() {
|
||||||
|
let title = self.tabs[i].title.clone();
|
||||||
|
self.tabs.remove(i);
|
||||||
|
if self.tabs.is_empty() { self.active_tab = 0; } else if self.active_tab >= self.tabs.len() { self.active_tab = self.tabs.len() - 1; }
|
||||||
|
self.chat_log.push(format!("nano> Closed: {}", title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::BottomTabSelected(tab) => { self.last_action_was_kill = false; self.bottom_tab = tab; }
|
||||||
|
Message::EditorAction(action) => {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
if !self.tabs.is_empty() {
|
||||||
|
self.tabs[self.active_tab].content.perform(action);
|
||||||
|
self.tabs[self.active_tab].is_dirty = true;
|
||||||
|
self.tabs[self.active_tab].parse_symbols();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::EditorRightClick => {
|
||||||
|
self.last_action_was_kill = false;
|
||||||
|
self.active_menu = if self.active_menu.as_deref() == Some("EditorContext") { None } else { Some("EditorContext".into()) };
|
||||||
|
}
|
||||||
|
Message::SyncAiderModels => {}
|
||||||
|
Message::ToggleWordWrap(v) => { self.config.word_wrap = v; }
|
||||||
|
Message::ToggleLineNumbers(v) => { self.config.show_line_numbers = v; }
|
||||||
|
Message::ToggleSymbolTree(v) => { self.show_symbol_tree = v; }
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- View ------------------------------------------------------
|
||||||
|
fn view(&self) -> Element<'_, Message> {
|
||||||
|
let menu_bar = container(
|
||||||
|
row![
|
||||||
|
button(text("File").size(13)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("File".into())),
|
||||||
|
button(text("Edit").size(13)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("Edit".into())),
|
||||||
|
button(text("Search").size(13)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("Search".into())),
|
||||||
|
button(text("View").size(13)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("View".into())),
|
||||||
|
button(text("Build").size(13)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("Build".into())),
|
||||||
|
button(text("Tools").size(13)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("Tools".into())),
|
||||||
|
button(text("Syntax").size(13)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("Syntax".into())),
|
||||||
|
button(text("Options").size(13)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("Options".into())),
|
||||||
|
button(text("Help").size(13)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("Help".into())),
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
button(text("AI-Bridge").size(13).color(theme::ACCENT_BLUE)).style(theme::menu_button).padding([2,8]).on_press(Message::MenuToggle("AI".into())),
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
text("Scitano v1.0.0").size(11).color(theme::TEXT_DIM).font(Font::MONOSPACE),
|
||||||
|
].spacing(1).align_y(iced::Alignment::Center),
|
||||||
|
).style(theme::panel_container).width(Length::Fill).padding([2,4]);
|
||||||
|
|
||||||
|
let mut layout = column![menu_bar];
|
||||||
|
|
||||||
|
// -- Submenus -------------------------------------------
|
||||||
|
if let Some(ref am) = self.active_menu {
|
||||||
|
let items: Vec<&str> = match am.as_str() {
|
||||||
|
"File" => vec!["New","Open (F3)","Open Selected","^O WriteOut (Save)","Save As...","^X Close Tab","Reload File","-","Save Session","Load Session","Print...","Exit App"],
|
||||||
|
"Edit" => vec!["Undo (^Z)","Redo (^Y)","-","^K Kill Line (Cut)","^U Unkill (Paste)","^C Cur Pos","-","^A Select All","Delete Line","Duplicate Line","Transpose Line","-","Match Brace ^E","Comment Line","Uncomment Line","Increase Indent","Decrease Indent"],
|
||||||
|
"Search" => vec!["^W Where Is (Find)","Find Next (F3)","Find Previous (Shift+F3)","^\\ Replace","Replace Next","-","Go To Line (^_)","-","Toggle Bookmark (^B)","Next Bookmark","Prev Bookmark","Clear All Bookmarks"],
|
||||||
|
"View" => vec!["Toggle Full Screen (F11)","Toggle Message Window","Toggle Sidebar","Toggle Line Numbers","Toggle Whitespace","Toggle Line Endings","Word Wrap","Toggle Syntax Tone","-","Fold All","Unfold All","Toggle Current Fold"],
|
||||||
|
"Build" => vec!["Compile","Build","^B Build & Run","Run (F5)","-","^L Lint Syntax","-","Stop Executing","Clear Output","-","Next Message","Previous Message"],
|
||||||
|
"Tools" => vec!["Run Command...","Run Lua Script (F5)"],
|
||||||
|
"Syntax" => vec!["Rust","Python","C/C++","Shell","JavaScript","TypeScript","Markdown","HTML","CSS","JSON","XML","YAML","Go","Ruby","PHP","Java","C#","SQL","Clear Override"],
|
||||||
|
"Options" => vec!["Global Properties","Open Abbreviations","User Properties","Local Properties"],
|
||||||
|
"Help" => vec!["^G Help","About"],
|
||||||
|
"AI" => vec!["Sync Aider Models","Aider: Architect Mode","Aider: Code Review","Aider: Refactor Buffer","Hermes: Local Inference","Hermes: System Prompt","Odysseus: Crawl Context","Odysseus: Vector Sync"],
|
||||||
|
"EditorContext" => vec!["^O Save Buffer","^X Close Tab","-","^B Build File","^L Lint Syntax","-","Comment Selection","Uncomment Selection","-","Upper Case","Lower Case","Insert Timestamp"],
|
||||||
|
_ => vec![],
|
||||||
|
};
|
||||||
|
let mut sr = row![].spacing(4);
|
||||||
|
for item in items {
|
||||||
|
if item == "-" {
|
||||||
|
sr = sr.push(Space::with_width(Length::Fixed(8.0)));
|
||||||
|
} else {
|
||||||
|
sr = sr.push(button(text(item).size(12).font(Font::MONOSPACE)
|
||||||
|
.color(if item.starts_with('^') { theme::ACCENT_YELLOW } else { theme::TEXT_PRIMARY }))
|
||||||
|
.style(theme::menu_button).padding([4,10]).on_press(Message::SubMenuAction(item.to_string())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout = layout.push(container(scrollable(sr).direction(
|
||||||
|
iced::widget::scrollable::Direction::Horizontal(iced::widget::scrollable::Scrollbar::new().width(0).margin(0))))
|
||||||
|
.width(Length::Fill).style(theme::panel_container).padding([4,8]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Tabs -----------------------------------------------
|
||||||
|
let mut tab_row = row![].spacing(0);
|
||||||
|
for (i, tab) in self.tabs.iter().enumerate() {
|
||||||
|
let active = self.active_tab == i;
|
||||||
|
let display = if tab.is_dirty { format!("\u{25cf} {}", tab.title) } else { tab.title.clone() };
|
||||||
|
let btn = button(text(display).size(13).font(Font::MONOSPACE).color(if active { theme::TEXT_PRIMARY } else { theme::TEXT_MUTED }))
|
||||||
|
.style(if active { theme::active_tab_button } else { theme::inactive_tab_button })
|
||||||
|
.on_press(Message::TabSelected(i)).padding([4,12]);
|
||||||
|
tab_row = tab_row.push(mouse_area(btn).on_right_press(Message::CloseTab(i)));
|
||||||
|
}
|
||||||
|
tab_row = tab_row.push(button(text("+").size(14).font(Font::MONOSPACE).color(theme::TEXT_MUTED)).style(theme::menu_button).padding([4,8]).on_press(Message::SubMenuAction("New".into())));
|
||||||
|
let tab_container = container(scrollable(tab_row).direction(
|
||||||
|
iced::widget::scrollable::Direction::Horizontal(iced::widget::scrollable::Scrollbar::new().width(0).margin(0))))
|
||||||
|
.width(Length::Fill).style(theme::panel_container);
|
||||||
|
|
||||||
|
// -- Editor + Sidebar ------------------------------------
|
||||||
|
let mut core_row = row![].width(Length::Fill).height(Length::Fill);
|
||||||
|
if self.show_symbol_tree {
|
||||||
|
let mut tc = column![text("SYMBOLS").size(11).font(Font::MONOSPACE).color(theme::TEXT_MUTED)].spacing(6).padding(8);
|
||||||
|
if !self.tabs.is_empty() {
|
||||||
|
if self.tabs[self.active_tab].symbols.is_empty() {
|
||||||
|
tc = tc.push(text(" (no symbols)").size(10).font(Font::MONOSPACE).color(theme::TEXT_DIM));
|
||||||
|
}
|
||||||
|
for (ln, sym) in &self.tabs[self.active_tab].symbols {
|
||||||
|
let d = if sym.len() > 22 { format!("{}: {}..", ln, &sym[..20]) } else { format!("{}: {}", ln, sym) };
|
||||||
|
tc = tc.push(text(d).size(10).font(Font::MONOSPACE).color(theme::ACCENT_BLUE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core_row = core_row.push(container(scrollable(tc)).width(Length::Fixed(180.0)).height(Length::Fill).style(theme::tree_container));
|
||||||
|
}
|
||||||
|
let editor_area = if self.tabs.is_empty() {
|
||||||
|
container(text("[ No files open -- ^G for help ]").color(theme::TEXT_MUTED).font(Font::MONOSPACE))
|
||||||
|
.style(theme::app_container).width(Length::Fill).height(Length::Fill).center_x(Length::Fill).center_y(Length::Fill)
|
||||||
|
} else {
|
||||||
|
let syn = self.tabs[self.active_tab].get_syntax();
|
||||||
|
let ed = text_editor(&self.tabs[self.active_tab].content).highlight(syn, self.highlight_tone.to_iced_theme())
|
||||||
|
.on_action(Message::EditorAction).font(Font::MONOSPACE).height(Length::Fill);
|
||||||
|
container(mouse_area(ed).on_right_press(Message::EditorRightClick))
|
||||||
|
.style(theme::app_container).width(Length::Fill).height(Length::Fill).padding(8)
|
||||||
|
};
|
||||||
|
core_row = core_row.push(editor_area);
|
||||||
|
|
||||||
|
// -- Status Bar -----------------------------------------
|
||||||
|
let st = if self.tabs.is_empty() { " Idle".to_string() } else {
|
||||||
|
let t = &self.tabs[self.active_tab];
|
||||||
|
format!(" {} {} | Syntax: {} | Bookmarks: {}", t.title, if t.is_dirty { "[MODIFIED]" } else { "[saved]" }, t.get_syntax().to_uppercase(), t.bookmarks.len())
|
||||||
|
};
|
||||||
|
let status_bar = container(row![
|
||||||
|
text(st).size(12).font(Font::MONOSPACE),
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
text(if self.config.word_wrap { "Wrap" } else { " nowrap" }).size(11).color(theme::TEXT_MUTED).font(Font::MONOSPACE),
|
||||||
|
text(" | UTF-8 | LF").size(11).color(theme::TEXT_MUTED).font(Font::MONOSPACE),
|
||||||
|
].padding([4,12])).style(theme::status_bar).width(Length::Fill);
|
||||||
|
|
||||||
|
// -- Bottom Panel ----------------------------------------
|
||||||
|
let bt = row![
|
||||||
|
button(text("Messages").size(11).font(Font::MONOSPACE)).style(if self.bottom_tab==BottomTab::Messages{theme::active_tab_button}else{theme::inactive_tab_button}).padding([2,10]).on_press(Message::BottomTabSelected(BottomTab::Messages)),
|
||||||
|
button(text("Diagnostics").size(11).font(Font::MONOSPACE)).style(if self.bottom_tab==BottomTab::Diagnostics{theme::active_tab_button}else{theme::inactive_tab_button}).padding([2,10]).on_press(Message::BottomTabSelected(BottomTab::Diagnostics)),
|
||||||
|
button(text("Build Logs").size(11).font(Font::MONOSPACE)).style(if self.bottom_tab==BottomTab::BuildLogs{theme::active_tab_button}else{theme::inactive_tab_button}).padding([2,10]).on_press(Message::BottomTabSelected(BottomTab::BuildLogs)),
|
||||||
|
button(text("AI Terminal").size(11).font(Font::MONOSPACE).color(theme::ACCENT_BLUE)).style(if self.bottom_tab==BottomTab::AiTerminal{theme::active_tab_button}else{theme::inactive_tab_button}).padding([2,10]).on_press(Message::BottomTabSelected(BottomTab::AiTerminal)),
|
||||||
|
].spacing(0);
|
||||||
|
let mut cc = column![].spacing(1);
|
||||||
|
for line in &self.chat_log {
|
||||||
|
let c = if line.starts_with("err>") { theme::ACCENT_RED } else if line.starts_with("nano>") { theme::ACCENT_GREEN } else if line.starts_with("ai>") { theme::ACCENT_BLUE } else if line.starts_with("sys>") { theme::TEXT_MUTED } else { theme::TEXT_PRIMARY };
|
||||||
|
cc = cc.push(text(line).size(12).color(c).font(Font::MONOSPACE));
|
||||||
|
}
|
||||||
|
let bottom = column![
|
||||||
|
container(bt).width(Length::Fill).style(theme::panel_container),
|
||||||
|
container(scrollable(cc)).height(Length::Fixed(160.0)).width(Length::Fill).style(theme::panel_container).padding(6),
|
||||||
|
];
|
||||||
|
|
||||||
|
// -- Footer ---------------------------------------------
|
||||||
|
let ft = if self.tabs.is_empty() {
|
||||||
|
" ^G=Help ^O=Save ^X=Exit ^W=Search ^K=Cut ^U=Paste ^C=CurPos ^R=Insert".to_string()
|
||||||
|
} else {
|
||||||
|
let tab = &self.tabs[self.active_tab];
|
||||||
|
format!(" {} | Lines: {} | {:.2} KB | {} | ^G=Help ^O=Save ^X=Exit ^W=Find ^K=Cut ^U=Paste",
|
||||||
|
tab.file_path.as_ref().map(|p|p.to_string_lossy().to_string()).unwrap_or_else(||tab.title.clone()),
|
||||||
|
tab.line_count(), tab.byte_size() as f64/1024.0, tab.get_syntax().to_uppercase())
|
||||||
|
};
|
||||||
|
let footer = container(text(ft).size(10).font(Font::MONOSPACE).color(theme::TEXT_MUTED)).width(Length::Fill).padding([2,8]).style(theme::footer_bar);
|
||||||
|
|
||||||
|
layout.push(tab_container).push(core_row).push(status_bar).push(bottom).push(footer).width(Length::Fill).height(Length::Fill).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() -> iced::Result {
|
||||||
|
iced::application("Scitano 1.0 - https://git.dcos.net/dcosnet/scitano/", Scitano::update, Scitano::view)
|
||||||
|
.theme(|_| Theme::Dark)
|
||||||
|
.subscription(Scitano::subscription)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
use iced::widget::{button, container};
|
||||||
|
use iced::{Background, Color, Theme, Border, Shadow};
|
||||||
|
|
||||||
|
pub const TEXT_PRIMARY: Color = Color::from_rgb(0.9, 0.9, 0.9);
|
||||||
|
pub const TEXT_MUTED: Color = Color::from_rgb(0.5, 0.5, 0.5);
|
||||||
|
pub const TEXT_DIM: Color = Color::from_rgb(0.35, 0.35, 0.35);
|
||||||
|
pub const ACCENT_BLUE: Color = Color::from_rgb(0.4, 0.7, 1.0);
|
||||||
|
pub const ACCENT_GREEN: Color = Color::from_rgb(0.5, 0.85, 0.5);
|
||||||
|
pub const ACCENT_YELLOW: Color = Color::from_rgb(0.95, 0.85, 0.4);
|
||||||
|
pub const ACCENT_RED: Color = Color::from_rgb(0.9, 0.4, 0.4);
|
||||||
|
pub const BG_PANEL: Color = Color::from_rgb(0.12, 0.12, 0.12);
|
||||||
|
pub const BG_ACTIVE: Color = Color::from_rgb(0.18, 0.18, 0.18);
|
||||||
|
pub const BG_STATUS: Color = Color::from_rgb(0.15, 0.25, 0.45);
|
||||||
|
pub const BG_TREE: Color = Color::from_rgb(0.08, 0.08, 0.08);
|
||||||
|
pub const BG_FOOTER: Color = Color::from_rgb(0.10, 0.10, 0.10);
|
||||||
|
|
||||||
|
pub fn panel_container(_theme: &Theme) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
text_color: Some(TEXT_PRIMARY),
|
||||||
|
background: Some(Background::Color(BG_PANEL)),
|
||||||
|
border: Border::default(),
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tree_container(_theme: &Theme) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
text_color: Some(TEXT_PRIMARY),
|
||||||
|
background: Some(Background::Color(BG_TREE)),
|
||||||
|
border: Border {
|
||||||
|
color: Color::from_rgb(0.2, 0.2, 0.2),
|
||||||
|
width: 1.0,
|
||||||
|
radius: 0.0.into(),
|
||||||
|
},
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app_container(_theme: &Theme) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
text_color: Some(TEXT_PRIMARY),
|
||||||
|
background: Some(Background::Color(Color::BLACK)),
|
||||||
|
border: Border::default(),
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status_bar(_theme: &Theme) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
text_color: Some(Color::WHITE),
|
||||||
|
background: Some(Background::Color(BG_STATUS)),
|
||||||
|
border: Border::default(),
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn footer_bar(_theme: &Theme) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
text_color: Some(TEXT_MUTED),
|
||||||
|
background: Some(Background::Color(BG_FOOTER)),
|
||||||
|
border: Border {
|
||||||
|
color: Color::from_rgb(0.2, 0.2, 0.2),
|
||||||
|
width: 1.0,
|
||||||
|
radius: 0.0.into(),
|
||||||
|
},
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn menu_button(_theme: &Theme, status: button::Status) -> button::Style {
|
||||||
|
match status {
|
||||||
|
button::Status::Hovered => button::Style {
|
||||||
|
background: Some(Background::Color(BG_ACTIVE)),
|
||||||
|
text_color: TEXT_PRIMARY,
|
||||||
|
..button::Style::default()
|
||||||
|
},
|
||||||
|
_ => button::Style {
|
||||||
|
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||||
|
text_color: TEXT_PRIMARY,
|
||||||
|
..button::Style::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_tab_button(_theme: &Theme, _status: button::Status) -> button::Style {
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(BG_ACTIVE)),
|
||||||
|
text_color: TEXT_PRIMARY,
|
||||||
|
..button::Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inactive_tab_button(_theme: &Theme, _status: button::Status) -> button::Style {
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(BG_PANEL)),
|
||||||
|
text_color: TEXT_MUTED,
|
||||||
|
..button::Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue